Add VM device hot attach/detach support

Adds a new API endpoint to support hot attach/detach of devices, and the
corresponding client-side logic to use this endpoint when doing VM
network/storage add/remove actions.

The live attach is now the default behaviour for these types of
additions and removals, and can be disabled if needed.

Closes #141
This commit is contained in:
Joshua Boniface 2021-09-12 15:41:05 -04:00
parent 46f1d761f6
commit e962743e51
6 changed files with 500 additions and 83 deletions

View File

@ -2003,6 +2003,72 @@ class API_VM_Rename(Resource):
api.add_resource(API_VM_Rename, '/vm/<vm>/rename') api.add_resource(API_VM_Rename, '/vm/<vm>/rename')
# /vm/<vm>/device
class API_VM_Device(Resource):
@RequestParser([
{'name': 'xml', 'required': True, 'helptext': "A Libvirt XML device document must be specified"},
])
@Authenticator
def post(self, vm, reqargs):
"""
Hot-attach device XML to {vm}
---
tags:
- vm
parameters:
- in: query
name: xml
type: string
required: true
description: The raw Libvirt XML definition of the device to attach
responses:
200:
description: OK
schema:
type: object
id: Message
400:
description: Bad request
schema:
type: object
id: Message
"""
return api_helper.vm_attach_device(vm, reqargs.get('xml', None))
@RequestParser([
{'name': 'xml', 'required': True, 'helptext': "A Libvirt XML device document must be specified"},
])
@Authenticator
def delete(self, vm, reqargs):
"""
Hot-detach device XML to {vm}
---
tags:
- vm
parameters:
- in: query
name: xml
type: string
required: true
description: The raw Libvirt XML definition of the device to detach
responses:
200:
description: OK
schema:
type: object
id: Message
400:
description: Bad request
schema:
type: object
id: Message
"""
return api_helper.vm_detach_device(vm, reqargs.get('xml', None))
api.add_resource(API_VM_Device, '/vm/<vm>/device')
########################################################## ##########################################################
# Client API - Network # Client API - Network
########################################################## ##########################################################

View File

@ -491,6 +491,58 @@ def vm_define(zkhandler, xml, node, limit, selector, autostart, migration_method
return output, retcode return output, retcode
@ZKConnection(config)
def vm_attach_device(zkhandler, vm, device_spec_xml):
"""
Hot-attach a device (via XML spec) to a VM.
"""
try:
_ = etree.fromstring(device_spec_xml)
except Exception as e:
return {'message': 'XML is malformed or incorrect: {}'.format(e)}, 400
retflag, retdata = pvc_vm.attach_vm_device(zkhandler, vm, device_spec_xml)
if retflag:
retcode = 200
output = {
'message': retdata.replace('\"', '\'')
}
else:
retcode = 400
output = {
'message': 'WARNING: Failed to perform hot attach; device will be added on next VM start/restart.'
}
return output, retcode
@ZKConnection(config)
def vm_detach_device(zkhandler, vm, device_spec_xml):
"""
Hot-detach a device (via XML spec) from a VM.
"""
try:
_ = etree.fromstring(device_spec_xml)
except Exception as e:
return {'message': 'XML is malformed or incorrect: {}'.format(e)}, 400
retflag, retdata = pvc_vm.detach_vm_device(zkhandler, vm, device_spec_xml)
if retflag:
retcode = 200
output = {
'message': retdata.replace('\"', '\'')
}
else:
retcode = 400
output = {
'message': 'WARNING: Failed to perform hot detach; device will be removed on next VM start/restart.'
}
return output, retcode
@pvc_common.Profiler(config) @pvc_common.Profiler(config)
@ZKConnection(config) @ZKConnection(config)
def get_vm_meta(zkhandler, vm): def get_vm_meta(zkhandler, vm):

View File

@ -134,6 +134,48 @@ def vm_modify(config, vm, xml, restart):
return retstatus, response.json().get('message', '') return retstatus, response.json().get('message', '')
def vm_device_attach(config, vm, xml):
"""
Attach a device to a VM
API endpoint: POST /vm/{vm}/device
API arguments: xml={xml}
API schema: {"message":"{data}"}
"""
data = {
'xml': xml
}
response = call_api(config, 'post', '/vm/{vm}/device'.format(vm=vm), data=data)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json().get('message', '')
def vm_device_detach(config, vm, xml):
"""
Detach a device from a VM
API endpoint: DELETE /vm/{vm}/device
API arguments: xml={xml}
API schema: {"message":"{data}"}
"""
data = {
'xml': xml
}
response = call_api(config, 'delete', '/vm/{vm}/device'.format(vm=vm), data=data)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json().get('message', '')
def vm_rename(config, vm, new_name): def vm_rename(config, vm, new_name):
""" """
Rename VM to new name Rename VM to new name
@ -618,13 +660,15 @@ def format_vm_memory(config, name, memory):
return '\n'.join(output_list) return '\n'.join(output_list)
def vm_networks_add(config, vm, network, macaddr, model, sriov, sriov_mode, restart): def vm_networks_add(config, vm, network, macaddr, model, sriov, sriov_mode, live, restart):
""" """
Add a new network to the VM Add a new network to the VM
Calls vm_info to get the VM XML. Calls vm_info to get the VM XML.
Calls vm_modify to set the VM XML. Calls vm_modify to set the VM XML.
Calls vm_device_attach if live to hot-attach the device.
""" """
from lxml.objectify import fromstring from lxml.objectify import fromstring
from lxml.etree import tostring from lxml.etree import tostring
@ -747,16 +791,36 @@ def vm_networks_add(config, vm, network, macaddr, model, sriov, sriov_mode, rest
except Exception: except Exception:
return False, 'ERROR: Failed to dump XML data.' return False, 'ERROR: Failed to dump XML data.'
return vm_modify(config, vm, new_xml, restart) modify_retcode, modify_retmsg = vm_modify(config, vm, new_xml, restart)
if not modify_retcode:
return modify_retcode, modify_retmsg
if live:
attach_retcode, attach_retmsg = vm_device_attach(config, vm, device_string)
if not attach_retcode:
retcode = attach_retcode
retmsg = attach_retmsg
else:
retcode = attach_retcode
retmsg = "Network '{}' successfully added to VM config and hot attached to running VM.".format(network)
else:
retcode = modify_retcode
retmsg = modify_retmsg
return retcode, retmsg
def vm_networks_remove(config, vm, network, sriov, restart): def vm_networks_remove(config, vm, network, sriov, live, restart):
""" """
Remove a network to the VM Remove a network to the VM
Calls vm_info to get the VM XML. Calls vm_info to get the VM XML.
Calls vm_modify to set the VM XML. Calls vm_modify to set the VM XML.
Calls vm_device_detach to hot-remove the device.
""" """
from lxml.objectify import fromstring from lxml.objectify import fromstring
from lxml.etree import tostring from lxml.etree import tostring
@ -775,6 +839,7 @@ def vm_networks_remove(config, vm, network, sriov, restart):
return False, 'ERROR: Failed to parse XML data.' return False, 'ERROR: Failed to parse XML data.'
changed = False changed = False
device_string = None
for interface in parsed_xml.devices.find('interface'): for interface in parsed_xml.devices.find('interface'):
if sriov: if sriov:
if interface.attrib.get('type') == 'hostdev': if interface.attrib.get('type') == 'hostdev':
@ -792,16 +857,37 @@ def vm_networks_remove(config, vm, network, sriov, restart):
if network == if_vni: if network == if_vni:
interface.getparent().remove(interface) interface.getparent().remove(interface)
changed = True changed = True
if changed:
device_string = tostring(interface)
if changed: if changed:
try: try:
new_xml = tostring(parsed_xml, pretty_print=True) new_xml = tostring(parsed_xml, pretty_print=True)
except Exception: except Exception:
return False, 'ERROR: Failed to dump XML data.' return False, 'ERROR: Failed to dump XML data.'
return vm_modify(config, vm, new_xml, restart)
else: else:
return False, 'ERROR: Network "{}" does not exist on VM.'.format(network) return False, 'ERROR: Network "{}" does not exist on VM.'.format(network)
modify_retcode, modify_retmsg = vm_modify(config, vm, new_xml, restart)
if not modify_retcode:
return modify_retcode, modify_retmsg
if live and device_string:
detach_retcode, detach_retmsg = vm_device_detach(config, vm, device_string)
if not detach_retcode:
retcode = detach_retcode
retmsg = detach_retmsg
else:
retcode = detach_retcode
retmsg = "Network '{}' successfully removed from VM config and hot detached from running VM.".format(network)
else:
retcode = modify_retcode
retmsg = modify_retmsg
return retcode, retmsg
def vm_networks_get(config, vm): def vm_networks_get(config, vm):
""" """
@ -913,7 +999,7 @@ def format_vm_networks(config, name, networks):
return '\n'.join(output_list) return '\n'.join(output_list)
def vm_volumes_add(config, vm, volume, disk_id, bus, disk_type, restart): def vm_volumes_add(config, vm, volume, disk_id, bus, disk_type, live, restart):
""" """
Add a new volume to the VM Add a new volume to the VM
@ -1001,6 +1087,7 @@ def vm_volumes_add(config, vm, volume, disk_id, bus, disk_type, restart):
new_disk_details.source.set('name', volume) new_disk_details.source.set('name', volume)
elif disk_type == 'file': elif disk_type == 'file':
new_disk_details.source.set('file', volume) new_disk_details.source.set('file', volume)
device_xml = new_disk_details
all_disks = parsed_xml.devices.find('disk') all_disks = parsed_xml.devices.find('disk')
if all_disks is None: if all_disks is None:
@ -1008,18 +1095,42 @@ def vm_volumes_add(config, vm, volume, disk_id, bus, disk_type, restart):
for disk in all_disks: for disk in all_disks:
last_disk = disk last_disk = disk
if last_disk is None: # Add the disk at the end of the list (or, right above emulator)
parsed_xml.devices.find('emulator').addprevious(new_disk_details) if len(all_disks) > 0:
for idx, disk in enumerate(parsed_xml.devices.find('disk')):
if idx == len(all_disks) - 1:
disk.addnext(device_xml)
else:
parsed_xml.devices.find('emulator').addprevious(device_xml)
try: try:
new_xml = tostring(parsed_xml, pretty_print=True) new_xml = tostring(parsed_xml, pretty_print=True)
except Exception: except Exception:
return False, 'ERROR: Failed to dump XML data.' return False, 'ERROR: Failed to dump XML data.'
return vm_modify(config, vm, new_xml, restart) modify_retcode, modify_retmsg = vm_modify(config, vm, new_xml, restart)
if not modify_retcode:
return modify_retcode, modify_retmsg
if live:
device_string = tostring(device_xml)
attach_retcode, attach_retmsg = vm_device_attach(config, vm, device_string)
if not attach_retcode:
retcode = attach_retcode
retmsg = attach_retmsg
else:
retcode = attach_retcode
retmsg = "Volume '{}/{}' successfully added to VM config and hot attached to running VM.".format(vpool, vname)
else:
retcode = modify_retcode
retmsg = modify_retmsg
return retcode, retmsg
def vm_volumes_remove(config, vm, volume, restart): def vm_volumes_remove(config, vm, volume, live, restart):
""" """
Remove a volume to the VM Remove a volume to the VM
@ -1043,19 +1154,44 @@ def vm_volumes_remove(config, vm, volume, restart):
except Exception: except Exception:
return False, 'ERROR: Failed to parse XML data.' return False, 'ERROR: Failed to parse XML data.'
changed = False
device_string = None
for disk in parsed_xml.devices.find('disk'): for disk in parsed_xml.devices.find('disk'):
disk_name = disk.source.attrib.get('name') disk_name = disk.source.attrib.get('name')
if not disk_name: if not disk_name:
disk_name = disk.source.attrib.get('file') disk_name = disk.source.attrib.get('file')
if volume == disk_name: if volume == disk_name:
device_string = tostring(disk)
disk.getparent().remove(disk) disk.getparent().remove(disk)
changed = True
try: if changed:
new_xml = tostring(parsed_xml, pretty_print=True) try:
except Exception: new_xml = tostring(parsed_xml, pretty_print=True)
return False, 'ERROR: Failed to dump XML data.' except Exception:
return False, 'ERROR: Failed to dump XML data.'
else:
return False, 'ERROR: Volume "{}" does not exist on VM.'.format(volume)
return vm_modify(config, vm, new_xml, restart) modify_retcode, modify_retmsg = vm_modify(config, vm, new_xml, restart)
if not modify_retcode:
return modify_retcode, modify_retmsg
if live and device_string:
detach_retcode, detach_retmsg = vm_device_detach(config, vm, device_string)
if not detach_retcode:
retcode = detach_retcode
retmsg = detach_retmsg
else:
retcode = detach_retcode
retmsg = "Volume '{}' successfully removed from VM config and hot detached from running VM.".format(volume)
else:
retcode = modify_retcode
retmsg = modify_retmsg
return retcode, retmsg
def vm_volumes_get(config, vm): def vm_volumes_get(config, vm):

View File

@ -1472,7 +1472,7 @@ def vm_network_get(domain, raw):
help='The model for the interface; must be a valid libvirt model. Not used for "netdev" SR-IOV NETs.' help='The model for the interface; must be a valid libvirt model. Not used for "netdev" SR-IOV NETs.'
) )
@click.option( @click.option(
'-s', '--sriov', 'sriov', is_flag=True, default=False, '-s', '--sriov', 'sriov_flag', is_flag=True, default=False,
help='Identify that NET is an SR-IOV device name and not a VNI. Required for adding SR-IOV NETs.' help='Identify that NET is an SR-IOV device name and not a VNI. Required for adding SR-IOV NETs.'
) )
@click.option( @click.option(
@ -1481,16 +1481,20 @@ def vm_network_get(domain, raw):
help='For SR-IOV NETs, the SR-IOV network device mode.' help='For SR-IOV NETs, the SR-IOV network device mode.'
) )
@click.option( @click.option(
'-r', '--restart', 'restart', is_flag=True, default=False, '-l/-L', '--live/--no-live', 'live_flag', is_flag=True, default=True,
help='Immediately restart VM to apply new config.' help='Immediately live-attach device to VM [default] or disable this behaviour.'
)
@click.option(
'-r', '--restart', 'restart_flag', is_flag=True, default=False,
help='Immediately restart VM to apply new config; requires "--no-live".'
) )
@click.option( @click.option(
'-y', '--yes', 'confirm_flag', '-y', '--yes', 'confirm_flag',
is_flag=True, default=False, is_flag=True, default=False,
help='Confirm the restart' help='Confirm the VM restart.'
) )
@cluster_req @cluster_req
def vm_network_add(domain, net, macaddr, model, sriov, sriov_mode, restart, confirm_flag): def vm_network_add(domain, net, macaddr, model, sriov_flag, sriov_mode, live_flag, restart_flag, confirm_flag):
""" """
Add the network NET to the virtual machine DOMAIN. Networks are always addded to the end of the current list of networks in the virtual machine. Add the network NET to the virtual machine DOMAIN. Networks are always addded to the end of the current list of networks in the virtual machine.
@ -1503,15 +1507,17 @@ def vm_network_add(domain, net, macaddr, model, sriov, sriov_mode, restart, conf
2. If an identical SR-IOV VF device is not present on the target node, post-migration startup will fail. It may be prudent to use a node limit here. 2. If an identical SR-IOV VF device is not present on the target node, post-migration startup will fail. It may be prudent to use a node limit here.
""" """
if restart and not confirm_flag and not config['unsafe']: if restart_flag and live_flag:
click.echo('WARNING: Live flag and restart flag both specified; this can cause unintended behaviour. To disable live changes, use "--no-live".')
exit(1)
if restart_flag and not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True) click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception: except Exception:
restart = False restart_flag = False
retcode, retmsg = pvc_vm.vm_networks_add(config, domain, net, macaddr, model, sriov, sriov_mode, restart) retcode, retmsg = pvc_vm.vm_networks_add(config, domain, net, macaddr, model, sriov_flag, sriov_mode, live_flag, restart_flag)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1526,34 +1532,40 @@ def vm_network_add(domain, net, macaddr, model, sriov, sriov_mode, restart, conf
'net' 'net'
) )
@click.option( @click.option(
'-s', '--sriov', 'sriov', is_flag=True, default=False, '-s', '--sriov', 'sriov_flag', is_flag=True, default=False,
help='Identify that NET is an SR-IOV device name and not a VNI. Required for removing SR-IOV NETs.' help='Identify that NET is an SR-IOV device name and not a VNI. Required for removing SR-IOV NETs.'
) )
@click.option( @click.option(
'-r', '--restart', 'restart', is_flag=True, default=False, '-l/-L', '--live/--no-live', 'live_flag', is_flag=True, default=True,
help='Immediately restart VM to apply new config.' help='Immediately live-attach device to VM [default] or disable this behaviour.'
)
@click.option(
'-r', '--restart', 'restart_flag', is_flag=True, default=False,
help='Immediately restart VM to apply new config; requires "--no-live".'
) )
@click.option( @click.option(
'-y', '--yes', 'confirm_flag', '-y', '--yes', 'confirm_flag',
is_flag=True, default=False, is_flag=True, default=False,
help='Confirm the restart' help='Confirm the restart.'
) )
@cluster_req @cluster_req
def vm_network_remove(domain, net, sriov, restart, confirm_flag): def vm_network_remove(domain, net, sriov_flag, live_flag, restart_flag, confirm_flag):
""" """
Remove the network NET from the virtual machine DOMAIN. Remove the network NET from the virtual machine DOMAIN.
NET may be a PVC network VNI, which is added as a bridged device, or a SR-IOV VF device connected in the given mode. NET may be a PVC network VNI, which is added as a bridged device, or a SR-IOV VF device connected in the given mode.
""" """
if restart and not confirm_flag and not config['unsafe']: if restart_flag and live_flag:
click.echo('WARNING: Live flag and restart flag both specified; this can cause unintended behaviour. To disable live changes, use "--no-live".')
exit(1)
if restart_flag and not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True) click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception: except Exception:
restart = False restart_flag = False
retcode, retmsg = pvc_vm.vm_networks_remove(config, domain, net, sriov, restart) retcode, retmsg = pvc_vm.vm_networks_remove(config, domain, net, sriov_flag, live_flag, restart_flag)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1623,8 +1635,12 @@ def vm_volume_get(domain, raw):
help='The type of volume to add.' help='The type of volume to add.'
) )
@click.option( @click.option(
'-r', '--restart', 'restart', is_flag=True, default=False, '-l/-L', '--live/--no-live', 'live_flag', is_flag=True, default=True,
help='Immediately restart VM to apply new config.' help='Immediately live-attach device to VM [default] or disable this behaviour.'
)
@click.option(
'-r', '--restart', 'restart_flag', is_flag=True, default=False,
help='Immediately restart VM to apply new config; requires "--no-live".'
) )
@click.option( @click.option(
'-y', '--yes', 'confirm_flag', '-y', '--yes', 'confirm_flag',
@ -1632,21 +1648,23 @@ def vm_volume_get(domain, raw):
help='Confirm the restart' help='Confirm the restart'
) )
@cluster_req @cluster_req
def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart, confirm_flag): def vm_volume_add(domain, volume, disk_id, bus, disk_type, live_flag, restart_flag, confirm_flag):
""" """
Add the volume VOLUME to the virtual machine DOMAIN. 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. 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.
""" """
if restart and not confirm_flag and not config['unsafe']: if restart_flag and live_flag:
click.echo('WARNING: Live flag and restart flag both specified; this can cause unintended behaviour. To disable live changes, use "--no-live".')
exit(1)
if restart_flag and not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True) click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception: except Exception:
restart = False restart_flag = False
retcode, retmsg = pvc_vm.vm_volumes_add(config, domain, volume, disk_id, bus, disk_type, restart) retcode, retmsg = pvc_vm.vm_volumes_add(config, domain, volume, disk_id, bus, disk_type, live_flag, restart_flag)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1661,8 +1679,12 @@ def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart, confirm_flag
'volume' 'volume'
) )
@click.option( @click.option(
'-r', '--restart', 'restart', is_flag=True, default=False, '-l/-L', '--live/--no-live', 'live_flag', is_flag=True, default=True,
help='Immediately restart VM to apply new config.' help='Immediately live-attach device to VM [default] or disable this behaviour.'
)
@click.option(
'-r', '--restart', 'restart_flag', is_flag=True, default=False,
help='Immediately restart VM to apply new config; requires "--no-live".'
) )
@click.option( @click.option(
'-y', '--yes', 'confirm_flag', '-y', '--yes', 'confirm_flag',
@ -1670,19 +1692,21 @@ def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart, confirm_flag
help='Confirm the restart' help='Confirm the restart'
) )
@cluster_req @cluster_req
def vm_volume_remove(domain, volume, restart, confirm_flag): def vm_volume_remove(domain, volume, live_flag, restart_flag, confirm_flag):
""" """
Remove VOLUME from the virtual machine DOMAIN; VOLUME must be a file path or RBD path in 'pool/volume' format. Remove VOLUME from the virtual machine DOMAIN; VOLUME must be a file path or RBD path in 'pool/volume' format.
""" """
if restart and not confirm_flag and not config['unsafe']: if restart_flag and live_flag:
click.echo('WARNING: Live flag and restart flag both specified; this can cause unintended behaviour. To disable live changes, use "--no-live".')
exit(1)
if restart_flag and not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True) click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception: except Exception:
restart = False restart_flag = False
retcode, retmsg = pvc_vm.vm_volumes_remove(config, domain, volume, restart) retcode, retmsg = pvc_vm.vm_volumes_remove(config, domain, volume, live_flag, restart_flag)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)

View File

@ -105,14 +105,14 @@ def getDomainName(zkhandler, domain):
# Helper functions # Helper functions
# #
def change_state(zkhandler, dom_uuid, new_state): def change_state(zkhandler, dom_uuid, new_state):
lock = zkhandler.exclusivelock(('domain.state', dom_uuid)) lock = zkhandler.exclusivelock(('domain.state', dom_uuid))
with lock: with lock:
zkhandler.write([ zkhandler.write([
(('domain.state', dom_uuid), new_state) (('domain.state', dom_uuid), new_state)
]) ])
# Wait for 1/2 second to allow state to flow to all nodes # Wait for 1/2 second to allow state to flow to all nodes
time.sleep(0.5) time.sleep(0.5)
# #
@ -262,6 +262,94 @@ def define_vm(zkhandler, config_data, target_node, node_limit, node_selector, no
return True, 'Added new VM with Name "{}" and UUID "{}" to database.'.format(dom_name, dom_uuid) return True, 'Added new VM with Name "{}" and UUID "{}" to database.'.format(dom_name, dom_uuid)
def attach_vm_device(zkhandler, domain, device_spec_xml):
# Validate that VM exists in cluster
dom_uuid = getDomainUUID(zkhandler, domain)
if not dom_uuid:
return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain)
# Verify that the VM is in a stopped state; freeing locks is not safe otherwise
state = zkhandler.read(('domain.state', dom_uuid))
if state != 'start':
return False, 'ERROR: VM "{}" is not in started state; live-add unneccessary.'.format(domain)
# Tell the cluster to attach the device
attach_device_string = 'attach_device {} {}'.format(dom_uuid, device_spec_xml)
zkhandler.write([
('base.cmd.domain', attach_device_string)
])
# Wait 1/2 second for the cluster to get the message and start working
time.sleep(0.5)
# Acquire a read lock, so we get the return exclusively
lock = zkhandler.readlock('base.cmd.domain')
with lock:
try:
result = zkhandler.read('base.cmd.domain').split()[0]
if result == 'success-attach_device':
message = 'Attached device on VM "{}"'.format(domain)
success = True
else:
message = 'ERROR: Failed to attach device on VM "{}"; check node logs for details.'.format(domain)
success = False
except Exception:
message = 'ERROR: Command ignored by node.'
success = False
# Acquire a write lock to ensure things go smoothly
lock = zkhandler.writelock('base.cmd.domain')
with lock:
time.sleep(0.5)
zkhandler.write([
('base.cmd.domain', '')
])
return success, message
def detach_vm_device(zkhandler, domain, device_spec_xml):
# Validate that VM exists in cluster
dom_uuid = getDomainUUID(zkhandler, domain)
if not dom_uuid:
return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain)
# Verify that the VM is in a stopped state; freeing locks is not safe otherwise
state = zkhandler.read(('domain.state', dom_uuid))
if state != 'start':
return False, 'ERROR: VM "{}" is not in started state; live-add unneccessary.'.format(domain)
# Tell the cluster to detach the device
detach_device_string = 'detach_device {} {}'.format(dom_uuid, device_spec_xml)
zkhandler.write([
('base.cmd.domain', detach_device_string)
])
# Wait 1/2 second for the cluster to get the message and start working
time.sleep(0.5)
# Acquire a read lock, so we get the return exclusively
lock = zkhandler.readlock('base.cmd.domain')
with lock:
try:
result = zkhandler.read('base.cmd.domain').split()[0]
if result == 'success-detach_device':
message = 'Attached device on VM "{}"'.format(domain)
success = True
else:
message = 'ERROR: Failed to detach device on VM "{}"; check node logs for details.'.format(domain)
success = False
except Exception:
message = 'ERROR: Command ignored by node.'
success = False
# Acquire a write lock to ensure things go smoothly
lock = zkhandler.writelock('base.cmd.domain')
with lock:
time.sleep(0.5)
zkhandler.write([
('base.cmd.domain', '')
])
return success, message
def modify_vm_metadata(zkhandler, domain, node_limit, node_selector, node_autostart, provisioner_profile, migration_method): def modify_vm_metadata(zkhandler, domain, node_limit, node_selector, node_autostart, provisioner_profile, migration_method):
dom_uuid = getDomainUUID(zkhandler, domain) dom_uuid = getDomainUUID(zkhandler, domain)
if not dom_uuid: if not dom_uuid:

View File

@ -28,6 +28,8 @@ from threading import Thread
from xml.etree import ElementTree from xml.etree import ElementTree
from re import match
import daemon_lib.common as common import daemon_lib.common as common
import pvcnoded.objects.VMConsoleWatcherInstance as VMConsoleWatcherInstance import pvcnoded.objects.VMConsoleWatcherInstance as VMConsoleWatcherInstance
@ -163,6 +165,34 @@ class VMInstance(object):
(('domain.console.vnc', self.domuuid), '') (('domain.console.vnc', self.domuuid), '')
]) ])
# Attach a device to the running domain
def attach_device(self, xml_spec):
if not self.dom:
self.logger.out('Cannot attach device to non-running domain', state='w', prefix='Domain {}'.format(self.domuuid))
return False
try:
self.logger.out('Attaching new device to VM', state='i', prefix='Domain {}'.format(self.domuuid))
self.dom.attachDevice(xml_spec)
return True
except Exception as e:
self.logger.out('Failed to attach device: {}'.format(e), state='e', prefix='Domain {}'.format(self.domuuid))
return False
# Detach a device from the running domain
def detach_device(self, xml_spec):
if not self.dom:
self.logger.out('Cannot detach device from non-running domain', state='w', prefix='Domain {}'.format(self.domuuid))
return False
try:
self.logger.out('Detaching device from VM', state='i', prefix='Domain {}'.format(self.domuuid))
self.dom.detachDevice(xml_spec)
return True
except Exception as e:
self.logger.out('Failed to detach device: {}'.format(e), state='e', prefix='Domain {}'.format(self.domuuid))
return False
# Start up the VM # Start up the VM
def start_vm(self): def start_vm(self):
# Start the log watcher # Start the log watcher
@ -851,30 +881,51 @@ class VMInstance(object):
# Primary command function # Primary command function
def vm_command(zkhandler, logger, this_node, data): def vm_command(zkhandler, logger, this_node, data):
# Get the command and args # Get the command and args
command, args = data.split() command, dom_uuid, *args = data.split()
# Flushing VM RBD locks if match('success-.*', command) or match('failure-.*', command):
if command == 'flush_locks': return
dom_uuid = args
# Verify that the VM is set to run on this node logger.out('Getting command "{}" for domain "{}"'.format(command, dom_uuid), state='i')
if this_node.d_domain[dom_uuid].getnode() == this_node.name:
# Lock the command queue # Verify that the VM is set to run on this node
zk_lock = zkhandler.writelock('base.cmd.domain') domain = this_node.d_domain.get(dom_uuid, None)
with zk_lock: if domain is None:
# Flush the lock return False
result = VMInstance.flush_locks(zkhandler, logger, dom_uuid, this_node)
# Command succeeded if domain.getnode() != this_node.name:
if result: return
# Update the command queue
zkhandler.write([ # Lock the command queue
('base.cmd.domain', 'success-{}'.format(data)) zk_lock = zkhandler.writelock('base.cmd.domain')
]) with zk_lock:
# Command failed # Flushing VM RBD locks
else: if command == 'flush_locks':
# Update the command queue result = VMInstance.flush_locks(zkhandler, logger, dom_uuid, this_node)
zkhandler.write([ # Attaching a device
('base.cmd.domain', 'failure-{}'.format(data)) elif command == 'attach_device':
]) xml_spec = ' '.join(args)
# Wait 1 seconds before we free the lock, to ensure the client hits the lock result = domain.attach_device(xml_spec)
time.sleep(1) # Detaching a device
elif command == 'detach_device':
xml_spec = ' '.join(args)
result = domain.detach_device(xml_spec)
# Command not defined
else:
result = False
# Command succeeded
if result:
# Update the command queue
zkhandler.write([
('base.cmd.domain', 'success-{}'.format(data))
])
# Command failed
else:
# Update the command queue
zkhandler.write([
('base.cmd.domain', 'failure-{}'.format(data))
])
# Wait 1 seconds before we free the lock, to ensure the client hits the lock
time.sleep(1)