diff --git a/client-cli/cli_lib/vm.py b/client-cli/cli_lib/vm.py index 2e34d469..6513eeb4 100644 --- a/client-cli/cli_lib/vm.py +++ b/client-cli/cli_lib/vm.py @@ -369,8 +369,8 @@ def format_vm_vcpus(config, name, vcpus): sockets_length=sockets_length, cores_length=cores_length, threads_length=threads_length, - bold=ansiprint.bold(), - end_bold=ansiprint.end(), + bold='', + end_bold='', name=name, vcpus=vcpus[0], sockets=vcpus[1][0], @@ -472,8 +472,8 @@ def format_vm_memory(config, name, memory): {memory: <{memory_length}}{end_bold}'.format( name_length=name_length, memory_length=memory_length, - bold=ansiprint.bold(), - end_bold=ansiprint.end(), + bold='', + end_bold='', name=name, memory=memory ) @@ -481,6 +481,207 @@ def format_vm_memory(config, name, memory): return '\n'.join(output_list) +def vm_networks_add(config, vm, network, macaddr, model, restart): + """ + Add a new network to the VM + + Calls vm_info to get the VM XML. + + Calls vm_modify to set the VM XML. + """ + from lxml.objectify import fromstring + from lxml.etree import tostring + from random import randint + + status, domain_information = vm_info(config, vm) + if not status: + return status, domain_information + + xml = domain_information.get('xml', None) + if xml is None: + return False, "VM does not have a valid XML doccument." + + try: + parsed_xml = fromstring(xml) + except Exception: + return False, 'ERROR: Failed to parse XML data.' + + if macaddr is None: + mac_prefix = '52:54:00' + random_octet_A = '{:x}'.format(randint(16, 238)) + random_octet_B = '{:x}'.format(randint(16, 238)) + random_octet_C = '{:x}'.format(randint(16, 238)) + macaddr = '{prefix}:{octetA}:{octetB}:{octetC}'.format( + prefix=mac_prefix, + octetA=random_octet_A, + octetB=random_octet_B, + octetC=random_octet_C + ) + + device_string = ''.format( + macaddr=macaddr, + bridge="vmbr{}".format(network), + model=model + ) + device_xml = fromstring(device_string) + + last_interface = None + for interface in parsed_xml.devices.find('interface'): + last_interface = re.match(r'vmbr([0-9]+)', interface.source.attrib.get('bridge')).group(1) + if last_interface == network: + return False, 'Network {} is already configured for VM {}.'.format(network, vm) + if last_interface is not None: + for interface in parsed_xml.devices.find('interface'): + if last_interface == re.match(r'vmbr([0-9]+)', interface.source.attrib.get('bridge')).group(1): + interface.addnext(device_xml) + + try: + new_xml = tostring(parsed_xml, pretty_print=True) + except Exception: + return False, 'ERROR: Failed to dump XML data.' + + return vm_modify(config, vm, new_xml, restart) + + +def vm_networks_remove(config, vm, network, restart): + """ + Remove a network to the VM + + Calls vm_info to get the VM XML. + + Calls vm_modify to set the VM XML. + """ + from lxml.objectify import fromstring + from lxml.etree import tostring + + status, domain_information = vm_info(config, vm) + if not status: + return status, domain_information + + xml = domain_information.get('xml', None) + if xml is None: + return False, "VM does not have a valid XML doccument." + + try: + parsed_xml = fromstring(xml) + except Exception: + return False, 'ERROR: Failed to parse XML data.' + + for interface in parsed_xml.devices.find('interface'): + if_vni = re.match(r'vmbr([0-9]+)', interface.source.attrib.get('bridge')).group(1) + if network == if_vni: + interface.getparent().remove(interface) + + try: + new_xml = tostring(parsed_xml, pretty_print=True) + except Exception: + return False, 'ERROR: Failed to dump XML data.' + + return vm_modify(config, vm, new_xml, restart) + + +def vm_networks_get(config, vm): + """ + Get the networks of the VM + + Calls vm_info to get VM XML. + + Returns a tuple of (network, (sockets, cores, threads)) + """ + from lxml.objectify import fromstring + + status, domain_information = vm_info(config, vm) + if not status: + return status, domain_information + + xml = domain_information.get('xml', None) + if xml is None: + return False, "VM does not have a valid XML doccument." + + try: + parsed_xml = fromstring(xml) + except Exception: + return False, 'ERROR: Failed to parse XML data.' + + network_data = list() + for interface in parsed_xml.devices.find('interface'): + mac_address = interface.mac.attrib.get('address') + model = interface.model.attrib.get('type') + network = re.match(r'vmbr([0-9]+)', interface.source.attrib.get('bridge')).group(1) + network_data.append((network, mac_address, model)) + + return True, network_data + + +def format_vm_networks(config, name, networks): + """ + Format the output of a network value in a nice table + """ + output_list = [] + + name_length = 5 + _name_length = len(name) + 1 + if _name_length > name_length: + name_length = _name_length + + for network in networks: + vni_length = 8 + _vni_length = len(network[0]) + 1 + if _vni_length > vni_length: + vni_length = _vni_length + + macaddr_length = 12 + _macaddr_length = len(network[1]) + 1 + if _macaddr_length > macaddr_length: + macaddr_length = _macaddr_length + + model_length = 6 + _model_length = len(network[2]) + 1 + if _model_length > model_length: + model_length = _model_length + + output_list.append( + '{bold}{name: <{name_length}} \ +{vni: <{vni_length}} \ +{macaddr: <{macaddr_length}} \ +{model: <{model_length}}{end_bold}'.format( + name_length=name_length, + vni_length=vni_length, + macaddr_length=macaddr_length, + model_length=model_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + name='Name', + vni='Network', + macaddr='MAC Address', + model='Model' + ) + ) + count = 0 + for network in networks: + if count > 0: + name = '' + count += 1 + output_list.append( + '{bold}{name: <{name_length}} \ +{vni: <{vni_length}} \ +{macaddr: <{macaddr_length}} \ +{model: <{model_length}}{end_bold}'.format( + name_length=name_length, + vni_length=vni_length, + macaddr_length=macaddr_length, + model_length=model_length, + bold='', + end_bold='', + name=name, + vni=network[0], + macaddr=network[1], + model=network[2] + ) + ) + return '\n'.join(output_list) + + def view_console_log(config, vm, lines=100): """ Return console log lines from the API (and display them in a pager in the main CLI) diff --git a/client-cli/pvc.py b/client-cli/pvc.py index 866fa872..32bfd575 100755 --- a/client-cli/pvc.py +++ b/client-cli/pvc.py @@ -1166,11 +1166,95 @@ def vm_memory_set(domain, memory, restart): @click.group(name='network', short_help='Manage attached networks of a virtual machine.', context_settings=CONTEXT_SETTINGS) def vm_network(): """ - Manage the attached networks of a virtual machine in the PVC cluster." + Manage the attached networks of a virtual machine in the PVC cluster. """ pass +############################################################################### +# pvc vm network get +############################################################################### +@click.command(name='get', short_help='Get the networks of a virtual machine.') +@click.argument( + 'domain' +) +@click.option( + '-r', '--raw', 'raw', is_flag=True, default=False, + help='Display the raw values only without formatting.' +) +@cluster_req +def vm_network_get(domain, raw): + """ + Get the networks of the virtual machine DOMAIN. + """ + + retcode, retdata = pvc_vm.vm_networks_get(config, domain) + if not raw: + retmsg = pvc_vm.format_vm_networks(config, domain, retdata) + else: + network_vnis = list() + for network in retdata: + network_vnis.append(network[0]) + retmsg = ','.join(network_vnis) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm network add +############################################################################### +@click.command(name='add', short_help='Add network to a virtual machine.') +@click.argument( + 'domain' +) +@click.argument( + 'vni' +) +@click.option( + '-a', '--macaddr', 'macaddr', default=None, + help='Use this MAC address instead of random generation; must be a valid MAC address in colon-deliniated format.' +) +@click.option( + '-m', '--model', 'model', default='virtio', + help='The model for the interface; must be a valid libvirt model.' +) +@click.option( + '-r', '--restart', 'restart', is_flag=True, default=False, + help='Immediately restart VM to apply new config.' +) +@cluster_req +def vm_network_add(domain, vni, macaddr, model, restart): + """ + Add the network VNI to the virtual machine DOMAIN. Networks are always addded to the end of the current list of networks in the virtual machine. + """ + + retcode, retmsg = pvc_vm.vm_networks_add(config, domain, vni, macaddr, model, restart) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm network remove +############################################################################### +@click.command(name='remove', short_help='Remove network from a virtual machine.') +@click.argument( + 'domain' +) +@click.argument( + 'vni' +) +@click.option( + '-r', '--restart', 'restart', is_flag=True, default=False, + help='Immediately restart VM to apply new config.' +) +@cluster_req +def vm_network_remove(domain, vni, restart): + """ + Remove the network VNI to the virtual machine DOMAIN. + """ + + retcode, retmsg = pvc_vm.vm_networks_remove(config, domain, vni, restart) + cleanup(retcode, retmsg) + + ############################################################################### # pvc vm volume ############################################################################### @@ -3995,10 +4079,9 @@ vm_vcpu.add_command(vm_vcpu_set) vm_memory.add_command(vm_memory_get) vm_memory.add_command(vm_memory_set) -# vm_network.add_command(vm_network_list) -# vm_network.add_command(vm_network_add) -# vm_network.add_command(vm_network_modify) -# vm_network_add_command(vm_network_remove) +vm_network.add_command(vm_network_get) +vm_network.add_command(vm_network_add) +vm_network.add_command(vm_network_remove) # vm_volume.add_command(vm_volume_list) # vm_volume.add_command(vm_volume_add)