Skip to content

Instantly share code, notes, and snippets.

@Vegasq
Last active March 22, 2016 17:36
Show Gist options
  • Save Vegasq/c016cf2b560a17f06bbd to your computer and use it in GitHub Desktop.
Save Vegasq/c016cf2b560a17f06bbd to your computer and use it in GitHub Desktop.
Custom tool to do snapshot/restore for envs created with fuel-devops. Reason for existing is bug in qemu that do not allow to do snapshots in systems with tough IO.
#!/usr/bin/env python
# 2016, Mykola Yakovliev <[email protected]>
import argparse
import logging
import os
import random
import shutil
import subprocess
import libvirt
import time
import uuid
from lxml import etree
FORMAT = '%(asctime)-15s %(message)s'
logging.basicConfig(format=FORMAT)
logger = logging.getLogger('kvapa')
logger.setLevel(logging.DEBUG)
parser = argparse.ArgumentParser(
description='Custom tool to do snapshot/restore for envs created '
'with fuel-devops. Reason for existing is bug in qemu '
'that do not allow to do snapshots in systems with '
'tough IO.')
subparsers = parser.add_subparsers(
dest='command', help='Command to be executed.')
project_var = {
'dest': 'project',
'metavar': 'PROJECT',
'type': str,
'help': 'Project name in fuel-devops'}
new_project_var = {
'dest': 'new_project',
'metavar': 'PROJECT',
'type': str,
'help': 'New project name'}
snapshot_var = {
'dest': 'snapshot_name',
'metavar': 'SNAPSHOT',
'type': str,
'help': 'Name for snapshot, doesn not work with fuel-devops snapshots'}
parser_map = [
{'snapshot': [project_var, snapshot_var]},
{'revert': [project_var, snapshot_var]},
{'snapshot-list': [project_var]},
{'snapshot-delete': [project_var, snapshot_var]},
{'suspend': [project_var]},
{'resume': [project_var]},
{'reset_slaves': [project_var]},
{'reset': [project_var]},
{'start': [project_var]},
{'stop': [project_var]},
{'clone': [project_var, new_project_var]},
]
for command in parser_map:
command_name = list(command.keys())[0]
parser_snapshot = subparsers.add_parser(command_name)
for var in list(command.values())[0]:
parser_snapshot.add_argument(**var)
SNAP_TEMPLATE = """<domainsnapshot>
<name>%s</name>
<description>%s</description>
</domainsnapshot>"""
class ExecutionException(Exception):
pass
def randomMAC():
'''Stolen from network'''
mac = [0x00, 0x16, 0x3e,
random.randint(0x00, 0x7f),
random.randint(0x00, 0x7f),
random.randint(0x00, 0x7f)]
return ':'.join(map(lambda x: "%02x" % x, mac))
def retry(fn):
def wrapper(cmd):
while True:
try:
return fn(cmd)
except ExecutionException:
logger.error('Execution "%s" failed. Retrying.' % cmd)
time.sleep(1)
return wrapper
def execute(cmd, error_on_fail=True):
logger.debug('Executing "%s"' % cmd)
proc = subprocess.Popen(cmd,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE, shell=True)
(out, err) = proc.communicate()
logger.debug('STDOUT: %s' % out)
logger.debug('STDERR: %s' % err)
logger.debug('Return code: %s' % proc.returncode)
if proc.returncode != 0 and error_on_fail:
raise ExecutionException(out + err)
class NetworkCloner(object):
networks_path = '/etc/libvirt/qemu/networks'
vm_path = '/etc/libvirt/qemu'
def _get_xmls(self, path, project):
xmls = [
os.path.join(self.vms_path, net)
for net in os.listdir(path)
if net.startswith(project + '_')]
return xmls
def _get_vms_xmls(self, project):
return self._get_xmls(self.vm_path, project)
def _get_networks_xmls(self, project):
return self._get_xmls(self.networks_path, project)
def _copy_xmls(self, xmls):
for xml in xmls:
logger.debug('Copy XML file %s' % xml)
shutil.copyfile(xml, xml.replace(self.project,
self.new_project))
def _copy_vms_xmls(self):
project_vms = self._get_vms_xmls(self.project)
self._copy_xmls(project_vms)
new_project_vms = self._get_vms_xmls(self.new_project)
for net in new_project_vms:
logger.debug('Update XML file %s' % net)
tree = etree.parse(net)
root = tree.getroot()
for element in root:
logger.debug('Update tag %s in XML file' % element.tag)
fixer = self.get_vm_xml_fixer(element.tag)
fixer(self, element)
with open(net, 'w') as fl:
logger.debug('Save XML file %s' % net)
fl.write(etree.tostring(root, pretty_print=True))
return new_project_vms
def _copy_network_xmls(self):
project_networks = self._get_networks_xmls(self.project)
self._copy_xmls(project_networks)
new_project_networks = self._get_networks_xmls(self.new_project)
for net in new_project_networks:
logger.debug('Update XML file %s' % net)
tree = etree.parse(net)
root = tree.getroot()
for element in root:
logger.debug('Update tag %s in XML file' % element.tag)
fixer = self.get_network_xml_fixer(element.tag)
fixer(self, element)
with open(net, 'w') as fl:
logger.debug('Save XML file %s' % net)
fl.write(etree.tostring(root, pretty_print=True))
return new_project_networks
def _define_networks(self, new_project_networks):
for net in new_project_networks:
os.system('virsh net-create %s' % net)
os.system('virsh net-start %s' % net.split('/')[-1].split('.')[0])
def _clone_networks(self):
new_project_networks = self._copy_network_xmls()
self._define_networks(new_project_networks)
def _create_extra_network(self):
pass
def _clone_vms(self):
if tag == 'name':
def fixer(context, data):
data.text = data.text.replace(context.project,
context.new_project)
elif tag == 'uuid':
def fixer(context, data):
new_uuid = uuid.uuid4()
data.text = str(new_uuid)
elif tag == 'bridge':
def fixer(context, data):
data.attrib['name'] = data.attrib['name'].replace(
'fuelbr', 'kvapa42')
elif tag == 'mac':
def fixer(context, data):
data.attrib['address'] = randomMAC()
elif tag == 'ip':
def fixer(context, data):
data.attrib['address'] = data.attrib['address'].replace(
'128', '131')
else:
def fixer(context, data):
logger.debug('Unknown tag: %s' % tag)
return fixer
def get_vm_xml_fixer(self, tag):
print(tag)
def get_network_xml_fixer(self, tag):
if tag == 'name':
def fixer(context, data):
data.text = data.text.replace(context.project,
context.new_project)
elif tag == 'uuid':
def fixer(context, data):
new_uuid = uuid.uuid4()
data.text = str(new_uuid)
elif tag == 'bridge':
def fixer(context, data):
data.attrib['name'] = data.attrib['name'].replace(
'fuelbr', 'kvapa42')
elif tag == 'mac':
def fixer(context, data):
data.attrib['address'] = randomMAC()
elif tag == 'ip':
def fixer(context, data):
data.attrib['address'] = data.attrib['address'].replace(
'128', '131')
else:
def fixer(context, data):
logger.debug('Unknown tag: %s' % tag)
return fixer
class Kvapa(object):
def __init__(self, project, snap_key=None, new_project=None):
self.project = project
self.new_project = new_project
self.snap_key = snap_key
self._conn = None
@property
def conn(self):
if self._conn:
return self._conn
self._conn = libvirt.open('qemu:///system')
if self._conn is None:
raise Exception('Failed to connect to libvirt')
return self._conn
def snap_name(self, domain):
return "%s_%s" % (domain, self.snap_key)
def snap_xml(self, domain):
return '/tmp/%s_snap.xml' % self.snap_name(domain)
@property
def domains(self):
domains = [d for d in self.conn.listAllDomains()
if d.name().startswith(self.project + "_")]
return domains
@property
def domains_names(self):
return [d.name() for d in self.domains]
@property
def snapshots(self):
snapshots = []
for d in self.domains:
for snap in d.snapshotListNames():
if not snap.startswith(d.name() + '_'):
continue
snapshots.append(snap.replace(d.name() + '_', ''))
snapshots = list(set(snapshots))
return snapshots
class KvapaLV(Kvapa, NetworkCloner):
def do_suspend(self):
logger.debug('Suspend %s' % self.project)
for d in self.domains:
logger.debug('Suspend node %s' % d.name())
try:
d.suspend()
except libvirt.libvirtError as err:
logger.error(err)
def do_resume(self):
logger.debug('Resume %s' % self.project)
for d in self.domains:
logger.debug('Resume node %s' % d.name())
try:
d.resume()
except libvirt.libvirtError as err:
logger.error(err)
def do_reset_slaves(self):
logger.debug('Reset slaves at %s' % self.project)
for d in self.domains:
if d.name().endswith('_admin'):
continue
logger.debug('Reset node %s' % d.name())
d.reset()
def do_reset(self):
logger.debug('Reset %s' % self.project)
for d in self.domains:
logger.debug('Reset node %s' % d.name())
d.reset()
def snap_domain(self, domain):
with open(self.snap_xml(domain), 'w') as fl:
fl.write(SNAP_TEMPLATE % (self.snap_name(domain),
self.snap_name(domain)))
execute('virsh snapshot-create {domain} {xml_path}'.format(
domain=domain,
xml_path=self.snap_xml(domain)))
def revert_domain(self, domain):
execute('virsh snapshot-revert {domain} {snapshot}'.format(
domain=domain,
snapshot=self.snap_name(domain)), error_on_fail=False)
def do_snapshot(self):
self.do_suspend()
logger.debug('Snapshotting project: %s' % self.project)
for d in self.domains_names:
logger.debug('Snapshotting domain: %s' % d)
self.snap_domain(d)
self.do_resume()
def do_revert(self):
logger.debug('Reverting project: %s' % self.project)
for d in self.domains_names:
logger.debug('Reverting domain: %s' % d)
self.revert_domain(d)
self.do_resume()
def do_start(self):
for d in self.domains:
d.create()
def do_stop(self):
for d in self.domains:
d.destroy()
def do_snapshot_list(self):
for snap in self.snapshots:
print(snap)
def do_snapshot_delete(self):
for d in self.domains_names:
execute('virsh snapshot-delete {domain} {snapshot_name}'.format(
domain=d,
snapshot_name=self.snap_name(d)),
error_on_fail=False)
def do_clone(self):
self._clone_networks()
self._clone_vdd()
self._clone_vms()
def main(args):
project = args.project
snapshot_name = getattr(args, 'snapshot_name', None)
new_project = getattr(args, 'new_project', None)
kvapa = KvapaLV(project,
snap_key=snapshot_name,
new_project=new_project)
command = args.command.replace('-', '_')
fn = getattr(kvapa, "do_%s" % command)
fn()
if __name__ == '__main__':
args = parser.parse_args()
main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment