diff --git a/.hooks/pre-commit b/.hooks/pre-commit index 957a0411..b32d74be 100755 --- a/.hooks/pre-commit +++ b/.hooks/pre-commit @@ -5,6 +5,7 @@ pushd $( git rev-parse --show-toplevel ) &>/dev/null ex=0 # Linting +echo -n "Linting... " ./lint if [[ $? -ne 0 ]]; then echo "Aborting commit due to linting errors." diff --git a/api-daemon/pvcapid/libvirt_schema.py b/api-daemon/pvcapid/libvirt_schema.py index 7b79b225..87559e68 100755 --- a/api-daemon/pvcapid/libvirt_schema.py +++ b/api-daemon/pvcapid/libvirt_schema.py @@ -119,7 +119,7 @@ devices_disk_footer = """ # vhostmd virtualization passthrough device devices_vhostmd = """ - + diff --git a/client-cli/cli_lib/vm.py b/client-cli/cli_lib/vm.py index 7c61f692..efa7e125 100644 --- a/client-cli/cli_lib/vm.py +++ b/client-cli/cli_lib/vm.py @@ -254,6 +254,709 @@ def vm_locks(config, vm): return retstatus, response.json().get('message', '') +def vm_vcpus_set(config, vm, vcpus, topology, restart): + """ + Set the vCPU count of the VM with topology + + 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.' + + parsed_xml.vcpu._setText(str(vcpus)) + parsed_xml.cpu.topology.set('sockets', str(topology[0])) + parsed_xml.cpu.topology.set('cores', str(topology[1])) + parsed_xml.cpu.topology.set('threads', str(topology[2])) + + 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_vcpus_get(config, vm): + """ + Get the vCPU count of the VM + + Calls vm_info to get VM XML. + + Returns a tuple of (vcpus, (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.' + + vm_vcpus = int(parsed_xml.vcpu.text) + vm_sockets = parsed_xml.cpu.topology.attrib.get('sockets') + vm_cores = parsed_xml.cpu.topology.attrib.get('cores') + vm_threads = parsed_xml.cpu.topology.attrib.get('threads') + + return True, (vm_vcpus, (vm_sockets, vm_cores, vm_threads)) + + +def format_vm_vcpus(config, name, vcpus): + """ + Format the output of a vCPU value in a nice table + """ + output_list = [] + + name_length = 5 + _name_length = len(name) + 1 + if _name_length > name_length: + name_length = _name_length + + vcpus_length = 6 + sockets_length = 8 + cores_length = 6 + threads_length = 8 + + output_list.append( + '{bold}{name: <{name_length}} \ +{vcpus: <{vcpus_length}} \ +{sockets: <{sockets_length}} \ +{cores: <{cores_length}} \ +{threads: <{threads_length}}{end_bold}'.format( + name_length=name_length, + vcpus_length=vcpus_length, + sockets_length=sockets_length, + cores_length=cores_length, + threads_length=threads_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + name='Name', + vcpus='vCPUs', + sockets='Sockets', + cores='Cores', + threads='Threads' + ) + ) + output_list.append( + '{bold}{name: <{name_length}} \ +{vcpus: <{vcpus_length}} \ +{sockets: <{sockets_length}} \ +{cores: <{cores_length}} \ +{threads: <{threads_length}}{end_bold}'.format( + name_length=name_length, + vcpus_length=vcpus_length, + sockets_length=sockets_length, + cores_length=cores_length, + threads_length=threads_length, + bold='', + end_bold='', + name=name, + vcpus=vcpus[0], + sockets=vcpus[1][0], + cores=vcpus[1][1], + threads=vcpus[1][2] + ) + ) + return '\n'.join(output_list) + + +def vm_memory_set(config, vm, memory, restart): + """ + Set the provisioned memory of the VM with topology + + 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.' + + parsed_xml.memory._setText(str(memory)) + + 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_memory_get(config, vm): + """ + Get the provisioned memory of the VM + + Calls vm_info to get VM XML. + + Returns an integer memory value. + """ + 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.' + + vm_memory = int(parsed_xml.memory.text) + + return True, vm_memory + + +def format_vm_memory(config, name, memory): + """ + Format the output of a memory value in a nice table + """ + output_list = [] + + name_length = 5 + _name_length = len(name) + 1 + if _name_length > name_length: + name_length = _name_length + + memory_length = 6 + + output_list.append( + '{bold}{name: <{name_length}} \ +{memory: <{memory_length}}{end_bold}'.format( + name_length=name_length, + memory_length=memory_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + name='Name', + memory='RAM (M)' + ) + ) + output_list.append( + '{bold}{name: <{name_length}} \ +{memory: <{memory_length}}{end_bold}'.format( + name_length=name_length, + memory_length=memory_length, + bold='', + end_bold='', + name=name, + memory=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 + import cli_lib.network as pvc_network + + # Verify that the provided network is valid + retcode, retdata = pvc_network.net_info(config, network) + if not retcode: + # Ignore the three special networks + if network not in ['upstream', 'cluster', 'storage']: + return False, "Network {} is not present in the cluster.".format(network) + + if network in ['upstream', 'cluster', 'storage']: + br_prefix = 'br' + else: + br_prefix = 'vmbr' + + 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="{}{}".format(br_prefix, network), + model=model + ) + device_xml = fromstring(device_string) + + last_interface = None + for interface in parsed_xml.devices.find('interface'): + last_interface = re.match(r'[vm]*br([0-9a-z]+)', 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'[vm]*br([0-9a-z]+)', 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'[vm]*br([0-9a-z]+)', 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 list of tuples of (network_vni, mac_address, model) + """ + 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'[vm]*br([0-9a-z]+)', 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 list in a nice table + """ + output_list = [] + + name_length = 5 + vni_length = 8 + macaddr_length = 12 + model_length = 6 + + _name_length = len(name) + 1 + if _name_length > name_length: + name_length = _name_length + + for network in networks: + _vni_length = len(network[0]) + 1 + if _vni_length > vni_length: + vni_length = _vni_length + + _macaddr_length = len(network[1]) + 1 + if _macaddr_length > macaddr_length: + macaddr_length = _macaddr_length + + _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 vm_volumes_add(config, vm, volume, disk_id, bus, disk_type, restart): + """ + Add a new volume 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 copy import deepcopy + import cli_lib.ceph as pvc_ceph + + if disk_type == 'rbd': + # Verify that the provided volume is valid + vpool = volume.split('/')[0] + vname = volume.split('/')[1] + retcode, retdata = pvc_ceph.ceph_volume_info(config, vpool, vname) + if not retcode: + return False, "Volume {} is not present in the cluster.".format(volume) + + 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.' + + last_disk = None + id_list = list() + for disk in parsed_xml.devices.find('disk'): + id_list.append(disk.target.attrib.get('dev')) + if disk.source.attrib.get('protocol') == disk_type: + if disk_type == 'rbd': + last_disk = disk.source.attrib.get('name') + elif disk_type == 'file': + last_disk = disk.source.attrib.get('file') + if last_disk == volume: + return False, 'Volume {} is already configured for VM {}.'.format(volume, vm) + last_disk_details = deepcopy(disk) + + if disk_id is not None: + if disk_id in id_list: + return False, 'Manually specified disk ID {} is already in use for VM {}.'.format(disk_id, vm) + else: + # Find the next free disk ID + first_dev_prefix = id_list[0][0:-1] + + for char in range(ord('a'), ord('z')): + char = chr(char) + next_id = "{}{}".format(first_dev_prefix, char) + if next_id not in id_list: + break + else: + next_id = None + if next_id is None: + return False, 'Failed to find a valid disk_id and none specified; too many disks for VM {}?'.format(vm) + disk_id = next_id + + if last_disk is None: + if disk_type == 'rbd': + # RBD volumes need an example to be based on + return False, "There are no existing RBD volumes attached to this VM. Autoconfiguration failed; use the 'vm modify' command to manually configure this volume with the required details for authentication, hosts, etc.." + elif disk_type == 'file': + # File types can be added ad-hoc + disk_template = ''.format( + source=volume, + dev=disk_id, + bus=bus + ) + last_disk_details = fromstring(disk_template) + + new_disk_details = last_disk_details + new_disk_details.target.set('dev', disk_id) + new_disk_details.target.set('bus', bus) + if disk_type == 'rbd': + new_disk_details.source.set('name', volume) + elif disk_type == 'file': + new_disk_details.source.set('file', volume) + + for disk in parsed_xml.devices.find('disk'): + last_disk = disk + last_disk.addnext(new_disk_details) + + 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_volumes_remove(config, vm, volume, restart): + """ + Remove a volume 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 disk in parsed_xml.devices.find('disk'): + disk_name = disk.source.attrib.get('name') + if not disk_name: + disk_name = disk.source.attrib.get('file') + if volume == disk_name: + disk.getparent().remove(disk) + + 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_volumes_get(config, vm): + """ + Get the volumes of the VM + + Calls vm_info to get VM XML. + + Returns a list of tuples of (volume, disk_id, type, bus) + """ + 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.' + + volume_data = list() + for disk in parsed_xml.devices.find('disk'): + protocol = disk.attrib.get('type') + disk_id = disk.target.attrib.get('dev') + bus = disk.target.attrib.get('bus') + if protocol == 'network': + protocol = disk.source.attrib.get('protocol') + source = disk.source.attrib.get('name') + elif protocol == 'file': + protocol = 'file' + source = disk.source.attrib.get('file') + else: + protocol = 'unknown' + source = 'unknown' + + volume_data.append((source, disk_id, protocol, bus)) + + return True, volume_data + + +def format_vm_volumes(config, name, volumes): + """ + Format the output of a volume value in a nice table + """ + output_list = [] + + name_length = 5 + volume_length = 7 + disk_id_length = 4 + protocol_length = 5 + bus_length = 4 + + _name_length = len(name) + 1 + if _name_length > name_length: + name_length = _name_length + + for volume in volumes: + _volume_length = len(volume[0]) + 1 + if _volume_length > volume_length: + volume_length = _volume_length + + _disk_id_length = len(volume[1]) + 1 + if _disk_id_length > disk_id_length: + disk_id_length = _disk_id_length + + _protocol_length = len(volume[2]) + 1 + if _protocol_length > protocol_length: + protocol_length = _protocol_length + + _bus_length = len(volume[3]) + 1 + if _bus_length > bus_length: + bus_length = _bus_length + + output_list.append( + '{bold}{name: <{name_length}} \ +{volume: <{volume_length}} \ +{disk_id: <{disk_id_length}} \ +{protocol: <{protocol_length}} \ +{bus: <{bus_length}}{end_bold}'.format( + name_length=name_length, + volume_length=volume_length, + disk_id_length=disk_id_length, + protocol_length=protocol_length, + bus_length=bus_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + name='Name', + volume='Volume', + disk_id='Dev', + protocol='Type', + bus='Bus' + ) + ) + count = 0 + for volume in volumes: + if count > 0: + name = '' + count += 1 + output_list.append( + '{bold}{name: <{name_length}} \ +{volume: <{volume_length}} \ +{disk_id: <{disk_id_length}} \ +{protocol: <{protocol_length}} \ +{bus: <{bus_length}}{end_bold}'.format( + name_length=name_length, + volume_length=volume_length, + disk_id_length=disk_id_length, + protocol_length=protocol_length, + bus_length=bus_length, + bold='', + end_bold='', + name=name, + volume=volume[0], + disk_id=volume[1], + protocol=volume[2], + bus=volume[3] + ) + ) + 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) @@ -484,7 +1187,7 @@ def format_info(config, domain_information, long_output): ainformation.append('') ainformation.append('{}Controllers:{} {}ID Type Model{}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), ansiprint.end())) for controller in domain_information['controllers']: - ainformation.append(' {0: <3} {1: <14} {2: <8}'.format(domain_information['controllers'].index(controller), controller['type'], controller['model'])) + ainformation.append(' {0: <3} {1: <14} {2: <8}'.format(domain_information['controllers'].index(controller), controller['type'], str(controller['model']))) # Join it all together ainformation.append('') diff --git a/client-cli/pvc.py b/client-cli/pvc.py index d5ba4caf..7c0afc77 100755 --- a/client-cli/pvc.py +++ b/client-cli/pvc.py @@ -1025,6 +1025,343 @@ def vm_flush_locks(domain): cleanup(retcode, retmsg) +############################################################################### +# pvc vm vcpu +############################################################################### +@click.group(name='vcpu', short_help='Manage vCPU counts of a virtual machine.', context_settings=CONTEXT_SETTINGS) +def vm_vcpu(): + """ + Manage the vCPU counts of a virtual machine in the PVC cluster." + """ + pass + + +############################################################################### +# pvc vm vcpu get +############################################################################### +@click.command(name='get', short_help='Get the current vCPU count of a virtual machine.') +@click.argument( + 'domain' +) +@click.option( + '-r', '--raw', 'raw', is_flag=True, default=False, + help='Display the raw value only without formatting.' +) +@cluster_req +def vm_vcpu_get(domain, raw): + """ + Get the current vCPU count of the virtual machine DOMAIN. + """ + + retcode, retmsg = pvc_vm.vm_vcpus_get(config, domain) + if not raw: + retmsg = pvc_vm.format_vm_vcpus(config, domain, retmsg) + else: + retmsg = retmsg[0] # Get only the first part of the tuple (vm_vcpus) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm vcpu set +############################################################################### +@click.command(name='set', short_help='Set the vCPU count of a virtual machine.') +@click.argument( + 'domain' +) +@click.argument( + 'vcpus' +) +@click.option( + '-t', '--topology', 'topology', default=None, + help='Use an alternative topology for the vCPUs in the CSV form ,,. SxCxT must equal VCPUS.' +) +@click.option( + '-r', '--restart', 'restart', is_flag=True, default=False, + help='Immediately restart VM to apply new config.' +) +@cluster_req +def vm_vcpu_set(domain, vcpus, topology, restart): + """ + Set the vCPU count of the virtual machine DOMAIN to VCPUS. + + By default, the topology of the vCPus is 1 socket, VCPUS cores per socket, 1 thread per core. + """ + + if topology is not None: + try: + sockets, cores, threads = topology.split(',') + if sockets * cores * threads != vcpus: + raise + except Exception: + cleanup(False, "The topology specified is not valid.") + topology = (sockets, cores, threads) + else: + topology = (1, vcpus, 1) + + retcode, retmsg = pvc_vm.vm_vcpus_set(config, domain, vcpus, topology, restart) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm memory +############################################################################### +@click.group(name='memory', short_help='Manage provisioned memory of a virtual machine.', context_settings=CONTEXT_SETTINGS) +def vm_memory(): + """ + Manage the provisioned memory of a virtual machine in the PVC cluster." + """ + pass + + +############################################################################### +# pvc vm memory get +############################################################################### +@click.command(name='get', short_help='Get the current provisioned memory of a virtual machine.') +@click.argument( + 'domain' +) +@click.option( + '-r', '--raw', 'raw', is_flag=True, default=False, + help='Display the raw value only without formatting.' +) +@cluster_req +def vm_memory_get(domain, raw): + """ + Get the current provisioned memory of the virtual machine DOMAIN. + """ + + retcode, retmsg = pvc_vm.vm_memory_get(config, domain) + if not raw: + retmsg = pvc_vm.format_vm_memory(config, domain, retmsg) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm memory set +############################################################################### +@click.command(name='set', short_help='Set the provisioned memory of a virtual machine.') +@click.argument( + 'domain' +) +@click.argument( + 'memory' +) +@click.option( + '-r', '--restart', 'restart', is_flag=True, default=False, + help='Immediately restart VM to apply new config.' +) +@cluster_req +def vm_memory_set(domain, memory, restart): + """ + Set the provisioned memory of the virtual machine DOMAIN to MEMORY; MEMORY must be an integer in MB. + """ + + retcode, retmsg = pvc_vm.vm_memory_set(config, domain, memory, restart) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm network +############################################################################### +@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. + + Network details cannot be modified here. To modify a network, first remove it, then readd it with the correct settings. Unless the '-r'/'--reboot' flag is provided, this will not affect the running VM until it is restarted. + """ + 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 +############################################################################### +@click.group(name='volume', short_help='Manage attached volumes of a virtual machine.', context_settings=CONTEXT_SETTINGS) +def vm_volume(): + """ + Manage the attached volumes of a virtual machine in the PVC cluster. + + Volume details cannot be modified here. To modify a volume, first remove it, then readd it with the correct settings. Unless the '-r'/'--reboot' flag is provided, this will not affect the running VM until it is restarted. + """ + pass + + +############################################################################### +# pvc vm volume get +############################################################################### +@click.command(name='get', short_help='Get the volumes 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_volume_get(domain, raw): + """ + Get the volumes of the virtual machine DOMAIN. + """ + + retcode, retdata = pvc_vm.vm_volumes_get(config, domain) + if not raw: + retmsg = pvc_vm.format_vm_volumes(config, domain, retdata) + else: + volume_paths = list() + for volume in retdata: + volume_paths.append("{}:{}".format(volume[2], volume[0])) + retmsg = ','.join(volume_paths) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm volume add +############################################################################### +@click.command(name='add', short_help='Add volume to a virtual machine.') +@click.argument( + 'domain' +) +@click.argument( + 'volume' +) +@click.option( + '-d', '--disk-id', 'disk_id', default=None, + help='The disk ID in sdX/vdX/hdX format; if not specified, the next available will be used.' +) +@click.option( + '-b', '--bus', 'bus', default='scsi', show_default=True, + type=click.Choice(['scsi', 'ide', 'usb', 'virtio']), + help='The bus to attach the disk to; must be present in the VM.' +) +@click.option( + '-t', '--type', 'disk_type', default='rbd', show_default=True, + type=click.Choice(['rbd', 'file']), + help='The type of volume to add.' +) +@click.option( + '-r', '--restart', 'restart', is_flag=True, default=False, + help='Immediately restart VM to apply new config.' +) +@cluster_req +def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart): + """ + Add the volume VOLUME to the virtual machine DOMAIN. + + VOLUME may be either an absolute file path (for type 'file') or an RBD volume in the form "pool/volume" (for type 'rbd'). RBD volumes are verified against the cluster before adding and must exist. + """ + + retcode, retmsg = pvc_vm.vm_volumes_add(config, domain, volume, disk_id, bus, disk_type, restart) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm volume remove +############################################################################### +@click.command(name='remove', short_help='Remove volume 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_volume_remove(domain, vni, restart): + """ + Remove the volume VNI to the virtual machine DOMAIN. + """ + + retcode, retmsg = pvc_vm.vm_volumes_remove(config, domain, vni, restart) + cleanup(retcode, retmsg) + + ############################################################################### # pvc vm log ############################################################################### @@ -3832,6 +4169,20 @@ cli_node.add_command(node_unflush) cli_node.add_command(node_info) cli_node.add_command(node_list) +vm_vcpu.add_command(vm_vcpu_get) +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_get) +vm_network.add_command(vm_network_add) +vm_network.add_command(vm_network_remove) + +vm_volume.add_command(vm_volume_get) +vm_volume.add_command(vm_volume_add) +vm_volume.add_command(vm_volume_remove) + cli_vm.add_command(vm_define) cli_vm.add_command(vm_meta) cli_vm.add_command(vm_modify) @@ -3847,6 +4198,10 @@ cli_vm.add_command(vm_move) cli_vm.add_command(vm_migrate) cli_vm.add_command(vm_unmigrate) cli_vm.add_command(vm_flush_locks) +cli_vm.add_command(vm_vcpu) +cli_vm.add_command(vm_memory) +cli_vm.add_command(vm_network) +cli_vm.add_command(vm_volume) cli_vm.add_command(vm_info) cli_vm.add_command(vm_log) cli_vm.add_command(vm_list) diff --git a/daemon-common/vm.py b/daemon-common/vm.py index bb9191bf..c3e076b8 100644 --- a/daemon-common/vm.py +++ b/daemon-common/vm.py @@ -285,7 +285,7 @@ def modify_vm(zk_conn, domain, restart, new_vm_config): zkhandler.writedata(zk_conn, {'/domains/{}/state'.format(dom_uuid): 'restart'}) lock.release() - return True, '' + return True, 'Successfully modified configuration of VM "{}".'.format(domain) def dump_vm(zk_conn, domain):