diff --git a/cli-client/lib b/cli-client/lib new file mode 120000 index 00000000..c9ad5dfd --- /dev/null +++ b/cli-client/lib @@ -0,0 +1 @@ +../client-common/lib \ No newline at end of file diff --git a/cli-client/pvc.py b/cli-client/pvc.py index d8f297dc..12e82b86 100755 --- a/cli-client/pvc.py +++ b/cli-client/pvc.py @@ -20,501 +20,34 @@ # ############################################################################### -import os import socket -import time -import uuid -import re -import tempfile -import subprocess -import difflib -import colorama import click -import lxml.objectify -import configparser -import kazoo.client -############################################################################### -# Supplemental functions -############################################################################### - -# -# ansiiprint output -# - -class ansiiprint: - # ANSII colours for output - def red(): - return '\033[91m' - def blue(): - return '\033[94m' - def green(): - return '\033[92m' - def yellow(): - return '\033[93m' - def purple(): - return '\033[95m' - def bold(): - return '\033[1m' - def end(): - return '\033[0m' - - -# -# Validate a UUID -# -def validateUUID(dom_uuid): - try: - uuid.UUID(dom_uuid) - return True - except: - return False - - -# -# Connect and disconnect from Zookeeper -# -def startZKConnection(zk_host): - zk_conn = kazoo.client.KazooClient(hosts=zk_host) - zk_conn.start() - return zk_conn - -def stopZKConnection(zk_conn): - zk_conn.stop() - zk_conn.close() - return 0 - - -# -# XML information parsing functions -# - -# Get the main details for a VM object from XML -def getDomainMainDetails(parsed_xml): - # Get the information we want from it - duuid = str(parsed_xml.uuid) - try: - ddescription = str(parsed_xml.description) - except AttributeError: - ddescription = "N/A" - dname = str(parsed_xml.name) - dmemory = str(parsed_xml.memory) - dmemory_unit = str(parsed_xml.memory.attrib['unit']) - if dmemory_unit == 'KiB': - dmemory = str(int(dmemory) * 1024) - elif dmemory_unit == 'GiB': - dmemory = str(int(dmemory) / 1024) - dvcpu = str(parsed_xml.vcpu) - try: - dvcputopo = '{}/{}/{}'.format(parsed_xml.cpu.topology.attrib['sockets'], parsed_xml.cpu.topology.attrib['cores'], parsed_xml.cpu.topology.attrib['threads']) - except: - dvcputopo = 'N/A' - - return duuid, dname, ddescription, dmemory, dvcpu, dvcputopo - -# Get long-format details -def getDomainExtraDetails(parsed_xml): - dtype = parsed_xml.os.type - darch = parsed_xml.os.type.attrib['arch'] - dmachine = parsed_xml.os.type.attrib['machine'] - dconsole = parsed_xml.devices.console.attrib['type'] - demulator = parsed_xml.devices.emulator - - return dtype, darch, dmachine, dconsole, demulator - -# Get CPU features -def getDomainCPUFeatures(parsed_xml): - dfeatures = [] - for feature in parsed_xml.features.getchildren(): - dfeatures.append(feature.tag) - - return dfeatures - -# Get disk devices -def getDomainDisks(parsed_xml): - ddisks = [] - for device in parsed_xml.devices.getchildren(): - if device.tag == 'disk': - disk_attrib = device.source.attrib - disk_target = device.target.attrib - disk_type = device.attrib['type'] - if disk_type == 'network': - disk_obj = { 'type': disk_attrib.get('protocol'), 'name': disk_attrib.get('name'), 'dev': disk_target.get('dev'), 'bus': disk_target.get('bus') } - elif disk_type == 'file': - disk_obj = { 'type': 'file', 'name': disk_attrib.get('file'), 'dev': disk_target.get('dev'), 'bus': disk_target.get('bus') } - else: - disk_obj = {} - ddisks.append(disk_obj) - - return ddisks - -# Get network devices -def getDomainNetworks(parsed_xml): - dnets = [] - for device in parsed_xml.devices.getchildren(): - if device.tag == 'interface': - net_type = device.attrib['type'] - net_mac = device.mac.attrib['address'] - net_bridge = device.source.attrib[net_type] - net_model = device.model.attrib['type'] - net_obj = { 'type': net_type, 'mac': net_mac, 'source': net_bridge, 'model': net_model } - dnets.append(net_obj) - - return dnets - -# Get controller devices -def getDomainControllers(parsed_xml): - dcontrollers = [] - for device in parsed_xml.devices.getchildren(): - if device.tag == 'controller': - controller_type = device.attrib['type'] - try: - controller_model = device.attrib['model'] - except KeyError: - controller_model = 'none' - controller_obj = { 'type': controller_type, 'model': controller_model } - dcontrollers.append(controller_obj) - - return dcontrollers - -# Parse an XML object -def getDomainXML(zk_conn, dom_uuid): - try: - xml = zk_conn.get('/domains/%s/xml' % dom_uuid)[0].decode('ascii') - except: - return None - - # Parse XML using lxml.objectify - parsed_xml = lxml.objectify.fromstring(xml) - return parsed_xml - -# Root functions -def getInformationFromNode(zk_conn, node_name, long_output): - node_daemon_state = zk_conn.get('/nodes/{}/daemonstate'.format(node_name))[0].decode('ascii') - node_domain_state = zk_conn.get('/nodes/{}/domainstate'.format(node_name))[0].decode('ascii') - node_cpu_count = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[0] - node_kernel = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[1] - node_os = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[2] - node_arch = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[3] - node_mem_used = zk_conn.get('/nodes/{}/memused'.format(node_name))[0].decode('ascii') - node_mem_free = zk_conn.get('/nodes/{}/memfree'.format(node_name))[0].decode('ascii') - node_mem_total = int(node_mem_used) + int(node_mem_free) - node_load = zk_conn.get('/nodes/{}/cpuload'.format(node_name))[0].decode('ascii') - node_domains_count = zk_conn.get('/nodes/{}/domainscount'.format(node_name))[0].decode('ascii') - node_running_domains = zk_conn.get('/nodes/{}/runningdomains'.format(node_name))[0].decode('ascii').split() - node_mem_allocated = 0 - for domain in node_running_domains: - try: - parsed_xml = getDomainXML(zk_conn, domain) - duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = getDomainMainDetails(parsed_xml) - node_mem_allocated += int(dmemory) - except AttributeError: - click.echo('Error: Domain {} does not exist.'.format(domain)) - - if node_daemon_state == 'run': - daemon_state_colour = ansiiprint.green() - elif node_daemon_state == 'stop': - daemon_state_colour = ansiiprint.red() - elif node_daemon_state == 'init': - daemon_state_colour = ansiiprint.yellow() - elif node_daemon_state == 'dead': - daemon_state_colour = ansiiprint.red() + ansiiprint.bold() - else: - daemon_state_colour = ansiiprint.blue() - - if node_domain_state == 'ready': - domain_state_colour = ansiiprint.green() - else: - domain_state_colour = ansiiprint.blue() - - # Format a nice output; do this line-by-line then concat the elements at the end - ainformation = [] - ainformation.append('{}Hypervisor Node information:{}'.format(ansiiprint.bold(), ansiiprint.end())) - ainformation.append('') - # Basic information - ainformation.append('{}Name:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_name)) - ainformation.append('{}Daemon State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), daemon_state_colour, node_daemon_state, ansiiprint.end())) - ainformation.append('{}Domain State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), domain_state_colour, node_domain_state, ansiiprint.end())) - ainformation.append('{}Active VM Count:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_domains_count)) - if long_output == True: - ainformation.append('') - ainformation.append('{}Architecture:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_arch)) - ainformation.append('{}Operating System:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_os)) - ainformation.append('{}Kernel Version:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_kernel)) - ainformation.append('') - ainformation.append('{}CPUs:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_cpu_count)) - ainformation.append('{}Load:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_load)) - ainformation.append('{}Total RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_total)) - ainformation.append('{}Used RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_used)) - ainformation.append('{}Free RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_free)) - ainformation.append('{}Allocated RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_allocated)) - - # Join it all together - information = '\n'.join(ainformation) - return information - - -def getInformationFromXML(zk_conn, uuid, long_output): - # Obtain the contents of the XML from Zookeeper - try: - dstate = zk_conn.get('/domains/{}/state'.format(uuid))[0].decode('ascii') - dhypervisor = zk_conn.get('/domains/{}/hypervisor'.format(uuid))[0].decode('ascii') - dlasthypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(uuid))[0].decode('ascii') - except: - return None - - if dlasthypervisor == '': - dlasthypervisor = 'N/A' - - try: - parsed_xml = getDomainXML(zk_conn, uuid) - duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = getDomainMainDetails(parsed_xml) - except AttributeError: - click.echo('Error: Domain {} does not exist.'.format(domain)) - - if long_output == True: - dtype, darch, dmachine, dconsole, demulator = getDomainExtraDetails(parsed_xml) - dfeatures = getDomainCPUFeatures(parsed_xml) - ddisks = getDomainDisks(parsed_xml) - dnets = getDomainNetworks(parsed_xml) - dcontrollers = getDomainControllers(parsed_xml) - - # Format a nice output; do this line-by-line then concat the elements at the end - ainformation = [] - ainformation.append('{}Virtual machine information:{}'.format(ansiiprint.bold(), ansiiprint.end())) - ainformation.append('') - # Basic information - ainformation.append('{}UUID:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), duuid)) - ainformation.append('{}Name:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dname)) - ainformation.append('{}Description:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), ddescription)) - ainformation.append('{}Memory (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dmemory)) - ainformation.append('{}vCPUs:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dvcpu)) - ainformation.append('{}Topology (S/C/T):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dvcputopo)) - - if long_output == True: - # Virtualization information - ainformation.append('') - ainformation.append('{}Emulator:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), demulator)) - ainformation.append('{}Type:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dtype)) - ainformation.append('{}Arch:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), darch)) - ainformation.append('{}Machine:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dmachine)) - ainformation.append('{}Features:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), ' '.join(dfeatures))) - - # PVC cluster information - ainformation.append('') - dstate_colour = { - 'start': ansiiprint.green(), - 'restart': ansiiprint.yellow(), - 'shutdown': ansiiprint.yellow(), - 'stop': ansiiprint.red(), - 'failed': ansiiprint.red(), - 'migrate': ansiiprint.blue(), - 'unmigrate': ansiiprint.blue() - } - ainformation.append('{}State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), dstate_colour[dstate], dstate, ansiiprint.end())) - ainformation.append('{}Active Hypervisor:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dhypervisor)) - ainformation.append('{}Last Hypervisor:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dlasthypervisor)) - - if long_output == True: - # Disk list - ainformation.append('') - name_length = 0 - for disk in ddisks: - _name_length = len(disk['name']) + 1 - if _name_length > name_length: - name_length = _name_length - ainformation.append('{0}Disks:{1} {2}ID Type {3: <{width}} Dev Bus{4}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), 'Name', ansiiprint.end(), width=name_length)) - for disk in ddisks: - ainformation.append(' {0: <3} {1: <5} {2: <{width}} {3: <4} {4: <5}'.format(ddisks.index(disk), disk['type'], disk['name'], disk['dev'], disk['bus'], width=name_length)) - # Network list - ainformation.append('') - ainformation.append('{}Interfaces:{} {}ID Type Source Model MAC{}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), ansiiprint.end())) - for net in dnets: - ainformation.append(' {0: <3} {1: <8} {2: <10} {3: <8} {4}'.format(dnets.index(net), net['type'], net['source'], net['model'], net['mac'])) - # Controller list - ainformation.append('') - ainformation.append('{}Controllers:{} {}ID Type Model{}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), ansiiprint.end())) - for controller in dcontrollers: - ainformation.append(' {0: <3} {1: <14} {2: <8}'.format(dcontrollers.index(controller), controller['type'], controller['model'])) - - # Join it all together - information = '\n'.join(ainformation) - return information - - -# -# Cluster search functions -# -def getClusterDomainList(zk_conn): - # Get a list of UUIDs by listing the children of /domains - uuid_list = zk_conn.get_children('/domains') - name_list = [] - # For each UUID, get the corresponding name from the data - for uuid in uuid_list: - name_list.append(zk_conn.get('/domains/%s' % uuid)[0].decode('ascii')) - return uuid_list, name_list - -def searchClusterByUUID(zk_conn, uuid): - try: - # Get the lists - uuid_list, name_list = getClusterDomainList(zk_conn) - # We're looking for UUID, so find that element ID - index = uuid_list.index(uuid) - # Get the name_list element at that index - name = name_list[index] - except ValueError: - # We didn't find anything - return None - - return name - -def searchClusterByName(zk_conn, name): - try: - # Get the lists - uuid_list, name_list = getClusterDomainList(zk_conn) - # We're looking for name, so find that element ID - index = name_list.index(name) - # Get the uuid_list element at that index - uuid = uuid_list[index] - except ValueError: - # We didn't find anything - return None - - return uuid - -def verifyNode(zk_conn, node): - # Verify node is valid - try: - zk_conn.get('/nodes/{}'.format(node)) - except: - click.echo('ERROR: No node named "{}" is present in the cluster.'.format(node)) - exit(1) - -# -# Find a migration target -# -def findTargetHypervisor(zk_conn, search_field, dom_uuid): - if search_field == 'mem': - return findTargetHypervisorMem(zk_conn, dom_uuid) - if search_field == 'load': - return findTargetHypervisorLoad(zk_conn, dom_uuid) - if search_field == 'vcpus': - return findTargetHypervisorVCPUs(zk_conn, dom_uuid) - if search_field == 'vms': - return findTargetHypervisorVMs(zk_conn, dom_uuid) - return None - -# Get the list of valid target hypervisors -def getHypervisors(zk_conn, dom_uuid): - valid_hypervisor_list = [] - full_hypervisor_list = zk_conn.get_children('/nodes') - - try: - current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') - except: - current_hypervisor = None - - for hypervisor in full_hypervisor_list: - daemon_state = zk_conn.get('/nodes/{}/daemonstate'.format(hypervisor))[0].decode('ascii') - domain_state = zk_conn.get('/nodes/{}/domainstate'.format(hypervisor))[0].decode('ascii') - - if hypervisor == current_hypervisor: - continue - - if daemon_state != 'run' or domain_state != 'ready': - continue - - valid_hypervisor_list.append(hypervisor) - - return valid_hypervisor_list - -# via free memory (relative to allocated memory) -def findTargetHypervisorMem(zk_conn, dom_uuid): - most_allocfree = 0 - target_hypervisor = None - - hypervisor_list = getHypervisors(zk_conn, dom_uuid) - for hypervisor in hypervisor_list: - memalloc = int(zk_conn.get('/nodes/{}/memalloc'.format(hypervisor))[0].decode('ascii')) - memused = int(zk_conn.get('/nodes/{}/memused'.format(hypervisor))[0].decode('ascii')) - memfree = int(zk_conn.get('/nodes/{}/memfree'.format(hypervisor))[0].decode('ascii')) - memtotal = memused + memfree - allocfree = memtotal - memalloc - - if allocfree > most_allocfree: - most_allocfree = allocfree - target_hypervisor = hypervisor - - if target_hypervisor == None: - click.echo('ERROR: Could not find a suitable target hypervisor!') - exit(1) - - return target_hypervisor - -# via load average -def findTargetHypervisorLoad(zk_conn, dom_uuid): - least_load = 9999 - target_hypervisor = None - - hypervisor_list = getHypervisors(zk_conn, dom_uuid) - for hypervisor in hypervisor_list: - load = float(zk_conn.get('/nodes/{}/cpuload'.format(hypervisor))[0].decode('ascii')) - - if load < least_load: - least_load = load - target_hypervisor = hypervisor - - return target_hypervisor - -# via total vCPUs -def findTargetHypervisorVCPUs(zk_conn, dom_uuid): - least_vcpus = 9999 - target_hypervisor = None - - hypervisor_list = getHypervisors(zk_conn, dom_uuid) - for hypervisor in hypervisor_list: - vcpus = int(zk_conn.get('/nodes/{}/vcpualloc'.format(hypervisor))[0].decode('ascii')) - - if vcpus < least_vcpus: - least_vcpus = vcpus - target_hypervisor = hypervisor - - return target_hypervisor - -# via total VMs -def findTargetHypervisorVMs(zk_conn, dom_uuid): - least_vms = 9999 - target_hypervisor = None - - hypervisor_list = getHypervisors(zk_conn, dom_uuid) - for hypervisor in hypervisor_list: - vms = int(zk_conn.get('/nodes/{}/domainscount'.format(hypervisor))[0].decode('ascii')) - - if vms < least_vms: - least_vms = vms - target_hypervisor = hypervisor - - return target_hypervisor - - -######################## -######################## -## ## -## CLICK COMPONENTS ## -## ## -######################## -######################## +import lib.common as common +import lib.node as node +import lib.vm as vm myhostname = socket.gethostname() zk_host = '' CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=120) +def cleanup(retcode, retmsg, zk_conn): + common.stopZKConnection(zk_conn) + if retcode == True: + if retmsg != '': + click.echo(retmsg) + exit(0) + else: + if retmsg != '': + click.echo(retmsg) + exit(1) + ############################################################################### # pvc node ############################################################################### @click.group(name='node', short_help='Manage a PVC hypervisor node.', context_settings=CONTEXT_SETTINGS) -def node(): +def cli_node(): """ Manage the state of a node in the PVC cluster. """ @@ -532,34 +65,14 @@ def node(): @click.argument( 'node', default=myhostname ) -def flush_host(node, wait): +def node_flush(node, wait): """ Take NODE out of active service and migrate away all VMs. If unspecified, defaults to this host. """ - - # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Verify node is valid - verifyNode(zk_conn, node) - - click.echo('Flushing hypervisor {} of running VMs.'.format(node)) - - # Add the new domain to Zookeeper - transaction = zk_conn.transaction() - transaction.set_data('/nodes/{}/domainstate'.format(node), 'flush'.encode('ascii')) - results = transaction.commit() - - if wait == True: - while True: - time.sleep(1) - node_state = zk_conn.get('/nodes/{}/domainstate'.format(node))[0].decode('ascii') - if node_state == "flushed": - break - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + + zk_conn = common.startZKConnection(zk_host) + retstate, retmsg = node.flush_node(node, wait) + cleanup(retstate, retmsg, zk_conn) ############################################################################### # pvc node ready/unflush @@ -568,46 +81,27 @@ def flush_host(node, wait): @click.argument( 'node', default=myhostname ) -def ready_host(node): +def node_ready(node): """ Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host. """ - do_ready_host(node) + zk_conn = common.startZKConnection(zk_host) + retstate, retmsg = node.ready_node(zk_conn, node) + cleanup(retstate, retcode, zk_conn) @click.command(name='unflush', short_help='Restore node to service.') @click.argument( 'node', default=myhostname ) -def unflush_host(node): +def node_unflush(node): """ Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host. """ - do_ready_host(node) - - -def do_ready_host(node): - """ - Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host. - """ - - # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Verify node is valid - verifyNode(zk_conn, node) - - click.echo('Restoring hypervisor {} to active service.'.format(node)) - - # Add the new domain to Zookeeper - transaction = zk_conn.transaction() - transaction.set_data('/nodes/{}/domainstate'.format(node), 'unflush'.encode('ascii')) - results = transaction.commit() - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retstate, retmsg = node.ready_node(zk_conn, node) + cleanup(retstate, retcode, zk_conn) ############################################################################### # pvc node info @@ -625,33 +119,9 @@ def node_info(node, long_output): Show information about node NODE. If unspecified, defaults to this host. """ - # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Verify node is valid - verifyNode(zk_conn, node) - - # Get information about node in a pretty format - information = getInformationFromNode(zk_conn, node, long_output) - - if information == None: - click.echo('ERROR: Could not find a node matching that name.') - stopZKConnection(zk_conn) - exit(1) - - click.echo(information) - - if long_output == True: - click.echo('') - click.echo('{}Virtual machines on node:{}'.format(ansiiprint.bold(), ansiiprint.end())) - # List all VMs on this node - get_vm_list(node, None) - - click.echo('') - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = node.get_info(node, long_output) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc node list @@ -665,151 +135,20 @@ def node_list(limit): List all hypervisor nodes in the cluster; optionally only match names matching regex LIMIT. """ - # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Match our limit - node_list = [] - full_node_list = zk_conn.get_children('/nodes') - for node in full_node_list: - if limit != None: - try: - if re.match(limit, node) == None: - continue - except Exception as e: - click.echo('Regex Error: {}'.format(e)) - exit(1) - node_list.append(node) - - node_list_output = [] - node_daemon_state = {} - node_daemon_state = {} - node_domain_state = {} - node_cpu_count = {} - node_mem_used = {} - node_mem_free = {} - node_mem_total = {} - node_domains_count = {} - node_running_domains = {} - node_mem_allocated = {} - node_load = {} - - # Gather information for printing - for node_name in node_list: - node_daemon_state[node_name] = zk_conn.get('/nodes/{}/daemonstate'.format(node_name))[0].decode('ascii') - node_domain_state[node_name] = zk_conn.get('/nodes/{}/domainstate'.format(node_name))[0].decode('ascii') - node_cpu_count[node_name] = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[0] - node_mem_used[node_name] = zk_conn.get('/nodes/{}/memused'.format(node_name))[0].decode('ascii') - node_mem_free[node_name] = zk_conn.get('/nodes/{}/memfree'.format(node_name))[0].decode('ascii') - node_mem_total[node_name] = int(node_mem_used[node_name]) + int(node_mem_free[node_name]) - node_load[node_name] = zk_conn.get('/nodes/{}/cpuload'.format(node_name))[0].decode('ascii') - node_domains_count[node_name] = zk_conn.get('/nodes/{}/domainscount'.format(node_name))[0].decode('ascii') - node_running_domains[node_name] = zk_conn.get('/nodes/{}/runningdomains'.format(node_name))[0].decode('ascii').split() - node_mem_allocated[node_name] = 0 - for domain in node_running_domains[node_name]: - try: - parsed_xml = getDomainXML(zk_conn, domain) - duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = getDomainMainDetails(parsed_xml) - node_mem_allocated[node_name] += int(dmemory) - except AttributeError: - click.echo('Error: Domain {} does not exist.'.format(domain)) - - # Determine optimal column widths - # Dynamic columns: node_name, hypervisor, migrated - node_name_length = 0 - for node_name in node_list: - # node_name column - _node_name_length = len(node_name) + 1 - if _node_name_length > node_name_length: - node_name_length = _node_name_length - - # Format the string (header) - node_list_output.append( - '{bold}{node_name: <{node_name_length}} \ -State: {daemon_state_colour}{node_daemon_state: <7}{end_colour} {domain_state_colour}{node_domain_state: <8}{end_colour} \ -Resources: {node_domains_count: <4} {node_cpu_count: <5} {node_load: <6} \ -RAM (MiB): {node_mem_total: <6} {node_mem_used: <6} {node_mem_free: <6} {node_mem_allocated: <6}{end_bold}'.format( - node_name_length=node_name_length, - bold=ansiiprint.bold(), - end_bold=ansiiprint.end(), - daemon_state_colour='', - domain_state_colour='', - end_colour='', - node_name='Name', - node_daemon_state='Daemon', - node_domain_state='Domains', - node_domains_count='VMs', - node_cpu_count='CPUs', - node_load='Load', - node_mem_total='Total', - node_mem_used='Used', - node_mem_free='Free', - node_mem_allocated='VMs', - ) - ) - - # Format the string (elements) - for node_name in node_list: - if node_daemon_state[node_name] == 'run': - daemon_state_colour = ansiiprint.green() - elif node_daemon_state[node_name] == 'stop': - daemon_state_colour = ansiiprint.red() - elif node_daemon_state[node_name] == 'init': - daemon_state_colour = ansiiprint.yellow() - elif node_daemon_state[node_name] == 'dead': - daemon_state_colour = ansiiprint.red() + ansiiprint.bold() - else: - daemon_state_colour = ansiiprint.blue() - - if node_mem_allocated[node_name] >= node_mem_total[node_name]: - node_domain_state[node_name] = 'overprov' - domain_state_colour = ansiiprint.yellow() - elif node_domain_state[node_name] == 'ready': - domain_state_colour = ansiiprint.green() - else: - domain_state_colour = ansiiprint.blue() - - node_list_output.append( - '{bold}{node_name: <{node_name_length}} \ - {daemon_state_colour}{node_daemon_state: <7}{end_colour} {domain_state_colour}{node_domain_state: <8}{end_colour} \ - {node_domains_count: <4} {node_cpu_count: <5} {node_load: <6} \ - {node_mem_total: <6} {node_mem_used: <6} {node_mem_free: <6} {node_mem_allocated: <6}{end_bold}'.format( - node_name_length=node_name_length, - bold='', - end_bold='', - daemon_state_colour=daemon_state_colour, - domain_state_colour=domain_state_colour, - end_colour=ansiiprint.end(), - node_name=node_name, - node_daemon_state=node_daemon_state[node_name], - node_domain_state=node_domain_state[node_name], - node_domains_count=node_domains_count[node_name], - node_cpu_count=node_cpu_count[node_name], - node_load=node_load[node_name], - node_mem_total=node_mem_total[node_name], - node_mem_used=node_mem_used[node_name], - node_mem_free=node_mem_free[node_name], - node_mem_allocated=node_mem_allocated[node_name] - ) - ) - - click.echo('\n'.join(sorted(node_list_output))) - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = node.get_list(zk_conn, limit) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm ############################################################################### @click.group(name='vm', short_help='Manage a PVC virtual machine.', context_settings=CONTEXT_SETTINGS) -def vm(): +def cli_vm(): """ Manage the state of a virtual machine in the PVC cluster. """ pass - ############################################################################### # pvc vm define ############################################################################### @@ -826,43 +165,18 @@ def vm(): @click.argument( 'config', type=click.File() ) -def define_vm(config, target_hypervisor, selector): +def vm_define(config, target_hypervisor, selector): """ Define a new virtual machine from Libvirt XML configuration file CONFIG. """ # Open the XML file - data = config.read() + config_data = config.read() config.close() - # Parse the XML data - parsed_xml = lxml.objectify.fromstring(data) - dom_uuid = parsed_xml.uuid.text - dom_name = parsed_xml.name.text - click.echo('Adding new VM with Name "{}" and UUID "{}" to database.'.format(dom_name, dom_uuid)) - - # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - if target_hypervisor == None: - target_hypervisor = findTargetHypervisor(zk_conn, selector, dom_uuid) - - # Verify node is valid - verifyNode(zk_conn, target_hypervisor) - - # Add the new domain to Zookeeper - transaction = zk_conn.transaction() - transaction.create('/domains/{}'.format(dom_uuid), dom_name.encode('ascii')) - transaction.create('/domains/{}/state'.format(dom_uuid), 'stop'.encode('ascii')) - transaction.create('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) - transaction.create('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) - transaction.create('/domains/{}/failedreason'.format(dom_uuid), ''.encode('ascii')) - transaction.create('/domains/{}/xml'.format(dom_uuid), data.encode('ascii')) - results = transaction.commit() - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.define_vm(zk_conn, config_data, target_hypervisor, selector) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm modify @@ -882,33 +196,21 @@ def define_vm(config, target_hypervisor, selector): @click.argument( 'config', type=click.File(), default=None, required=False ) -def modify_vm(domain, config, editor, restart): +def vm_modify(domain, config, editor, restart): """ Modify existing virtual machine DOMAIN, either in-editor or with replacement CONFIG. DOMAIN may be a UUID or name. """ if editor == False and config == None: - click.echo('Either an XML config file or the "--editor" option must be specified.') - exit(1) + cleanup(False, 'Either an XML config file or the "--editor" option must be specified.') - # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) + zk_conn = common.startZKConnection(zk_host) - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - exit(1) - - # We're operating in edit-in-place mode if editor == True: + dom_uuid = vm.getDomainUUID(zk_conn, domain) + if dom_uuid == None: + cleanup(False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + # Grab the current config current_vm_config = zk_conn.get('/domains/{}/xml'.format(dom_uuid))[0].decode('ascii') @@ -961,17 +263,8 @@ def modify_vm(domain, config, editor, restart): click.echo('Replacing config of VM "{}".'.format(dom_name, config)) - # Add the modified config to Zookeeper - transaction = zk_conn.transaction() - transaction.set_data('/domains/{}'.format(dom_uuid), dom_name.encode('ascii')) - transaction.set_data('/domains/{}/xml'.format(dom_uuid), new_vm_config.encode('ascii')) - if restart == True: - transaction.set_data('/domains/{}/state'.format(dom_uuid), 'restart'.encode('ascii')) - results = transaction.commit() - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + retcode, retmsg = vm.modify_vm(zk_conn, domain, restart) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm undefine @@ -980,7 +273,7 @@ def modify_vm(domain, config, editor, restart): @click.argument( 'domain' ) -def undefine_vm(domain): +def vm_undefine(domain): """ Stop virtual machine DOMAIN and remove it from the cluster database. DOMAIN may be a UUID or name. """ @@ -991,57 +284,9 @@ def undefine_vm(domain): exit(1) # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - # Shut down the VM - try: - current_vm_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') - if current_vm_state != 'stop': - click.echo('Forcibly stopping VM "{}".'.format(dom_uuid)) - # Set the domain into stop mode - transaction = zk_conn.transaction() - transaction.set_data('/domains/{}/state'.format(dom_uuid), 'stop'.encode('ascii')) - transaction.commit() - - # Wait for 3 seconds to allow state to flow to all hypervisors - click.echo('Waiting for cluster to update.') - time.sleep(1) - except: - pass - - # Gracefully terminate the class instances - try: - click.echo('Deleting VM "{}" from nodes.'.format(dom_uuid)) - zk_conn.set('/domains/{}/state'.format(dom_uuid), 'delete'.encode('ascii')) - time.sleep(5) - except: - pass - - # Delete the configurations - try: - click.echo('Undefining VM "{}".'.format(dom_uuid)) - transaction = zk_conn.transaction() - transaction.delete('/domains/{}/state'.format(dom_uuid)) - transaction.delete('/domains/{}/hypervisor'.format(dom_uuid)) - transaction.delete('/domains/{}/lasthypervisor'.format(dom_uuid)) - transaction.delete('/domains/{}/failedreason'.format(dom_uuid)) - transaction.delete('/domains/{}/xml'.format(dom_uuid)) - transaction.delete('/domains/{}'.format(dom_uuid)) - transaction.commit() - except: - pass - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.undefine_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm start @@ -1050,34 +295,15 @@ def undefine_vm(domain): @click.argument( 'domain' ) -def start_vm(domain): +def vm_start(domain): """ Start virtual machine DOMAIN on its configured hypervisor. DOMAIN may be a UUID or name. """ # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - exit(1) - - # Set the VM to start - click.echo('Starting VM "{}".'.format(dom_uuid)) - zk_conn.set('/domains/%s/state' % dom_uuid, 'start'.encode('ascii')) - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.start_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm restart @@ -1086,41 +312,15 @@ def start_vm(domain): @click.argument( 'domain' ) -def restart_vm(domain): +def vm_restart(domain): """ Restart running virtual machine DOMAIN. DOMAIN may be a UUID or name. """ # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - exit(1) - - # Get state and verify we're OK to proceed - current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') - if current_state != 'start': - click.echo('ERROR: VM "{}" is not in "start" state!'.format(dom_uuid)) - stopZKConnection(zk_conn) - exit(1) - - # Set the VM to start - click.echo('Restarting VM "{}".'.format(dom_uuid)) - zk_conn.set('/domains/%s/state' % dom_uuid, 'restart'.encode('ascii')) - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.restart_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm shutdown @@ -1129,41 +329,15 @@ def restart_vm(domain): @click.argument( 'domain' ) -def shutdown_vm(domain): +def vm_shutdown(domain): """ Gracefully shut down virtual machine DOMAIN. DOMAIN may be a UUID or name. """ # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - exit(1) - - # Get state and verify we're OK to proceed - current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') - if current_state != 'start': - click.echo('ERROR: VM "{}" is not in "start" state!'.format(dom_uuid)) - stopZKConnection(zk_conn) - exit(1) - - # Set the VM to shutdown - click.echo('Shutting down VM "{}".'.format(dom_uuid)) - zk_conn.set('/domains/%s/state' % dom_uuid, 'shutdown'.encode('ascii')) - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.shutdown_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm stop @@ -1172,41 +346,15 @@ def shutdown_vm(domain): @click.argument( 'domain' ) -def stop_vm(domain): +def vm_stop(domain): """ Forcibly halt (destroy) running virtual machine DOMAIN. DOMAIN may be a UUID or name. """ # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - exit(1) - - # Get state and verify we're OK to proceed - current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') - if current_state != 'start': - click.echo('ERROR: VM "{}" is not in "start" state!'.format(dom_uuid)) - stopZKConnection(zk_conn) - exit(1) - - # Set the VM to start - click.echo('Forcibly stopping VM "{}".'.format(dom_uuid)) - zk_conn.set('/domains/%s/state' % dom_uuid, 'stop'.encode('ascii')) - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.stop_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm move @@ -1224,58 +372,15 @@ def stop_vm(domain): type=click.Choice(['mem','load','vcpus','vms']), help='Method to determine optimal target hypervisor during autodetect.' ) -def move_vm(domain, target_hypervisor, selector): +def vm_move(domain, target_hypervisor, selector): """ Permanently move virtual machine DOMAIN, via live migration if running and possible, to another hypervisor node. DOMAIN may be a UUID or name. """ # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - return - - current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') - - if target_hypervisor == None: - target_hypervisor = findTargetHypervisor(zk_conn, selector, dom_uuid) - else: - if target_hypervisor == current_hypervisor: - click.echo('ERROR: VM "{}" is already running on hypervisor "{}".'.format(dom_uuid, current_hypervisor)) - stopZKConnection(zk_conn) - exit(1) - - # Verify node is valid - verifyNode(zk_conn, target_hypervisor) - - current_vm_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') - if current_vm_state == 'start': - click.echo('Permanently migrating VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) - transaction = zk_conn.transaction() - transaction.set_data('/domains/{}/state'.format(dom_uuid), 'migrate'.encode('ascii')) - transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) - transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) - transaction.commit() - else: - click.echo('Permanently moving VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) - transaction = zk_conn.transaction() - transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) - transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) - transaction.commit() - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.move_vm(zk_conn, domain, target_hypervisor, selector) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm migrate @@ -1297,66 +402,15 @@ def move_vm(domain, target_hypervisor, selector): '-f', '--force', 'force_migrate', is_flag=True, default=False, help='Force migrate an already migrated VM.' ) -def migrate_vm(domain, target_hypervisor, selector, force_migrate): +def vm_migrate(domain, target_hypervisor, selector, force_migrate): """ Temporarily migrate running virtual machine DOMAIN, via live migration if possible, to another hypervisor node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node. """ # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - exit(1) - - # Get state and verify we're OK to proceed - current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') - if current_state != 'start': - target_state = 'start' - else: - target_state = 'migrate' - - current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') - last_hypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(dom_uuid))[0].decode('ascii') - - if last_hypervisor != '' and force_migrate != True: - click.echo('ERROR: VM "{}" has been previously migrated.'.format(dom_uuid)) - click.echo('> Last hypervisor: {}'.format(last_hypervisor)) - click.echo('> Current hypervisor: {}'.format(current_hypervisor)) - click.echo('Run `vm unmigrate` to restore the VM to its previous hypervisor, or use `--force` to override this check.') - stopZKConnection(zk_conn) - exit(1) - - if target_hypervisor == None: - target_hypervisor = findTargetHypervisor(zk_conn, selector, dom_uuid) - else: - if target_hypervisor == current_hypervisor: - click.echo('ERROR: VM "{}" is already running on hypervisor "{}".'.format(dom_uuid, current_hypervisor)) - stopZKConnection(zk_conn) - exit(1) - - # Verify node is valid - verifyNode(zk_conn, target_hypervisor) - - click.echo('Migrating VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) - transaction = zk_conn.transaction() - transaction.set_data('/domains/{}/state'.format(dom_uuid), target_state.encode('ascii')) - transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) - transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), current_hypervisor.encode('ascii')) - transaction.commit() - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.migrate_vm(zk_conn, domain, target_hypervisor, selector, force_migrate) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm unmigrate @@ -1365,51 +419,15 @@ def migrate_vm(domain, target_hypervisor, selector, force_migrate): @click.argument( 'domain' ) -def unmigrate_vm(domain): +def vm_unmigrate(domain): """ Restore previously migrated virtual machine DOMAIN, via live migration if possible, to its original hypervisor node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node. """ # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - exit(1) - - # Get state and verify we're OK to proceed - current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') - if current_state != 'start': - target_state = 'start' - else: - target_state = 'migrate' - - target_hypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(dom_uuid))[0].decode('ascii') - - if target_hypervisor == '': - click.echo('ERROR: VM "{}" has not been previously migrated.'.format(dom_uuid)) - stopZKConnection(zk_conn) - exit(1) - - click.echo('Unmigrating VM "{}" back to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) - transaction = zk_conn.transaction() - transaction.set_data('/domains/{}/state'.format(dom_uuid), target_state.encode('ascii')) - transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) - transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) - transaction.commit() - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.unmigrate_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm info @@ -1428,36 +446,9 @@ def vm_info(domain, long_output): """ # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - # Validate and obtain alternate passed value - if validateUUID(domain): - dom_name = searchClusterByUUID(zk_conn, domain) - dom_uuid = searchClusterByName(zk_conn, dom_name) - else: - dom_uuid = searchClusterByName(zk_conn, domain) - dom_name = searchClusterByUUID(zk_conn, dom_uuid) - - if dom_uuid == None: - click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) - stopZKConnection(zk_conn) - exit(1) - - # Gather information from XML config and print it - information = getInformationFromXML(zk_conn, dom_uuid, long_output) - click.echo(information) - - # Get a failure reason if applicable - failedreason = zk_conn.get('/domains/{}/failedreason'.format(dom_uuid))[0].decode('ascii') - if failedreason != '': - click.echo('') - click.echo('{}Failure reason:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), failedreason)) - - click.echo('') - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.get_info(zk_conn, domain, long_output) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc vm list @@ -1475,155 +466,9 @@ def vm_list(hypervisor, limit): List all virtual machines in the cluster; optionally only match names matching regex LIMIT. """ - get_vm_list(hypervisor, limit) - -# Wrapped function to allow calling from `node info` -def get_vm_list(hypervisor, limit): - """ - List all virtual machines in the cluster. - """ - - # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) - - if hypervisor != None: - # Verify node is valid - verifyNode(zk_conn, hypervisor) - - vm_list_raw = zk_conn.get_children('/domains') - vm_list = [] - vm_list_output = [] - - vm_hypervisor = {} - vm_state = {} - vm_migrated = {} - vm_uuid = {} - vm_name = {} - vm_description = {} - vm_memory = {} - vm_vcpu = {} - - # If we're limited, remove other nodes' VMs - for vm in vm_list_raw: - # Check we don't match the limit - name = zk_conn.get('/domains/{}'.format(vm))[0].decode('ascii') - if limit != None: - try: - if re.match(limit, name) == None: - continue - except Exception as e: - click.echo('Regex Error: {}'.format(e)) - exit(1) - # Check hypervisor to avoid unneeded ZK calls - vm_hypervisor[vm] = zk_conn.get('/domains/{}/hypervisor'.format(vm))[0].decode('ascii') - if hypervisor == None: - vm_list.append(vm) - else: - if vm_hypervisor[vm] == hypervisor: - vm_list.append(vm) - - # Gather information for printing - for vm in vm_list: - vm_state[vm] = zk_conn.get('/domains/{}/state'.format(vm))[0].decode('ascii') - vm_lasthypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(vm))[0].decode('ascii') - if vm_lasthypervisor != '': - vm_migrated[vm] = 'from {}'.format(vm_lasthypervisor) - else: - vm_migrated[vm] = 'no' - - try: - vm_xml = getDomainXML(zk_conn, vm) - vm_uuid[vm], vm_name[vm], vm_description[vm], vm_memory[vm], vm_vcpu[vm], vm_vcputopo = getDomainMainDetails(vm_xml) - except AttributeError: - click.echo('Error: Domain {} does not exist.'.format(domain)) - - # Determine optimal column widths - # Dynamic columns: node_name, hypervisor, migrated - vm_name_length = 0 - vm_hypervisor_length = 0 - vm_migrated_length = 0 - for vm in vm_list: - # vm_name column - _vm_name_length = len(vm_name[vm]) + 1 - if _vm_name_length > vm_name_length: - vm_name_length = _vm_name_length - # vm_hypervisor column - _vm_hypervisor_length = len(vm_hypervisor[vm]) + 1 - if _vm_hypervisor_length > vm_hypervisor_length: - vm_hypervisor_length = _vm_hypervisor_length - # vm_migrated column - _vm_migrated_length = len(vm_migrated[vm]) + 1 - if _vm_migrated_length > vm_migrated_length: - vm_migrated_length = _vm_migrated_length - - # Format the string (header) - vm_list_header = ansiiprint.bold() + 'Name UUID State RAM [MiB] vCPUs Hypervisor Migrated?' + ansiiprint.end() - vm_list_output.append( - '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <37} \ -{vm_state_colour}{vm_state: <8}{end_colour} \ -{vm_memory: <10} {vm_vcpu: <6} \ -{vm_hypervisor: <{vm_hypervisor_length}} \ -{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( - vm_name_length=vm_name_length, - vm_hypervisor_length=vm_hypervisor_length, - vm_migrated_length=vm_migrated_length, - bold=ansiiprint.bold(), - end_bold=ansiiprint.end(), - vm_state_colour='', - end_colour='', - vm_name='Name', - vm_uuid='UUID', - vm_state='State', - vm_memory='RAM (MiB)', - vm_vcpu='vCPUs', - vm_hypervisor='Hypervisor', - vm_migrated='Migrated' - ) - ) - - # Format the string (elements) - for vm in vm_list: - if vm_state[vm] == 'start': - vm_state_colour = ansiiprint.green() - elif vm_state[vm] == 'restart': - vm_state_colour = ansiiprint.yellow() - elif vm_state[vm] == 'shutdown': - vm_state_colour = ansiiprint.yellow() - elif vm_state[vm] == 'stop': - vm_state_colour = ansiiprint.red() - elif vm_state[vm] == 'failed': - vm_state_colour = ansiiprint.red() - else: - vm_state_colour = ansiiprint.blue() - - vm_list_output.append( - '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <37} \ -{vm_state_colour}{vm_state: <8}{end_colour} \ -{vm_memory: <10} {vm_vcpu: <6} \ -{vm_hypervisor: <{vm_hypervisor_length}} \ -{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( - vm_name_length=vm_name_length, - vm_hypervisor_length=vm_hypervisor_length, - vm_migrated_length=vm_migrated_length, - bold='', - end_bold='', - vm_state_colour=vm_state_colour, - end_colour=ansiiprint.end(), - vm_name=vm_name[vm], - vm_uuid=vm_uuid[vm], - vm_state=vm_state[vm], - vm_memory=vm_memory[vm], - vm_vcpu=vm_vcpu[vm], - vm_hypervisor=vm_hypervisor[vm], - vm_migrated=vm_migrated[vm] - ) - ) - - click.echo('\n'.join(sorted(vm_list_output))) - - # Close the Zookeeper connection - stopZKConnection(zk_conn) - + zk_conn = common.startZKConnection(zk_host) + retcode, retmsg = vm.get_list(zk_conn, hypervisor, limit) + cleanup(retcode, retmsg, zk_conn) ############################################################################### # pvc init @@ -1642,7 +487,7 @@ def init_cluster(): click.echo('Initializing a new cluster with Zookeeper address "{}".'.format(zk_host)) # Open a Zookeeper connection - zk_conn = startZKConnection(zk_host) + zk_conn = common.startZKConnection(zk_host) # Destroy the existing data try: @@ -1659,7 +504,7 @@ def init_cluster(): transaction.commit() # Close the Zookeeper connection - stopZKConnection(zk_conn) + common.stopZKConnection(zk_conn) click.echo('Successfully initialized new cluster. Any running PVC daemons will need to be restarted.') @@ -1688,27 +533,27 @@ def cli(_zk_host): # # Click command tree # -node.add_command(flush_host) -node.add_command(ready_host) -node.add_command(unflush_host) -node.add_command(node_info) -node.add_command(node_list) +cli_node.add_command(node_flush) +cli_node.add_command(node_ready) +cli_node.add_command(node_unflush) +cli_node.add_command(node_info) +cli_node.add_command(node_list) -vm.add_command(define_vm) -vm.add_command(modify_vm) -vm.add_command(undefine_vm) -vm.add_command(start_vm) -vm.add_command(restart_vm) -vm.add_command(shutdown_vm) -vm.add_command(stop_vm) -vm.add_command(move_vm) -vm.add_command(migrate_vm) -vm.add_command(unmigrate_vm) -vm.add_command(vm_info) -vm.add_command(vm_list) +cli_vm.add_command(vm_define) +cli_vm.add_command(vm_modify) +cli_vm.add_command(vm_undefine) +cli_vm.add_command(vm_start) +cli_vm.add_command(vm_restart) +cli_vm.add_command(vm_shutdown) +cli_vm.add_command(vm_stop) +cli_vm.add_command(vm_move) +cli_vm.add_command(vm_migrate) +cli_vm.add_command(vm_unmigrate) +cli_vm.add_command(vm_info) +cli_vm.add_command(vm_list) -cli.add_command(node) -cli.add_command(vm) +cli.add_command(cli_node) +cli.add_command(cli_vm) cli.add_command(init_cluster) # diff --git a/client-common/lib/ansiiprint.py b/client-common/lib/ansiiprint.py new file mode 100644 index 00000000..69cc1b0e --- /dev/null +++ b/client-common/lib/ansiiprint.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +# ansiprint.py - Printing function for formatted messages +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import datetime + +# ANSII colours for output +def red(): + return '\033[91m' +def blue(): + return '\033[94m' +def green(): + return '\033[92m' +def yellow(): + return '\033[93m' +def purple(): + return '\033[95m' +def bold(): + return '\033[1m' +def end(): + return '\033[0m' + +# Print function +def echo(message, prefix, state): + # Get the date + date = '{} - '.format(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S.%f')) + endc = end() + + # Continuation + if state == 'c': + date = '' + colour = '' + prompt = ' ' + # OK + elif state == 'o': + colour = green() + prompt = '>>> ' + # Error + elif state == 'e': + colour = red() + prompt = '>>> ' + # Warning + elif state == 'w': + colour = yellow() + prompt = '>>> ' + # Tick + elif state == 't': + colour = purple() + prompt = '>>> ' + # Information + elif state == 'i': + colour = blue() + prompt = '>>> ' + else: + colour = bold() + prompt = '>>> ' + + # Append space to prefix + if prefix != '': + prefix = prefix + ' ' + + print(colour + prompt + endc + date + prefix + message) diff --git a/client-common/lib/common.py b/client-common/lib/common.py new file mode 100644 index 00000000..635ddbdb --- /dev/null +++ b/client-common/lib/common.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 + +# common.py - PVC client function library, common fuctions +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import uuid +import lxml +import kazoo.client + +############################################################################### +# Supplemental functions +############################################################################### + +# +# Validate a UUID +# +def validateUUID(dom_uuid): + try: + uuid.UUID(dom_uuid) + return True + except: + return False + + +# +# Connect and disconnect from Zookeeper +# +def startZKConnection(zk_host): + zk_conn = kazoo.client.KazooClient(hosts=zk_host) + zk_conn.start() + return zk_conn + +def stopZKConnection(zk_conn): + zk_conn.stop() + zk_conn.close() + return 0 +# +# Parse a Domain XML object +# +def getDomainXML(zk_conn, dom_uuid): + try: + xml = zk_conn.get('/domains/%s/xml' % dom_uuid)[0].decode('ascii') + except: + return None + + # Parse XML using lxml.objectify + parsed_xml = lxml.objectify.fromstring(xml) + return parsed_xml + +# +# Get the main details for a VM object from XML +# +def getDomainMainDetails(parsed_xml): + # Get the information we want from it + duuid = str(parsed_xml.uuid) + try: + ddescription = str(parsed_xml.description) + except AttributeError: + ddescription = "N/A" + dname = str(parsed_xml.name) + dmemory = str(parsed_xml.memory) + dmemory_unit = str(parsed_xml.memory.attrib['unit']) + if dmemory_unit == 'KiB': + dmemory = str(int(dmemory) * 1024) + elif dmemory_unit == 'GiB': + dmemory = str(int(dmemory) / 1024) + dvcpu = str(parsed_xml.vcpu) + try: + dvcputopo = '{}/{}/{}'.format(parsed_xml.cpu.topology.attrib['sockets'], parsed_xml.cpu.topology.attrib['cores'], parsed_xml.cpu.topology.attrib['threads']) + except: + dvcputopo = 'N/A' + + return duuid, dname, ddescription, dmemory, dvcpu, dvcputopo + +# +# Get long-format details +# +def getDomainExtraDetails(parsed_xml): + dtype = parsed_xml.os.type + darch = parsed_xml.os.type.attrib['arch'] + dmachine = parsed_xml.os.type.attrib['machine'] + dconsole = parsed_xml.devices.console.attrib['type'] + demulator = parsed_xml.devices.emulator + + return dtype, darch, dmachine, dconsole, demulator + +# +# Get CPU features +# +def getDomainCPUFeatures(parsed_xml): + dfeatures = [] + for feature in parsed_xml.features.getchildren(): + dfeatures.append(feature.tag) + + return dfeatures + +# +# Get disk devices +# +def getDomainDisks(parsed_xml): + ddisks = [] + for device in parsed_xml.devices.getchildren(): + if device.tag == 'disk': + disk_attrib = device.source.attrib + disk_target = device.target.attrib + disk_type = device.attrib['type'] + if disk_type == 'network': + disk_obj = { 'type': disk_attrib.get('protocol'), 'name': disk_attrib.get('name'), 'dev': disk_target.get('dev'), 'bus': disk_target.get('bus') } + elif disk_type == 'file': + disk_obj = { 'type': 'file', 'name': disk_attrib.get('file'), 'dev': disk_target.get('dev'), 'bus': disk_target.get('bus') } + else: + disk_obj = {} + ddisks.append(disk_obj) + + return ddisks + +# +# Get network devices +# +def getDomainNetworks(parsed_xml): + dnets = [] + for device in parsed_xml.devices.getchildren(): + if device.tag == 'interface': + net_type = device.attrib['type'] + net_mac = device.mac.attrib['address'] + net_bridge = device.source.attrib[net_type] + net_model = device.model.attrib['type'] + net_obj = { 'type': net_type, 'mac': net_mac, 'source': net_bridge, 'model': net_model } + dnets.append(net_obj) + + return dnets + +# +# Get controller devices +# +def getDomainControllers(parsed_xml): + dcontrollers = [] + for device in parsed_xml.devices.getchildren(): + if device.tag == 'controller': + controller_type = device.attrib['type'] + try: + controller_model = device.attrib['model'] + except KeyError: + controller_model = 'none' + controller_obj = { 'type': controller_type, 'model': controller_model } + dcontrollers.append(controller_obj) + + return dcontrollers + +# +# Verify node is valid in cluster +# +def verifyNode(zk_conn, node): + try: + zk_conn.get('/nodes/{}'.format(node)) + except: + click.echo('ERROR: No node named "{}" is present in the cluster.'.format(node)) + exit(1) + +# +# Find a migration target +# +def findTargetHypervisor(zk_conn, search_field, dom_uuid): + if search_field == 'mem': + return findTargetHypervisorMem(zk_conn, dom_uuid) + if search_field == 'load': + return findTargetHypervisorLoad(zk_conn, dom_uuid) + if search_field == 'vcpus': + return findTargetHypervisorVCPUs(zk_conn, dom_uuid) + if search_field == 'vms': + return findTargetHypervisorVMs(zk_conn, dom_uuid) + return None + +# via free memory (relative to allocated memory) +def findTargetHypervisorMem(zk_conn, dom_uuid): + most_allocfree = 0 + target_hypervisor = None + + hypervisor_list = getHypervisors(zk_conn, dom_uuid) + for hypervisor in hypervisor_list: + memalloc = int(zk_conn.get('/nodes/{}/memalloc'.format(hypervisor))[0].decode('ascii')) + memused = int(zk_conn.get('/nodes/{}/memused'.format(hypervisor))[0].decode('ascii')) + memfree = int(zk_conn.get('/nodes/{}/memfree'.format(hypervisor))[0].decode('ascii')) + memtotal = memused + memfree + allocfree = memtotal - memalloc + + if allocfree > most_allocfree: + most_allocfree = allocfree + target_hypervisor = hypervisor + + return target_hypervisor + +# via load average +def findTargetHypervisorLoad(zk_conn, dom_uuid): + least_load = 9999 + target_hypervisor = None + + hypervisor_list = getHypervisors(zk_conn, dom_uuid) + for hypervisor in hypervisor_list: + load = float(zk_conn.get('/nodes/{}/cpuload'.format(hypervisor))[0].decode('ascii')) + + if load < least_load: + least_load = load + target_hypervisor = hypervisor + + return target_hypervisor + +# via total vCPUs +def findTargetHypervisorVCPUs(zk_conn, dom_uuid): + least_vcpus = 9999 + target_hypervisor = None + + hypervisor_list = getHypervisors(zk_conn, dom_uuid) + for hypervisor in hypervisor_list: + vcpus = int(zk_conn.get('/nodes/{}/vcpualloc'.format(hypervisor))[0].decode('ascii')) + + if vcpus < least_vcpus: + least_vcpus = vcpus + target_hypervisor = hypervisor + + return target_hypervisor + +# via total VMs +def findTargetHypervisorVMs(zk_conn, dom_uuid): + least_vms = 9999 + target_hypervisor = None + + hypervisor_list = getHypervisors(zk_conn, dom_uuid) + for hypervisor in hypervisor_list: + vms = int(zk_conn.get('/nodes/{}/domainscount'.format(hypervisor))[0].decode('ascii')) + + if vms < least_vms: + least_vms = vms + target_hypervisor = hypervisor + + return target_hypervisor + diff --git a/client-common/lib/network.py b/client-common/lib/network.py new file mode 100644 index 00000000..188b16d2 --- /dev/null +++ b/client-common/lib/network.py @@ -0,0 +1,1717 @@ +#!/usr/bin/env python3 + +# pvcd.py - PVC client command-line interface +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import os +import socket +import time +import uuid +import re +import tempfile +import subprocess +import difflib +import colorama +import click +import lxml.objectify +import configparser +import kazoo.client + +############################################################################### +# Supplemental functions +############################################################################### + +# +# ansiiprint output +# + +class ansiiprint: + # ANSII colours for output + def red(): + return '\033[91m' + def blue(): + return '\033[94m' + def green(): + return '\033[92m' + def yellow(): + return '\033[93m' + def purple(): + return '\033[95m' + def bold(): + return '\033[1m' + def end(): + return '\033[0m' + + +# +# Validate a UUID +# +def validateUUID(dom_uuid): + try: + uuid.UUID(dom_uuid) + return True + except: + return False + + +# +# Connect and disconnect from Zookeeper +# +def startZKConnection(zk_host): + zk_conn = kazoo.client.KazooClient(hosts=zk_host) + zk_conn.start() + return zk_conn + +def stopZKConnection(zk_conn): + zk_conn.stop() + zk_conn.close() + return 0 + + +# +# XML information parsing functions +# + +# Get the main details for a VM object from XML +def getDomainMainDetails(parsed_xml): + # Get the information we want from it + duuid = str(parsed_xml.uuid) + try: + ddescription = str(parsed_xml.description) + except AttributeError: + ddescription = "N/A" + dname = str(parsed_xml.name) + dmemory = str(parsed_xml.memory) + dmemory_unit = str(parsed_xml.memory.attrib['unit']) + if dmemory_unit == 'KiB': + dmemory = str(int(dmemory) * 1024) + elif dmemory_unit == 'GiB': + dmemory = str(int(dmemory) / 1024) + dvcpu = str(parsed_xml.vcpu) + try: + dvcputopo = '{}/{}/{}'.format(parsed_xml.cpu.topology.attrib['sockets'], parsed_xml.cpu.topology.attrib['cores'], parsed_xml.cpu.topology.attrib['threads']) + except: + dvcputopo = 'N/A' + + return duuid, dname, ddescription, dmemory, dvcpu, dvcputopo + +# Get long-format details +def getDomainExtraDetails(parsed_xml): + dtype = parsed_xml.os.type + darch = parsed_xml.os.type.attrib['arch'] + dmachine = parsed_xml.os.type.attrib['machine'] + dconsole = parsed_xml.devices.console.attrib['type'] + demulator = parsed_xml.devices.emulator + + return dtype, darch, dmachine, dconsole, demulator + +# Get CPU features +def getDomainCPUFeatures(parsed_xml): + dfeatures = [] + for feature in parsed_xml.features.getchildren(): + dfeatures.append(feature.tag) + + return dfeatures + +# Get disk devices +def getDomainDisks(parsed_xml): + ddisks = [] + for device in parsed_xml.devices.getchildren(): + if device.tag == 'disk': + disk_attrib = device.source.attrib + disk_target = device.target.attrib + disk_type = device.attrib['type'] + if disk_type == 'network': + disk_obj = { 'type': disk_attrib.get('protocol'), 'name': disk_attrib.get('name'), 'dev': disk_target.get('dev'), 'bus': disk_target.get('bus') } + elif disk_type == 'file': + disk_obj = { 'type': 'file', 'name': disk_attrib.get('file'), 'dev': disk_target.get('dev'), 'bus': disk_target.get('bus') } + else: + disk_obj = {} + ddisks.append(disk_obj) + + return ddisks + +# Get network devices +def getDomainNetworks(parsed_xml): + dnets = [] + for device in parsed_xml.devices.getchildren(): + if device.tag == 'interface': + net_type = device.attrib['type'] + net_mac = device.mac.attrib['address'] + net_bridge = device.source.attrib[net_type] + net_model = device.model.attrib['type'] + net_obj = { 'type': net_type, 'mac': net_mac, 'source': net_bridge, 'model': net_model } + dnets.append(net_obj) + + return dnets + +# Get controller devices +def getDomainControllers(parsed_xml): + dcontrollers = [] + for device in parsed_xml.devices.getchildren(): + if device.tag == 'controller': + controller_type = device.attrib['type'] + try: + controller_model = device.attrib['model'] + except KeyError: + controller_model = 'none' + controller_obj = { 'type': controller_type, 'model': controller_model } + dcontrollers.append(controller_obj) + + return dcontrollers + +# Parse an XML object +def getDomainXML(zk_conn, dom_uuid): + try: + xml = zk_conn.get('/domains/%s/xml' % dom_uuid)[0].decode('ascii') + except: + return None + + # Parse XML using lxml.objectify + parsed_xml = lxml.objectify.fromstring(xml) + return parsed_xml + +# Root functions +def getInformationFromNode(zk_conn, node_name, long_output): + node_daemon_state = zk_conn.get('/nodes/{}/daemonstate'.format(node_name))[0].decode('ascii') + node_domain_state = zk_conn.get('/nodes/{}/domainstate'.format(node_name))[0].decode('ascii') + node_cpu_count = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[0] + node_kernel = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[1] + node_os = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[2] + node_arch = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[3] + node_mem_used = zk_conn.get('/nodes/{}/memused'.format(node_name))[0].decode('ascii') + node_mem_free = zk_conn.get('/nodes/{}/memfree'.format(node_name))[0].decode('ascii') + node_mem_total = int(node_mem_used) + int(node_mem_free) + node_load = zk_conn.get('/nodes/{}/cpuload'.format(node_name))[0].decode('ascii') + node_domains_count = zk_conn.get('/nodes/{}/domainscount'.format(node_name))[0].decode('ascii') + node_running_domains = zk_conn.get('/nodes/{}/runningdomains'.format(node_name))[0].decode('ascii').split() + node_mem_allocated = 0 + for domain in node_running_domains: + try: + parsed_xml = getDomainXML(zk_conn, domain) + duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = getDomainMainDetails(parsed_xml) + node_mem_allocated += int(dmemory) + except AttributeError: + click.echo('Error: Domain {} does not exist.'.format(domain)) + + if node_daemon_state == 'run': + daemon_state_colour = ansiiprint.green() + elif node_daemon_state == 'stop': + daemon_state_colour = ansiiprint.red() + elif node_daemon_state == 'init': + daemon_state_colour = ansiiprint.yellow() + elif node_daemon_state == 'dead': + daemon_state_colour = ansiiprint.red() + ansiiprint.bold() + else: + daemon_state_colour = ansiiprint.blue() + + if node_domain_state == 'ready': + domain_state_colour = ansiiprint.green() + else: + domain_state_colour = ansiiprint.blue() + + # Format a nice output; do this line-by-line then concat the elements at the end + ainformation = [] + ainformation.append('{}Hypervisor Node information:{}'.format(ansiiprint.bold(), ansiiprint.end())) + ainformation.append('') + # Basic information + ainformation.append('{}Name:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_name)) + ainformation.append('{}Daemon State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), daemon_state_colour, node_daemon_state, ansiiprint.end())) + ainformation.append('{}Domain State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), domain_state_colour, node_domain_state, ansiiprint.end())) + ainformation.append('{}Active VM Count:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_domains_count)) + if long_output == True: + ainformation.append('') + ainformation.append('{}Architecture:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_arch)) + ainformation.append('{}Operating System:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_os)) + ainformation.append('{}Kernel Version:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_kernel)) + ainformation.append('') + ainformation.append('{}CPUs:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_cpu_count)) + ainformation.append('{}Load:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_load)) + ainformation.append('{}Total RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_total)) + ainformation.append('{}Used RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_used)) + ainformation.append('{}Free RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_free)) + ainformation.append('{}Allocated RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_allocated)) + + # Join it all together + information = '\n'.join(ainformation) + return information + + +def getInformationFromXML(zk_conn, uuid, long_output): + # Obtain the contents of the XML from Zookeeper + try: + dstate = zk_conn.get('/domains/{}/state'.format(uuid))[0].decode('ascii') + dhypervisor = zk_conn.get('/domains/{}/hypervisor'.format(uuid))[0].decode('ascii') + dlasthypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(uuid))[0].decode('ascii') + except: + return None + + if dlasthypervisor == '': + dlasthypervisor = 'N/A' + + try: + parsed_xml = getDomainXML(zk_conn, uuid) + duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = getDomainMainDetails(parsed_xml) + except AttributeError: + click.echo('Error: Domain {} does not exist.'.format(domain)) + + if long_output == True: + dtype, darch, dmachine, dconsole, demulator = getDomainExtraDetails(parsed_xml) + dfeatures = getDomainCPUFeatures(parsed_xml) + ddisks = getDomainDisks(parsed_xml) + dnets = getDomainNetworks(parsed_xml) + dcontrollers = getDomainControllers(parsed_xml) + + # Format a nice output; do this line-by-line then concat the elements at the end + ainformation = [] + ainformation.append('{}Virtual machine information:{}'.format(ansiiprint.bold(), ansiiprint.end())) + ainformation.append('') + # Basic information + ainformation.append('{}UUID:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), duuid)) + ainformation.append('{}Name:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dname)) + ainformation.append('{}Description:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), ddescription)) + ainformation.append('{}Memory (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dmemory)) + ainformation.append('{}vCPUs:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dvcpu)) + ainformation.append('{}Topology (S/C/T):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dvcputopo)) + + if long_output == True: + # Virtualization information + ainformation.append('') + ainformation.append('{}Emulator:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), demulator)) + ainformation.append('{}Type:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dtype)) + ainformation.append('{}Arch:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), darch)) + ainformation.append('{}Machine:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dmachine)) + ainformation.append('{}Features:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), ' '.join(dfeatures))) + + # PVC cluster information + ainformation.append('') + dstate_colour = { + 'start': ansiiprint.green(), + 'restart': ansiiprint.yellow(), + 'shutdown': ansiiprint.yellow(), + 'stop': ansiiprint.red(), + 'failed': ansiiprint.red(), + 'migrate': ansiiprint.blue(), + 'unmigrate': ansiiprint.blue() + } + ainformation.append('{}State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), dstate_colour[dstate], dstate, ansiiprint.end())) + ainformation.append('{}Active Hypervisor:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dhypervisor)) + ainformation.append('{}Last Hypervisor:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dlasthypervisor)) + + if long_output == True: + # Disk list + ainformation.append('') + name_length = 0 + for disk in ddisks: + _name_length = len(disk['name']) + 1 + if _name_length > name_length: + name_length = _name_length + ainformation.append('{0}Disks:{1} {2}ID Type {3: <{width}} Dev Bus{4}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), 'Name', ansiiprint.end(), width=name_length)) + for disk in ddisks: + ainformation.append(' {0: <3} {1: <5} {2: <{width}} {3: <4} {4: <5}'.format(ddisks.index(disk), disk['type'], disk['name'], disk['dev'], disk['bus'], width=name_length)) + # Network list + ainformation.append('') + ainformation.append('{}Interfaces:{} {}ID Type Source Model MAC{}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), ansiiprint.end())) + for net in dnets: + ainformation.append(' {0: <3} {1: <8} {2: <10} {3: <8} {4}'.format(dnets.index(net), net['type'], net['source'], net['model'], net['mac'])) + # Controller list + ainformation.append('') + ainformation.append('{}Controllers:{} {}ID Type Model{}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), ansiiprint.end())) + for controller in dcontrollers: + ainformation.append(' {0: <3} {1: <14} {2: <8}'.format(dcontrollers.index(controller), controller['type'], controller['model'])) + + # Join it all together + information = '\n'.join(ainformation) + return information + + +# +# Cluster search functions +# +def getClusterDomainList(zk_conn): + # Get a list of UUIDs by listing the children of /domains + uuid_list = zk_conn.get_children('/domains') + name_list = [] + # For each UUID, get the corresponding name from the data + for uuid in uuid_list: + name_list.append(zk_conn.get('/domains/%s' % uuid)[0].decode('ascii')) + return uuid_list, name_list + +def searchClusterByUUID(zk_conn, uuid): + try: + # Get the lists + uuid_list, name_list = getClusterDomainList(zk_conn) + # We're looking for UUID, so find that element ID + index = uuid_list.index(uuid) + # Get the name_list element at that index + name = name_list[index] + except ValueError: + # We didn't find anything + return None + + return name + +def searchClusterByName(zk_conn, name): + try: + # Get the lists + uuid_list, name_list = getClusterDomainList(zk_conn) + # We're looking for name, so find that element ID + index = name_list.index(name) + # Get the uuid_list element at that index + uuid = uuid_list[index] + except ValueError: + # We didn't find anything + return None + + return uuid + +def verifyNode(zk_conn, node): + # Verify node is valid + try: + zk_conn.get('/nodes/{}'.format(node)) + except: + click.echo('ERROR: No node named "{}" is present in the cluster.'.format(node)) + exit(1) + +# +# Find a migration target +# +def findTargetHypervisor(zk_conn, search_field, dom_uuid): + if search_field == 'mem': + return findTargetHypervisorMem(zk_conn, dom_uuid) + if search_field == 'load': + return findTargetHypervisorLoad(zk_conn, dom_uuid) + if search_field == 'vcpus': + return findTargetHypervisorVCPUs(zk_conn, dom_uuid) + if search_field == 'vms': + return findTargetHypervisorVMs(zk_conn, dom_uuid) + return None + +# Get the list of valid target hypervisors +def getHypervisors(zk_conn, dom_uuid): + valid_hypervisor_list = [] + full_hypervisor_list = zk_conn.get_children('/nodes') + + try: + current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') + except: + current_hypervisor = None + + for hypervisor in full_hypervisor_list: + daemon_state = zk_conn.get('/nodes/{}/daemonstate'.format(hypervisor))[0].decode('ascii') + domain_state = zk_conn.get('/nodes/{}/domainstate'.format(hypervisor))[0].decode('ascii') + + if hypervisor == current_hypervisor: + continue + + if daemon_state != 'run' or domain_state != 'ready': + continue + + valid_hypervisor_list.append(hypervisor) + + return valid_hypervisor_list + +# via free memory (relative to allocated memory) +def findTargetHypervisorMem(zk_conn, dom_uuid): + most_allocfree = 0 + target_hypervisor = None + + hypervisor_list = getHypervisors(zk_conn, dom_uuid) + for hypervisor in hypervisor_list: + memalloc = int(zk_conn.get('/nodes/{}/memalloc'.format(hypervisor))[0].decode('ascii')) + memused = int(zk_conn.get('/nodes/{}/memused'.format(hypervisor))[0].decode('ascii')) + memfree = int(zk_conn.get('/nodes/{}/memfree'.format(hypervisor))[0].decode('ascii')) + memtotal = memused + memfree + allocfree = memtotal - memalloc + + if allocfree > most_allocfree: + most_allocfree = allocfree + target_hypervisor = hypervisor + + return target_hypervisor + +# via load average +def findTargetHypervisorLoad(zk_conn, dom_uuid): + least_load = 9999 + target_hypervisor = None + + hypervisor_list = getHypervisors(zk_conn, dom_uuid) + for hypervisor in hypervisor_list: + load = float(zk_conn.get('/nodes/{}/cpuload'.format(hypervisor))[0].decode('ascii')) + + if load < least_load: + least_load = load + target_hypervisor = hypervisor + + return target_hypervisor + +# via total vCPUs +def findTargetHypervisorVCPUs(zk_conn, dom_uuid): + least_vcpus = 9999 + target_hypervisor = None + + hypervisor_list = getHypervisors(zk_conn, dom_uuid) + for hypervisor in hypervisor_list: + vcpus = int(zk_conn.get('/nodes/{}/vcpualloc'.format(hypervisor))[0].decode('ascii')) + + if vcpus < least_vcpus: + least_vcpus = vcpus + target_hypervisor = hypervisor + + return target_hypervisor + +# via total VMs +def findTargetHypervisorVMs(zk_conn, dom_uuid): + least_vms = 9999 + target_hypervisor = None + + hypervisor_list = getHypervisors(zk_conn, dom_uuid) + for hypervisor in hypervisor_list: + vms = int(zk_conn.get('/nodes/{}/domainscount'.format(hypervisor))[0].decode('ascii')) + + if vms < least_vms: + least_vms = vms + target_hypervisor = hypervisor + + return target_hypervisor + + +######################## +######################## +## ## +## CLICK COMPONENTS ## +## ## +######################## +######################## + +myhostname = socket.gethostname() +zk_host = '' + +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=120) + +############################################################################### +# pvc node +############################################################################### +@click.group(name='node', short_help='Manage a PVC hypervisor node.', context_settings=CONTEXT_SETTINGS) +def node(): + """ + Manage the state of a node in the PVC cluster. + """ + pass + + +############################################################################### +# pvc node flush +############################################################################### +@click.command(name='flush', short_help='Take a node out of service.') +@click.option( + '-w', '--wait', 'wait', is_flag=True, default=False, + help='Wait for migrations to complete before returning.' +) +@click.argument( + 'node', default=myhostname +) +def flush_host(node, wait): + """ + Take NODE out of active service and migrate away all VMs. If unspecified, defaults to this host. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Verify node is valid + verifyNode(zk_conn, node) + + click.echo('Flushing hypervisor {} of running VMs.'.format(node)) + + # Add the new domain to Zookeeper + transaction = zk_conn.transaction() + transaction.set_data('/nodes/{}/domainstate'.format(node), 'flush'.encode('ascii')) + results = transaction.commit() + + if wait == True: + while True: + time.sleep(1) + node_state = zk_conn.get('/nodes/{}/domainstate'.format(node))[0].decode('ascii') + if node_state == "flushed": + break + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc node ready/unflush +############################################################################### +@click.command(name='ready', short_help='Restore node to service.') +@click.argument( + 'node', default=myhostname +) +def ready_host(node): + """ + Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host. + """ + + do_ready_host(node) + +@click.command(name='unflush', short_help='Restore node to service.') +@click.argument( + 'node', default=myhostname +) +def unflush_host(node): + """ + Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host. + """ + + do_ready_host(node) + + +def do_ready_host(node): + """ + Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Verify node is valid + verifyNode(zk_conn, node) + + click.echo('Restoring hypervisor {} to active service.'.format(node)) + + # Add the new domain to Zookeeper + transaction = zk_conn.transaction() + transaction.set_data('/nodes/{}/domainstate'.format(node), 'unflush'.encode('ascii')) + results = transaction.commit() + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc node info +############################################################################### +@click.command(name='info', short_help='Show details of a node object.') +@click.argument( + 'node', default=myhostname +) +@click.option( + '-l', '--long', 'long_output', is_flag=True, default=False, + help='Display more detailed information.' +) +def node_info(node, long_output): + """ + Show information about node NODE. If unspecified, defaults to this host. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Verify node is valid + verifyNode(zk_conn, node) + + # Get information about node in a pretty format + information = getInformationFromNode(zk_conn, node, long_output) + + if information == None: + click.echo('ERROR: Could not find a node matching that name.') + stopZKConnection(zk_conn) + exit(1) + + click.echo(information) + + if long_output == True: + click.echo('') + click.echo('{}Virtual machines on node:{}'.format(ansiiprint.bold(), ansiiprint.end())) + # List all VMs on this node + get_vm_list(node, None) + + click.echo('') + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc node list +############################################################################### +@click.command(name='list', short_help='List all node objects.') +@click.argument( + 'limit', default=None, required=False +) +def node_list(limit): + """ + List all hypervisor nodes in the cluster; optionally only match names matching regex LIMIT. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Match our limit + node_list = [] + full_node_list = zk_conn.get_children('/nodes') + for node in full_node_list: + if limit != None: + try: + if re.match(limit, node) == None: + continue + except Exception as e: + click.echo('Regex Error: {}'.format(e)) + exit(1) + node_list.append(node) + + node_list_output = [] + node_daemon_state = {} + node_daemon_state = {} + node_domain_state = {} + node_cpu_count = {} + node_mem_used = {} + node_mem_free = {} + node_mem_total = {} + node_domains_count = {} + node_running_domains = {} + node_mem_allocated = {} + node_load = {} + + # Gather information for printing + for node_name in node_list: + node_daemon_state[node_name] = zk_conn.get('/nodes/{}/daemonstate'.format(node_name))[0].decode('ascii') + node_domain_state[node_name] = zk_conn.get('/nodes/{}/domainstate'.format(node_name))[0].decode('ascii') + node_cpu_count[node_name] = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[0] + node_mem_used[node_name] = zk_conn.get('/nodes/{}/memused'.format(node_name))[0].decode('ascii') + node_mem_free[node_name] = zk_conn.get('/nodes/{}/memfree'.format(node_name))[0].decode('ascii') + node_mem_total[node_name] = int(node_mem_used[node_name]) + int(node_mem_free[node_name]) + node_load[node_name] = zk_conn.get('/nodes/{}/cpuload'.format(node_name))[0].decode('ascii') + node_domains_count[node_name] = zk_conn.get('/nodes/{}/domainscount'.format(node_name))[0].decode('ascii') + node_running_domains[node_name] = zk_conn.get('/nodes/{}/runningdomains'.format(node_name))[0].decode('ascii').split() + node_mem_allocated[node_name] = 0 + for domain in node_running_domains[node_name]: + try: + parsed_xml = getDomainXML(zk_conn, domain) + duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = getDomainMainDetails(parsed_xml) + node_mem_allocated[node_name] += int(dmemory) + except AttributeError: + click.echo('Error: Domain {} does not exist.'.format(domain)) + + # Determine optimal column widths + # Dynamic columns: node_name, hypervisor, migrated + node_name_length = 0 + for node_name in node_list: + # node_name column + _node_name_length = len(node_name) + 1 + if _node_name_length > node_name_length: + node_name_length = _node_name_length + + # Format the string (header) + node_list_output.append( + '{bold}{node_name: <{node_name_length}} \ +State: {daemon_state_colour}{node_daemon_state: <7}{end_colour} {domain_state_colour}{node_domain_state: <8}{end_colour} \ +Resources: {node_domains_count: <4} {node_cpu_count: <5} {node_load: <6} \ +RAM (MiB): {node_mem_total: <6} {node_mem_used: <6} {node_mem_free: <6} {node_mem_allocated: <6}{end_bold}'.format( + node_name_length=node_name_length, + bold=ansiiprint.bold(), + end_bold=ansiiprint.end(), + daemon_state_colour='', + domain_state_colour='', + end_colour='', + node_name='Name', + node_daemon_state='Daemon', + node_domain_state='Domains', + node_domains_count='VMs', + node_cpu_count='CPUs', + node_load='Load', + node_mem_total='Total', + node_mem_used='Used', + node_mem_free='Free', + node_mem_allocated='VMs', + ) + ) + + # Format the string (elements) + for node_name in node_list: + if node_daemon_state[node_name] == 'run': + daemon_state_colour = ansiiprint.green() + elif node_daemon_state[node_name] == 'stop': + daemon_state_colour = ansiiprint.red() + elif node_daemon_state[node_name] == 'init': + daemon_state_colour = ansiiprint.yellow() + elif node_daemon_state[node_name] == 'dead': + daemon_state_colour = ansiiprint.red() + ansiiprint.bold() + else: + daemon_state_colour = ansiiprint.blue() + + if node_mem_allocated[node_name] >= node_mem_total[node_name]: + node_domain_state[node_name] = 'overprov' + domain_state_colour = ansiiprint.yellow() + elif node_domain_state[node_name] == 'ready': + domain_state_colour = ansiiprint.green() + else: + domain_state_colour = ansiiprint.blue() + + node_list_output.append( + '{bold}{node_name: <{node_name_length}} \ + {daemon_state_colour}{node_daemon_state: <7}{end_colour} {domain_state_colour}{node_domain_state: <8}{end_colour} \ + {node_domains_count: <4} {node_cpu_count: <5} {node_load: <6} \ + {node_mem_total: <6} {node_mem_used: <6} {node_mem_free: <6} {node_mem_allocated: <6}{end_bold}'.format( + node_name_length=node_name_length, + bold='', + end_bold='', + daemon_state_colour=daemon_state_colour, + domain_state_colour=domain_state_colour, + end_colour=ansiiprint.end(), + node_name=node_name, + node_daemon_state=node_daemon_state[node_name], + node_domain_state=node_domain_state[node_name], + node_domains_count=node_domains_count[node_name], + node_cpu_count=node_cpu_count[node_name], + node_load=node_load[node_name], + node_mem_total=node_mem_total[node_name], + node_mem_used=node_mem_used[node_name], + node_mem_free=node_mem_free[node_name], + node_mem_allocated=node_mem_allocated[node_name] + ) + ) + + click.echo('\n'.join(sorted(node_list_output))) + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm +############################################################################### +@click.group(name='vm', short_help='Manage a PVC virtual machine.', context_settings=CONTEXT_SETTINGS) +def vm(): + """ + Manage the state of a virtual machine in the PVC cluster. + """ + pass + + +############################################################################### +# pvc vm define +############################################################################### +@click.command(name='define', short_help='Define a new virtual machine from a Libvirt XML file.') +@click.option( + '-t', '--hypervisor', 'target_hypervisor', + help='Home hypervisor for this domain; autodetect if unspecified.' +) +@click.option( + '-s', '--selector', 'selector', default='mem', show_default=True, + type=click.Choice(['mem','load','vcpus','vms']), + help='Method to determine optimal target hypervisor during autodetect.' +) +@click.argument( + 'config', type=click.File() +) +def define_vm(config, target_hypervisor, selector): + """ + Define a new virtual machine from Libvirt XML configuration file CONFIG. + """ + + # Open the XML file + data = config.read() + config.close() + + # Parse the XML data + parsed_xml = lxml.objectify.fromstring(data) + dom_uuid = parsed_xml.uuid.text + dom_name = parsed_xml.name.text + click.echo('Adding new VM with Name "{}" and UUID "{}" to database.'.format(dom_name, dom_uuid)) + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + if target_hypervisor == None: + target_hypervisor = findTargetHypervisor(zk_conn, selector, dom_uuid) + + # Verify node is valid + verifyNode(zk_conn, target_hypervisor) + + # Add the new domain to Zookeeper + transaction = zk_conn.transaction() + transaction.create('/domains/{}'.format(dom_uuid), dom_name.encode('ascii')) + transaction.create('/domains/{}/state'.format(dom_uuid), 'stop'.encode('ascii')) + transaction.create('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.create('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) + transaction.create('/domains/{}/failedreason'.format(dom_uuid), ''.encode('ascii')) + transaction.create('/domains/{}/xml'.format(dom_uuid), data.encode('ascii')) + results = transaction.commit() + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm modify +############################################################################### +@click.command(name='modify', short_help='Modify an existing VM configuration.') +@click.option( + '-e', '--editor', 'editor', is_flag=True, + help='Use local editor to modify existing config.' +) +@click.option( + '-r', '--restart', 'restart', is_flag=True, + help='Immediately restart VM to apply new config.' +) +@click.argument( + 'domain' +) +@click.argument( + 'config', type=click.File(), default=None, required=False +) +def modify_vm(domain, config, editor, restart): + """ + Modify existing virtual machine DOMAIN, either in-editor or with replacement CONFIG. DOMAIN may be a UUID or name. + """ + + if editor == False and config == None: + click.echo('Either an XML config file or the "--editor" option must be specified.') + exit(1) + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + exit(1) + + # We're operating in edit-in-place mode + if editor == True: + # Grab the current config + current_vm_config = zk_conn.get('/domains/{}/xml'.format(dom_uuid))[0].decode('ascii') + + # Write it to a tempfile + fd, path = tempfile.mkstemp() + fw = os.fdopen(fd, 'w') + fw.write(current_vm_config) + fw.close() + + # Edit it + editor = os.getenv('EDITOR', 'vi') + subprocess.call('%s %s' % (editor, path), shell=True) + + # Open the tempfile to read + with open(path, 'r') as fr: + new_vm_config = fr.read() + fr.close() + + # Delete the tempfile + os.unlink(path) + + # Show a diff and confirm + diff = list(difflib.unified_diff(current_vm_config.split('\n'), new_vm_config.split('\n'), fromfile='current', tofile='modified', fromfiledate='', tofiledate='', n=3, lineterm='')) + if len(diff) < 1: + click.echo('Aborting with no modifications.') + exit(0) + + click.echo('Pending modifications:') + click.echo('') + for line in diff: + if re.match('^\+', line) != None: + click.echo(colorama.Fore.GREEN + line + colorama.Fore.RESET) + elif re.match('^\-', line) != None: + click.echo(colorama.Fore.RED + line + colorama.Fore.RESET) + elif re.match('^\^', line) != None: + click.echo(colorama.Fore.BLUE + line + colorama.Fore.RESET) + else: + click.echo(line) + click.echo('') + + click.confirm('Write modifications to Zookeeper?', abort=True) + + click.echo('Writing modified config of VM "{}".'.format(dom_name)) + + # We're operating in replace mode + else: + # Open the XML file + new_vm_config = config.read() + config.close() + + click.echo('Replacing config of VM "{}".'.format(dom_name, config)) + + # Add the modified config to Zookeeper + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}'.format(dom_uuid), dom_name.encode('ascii')) + transaction.set_data('/domains/{}/xml'.format(dom_uuid), new_vm_config.encode('ascii')) + if restart == True: + transaction.set_data('/domains/{}/state'.format(dom_uuid), 'restart'.encode('ascii')) + results = transaction.commit() + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm undefine +############################################################################### +@click.command(name='undefine', short_help='Undefine and stop a virtual machine.') +@click.argument( + 'domain' +) +def undefine_vm(domain): + """ + Stop virtual machine DOMAIN and remove it from the cluster database. DOMAIN may be a UUID or name. + """ + + # Ensure at least one search method is set + if domain == None: + click.echo("ERROR: You must specify either a name or UUID value.") + exit(1) + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + # Shut down the VM + try: + current_vm_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_vm_state != 'stop': + click.echo('Forcibly stopping VM "{}".'.format(dom_uuid)) + # Set the domain into stop mode + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/state'.format(dom_uuid), 'stop'.encode('ascii')) + transaction.commit() + + # Wait for 3 seconds to allow state to flow to all hypervisors + click.echo('Waiting for cluster to update.') + time.sleep(1) + except: + pass + + # Gracefully terminate the class instances + try: + click.echo('Deleting VM "{}" from nodes.'.format(dom_uuid)) + zk_conn.set('/domains/{}/state'.format(dom_uuid), 'delete'.encode('ascii')) + time.sleep(5) + except: + pass + + # Delete the configurations + try: + click.echo('Undefining VM "{}".'.format(dom_uuid)) + transaction = zk_conn.transaction() + transaction.delete('/domains/{}/state'.format(dom_uuid)) + transaction.delete('/domains/{}/hypervisor'.format(dom_uuid)) + transaction.delete('/domains/{}/lasthypervisor'.format(dom_uuid)) + transaction.delete('/domains/{}/failedreason'.format(dom_uuid)) + transaction.delete('/domains/{}/xml'.format(dom_uuid)) + transaction.delete('/domains/{}'.format(dom_uuid)) + transaction.commit() + except: + pass + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm start +############################################################################### +@click.command(name='start', short_help='Start up a defined virtual machine.') +@click.argument( + 'domain' +) +def start_vm(domain): + """ + Start virtual machine DOMAIN on its configured hypervisor. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + exit(1) + + # Set the VM to start + click.echo('Starting VM "{}".'.format(dom_uuid)) + zk_conn.set('/domains/%s/state' % dom_uuid, 'start'.encode('ascii')) + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm restart +############################################################################### +@click.command(name='restart', short_help='Restart a running virtual machine.') +@click.argument( + 'domain' +) +def restart_vm(domain): + """ + Restart running virtual machine DOMAIN. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + exit(1) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + click.echo('ERROR: VM "{}" is not in "start" state!'.format(dom_uuid)) + stopZKConnection(zk_conn) + exit(1) + + # Set the VM to start + click.echo('Restarting VM "{}".'.format(dom_uuid)) + zk_conn.set('/domains/%s/state' % dom_uuid, 'restart'.encode('ascii')) + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm shutdown +############################################################################### +@click.command(name='shutdown', short_help='Gracefully shut down a running virtual machine.') +@click.argument( + 'domain' +) +def shutdown_vm(domain): + """ + Gracefully shut down virtual machine DOMAIN. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + exit(1) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + click.echo('ERROR: VM "{}" is not in "start" state!'.format(dom_uuid)) + stopZKConnection(zk_conn) + exit(1) + + # Set the VM to shutdown + click.echo('Shutting down VM "{}".'.format(dom_uuid)) + zk_conn.set('/domains/%s/state' % dom_uuid, 'shutdown'.encode('ascii')) + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm stop +############################################################################### +@click.command(name='stop', short_help='Forcibly halt a running virtual machine.') +@click.argument( + 'domain' +) +def stop_vm(domain): + """ + Forcibly halt (destroy) running virtual machine DOMAIN. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + exit(1) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + click.echo('ERROR: VM "{}" is not in "start" state!'.format(dom_uuid)) + stopZKConnection(zk_conn) + exit(1) + + # Set the VM to start + click.echo('Forcibly stopping VM "{}".'.format(dom_uuid)) + zk_conn.set('/domains/%s/state' % dom_uuid, 'stop'.encode('ascii')) + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm move +############################################################################### +@click.command(name='move', short_help='Permanently move a virtual machine to another node.') +@click.argument( + 'domain' +) +@click.option( + '-t', '--hypervisor', 'target_hypervisor', default=None, + help='Target hypervisor to migrate to; autodetect if unspecified.' +) +@click.option( + '-s', '--selector', 'selector', default='mem', show_default=True, + type=click.Choice(['mem','load','vcpus','vms']), + help='Method to determine optimal target hypervisor during autodetect.' +) +def move_vm(domain, target_hypervisor, selector): + """ + Permanently move virtual machine DOMAIN, via live migration if running and possible, to another hypervisor node. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + return + + current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') + + if target_hypervisor == None: + target_hypervisor = findTargetHypervisor(zk_conn, selector, dom_uuid) + else: + if target_hypervisor == current_hypervisor: + click.echo('ERROR: VM "{}" is already running on hypervisor "{}".'.format(dom_uuid, current_hypervisor)) + stopZKConnection(zk_conn) + exit(1) + + # Verify node is valid + verifyNode(zk_conn, target_hypervisor) + + current_vm_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_vm_state == 'start': + click.echo('Permanently migrating VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/state'.format(dom_uuid), 'migrate'.encode('ascii')) + transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) + transaction.commit() + else: + click.echo('Permanently moving VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) + transaction.commit() + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm migrate +############################################################################### +@click.command(name='migrate', short_help='Temporarily migrate a virtual machine to another node.') +@click.argument( + 'domain' +) +@click.option( + '-t', '--hypervisor', 'target_hypervisor', default=None, + help='Target hypervisor to migrate to; autodetect if unspecified.' +) +@click.option( + '-s', '--selector', 'selector', default='mem', show_default=True, + type=click.Choice(['mem','load','vcpus','vms']), + help='Method to determine optimal target hypervisor during autodetect.' +) +@click.option( + '-f', '--force', 'force_migrate', is_flag=True, default=False, + help='Force migrate an already migrated VM.' +) +def migrate_vm(domain, target_hypervisor, selector, force_migrate): + """ + Temporarily migrate running virtual machine DOMAIN, via live migration if possible, to another hypervisor node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + exit(1) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + target_state = 'start' + else: + target_state = 'migrate' + + current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') + last_hypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(dom_uuid))[0].decode('ascii') + + if last_hypervisor != '' and force_migrate != True: + click.echo('ERROR: VM "{}" has been previously migrated.'.format(dom_uuid)) + click.echo('> Last hypervisor: {}'.format(last_hypervisor)) + click.echo('> Current hypervisor: {}'.format(current_hypervisor)) + click.echo('Run `vm unmigrate` to restore the VM to its previous hypervisor, or use `--force` to override this check.') + stopZKConnection(zk_conn) + exit(1) + + if target_hypervisor == None: + target_hypervisor = findTargetHypervisor(zk_conn, selector, dom_uuid) + else: + if target_hypervisor == current_hypervisor: + click.echo('ERROR: VM "{}" is already running on hypervisor "{}".'.format(dom_uuid, current_hypervisor)) + stopZKConnection(zk_conn) + exit(1) + + # Verify node is valid + verifyNode(zk_conn, target_hypervisor) + + click.echo('Migrating VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/state'.format(dom_uuid), target_state.encode('ascii')) + transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), current_hypervisor.encode('ascii')) + transaction.commit() + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm unmigrate +############################################################################### +@click.command(name='unmigrate', short_help='Restore a migrated virtual machine to its original node.') +@click.argument( + 'domain' +) +def unmigrate_vm(domain): + """ + Restore previously migrated virtual machine DOMAIN, via live migration if possible, to its original hypervisor node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + exit(1) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + target_state = 'start' + else: + target_state = 'migrate' + + target_hypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(dom_uuid))[0].decode('ascii') + + if target_hypervisor == '': + click.echo('ERROR: VM "{}" has not been previously migrated.'.format(dom_uuid)) + stopZKConnection(zk_conn) + exit(1) + + click.echo('Unmigrating VM "{}" back to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/state'.format(dom_uuid), target_state.encode('ascii')) + transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) + transaction.commit() + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm info +############################################################################### +@click.command(name='info', short_help='Show details of a VM object.') +@click.argument( + 'domain' +) +@click.option( + '-l', '--long', 'long_output', is_flag=True, default=False, + help='Display more detailed information.' +) +def vm_info(domain, long_output): + """ + Show information about virtual machine DOMAIN. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Validate and obtain alternate passed value + if validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + if dom_uuid == None: + click.echo('ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + stopZKConnection(zk_conn) + exit(1) + + # Gather information from XML config and print it + information = getInformationFromXML(zk_conn, dom_uuid, long_output) + click.echo(information) + + # Get a failure reason if applicable + failedreason = zk_conn.get('/domains/{}/failedreason'.format(dom_uuid))[0].decode('ascii') + if failedreason != '': + click.echo('') + click.echo('{}Failure reason:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), failedreason)) + + click.echo('') + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc vm list +############################################################################### +@click.command(name='list', short_help='List all VM objects.') +@click.argument( + 'limit', default=None, required=False +) +@click.option( + '-t', '--hypervisor', 'hypervisor', default=None, + help='Limit list to this hypervisor.' +) +def vm_list(hypervisor, limit): + """ + List all virtual machines in the cluster; optionally only match names matching regex LIMIT. + """ + + get_vm_list(hypervisor, limit) + +# Wrapped function to allow calling from `node info` +def get_vm_list(hypervisor, limit): + """ + List all virtual machines in the cluster. + """ + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + if hypervisor != None: + # Verify node is valid + verifyNode(zk_conn, hypervisor) + + vm_list_raw = zk_conn.get_children('/domains') + vm_list = [] + vm_list_output = [] + + vm_hypervisor = {} + vm_state = {} + vm_migrated = {} + vm_uuid = {} + vm_name = {} + vm_description = {} + vm_memory = {} + vm_vcpu = {} + + # If we're limited, remove other nodes' VMs + for vm in vm_list_raw: + # Check we don't match the limit + name = zk_conn.get('/domains/{}'.format(vm))[0].decode('ascii') + if limit != None: + try: + if re.match(limit, name) == None: + continue + except Exception as e: + click.echo('Regex Error: {}'.format(e)) + exit(1) + # Check hypervisor to avoid unneeded ZK calls + vm_hypervisor[vm] = zk_conn.get('/domains/{}/hypervisor'.format(vm))[0].decode('ascii') + if hypervisor == None: + vm_list.append(vm) + else: + if vm_hypervisor[vm] == hypervisor: + vm_list.append(vm) + + # Gather information for printing + for vm in vm_list: + vm_state[vm] = zk_conn.get('/domains/{}/state'.format(vm))[0].decode('ascii') + vm_lasthypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(vm))[0].decode('ascii') + if vm_lasthypervisor != '': + vm_migrated[vm] = 'from {}'.format(vm_lasthypervisor) + else: + vm_migrated[vm] = 'no' + + try: + vm_xml = getDomainXML(zk_conn, vm) + vm_uuid[vm], vm_name[vm], vm_description[vm], vm_memory[vm], vm_vcpu[vm], vm_vcputopo = getDomainMainDetails(vm_xml) + except AttributeError: + click.echo('Error: Domain {} does not exist.'.format(domain)) + + # Determine optimal column widths + # Dynamic columns: node_name, hypervisor, migrated + vm_name_length = 0 + vm_hypervisor_length = 0 + vm_migrated_length = 0 + for vm in vm_list: + # vm_name column + _vm_name_length = len(vm_name[vm]) + 1 + if _vm_name_length > vm_name_length: + vm_name_length = _vm_name_length + # vm_hypervisor column + _vm_hypervisor_length = len(vm_hypervisor[vm]) + 1 + if _vm_hypervisor_length > vm_hypervisor_length: + vm_hypervisor_length = _vm_hypervisor_length + # vm_migrated column + _vm_migrated_length = len(vm_migrated[vm]) + 1 + if _vm_migrated_length > vm_migrated_length: + vm_migrated_length = _vm_migrated_length + + # Format the string (header) + vm_list_header = ansiiprint.bold() + 'Name UUID State RAM [MiB] vCPUs Hypervisor Migrated?' + ansiiprint.end() + vm_list_output.append( + '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <37} \ +{vm_state_colour}{vm_state: <8}{end_colour} \ +{vm_memory: <10} {vm_vcpu: <6} \ +{vm_hypervisor: <{vm_hypervisor_length}} \ +{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( + vm_name_length=vm_name_length, + vm_hypervisor_length=vm_hypervisor_length, + vm_migrated_length=vm_migrated_length, + bold=ansiiprint.bold(), + end_bold=ansiiprint.end(), + vm_state_colour='', + end_colour='', + vm_name='Name', + vm_uuid='UUID', + vm_state='State', + vm_memory='RAM (MiB)', + vm_vcpu='vCPUs', + vm_hypervisor='Hypervisor', + vm_migrated='Migrated' + ) + ) + + # Format the string (elements) + for vm in vm_list: + if vm_state[vm] == 'start': + vm_state_colour = ansiiprint.green() + elif vm_state[vm] == 'restart': + vm_state_colour = ansiiprint.yellow() + elif vm_state[vm] == 'shutdown': + vm_state_colour = ansiiprint.yellow() + elif vm_state[vm] == 'stop': + vm_state_colour = ansiiprint.red() + elif vm_state[vm] == 'failed': + vm_state_colour = ansiiprint.red() + else: + vm_state_colour = ansiiprint.blue() + + vm_list_output.append( + '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <37} \ +{vm_state_colour}{vm_state: <8}{end_colour} \ +{vm_memory: <10} {vm_vcpu: <6} \ +{vm_hypervisor: <{vm_hypervisor_length}} \ +{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( + vm_name_length=vm_name_length, + vm_hypervisor_length=vm_hypervisor_length, + vm_migrated_length=vm_migrated_length, + bold='', + end_bold='', + vm_state_colour=vm_state_colour, + end_colour=ansiiprint.end(), + vm_name=vm_name[vm], + vm_uuid=vm_uuid[vm], + vm_state=vm_state[vm], + vm_memory=vm_memory[vm], + vm_vcpu=vm_vcpu[vm], + vm_hypervisor=vm_hypervisor[vm], + vm_migrated=vm_migrated[vm] + ) + ) + + click.echo('\n'.join(sorted(vm_list_output))) + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + +############################################################################### +# pvc init +############################################################################### +@click.command(name='init', short_help='Initialize a new cluster.') +@click.option('--yes', is_flag=True, + expose_value=False, + prompt='DANGER: This command will destroy any existing cluster data. Do you want to continue?') +def init_cluster(): + """ + Perform initialization of Zookeeper to act as a PVC cluster. + + DANGER: This command will overwrite any existing cluster data and provision a new cluster at the specified Zookeeper connection string. Do not run this against a cluster unless you are sure this is what you want. + """ + + click.echo('Initializing a new cluster with Zookeeper address "{}".'.format(zk_host)) + + # Open a Zookeeper connection + zk_conn = startZKConnection(zk_host) + + # Destroy the existing data + try: + zk_conn.delete('/domains', recursive=True) + zk_conn.delete('nodes', recursive=True) + except: + pass + + # Create the root keys + transaction = zk_conn.transaction() + transaction.create('/domains', ''.encode('ascii')) + transaction.create('/nodes', ''.encode('ascii')) + transaction.commit() + + # Close the Zookeeper connection + stopZKConnection(zk_conn) + + click.echo('Successfully initialized new cluster. Any running PVC daemons will need to be restarted.') + + +############################################################################### +# pvc +############################################################################### +@click.group(context_settings=CONTEXT_SETTINGS) +@click.option( + '-z', '--zookeeper', '_zk_host', envvar='PVC_ZOOKEEPER', default='{}:2181'.format(myhostname), show_default=True, + help='Zookeeper connection string.' +) +def cli(_zk_host): + """ + Parallel Virtual Cluster CLI management tool + + Environment variables: + + "PVC_ZOOKEEPER": Set the cluster Zookeeper address instead of using "--zookeeper". + """ + + global zk_host + zk_host = _zk_host + + +# +# Click command tree +# +node.add_command(flush_host) +node.add_command(ready_host) +node.add_command(unflush_host) +node.add_command(node_info) +node.add_command(node_list) + +vm.add_command(define_vm) +vm.add_command(modify_vm) +vm.add_command(undefine_vm) +vm.add_command(start_vm) +vm.add_command(restart_vm) +vm.add_command(shutdown_vm) +vm.add_command(stop_vm) +vm.add_command(move_vm) +vm.add_command(migrate_vm) +vm.add_command(unmigrate_vm) +vm.add_command(vm_info) +vm.add_command(vm_list) + +cli.add_command(node) +cli.add_command(vm) +cli.add_command(init_cluster) + +# +# Main entry point +# +def main(): + return cli(obj={}) + +if __name__ == '__main__': + main() + diff --git a/client-common/lib/node.py b/client-common/lib/node.py new file mode 100644 index 00000000..50a58a87 --- /dev/null +++ b/client-common/lib/node.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 + +# node.py - PVC client function library, node management +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import os +import socket +import time +import uuid +import re +import tempfile +import subprocess +import difflib +import colorama +import click +import lxml.objectify +import configparser +import kazoo.client + +import lib.ansiiprint as ansiiprint +import lib.common as common + +def getInformationFromNode(zk_conn, node_name, long_output): + node_daemon_state = zk_conn.get('/nodes/{}/daemonstate'.format(node_name))[0].decode('ascii') + node_domain_state = zk_conn.get('/nodes/{}/domainstate'.format(node_name))[0].decode('ascii') + node_cpu_count = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[0] + node_kernel = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[1] + node_os = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[2] + node_arch = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[3] + node_mem_used = zk_conn.get('/nodes/{}/memused'.format(node_name))[0].decode('ascii') + node_mem_free = zk_conn.get('/nodes/{}/memfree'.format(node_name))[0].decode('ascii') + node_mem_total = int(node_mem_used) + int(node_mem_free) + node_load = zk_conn.get('/nodes/{}/cpuload'.format(node_name))[0].decode('ascii') + node_domains_count = zk_conn.get('/nodes/{}/domainscount'.format(node_name))[0].decode('ascii') + node_running_domains = zk_conn.get('/nodes/{}/runningdomains'.format(node_name))[0].decode('ascii').split() + node_mem_allocated = 0 + for domain in node_running_domains: + try: + parsed_xml = common.getDomainXML(zk_conn, domain) + duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = common.getDomainMainDetails(parsed_xml) + node_mem_allocated += int(dmemory) + except AttributeError: + click.echo('Error: Domain {} does not exist.'.format(domain)) + + if node_daemon_state == 'run': + daemon_state_colour = ansiiprint.green() + elif node_daemon_state == 'stop': + daemon_state_colour = ansiiprint.red() + elif node_daemon_state == 'init': + daemon_state_colour = ansiiprint.yellow() + elif node_daemon_state == 'dead': + daemon_state_colour = ansiiprint.red() + ansiiprint.bold() + else: + daemon_state_colour = ansiiprint.blue() + + if node_domain_state == 'ready': + domain_state_colour = ansiiprint.green() + else: + domain_state_colour = ansiiprint.blue() + + # Format a nice output; do this line-by-line then concat the elements at the end + ainformation = [] + ainformation.append('{}Hypervisor Node information:{}'.format(ansiiprint.bold(), ansiiprint.end())) + ainformation.append('') + # Basic information + ainformation.append('{}Name:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_name)) + ainformation.append('{}Daemon State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), daemon_state_colour, node_daemon_state, ansiiprint.end())) + ainformation.append('{}Domain State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), domain_state_colour, node_domain_state, ansiiprint.end())) + ainformation.append('{}Active VM Count:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_domains_count)) + if long_output == True: + ainformation.append('') + ainformation.append('{}Architecture:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_arch)) + ainformation.append('{}Operating System:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_os)) + ainformation.append('{}Kernel Version:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_kernel)) + ainformation.append('') + ainformation.append('{}CPUs:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_cpu_count)) + ainformation.append('{}Load:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_load)) + ainformation.append('{}Total RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_total)) + ainformation.append('{}Used RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_used)) + ainformation.append('{}Free RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_free)) + ainformation.append('{}Allocated RAM (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), node_mem_allocated)) + + # Join it all together + information = '\n'.join(ainformation) + return information + +# Get the list of valid target hypervisors +def getHypervisors(zk_conn, dom_uuid): + valid_hypervisor_list = [] + full_hypervisor_list = zk_conn.get_children('/nodes') + + try: + current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') + except: + current_hypervisor = None + + for hypervisor in full_hypervisor_list: + daemon_state = zk_conn.get('/nodes/{}/daemonstate'.format(hypervisor))[0].decode('ascii') + domain_state = zk_conn.get('/nodes/{}/domainstate'.format(hypervisor))[0].decode('ascii') + + if hypervisor == current_hypervisor: + continue + + if daemon_state != 'run' or domain_state != 'ready': + continue + + valid_hypervisor_list.append(hypervisor) + + return valid_hypervisor_list + +# +# Direct Functions +# +def flush_node(zk_conn, node, wait): + # Verify node is valid + verifyNode(zk_conn, node) + + click.echo('Flushing hypervisor {} of running VMs.'.format(node)) + + # Add the new domain to Zookeeper + transaction = zk_conn.transaction() + transaction.set_data('/nodes/{}/domainstate'.format(node), 'flush'.encode('ascii')) + results = transaction.commit() + + if wait == True: + while True: + time.sleep(1) + node_state = zk_conn.get('/nodes/{}/domainstate'.format(node))[0].decode('ascii') + if node_state == "flushed": + break + + return True, '' + +def ready_node(zk_conn, node): + # Verify node is valid + verifyNode(zk_conn, node) + + click.echo('Restoring hypervisor {} to active service.'.format(node)) + + # Add the new domain to Zookeeper + transaction = zk_conn.transaction() + transaction.set_data('/nodes/{}/domainstate'.format(node), 'unflush'.encode('ascii')) + results = transaction.commit() + + return True, '' + +def get_info(zk_conn, node, long_output): + # Verify node is valid + verifyNode(zk_conn, node) + + # Get information about node in a pretty format + information = getInformationFromNode(zk_conn, node, long_output) + + if information == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find a node matching that name.' + + click.echo(information) + + if long_output == True: + click.echo('') + click.echo('{}Virtual machines on node:{}'.format(ansiiprint.bold(), ansiiprint.end())) + # List all VMs on this node + common.get_list(zk_conn, node, None) + + click.echo('') + + return True, '' + +def get_list(zk_conn, limit): + # Match our limit + node_list = [] + full_node_list = zk_conn.get_children('/nodes') + for node in full_node_list: + if limit != None: + try: + if re.match(limit, node) == None: + continue + except Exception as e: + common.stopZKConnection(zk_conn) + return False, 'Regex Error: {}'.format(e) + node_list.append(node) + + node_list_output = [] + node_daemon_state = {} + node_daemon_state = {} + node_domain_state = {} + node_cpu_count = {} + node_mem_used = {} + node_mem_free = {} + node_mem_total = {} + node_domains_count = {} + node_running_domains = {} + node_mem_allocated = {} + node_load = {} + + # Gather information for printing + for node_name in node_list: + node_daemon_state[node_name] = zk_conn.get('/nodes/{}/daemonstate'.format(node_name))[0].decode('ascii') + node_domain_state[node_name] = zk_conn.get('/nodes/{}/domainstate'.format(node_name))[0].decode('ascii') + node_cpu_count[node_name] = zk_conn.get('/nodes/{}/staticdata'.format(node_name))[0].decode('ascii').split()[0] + node_mem_used[node_name] = zk_conn.get('/nodes/{}/memused'.format(node_name))[0].decode('ascii') + node_mem_free[node_name] = zk_conn.get('/nodes/{}/memfree'.format(node_name))[0].decode('ascii') + node_mem_total[node_name] = int(node_mem_used[node_name]) + int(node_mem_free[node_name]) + node_load[node_name] = zk_conn.get('/nodes/{}/cpuload'.format(node_name))[0].decode('ascii') + node_domains_count[node_name] = zk_conn.get('/nodes/{}/domainscount'.format(node_name))[0].decode('ascii') + node_running_domains[node_name] = zk_conn.get('/nodes/{}/runningdomains'.format(node_name))[0].decode('ascii').split() + node_mem_allocated[node_name] = 0 + for domain in node_running_domains[node_name]: + try: + parsed_xml = common.getDomainXML(zk_conn, domain) + duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = common.getDomainMainDetails(parsed_xml) + node_mem_allocated[node_name] += int(dmemory) + except AttributeError: + click.echo('Error: Domain {} does not exist.'.format(domain)) + + # Determine optimal column widths + # Dynamic columns: node_name, hypervisor, migrated + node_name_length = 0 + for node_name in node_list: + # node_name column + _node_name_length = len(node_name) + 1 + if _node_name_length > node_name_length: + node_name_length = _node_name_length + + # Format the string (header) + node_list_output.append( + '{bold}{node_name: <{node_name_length}} \ +State: {daemon_state_colour}{node_daemon_state: <7}{end_colour} {domain_state_colour}{node_domain_state: <8}{end_colour} \ +Resources: {node_domains_count: <4} {node_cpu_count: <5} {node_load: <6} \ +RAM (MiB): {node_mem_total: <6} {node_mem_used: <6} {node_mem_free: <6} {node_mem_allocated: <6}{end_bold}'.format( + node_name_length=node_name_length, + bold=ansiiprint.bold(), + end_bold=ansiiprint.end(), + daemon_state_colour='', + domain_state_colour='', + end_colour='', + node_name='Name', + node_daemon_state='Daemon', + node_domain_state='Domains', + node_domains_count='VMs', + node_cpu_count='CPUs', + node_load='Load', + node_mem_total='Total', + node_mem_used='Used', + node_mem_free='Free', + node_mem_allocated='VMs', + ) + ) + + # Format the string (elements) + for node_name in node_list: + if node_daemon_state[node_name] == 'run': + daemon_state_colour = ansiiprint.green() + elif node_daemon_state[node_name] == 'stop': + daemon_state_colour = ansiiprint.red() + elif node_daemon_state[node_name] == 'init': + daemon_state_colour = ansiiprint.yellow() + elif node_daemon_state[node_name] == 'dead': + daemon_state_colour = ansiiprint.red() + ansiiprint.bold() + else: + daemon_state_colour = ansiiprint.blue() + + if node_mem_allocated[node_name] >= node_mem_total[node_name]: + node_domain_state[node_name] = 'overprov' + domain_state_colour = ansiiprint.yellow() + elif node_domain_state[node_name] == 'ready': + domain_state_colour = ansiiprint.green() + else: + domain_state_colour = ansiiprint.blue() + + node_list_output.append( + '{bold}{node_name: <{node_name_length}} \ + {daemon_state_colour}{node_daemon_state: <7}{end_colour} {domain_state_colour}{node_domain_state: <8}{end_colour} \ + {node_domains_count: <4} {node_cpu_count: <5} {node_load: <6} \ + {node_mem_total: <6} {node_mem_used: <6} {node_mem_free: <6} {node_mem_allocated: <6}{end_bold}'.format( + node_name_length=node_name_length, + bold='', + end_bold='', + daemon_state_colour=daemon_state_colour, + domain_state_colour=domain_state_colour, + end_colour=ansiiprint.end(), + node_name=node_name, + node_daemon_state=node_daemon_state[node_name], + node_domain_state=node_domain_state[node_name], + node_domains_count=node_domains_count[node_name], + node_cpu_count=node_cpu_count[node_name], + node_load=node_load[node_name], + node_mem_total=node_mem_total[node_name], + node_mem_used=node_mem_used[node_name], + node_mem_free=node_mem_free[node_name], + node_mem_allocated=node_mem_allocated[node_name] + ) + ) + + click.echo('\n'.join(sorted(node_list_output))) + + return True, '' diff --git a/client-common/lib/vm.py b/client-common/lib/vm.py new file mode 100644 index 00000000..6ba7e5d5 --- /dev/null +++ b/client-common/lib/vm.py @@ -0,0 +1,607 @@ +#!/usr/bin/env python3 + +# vm.py - PVC client function library, VM fuctions +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import os +import socket +import time +import uuid +import re +import tempfile +import subprocess +import difflib +import colorama +import click +import lxml.objectify +import configparser +import kazoo.client + +import lib.ansiiprint as ansiiprint +import lib.common as common + +# +# XML information parsing functions +# +def getInformationFromXML(zk_conn, uuid, long_output): + # Obtain the contents of the XML from Zookeeper + try: + dstate = zk_conn.get('/domains/{}/state'.format(uuid))[0].decode('ascii') + dhypervisor = zk_conn.get('/domains/{}/hypervisor'.format(uuid))[0].decode('ascii') + dlasthypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(uuid))[0].decode('ascii') + except: + return None + + if dlasthypervisor == '': + dlasthypervisor = 'N/A' + + try: + parsed_xml = common.getDomainXML(zk_conn, uuid) + duuid, dname, ddescription, dmemory, dvcpu, dvcputopo = common.getDomainMainDetails(parsed_xml) + except AttributeError: + click.echo('Error: Domain {} does not exist.'.format(domain)) + + if long_output == True: + dtype, darch, dmachine, dconsole, demulator = common.getDomainExtraDetails(parsed_xml) + dfeatures = common.getDomainCPUFeatures(parsed_xml) + ddisks = common.getDomainDisks(parsed_xml) + dnets = common.getDomainNetworks(parsed_xml) + dcontrollers = common.getDomainControllers(parsed_xml) + + # Format a nice output; do this line-by-line then concat the elements at the end + ainformation = [] + ainformation.append('{}Virtual machine information:{}'.format(ansiiprint.bold(), ansiiprint.end())) + ainformation.append('') + # Basic information + ainformation.append('{}UUID:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), duuid)) + ainformation.append('{}Name:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dname)) + ainformation.append('{}Description:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), ddescription)) + ainformation.append('{}Memory (MiB):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dmemory)) + ainformation.append('{}vCPUs:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dvcpu)) + ainformation.append('{}Topology (S/C/T):{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dvcputopo)) + + if long_output == True: + # Virtualization information + ainformation.append('') + ainformation.append('{}Emulator:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), demulator)) + ainformation.append('{}Type:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dtype)) + ainformation.append('{}Arch:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), darch)) + ainformation.append('{}Machine:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dmachine)) + ainformation.append('{}Features:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), ' '.join(dfeatures))) + + # PVC cluster information + ainformation.append('') + dstate_colour = { + 'start': ansiiprint.green(), + 'restart': ansiiprint.yellow(), + 'shutdown': ansiiprint.yellow(), + 'stop': ansiiprint.red(), + 'failed': ansiiprint.red(), + 'migrate': ansiiprint.blue(), + 'unmigrate': ansiiprint.blue() + } + ainformation.append('{}State:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), dstate_colour[dstate], dstate, ansiiprint.end())) + ainformation.append('{}Active Hypervisor:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dhypervisor)) + ainformation.append('{}Last Hypervisor:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), dlasthypervisor)) + + if long_output == True: + # Disk list + ainformation.append('') + name_length = 0 + for disk in ddisks: + _name_length = len(disk['name']) + 1 + if _name_length > name_length: + name_length = _name_length + ainformation.append('{0}Disks:{1} {2}ID Type {3: <{width}} Dev Bus{4}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), 'Name', ansiiprint.end(), width=name_length)) + for disk in ddisks: + ainformation.append(' {0: <3} {1: <5} {2: <{width}} {3: <4} {4: <5}'.format(ddisks.index(disk), disk['type'], disk['name'], disk['dev'], disk['bus'], width=name_length)) + # Network list + ainformation.append('') + ainformation.append('{}Interfaces:{} {}ID Type Source Model MAC{}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), ansiiprint.end())) + for net in dnets: + ainformation.append(' {0: <3} {1: <8} {2: <10} {3: <8} {4}'.format(dnets.index(net), net['type'], net['source'], net['model'], net['mac'])) + # Controller list + ainformation.append('') + ainformation.append('{}Controllers:{} {}ID Type Model{}'.format(ansiiprint.purple(), ansiiprint.end(), ansiiprint.bold(), ansiiprint.end())) + for controller in dcontrollers: + ainformation.append(' {0: <3} {1: <14} {2: <8}'.format(dcontrollers.index(controller), controller['type'], controller['model'])) + + # Join it all together + information = '\n'.join(ainformation) + return information + + +# +# Cluster search functions +# +def getClusterDomainList(zk_conn): + # Get a list of UUIDs by listing the children of /domains + uuid_list = zk_conn.get_children('/domains') + name_list = [] + # For each UUID, get the corresponding name from the data + for uuid in uuid_list: + name_list.append(zk_conn.get('/domains/%s' % uuid)[0].decode('ascii')) + return uuid_list, name_list + +def searchClusterByUUID(zk_conn, uuid): + try: + # Get the lists + uuid_list, name_list = getClusterDomainList(zk_conn) + # We're looking for UUID, so find that element ID + index = uuid_list.index(uuid) + # Get the name_list element at that index + name = name_list[index] + except ValueError: + # We didn't find anything + return None + + return name + +def searchClusterByName(zk_conn, name): + try: + # Get the lists + uuid_list, name_list = getClusterDomainList(zk_conn) + # We're looking for name, so find that element ID + index = name_list.index(name) + # Get the uuid_list element at that index + uuid = uuid_list[index] + except ValueError: + # We didn't find anything + return None + + return uuid + +def getDomainUUID(zk_conn, domain): + # Validate and obtain alternate passed value + if common.validateUUID(domain): + dom_name = searchClusterByUUID(zk_conn, domain) + dom_uuid = searchClusterByName(zk_conn, dom_name) + else: + dom_uuid = searchClusterByName(zk_conn, domain) + dom_name = searchClusterByUUID(zk_conn, dom_uuid) + + return dom_uuid + +# +# Direct functions +# +def define_vm(zk_conn, config_data, target_hypervisor, selector): + # Parse the XML data + parsed_xml = lxml.objectify.fromstring(data) + dom_uuid = parsed_xml.uuid.text + dom_name = parsed_xml.name.text + click.echo('Adding new VM with Name "{}" and UUID "{}" to database.'.format(dom_name, dom_uuid)) + + if target_hypervisor == None: + target_hypervisor = common.findTargetHypervisor(zk_conn, selector, dom_uuid) + + # Verify node is valid + common.verifyNode(zk_conn, target_hypervisor) + + # Add the new domain to Zookeeper + transaction = zk_conn.transaction() + transaction.create('/domains/{}'.format(dom_uuid), dom_name.encode('ascii')) + transaction.create('/domains/{}/state'.format(dom_uuid), 'stop'.encode('ascii')) + transaction.create('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.create('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) + transaction.create('/domains/{}/failedreason'.format(dom_uuid), ''.encode('ascii')) + transaction.create('/domains/{}/xml'.format(dom_uuid), data.encode('ascii')) + results = transaction.commit() + + return True, '' + +def modify_vm(zk_conn, domain, restart): + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Add the modified config to Zookeeper + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}'.format(dom_uuid), dom_name.encode('ascii')) + transaction.set_data('/domains/{}/xml'.format(dom_uuid), new_vm_config.encode('ascii')) + if restart == True: + transaction.set_data('/domains/{}/state'.format(dom_uuid), 'restart'.encode('ascii')) + results = transaction.commit() + + return True, '' + +def undefine_vm(zk_conn, domain): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Shut down the VM + try: + current_vm_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_vm_state != 'stop': + click.echo('Forcibly stopping VM "{}".'.format(dom_uuid)) + # Set the domain into stop mode + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/state'.format(dom_uuid), 'stop'.encode('ascii')) + transaction.commit() + + # Wait for 3 seconds to allow state to flow to all hypervisors + click.echo('Waiting for cluster to update.') + time.sleep(1) + except: + pass + + # Gracefully terminate the class instances + try: + click.echo('Deleting VM "{}" from nodes.'.format(dom_uuid)) + zk_conn.set('/domains/{}/state'.format(dom_uuid), 'delete'.encode('ascii')) + time.sleep(5) + except: + pass + + # Delete the configurations + try: + click.echo('Undefining VM "{}".'.format(dom_uuid)) + transaction = zk_conn.transaction() + transaction.delete('/domains/{}/state'.format(dom_uuid)) + transaction.delete('/domains/{}/hypervisor'.format(dom_uuid)) + transaction.delete('/domains/{}/lasthypervisor'.format(dom_uuid)) + transaction.delete('/domains/{}/failedreason'.format(dom_uuid)) + transaction.delete('/domains/{}/xml'.format(dom_uuid)) + transaction.delete('/domains/{}'.format(dom_uuid)) + transaction.commit() + except: + pass + + return True, '' + +def start_vm(zk_conn, domain): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Set the VM to start + click.echo('Starting VM "{}".'.format(dom_uuid)) + zk_conn.set('/domains/%s/state' % dom_uuid, 'start'.encode('ascii')) + + return True, '' + +def restart_vm(zk_conn, domain): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + common.stopZKConnection(zk_conn) + return False, 'ERROR: VM "{}" is not in "start" state!'.format(dom_uuid) + + # Set the VM to start + click.echo('Restarting VM "{}".'.format(dom_uuid)) + zk_conn.set('/domains/%s/state' % dom_uuid, 'restart'.encode('ascii')) + + return True, '' + +def shutdown_vm(zk_conn, domain): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + common.stopZKConnection(zk_conn) + return False, 'ERROR: VM "{}" is not in "start" state!'.format(dom_uuid) + + # Set the VM to shutdown + click.echo('Shutting down VM "{}".'.format(dom_uuid)) + zk_conn.set('/domains/%s/state' % dom_uuid, 'shutdown'.encode('ascii')) + + return True, '' + +def stop_vm(zk_conn, domain): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + common.stopZKConnection(zk_conn) + return False, 'ERROR: VM "{}" is not in "start" state!'.format(dom_uuid) + + # Set the VM to start + click.echo('Forcibly stopping VM "{}".'.format(dom_uuid)) + zk_conn.set('/domains/%s/state' % dom_uuid, 'stop'.encode('ascii')) + + return True, '' + +def move_vm(zk_conn, domain, target_hypervisor, selector): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') + + if target_hypervisor == None: + target_hypervisor = common.findTargetHypervisor(zk_conn, selector, dom_uuid) + else: + if target_hypervisor == current_hypervisor: + common.stopZKConnection(zk_conn) + return False, 'ERROR: VM "{}" is already running on hypervisor "{}".'.format(dom_uuid, current_hypervisor) + + # Verify node is valid + common.verifyNode(zk_conn, target_hypervisor) + + current_vm_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_vm_state == 'start': + click.echo('Permanently migrating VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/state'.format(dom_uuid), 'migrate'.encode('ascii')) + transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) + transaction.commit() + else: + click.echo('Permanently moving VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) + transaction.commit() + + return True, '' + +def migrate_vm(zk_conn, domain, target_hypervisor, selector, force_migrate): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + target_state = 'start' + else: + target_state = 'migrate' + + current_hypervisor = zk_conn.get('/domains/{}/hypervisor'.format(dom_uuid))[0].decode('ascii') + last_hypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(dom_uuid))[0].decode('ascii') + + if last_hypervisor != '' and force_migrate != True: + click.echo('ERROR: VM "{}" has been previously migrated.'.format(dom_uuid)) + click.echo('> Last hypervisor: {}'.format(last_hypervisor)) + click.echo('> Current hypervisor: {}'.format(current_hypervisor)) + click.echo('Run `vm unmigrate` to restore the VM to its previous hypervisor, or use `--force` to override this check.') + common.stopZKConnection(zk_conn) + return False, '' + + if target_hypervisor == None: + target_hypervisor = findTargetHypervisor(zk_conn, selector, dom_uuid) + else: + if target_hypervisor == current_hypervisor: + common.stopZKConnection(zk_conn) + return False, 'ERROR: VM "{}" is already running on hypervisor "{}".'.format(dom_uuid, current_hypervisor) + + # Verify node is valid + common.verifyNode(zk_conn, target_hypervisor) + + click.echo('Migrating VM "{}" to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/state'.format(dom_uuid), target_state.encode('ascii')) + transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), current_hypervisor.encode('ascii')) + transaction.commit() + + return True, '' + +def unmigrate_vm(zk_conn, domain): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Get state and verify we're OK to proceed + current_state = zk_conn.get('/domains/{}/state'.format(dom_uuid))[0].decode('ascii') + if current_state != 'start': + target_state = 'start' + else: + target_state = 'migrate' + + target_hypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(dom_uuid))[0].decode('ascii') + + if target_hypervisor == '': + common.stopZKConnection(zk_conn) + return False, 'ERROR: VM "{}" has not been previously migrated.'.format(dom_uuid) + + click.echo('Unmigrating VM "{}" back to hypervisor "{}".'.format(dom_uuid, target_hypervisor)) + transaction = zk_conn.transaction() + transaction.set_data('/domains/{}/state'.format(dom_uuid), target_state.encode('ascii')) + transaction.set_data('/domains/{}/hypervisor'.format(dom_uuid), target_hypervisor.encode('ascii')) + transaction.set_data('/domains/{}/lasthypervisor'.format(dom_uuid), ''.encode('ascii')) + transaction.commit() + + return True, '' + +def get_info(zk_conn, domain, long_output): + # Validate and obtain alternate passed value + dom_uuid = getDomainUUID(zk_conn, domain) + if dom_uuid == None: + common.stopZKConnection(zk_conn) + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Gather information from XML config and print it + information = getInformationFromXML(zk_conn, dom_uuid, long_output) + click.echo(information) + + # Get a failure reason if applicable + failedreason = zk_conn.get('/domains/{}/failedreason'.format(dom_uuid))[0].decode('ascii') + if failedreason != '': + click.echo('') + click.echo('{}Failure reason:{} {}'.format(ansiiprint.purple(), ansiiprint.end(), failedreason)) + + click.echo('') + + return True, '' + +def get_list(zk_conn, hypervisor, limit): + if hypervisor != None: + # Verify node is valid + common.verifyNode(zk_conn, hypervisor) + + vm_list_raw = zk_conn.get_children('/domains') + vm_list = [] + vm_list_output = [] + + vm_hypervisor = {} + vm_state = {} + vm_migrated = {} + vm_uuid = {} + vm_name = {} + vm_description = {} + vm_memory = {} + vm_vcpu = {} + + # If we're limited, remove other nodes' VMs + for vm in vm_list_raw: + # Check we don't match the limit + name = zk_conn.get('/domains/{}'.format(vm))[0].decode('ascii') + if limit != None: + try: + if re.match(limit, name) == None: + continue + except Exception as e: + click.echo('Regex Error: {}'.format(e)) + exit(1) + # Check hypervisor to avoid unneeded ZK calls + vm_hypervisor[vm] = zk_conn.get('/domains/{}/hypervisor'.format(vm))[0].decode('ascii') + if hypervisor == None: + vm_list.append(vm) + else: + if vm_hypervisor[vm] == hypervisor: + vm_list.append(vm) + + # Gather information for printing + for vm in vm_list: + vm_state[vm] = zk_conn.get('/domains/{}/state'.format(vm))[0].decode('ascii') + vm_lasthypervisor = zk_conn.get('/domains/{}/lasthypervisor'.format(vm))[0].decode('ascii') + if vm_lasthypervisor != '': + vm_migrated[vm] = 'from {}'.format(vm_lasthypervisor) + else: + vm_migrated[vm] = 'no' + + try: + vm_xml = common.getDomainXML(zk_conn, vm) + vm_uuid[vm], vm_name[vm], vm_description[vm], vm_memory[vm], vm_vcpu[vm], vm_vcputopo = common.getDomainMainDetails(vm_xml) + except AttributeError: + click.echo('Error: Domain {} does not exist.'.format(domain)) + + # Determine optimal column widths + # Dynamic columns: node_name, hypervisor, migrated + vm_name_length = 0 + vm_hypervisor_length = 0 + vm_migrated_length = 0 + for vm in vm_list: + # vm_name column + _vm_name_length = len(vm_name[vm]) + 1 + if _vm_name_length > vm_name_length: + vm_name_length = _vm_name_length + # vm_hypervisor column + _vm_hypervisor_length = len(vm_hypervisor[vm]) + 1 + if _vm_hypervisor_length > vm_hypervisor_length: + vm_hypervisor_length = _vm_hypervisor_length + # vm_migrated column + _vm_migrated_length = len(vm_migrated[vm]) + 1 + if _vm_migrated_length > vm_migrated_length: + vm_migrated_length = _vm_migrated_length + + # Format the string (header) + vm_list_header = ansiiprint.bold() + 'Name UUID State RAM [MiB] vCPUs Hypervisor Migrated?' + ansiiprint.end() + vm_list_output.append( + '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <37} \ +{vm_state_colour}{vm_state: <8}{end_colour} \ +{vm_memory: <10} {vm_vcpu: <6} \ +{vm_hypervisor: <{vm_hypervisor_length}} \ +{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( + vm_name_length=vm_name_length, + vm_hypervisor_length=vm_hypervisor_length, + vm_migrated_length=vm_migrated_length, + bold=ansiiprint.bold(), + end_bold=ansiiprint.end(), + vm_state_colour='', + end_colour='', + vm_name='Name', + vm_uuid='UUID', + vm_state='State', + vm_memory='RAM (MiB)', + vm_vcpu='vCPUs', + vm_hypervisor='Hypervisor', + vm_migrated='Migrated' + ) + ) + + # Format the string (elements) + for vm in vm_list: + if vm_state[vm] == 'start': + vm_state_colour = ansiiprint.green() + elif vm_state[vm] == 'restart': + vm_state_colour = ansiiprint.yellow() + elif vm_state[vm] == 'shutdown': + vm_state_colour = ansiiprint.yellow() + elif vm_state[vm] == 'stop': + vm_state_colour = ansiiprint.red() + elif vm_state[vm] == 'failed': + vm_state_colour = ansiiprint.red() + else: + vm_state_colour = ansiiprint.blue() + + vm_list_output.append( + '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <37} \ +{vm_state_colour}{vm_state: <8}{end_colour} \ +{vm_memory: <10} {vm_vcpu: <6} \ +{vm_hypervisor: <{vm_hypervisor_length}} \ +{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( + vm_name_length=vm_name_length, + vm_hypervisor_length=vm_hypervisor_length, + vm_migrated_length=vm_migrated_length, + bold='', + end_bold='', + vm_state_colour=vm_state_colour, + end_colour=ansiiprint.end(), + vm_name=vm_name[vm], + vm_uuid=vm_uuid[vm], + vm_state=vm_state[vm], + vm_memory=vm_memory[vm], + vm_vcpu=vm_vcpu[vm], + vm_hypervisor=vm_hypervisor[vm], + vm_migrated=vm_migrated[vm] + ) + ) + + click.echo('\n'.join(sorted(vm_list_output))) + + return True, '' diff --git a/client-common/lib/zkhandler.py b/client-common/lib/zkhandler.py new file mode 100644 index 00000000..2cfcf36f --- /dev/null +++ b/client-common/lib/zkhandler.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +# zkhandler.py - Secure versioned ZooKeeper updates +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import kazoo.client +import lib.ansiiprint + +# Child list function +def listchildren(zk_conn, key): + children = zk_conn.get_children(key) + return children + +# Data read function +def readdata(zk_conn, key): + data_raw = zk_conn.get(key) + data = data_raw[0].decode('ascii') + meta = data_raw[1] + return data + +# Data write function +def writedata(zk_conn, kv): + # Get the current version; we base this off the first key (ordering in multi-key calls is irrelevant) + first_key = list(kv.keys())[0] + orig_data_raw = zk_conn.get(first_key) + meta = orig_data_raw[1] + if meta == None: + ansiiprint.echo('Zookeeper key "{}" does not exist'.format(first_key), '', 'e') + return 1 + + version = meta.version + new_version = version + 1 + zk_transaction = zk_conn.transaction() + for key, data in kv.items(): + zk_transaction.set_data(key, data.encode('ascii')) + try: + zk_transaction.check(first_key, new_version) + except TypeError: + ansiiprint.echo('Zookeeper key "{}" does not match expected version'.format(first_key), '', 'e') + return 1 + zk_transaction.commit() + return 0 +