Last active
March 22, 2016 17:36
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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