From ec0b8acf90622bd3b4b58194f9d5d5a7ed99c269 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Thu, 29 Oct 2020 11:31:32 -0400 Subject: [PATCH] Support per-VM migration type selectors Allow a VM to specify its migration type as a default choice. The valid options are "default" (i.e. behave as now), "live" which forces a live migration only, and "shutdown" which forces a shutdown migration only. The new option is treated as a VM meta option and is set to default if not found. --- .../3efe890e1d87_pvc_version_0_9_0.py | 28 +++++++ api-daemon/pvcapid/flaskapi.py | 83 +++++++++++++++++-- api-daemon/pvcapid/helper.py | 11 +-- api-daemon/pvcapid/models.py | 4 +- api-daemon/pvcapid/provisioner.py | 14 ++-- client-cli/cli_lib/provisioner.py | 33 +++++--- client-cli/cli_lib/vm.py | 26 ++++-- client-cli/pvc.py | 39 +++++++-- daemon-common/common.py | 5 ++ daemon-common/vm.py | 16 +++- node-daemon/pvcnoded/VMInstance.py | 38 ++++++--- 11 files changed, 236 insertions(+), 61 deletions(-) create mode 100644 api-daemon/migrations/versions/3efe890e1d87_pvc_version_0_9_0.py diff --git a/api-daemon/migrations/versions/3efe890e1d87_pvc_version_0_9_0.py b/api-daemon/migrations/versions/3efe890e1d87_pvc_version_0_9_0.py new file mode 100644 index 00000000..567ed884 --- /dev/null +++ b/api-daemon/migrations/versions/3efe890e1d87_pvc_version_0_9_0.py @@ -0,0 +1,28 @@ +"""PVC version 0.9.0 + +Revision ID: 3efe890e1d87 +Revises: 3bc6117ea44d +Create Date: 2020-10-29 11:49:58.756626 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3efe890e1d87' +down_revision = '3bc6117ea44d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('system_template', sa.Column('migration_method', sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('system_template', 'migration_method') + # ### end Alembic commands ### diff --git a/api-daemon/pvcapid/flaskapi.py b/api-daemon/pvcapid/flaskapi.py index 6518051a..c243b2b4 100755 --- a/api-daemon/pvcapid/flaskapi.py +++ b/api-daemon/pvcapid/flaskapi.py @@ -834,6 +834,9 @@ class API_VM_Root(Resource): node_autostart: type: boolean description: Whether to autostart the VM when its node returns to ready domain state + migration_method: + type: string + description: The preferred migration method (live, shutdown, none) description: type: string description: The description of the VM @@ -1036,6 +1039,7 @@ class API_VM_Root(Resource): { 'name': 'node' }, { 'name': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms'), 'helptext': "A valid selector must be specified" }, { 'name': 'autostart' }, + { 'name': 'migration_method', 'choices': ('live', 'shutdown', 'none'), 'helptext': "A valid migration_method must be specified" }, { 'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified" }, ]) @Authenticator @@ -1077,6 +1081,16 @@ class API_VM_Root(Resource): type: boolean required: false description: Whether to autostart the VM when its node returns to ready domain state + - in: query + name: migration_method + type: string + required: false + description: The preferred migration method (live, shutdown, none) + default: none + enum: + - live + - shutdown + - none responses: 200: description: OK @@ -1094,7 +1108,8 @@ class API_VM_Root(Resource): reqargs.get('node', None), reqargs.get('limit', None), reqargs.get('selector', 'mem'), - bool(strtobool(reqargs.get('autostart', 'false'))) + bool(strtobool(reqargs.get('autostart', 'false'))), + reqargs.get('migration_method', 'none') ) api.add_resource(API_VM_Root, '/vm') @@ -1125,6 +1140,7 @@ class API_VM_Element(Resource): { 'name': 'node' }, { 'name': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms'), 'helptext': "A valid selector must be specified" }, { 'name': 'autostart' }, + { 'name': 'migration_method', 'choices': ('live', 'shutdown', 'none'), 'helptext': "A valid migration_method must be specified" }, { 'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified" }, ]) @Authenticator @@ -1168,6 +1184,16 @@ class API_VM_Element(Resource): type: boolean required: false description: Whether to autostart the VM when its node returns to ready domain state + - in: query + name: migration_method + type: string + required: false + description: The preferred migration method (live, shutdown, none) + default: none + enum: + - live + - shutdown + - none responses: 200: description: OK @@ -1185,7 +1211,8 @@ class API_VM_Element(Resource): reqargs.get('node', None), reqargs.get('limit', None), reqargs.get('selector', 'mem'), - bool(strtobool(reqargs.get('autostart', 'false'))) + bool(strtobool(reqargs.get('autostart', 'false'))), + reqargs.get('migration_method', 'none') ) @RequestParser([ @@ -1296,6 +1323,9 @@ class API_VM_Metadata(Resource): node_autostart: type: string description: Whether to autostart the VM when its node returns to ready domain state + migration_method: + type: string + description: The preferred migration method (live, shutdown, none) 404: description: Not found schema: @@ -1309,6 +1339,7 @@ class API_VM_Metadata(Resource): { 'name': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms'), 'helptext': "A valid selector must be specified" }, { 'name': 'autostart' }, { 'name': 'profile' }, + { 'name': 'migration_method', 'choices': ('live', 'shutdown', 'none'), 'helptext': "A valid migration_method must be specified" }, ]) @Authenticator def post(self, vm, reqargs): @@ -1343,6 +1374,16 @@ class API_VM_Metadata(Resource): type: string required: false description: The PVC provisioner profile for the VM + - in: query + name: migration_method + type: string + required: false + description: The preferred migration method (live, shutdown, none) + default: none + enum: + - live + - shutdown + - none responses: 200: description: OK @@ -1360,7 +1401,8 @@ class API_VM_Metadata(Resource): reqargs.get('limit', None), reqargs.get('selector', None), reqargs.get('autostart', None), - reqargs.get('profile', None) + reqargs.get('profile', None), + reqargs.get('migration_method', None) ) api.add_resource(API_VM_Metadata, '/vm//meta') @@ -4057,6 +4099,9 @@ class API_Provisioner_Template_System_Root(Resource): node_autostart: type: boolean description: Whether to start VM with node ready state (one-time) + migration_method: + type: string + description: The preferred migration method (live, shutdown, none) parameters: - in: query name: limit @@ -4084,7 +4129,8 @@ class API_Provisioner_Template_System_Root(Resource): { 'name': 'vnc_bind' }, { 'name': 'node_limit' }, { 'name': 'node_selector' }, - { 'name': 'node_autostart' } + { 'name': 'node_autostart' }, + { 'name': 'migration_method' } ]) @Authenticator def post(self, reqargs): @@ -4139,6 +4185,11 @@ class API_Provisioner_Template_System_Root(Resource): type: boolean required: false description: Whether to start VM with node ready state (one-time) + - in: query + name: migration_method + type: string + required: false + description: The preferred migration method (live, shutdown, none) responses: 200: description: OK @@ -4185,7 +4236,8 @@ class API_Provisioner_Template_System_Root(Resource): vnc_bind, reqargs.get('node_limit', None), reqargs.get('node_selector', None), - node_autostart + node_autostart, + reqargs.get('migration_method', None), ) api.add_resource(API_Provisioner_Template_System_Root, '/provisioner/template/system') @@ -4222,7 +4274,8 @@ class API_Provisioner_Template_System_Element(Resource): { 'name': 'vnc_bind' }, { 'name': 'node_limit' }, { 'name': 'node_selector' }, - { 'name': 'node_autostart' } + { 'name': 'node_autostart' }, + { 'name': 'migration_method' } ]) @Authenticator def post(self, template, reqargs): @@ -4272,6 +4325,11 @@ class API_Provisioner_Template_System_Element(Resource): type: boolean required: false description: Whether to start VM with node ready state (one-time) + - in: query + name: migration_method + type: string + required: false + description: The preferred migration method (live, shutdown, none) responses: 200: description: OK @@ -4318,7 +4376,8 @@ class API_Provisioner_Template_System_Element(Resource): vnc_bind, reqargs.get('node_limit', None), reqargs.get('node_selector', None), - node_autostart + node_autostart, + reqargs.get('migration_method', None), ) @RequestParser([ @@ -4329,7 +4388,8 @@ class API_Provisioner_Template_System_Element(Resource): { 'name': 'vnc_bind' }, { 'name': 'node_limit' }, { 'name': 'node_selector' }, - { 'name': 'node_autostart' } + { 'name': 'node_autostart' }, + { 'name': 'migration_method' } ]) @Authenticator def put(self, template, reqargs): @@ -4371,6 +4431,10 @@ class API_Provisioner_Template_System_Element(Resource): name: node_autostart type: boolean description: Whether to start VM with node ready state (one-time) + - in: query + name: migration_method + type: string + description: The preferred migration method (live, shutdown, none) responses: 200: description: OK @@ -4392,7 +4456,8 @@ class API_Provisioner_Template_System_Element(Resource): reqargs.get('vnc_bind'), reqargs.get('node_limit', None), reqargs.get('node_selector', None), - reqargs.get('node_autostart', None) + reqargs.get('node_autostart', None), + reqargs.get('migration_method', None) ) @Authenticator diff --git a/api-daemon/pvcapid/helper.py b/api-daemon/pvcapid/helper.py index fbf74d79..df1ac3b6 100755 --- a/api-daemon/pvcapid/helper.py +++ b/api-daemon/pvcapid/helper.py @@ -431,7 +431,7 @@ def vm_list(node=None, state=None, limit=None, is_fuzzy=True): return retdata, retcode -def vm_define(xml, node, limit, selector, autostart): +def vm_define(xml, node, limit, selector, autostart, migration_method): """ Define a VM from Libvirt XML in the PVC cluster. """ @@ -443,7 +443,7 @@ def vm_define(xml, node, limit, selector, autostart): return { 'message': 'XML is malformed or incorrect: {}'.format(e) }, 400 zk_conn = pvc_common.startZKConnection(config['coordinators']) - retflag, retdata = pvc_vm.define_vm(zk_conn, new_cfg, node, limit, selector, autostart, profile=None) + retflag, retdata = pvc_vm.define_vm(zk_conn, new_cfg, node, limit, selector, autostart, migration_method, profile=None) pvc_common.stopZKConnection(zk_conn) if retflag: @@ -475,7 +475,8 @@ def get_vm_meta(vm): 'name': vm, 'node_limit': retdata['node_limit'], 'node_selector': retdata['node_selector'], - 'node_autostart': retdata['node_autostart'] + 'node_autostart': retdata['node_autostart'], + 'migration_method': retdata['migration_method'] } else: retcode = 404 @@ -490,7 +491,7 @@ def get_vm_meta(vm): return retdata, retcode -def update_vm_meta(vm, limit, selector, autostart, provisioner_profile): +def update_vm_meta(vm, limit, selector, autostart, provisioner_profile, migration_method): """ Update metadata of a VM. """ @@ -500,7 +501,7 @@ def update_vm_meta(vm, limit, selector, autostart, provisioner_profile): autostart = bool(strtobool(autostart)) except: autostart = False - retflag, retdata = pvc_vm.modify_vm_metadata(zk_conn, vm, limit, selector, autostart, provisioner_profile) + retflag, retdata = pvc_vm.modify_vm_metadata(zk_conn, vm, limit, selector, autostart, provisioner_profile, migration_method) pvc_common.stopZKConnection(zk_conn) if retflag: diff --git a/api-daemon/pvcapid/models.py b/api-daemon/pvcapid/models.py index 008090ec..c4799b64 100755 --- a/api-daemon/pvcapid/models.py +++ b/api-daemon/pvcapid/models.py @@ -35,9 +35,10 @@ class DBSystemTemplate(db.Model): node_limit = db.Column(db.Text) node_selector = db.Column(db.Text) node_autostart = db.Column(db.Boolean, nullable=False) + migration_method = db.Column(db.Text) ova = db.Column(db.Integer, db.ForeignKey("ova.id"), nullable=True) - def __init__(self, name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, ova=None): + def __init__(self, name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, migration_method, ova=None): self.name = name self.vcpu_count = vcpu_count self.vram_mb = vram_mb @@ -47,6 +48,7 @@ class DBSystemTemplate(db.Model): self.node_limit = node_limit self.node_selector = node_selector self.node_autostart = node_autostart + self.migration_method = migration_method self.ova = ova def __repr__(self): diff --git a/api-daemon/pvcapid/provisioner.py b/api-daemon/pvcapid/provisioner.py index 86f24fd3..d89eeb64 100755 --- a/api-daemon/pvcapid/provisioner.py +++ b/api-daemon/pvcapid/provisioner.py @@ -214,14 +214,14 @@ def template_list(limit): # # Template Create functions # -def create_template_system(name, vcpu_count, vram_mb, serial=False, vnc=False, vnc_bind=None, node_limit=None, node_selector=None, node_autostart=False, ova=None): +def create_template_system(name, vcpu_count, vram_mb, serial=False, vnc=False, vnc_bind=None, node_limit=None, node_selector=None, node_autostart=False, migration_method=None, ova=None): if list_template_system(name, is_fuzzy=False)[-1] != 404: retmsg = { 'message': 'The system template "{}" already exists.'.format(name) } retcode = 400 return retmsg, retcode - query = "INSERT INTO system_template (name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, ova) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s);" - args = (name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, ova) + query = "INSERT INTO system_template (name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, migration_method, ova) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);" + args = (name, vcpu_count, vram_mb, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, migration_method, ova) conn, cur = open_database(config) try: @@ -359,7 +359,7 @@ def create_template_storage_element(name, disk_id, pool, source_volume=None, dis # # Template Modify functions # -def modify_template_system(name, vcpu_count=None, vram_mb=None, serial=None, vnc=None, vnc_bind=None, node_limit=None, node_selector=None, node_autostart=None): +def modify_template_system(name, vcpu_count=None, vram_mb=None, serial=None, vnc=None, vnc_bind=None, node_limit=None, node_selector=None, node_autostart=None, migration_method=None): if list_profile(name, is_fuzzy=False)[-1] != 200: retmsg = { 'message': 'The system template "{}" does not exist.'.format(name) } retcode = 400 @@ -420,6 +420,9 @@ def modify_template_system(name, vcpu_count=None, vram_mb=None, serial=None, vnc retcode = 400 fields.append({'field': 'node_autostart', 'data': node_autostart}) + if migration_method is not None: + fields.append({'field': 'migration_method', 'data': migration_method}) + conn, cur = open_database(config) try: for field in fields: @@ -1403,7 +1406,8 @@ def create_vm(self, vm_name, vm_profile, define_vm=True, start_vm=True, script_r node_limit = node_limit.split(',') node_selector = vm_data['system_details']['node_selector'] node_autostart = vm_data['system_details']['node_autostart'] - retcode, retmsg = pvc_vm.define_vm(zk_conn, vm_schema.strip(), target_node, node_limit, node_selector, node_autostart, vm_profile, initial_state='provision') + migration_method = vm_data['system_details']['migration_method'] + retcode, retmsg = pvc_vm.define_vm(zk_conn, vm_schema.strip(), target_node, node_limit, node_selector, node_autostart, migration_method, vm_profile, initial_state='provision') print(retmsg) else: print("Skipping VM definition") diff --git a/client-cli/cli_lib/provisioner.py b/client-cli/cli_lib/provisioner.py index 33cc86d4..0fd13c41 100644 --- a/client-cli/cli_lib/provisioner.py +++ b/client-cli/cli_lib/provisioner.py @@ -717,9 +717,10 @@ def format_list_template_system(template_data): template_serial_length = 7 template_vnc_length = 4 template_vnc_bind_length = 10 - template_node_limit_length = 9 - template_node_selector_length = 11 - template_node_autostart_length = 11 + template_node_limit_length = 6 + template_node_selector_length = 9 + template_node_autostart_length = 10 + template_migration_method_length = 10 for template in template_data: # template_name column @@ -762,17 +763,22 @@ def format_list_template_system(template_data): _template_node_autostart_length = len(str(template['node_autostart'])) + 1 if _template_node_autostart_length > template_node_autostart_length: template_node_autostart_length = _template_node_autostart_length + # template_migration_method column + _template_migration_method_length = len(str(template['migration_method'])) + 1 + if _template_migration_method_length > template_migration_method_length: + template_migration_method_length = _template_migration_method_length # Format the string (header) template_list_output_header = '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ {template_vcpu: <{template_vcpu_length}} \ {template_vram: <{template_vram_length}} \ -Consoles: {template_serial: <{template_serial_length}} \ +Console: {template_serial: <{template_serial_length}} \ {template_vnc: <{template_vnc_length}} \ {template_vnc_bind: <{template_vnc_bind_length}} \ -Metadata: {template_node_limit: <{template_node_limit_length}} \ +Meta: {template_node_limit: <{template_node_limit_length}} \ {template_node_selector: <{template_node_selector_length}} \ -{template_node_autostart: <{template_node_autostart_length}}{end_bold}'.format( +{template_node_autostart: <{template_node_autostart_length}} \ +{template_migration_method: <{template_migration_method_length}}{end_bold}'.format( template_name_length=template_name_length, template_id_length=template_id_length, template_vcpu_length=template_vcpu_length, @@ -783,6 +789,7 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ template_node_limit_length=template_node_limit_length, template_node_selector_length=template_node_selector_length, template_node_autostart_length=template_node_autostart_length, + template_migration_method_length=template_migration_method_length, bold=ansiprint.bold(), end_bold=ansiprint.end(), template_state_colour='', @@ -796,7 +803,8 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ template_vnc_bind='VNC bind', template_node_limit='Limit', template_node_selector='Selector', - template_node_autostart='Autostart' + template_node_autostart='Autostart', + template_migration_method='Migration' ) # Keep track of nets we found to be valid to cut down on duplicate API hits @@ -808,12 +816,13 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ {template_vcpu: <{template_vcpu_length}} \ {template_vram: <{template_vram_length}} \ - {template_serial: <{template_serial_length}} \ + {template_serial: <{template_serial_length}} \ {template_vnc: <{template_vnc_length}} \ {template_vnc_bind: <{template_vnc_bind_length}} \ - {template_node_limit: <{template_node_limit_length}} \ + {template_node_limit: <{template_node_limit_length}} \ {template_node_selector: <{template_node_selector_length}} \ -{template_node_autostart: <{template_node_autostart_length}}{end_bold}'.format( +{template_node_autostart: <{template_node_autostart_length}} \ +{template_migration_method: <{template_migration_method_length}}{end_bold}'.format( template_name_length=template_name_length, template_id_length=template_id_length, template_vcpu_length=template_vcpu_length, @@ -824,6 +833,7 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ template_node_limit_length=template_node_limit_length, template_node_selector_length=template_node_selector_length, template_node_autostart_length=template_node_autostart_length, + template_migration_method_length=template_migration_method_length, bold='', end_bold='', template_name=str(template['name']), @@ -835,7 +845,8 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ template_vnc_bind=str(template['vnc_bind']), template_node_limit=str(template['node_limit']), template_node_selector=str(template['node_selector']), - template_node_autostart=str(template['node_autostart']) + template_node_autostart=str(template['node_autostart']), + template_migration_method=str(template['migration_method']) ) ) diff --git a/client-cli/cli_lib/vm.py b/client-cli/cli_lib/vm.py index 2dc3fcae..ca782e7d 100644 --- a/client-cli/cli_lib/vm.py +++ b/client-cli/cli_lib/vm.py @@ -80,19 +80,20 @@ def vm_list(config, limit, target_node, target_state): else: return False, response.json().get('message', '') -def vm_define(config, xml, node, node_limit, node_selector, node_autostart): +def vm_define(config, xml, node, node_limit, node_selector, node_autostart, migration_method): """ Define a new VM on the cluster API endpoint: POST /vm - API arguments: xml={xml}, node={node}, limit={node_limit}, selector={node_selector}, autostart={node_autostart} + API arguments: xml={xml}, node={node}, limit={node_limit}, selector={node_selector}, autostart={node_autostart}, migration_method={migration_method} API schema: {"message":"{data}"} """ params = { 'node': node, 'limit': node_limit, 'selector': node_selector, - 'autostart': node_autostart + 'autostart': node_autostart, + 'migration_method': migration_method } data = { 'xml': xml @@ -129,12 +130,12 @@ def vm_modify(config, vm, xml, restart): return retstatus, response.json().get('message', '') -def vm_metadata(config, vm, node_limit, node_selector, node_autostart, provisioner_profile): +def vm_metadata(config, vm, node_limit, node_selector, node_autostart, migration_method, provisioner_profile): """ Modify PVC metadata of a VM API endpoint: GET /vm/{vm}/meta, POST /vm/{vm}/meta - API arguments: limit={node_limit}, selector={node_selector}, autostart={node_autostart}, profile={provisioner_profile} + API arguments: limit={node_limit}, selector={node_selector}, autostart={node_autostart}, migration_method={migration_method} profile={provisioner_profile} API schema: {"message":"{data}"} """ params = dict() @@ -149,6 +150,9 @@ def vm_metadata(config, vm, node_limit, node_selector, node_autostart, provision if node_autostart is not None: params['autostart'] = node_autostart + if migration_method is not None: + params['migration_method'] = migration_method + if provisioner_profile is not None: params['profile'] = provisioner_profile @@ -391,24 +395,30 @@ def format_info(config, domain_information, long_output): ainformation.append('') ainformation.append('{}Failure reason:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['failed_reason'])) - if not domain_information['node_selector']: + if not domain_information.get('node_selector'): formatted_node_selector = "False" else: formatted_node_selector = domain_information['node_selector'] - if not domain_information['node_limit']: + if not domain_information.get('node_limit'): formatted_node_limit = "False" else: formatted_node_limit = ', '.join(domain_information['node_limit']) - if not domain_information['node_autostart']: + if not domain_information.get('node_autostart'): formatted_node_autostart = "False" else: formatted_node_autostart = domain_information['node_autostart'] + if not domain_information.get('migration_method'): + formatted_migration_method = "default" + else: + formatted_migration_method = domain_information['migration_method'] + ainformation.append('{}Migration selector:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_selector)) ainformation.append('{}Node limit:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_limit)) ainformation.append('{}Autostart:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_autostart)) + ainformation.append('{}Migration Method:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_migration_method)) # Network list net_list = [] diff --git a/client-cli/pvc.py b/client-cli/pvc.py index 2a7bcd75..8b1e637c 100755 --- a/client-cli/pvc.py +++ b/client-cli/pvc.py @@ -593,11 +593,16 @@ def cli_vm(): '-a/-A', '--autostart/--no-autostart', 'node_autostart', is_flag=True, default=False, help='Start VM automatically on next unflush/ready state of home node; unset by daemon once used.' ) +@click.option( + '-m', '--method', 'migration_method', default='none', show_default=True, + type=click.Choice(['none','live','shutdown']), + help='The preferred migration method of the VM between nodes; saved with VM.' +) @click.argument( 'vmconfig', type=click.File() ) @cluster_req -def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart): +def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart, migration_method): """ Define a new virtual machine from Libvirt XML configuration file VMCONFIG. """ @@ -613,7 +618,7 @@ def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart): except: cleanup(False, 'Error: XML is malformed or invalid') - retcode, retmsg = pvc_vm.vm_define(config, new_cfg, target_node, node_limit, node_selector, node_autostart) + retcode, retmsg = pvc_vm.vm_define(config, new_cfg, target_node, node_limit, node_selector, node_autostart, migration_method) cleanup(retcode, retmsg) ############################################################################### @@ -633,6 +638,11 @@ def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart): '-a/-A', '--autostart/--no-autostart', 'node_autostart', is_flag=True, default=None, help='Start VM automatically on next unflush/ready state of home node; unset by daemon once used.' ) +@click.option( + '-m', '--method', 'migration_method', default='none', show_default=True, + type=click.Choice(['none','live','shutdown']), + help='The preferred migration method of the VM between nodes; saved with VM.' +) @click.option( '-p', '--profile', 'provisioner_profile', default=None, show_default=False, help='PVC provisioner profile name for VM.' @@ -641,15 +651,15 @@ def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart): 'domain' ) @cluster_req -def vm_meta(domain, node_limit, node_selector, node_autostart, provisioner_profile): +def vm_meta(domain, node_limit, node_selector, node_autostart, migration_method, provisioner_profile): """ Modify the PVC metadata of existing virtual machine DOMAIN. At least one option to update must be specified. DOMAIN may be a UUID or name. """ - if node_limit is None and node_selector is None and node_autostart is None and provisioner_profile is None: + if node_limit is None and node_selector is None and node_autostart is None and migration_method is None and provisioner_profile is None: cleanup(False, 'At least one metadata option must be specified to update.') - retcode, retmsg = pvc_vm.vm_metadata(config, domain, node_limit, node_selector, node_autostart, provisioner_profile) + retcode, retmsg = pvc_vm.vm_metadata(config, domain, node_limit, node_selector, node_autostart, migration_method, provisioner_profile) cleanup(retcode, retmsg) ############################################################################### @@ -2301,8 +2311,14 @@ def provisioner_template_system_list(limit): is_flag=True, default=False, help='Autostart VM with their parent Node on first/next boot.' ) +@click.option( + '--migration-method', 'migration_method', + type=click.Choice(['none','live','shutdown'], case_sensitive=False), + default=None, # Use cluster default + help='The preferred migration method of the VM between nodes' +) @cluster_req -def provisioner_template_system_add(name, vcpus, vram, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart): +def provisioner_template_system_add(name, vcpus, vram, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, migration_method): """ Add a new system template NAME to the PVC cluster provisioner. """ @@ -2320,6 +2336,8 @@ def provisioner_template_system_add(name, vcpus, vram, serial, vnc, vnc_bind, no params['node_selector'] = node_selector if node_autostart: params['node_autostart'] = node_autostart + if migration_method: + params['migration_method'] = migration_method retcode, retdata = pvc_provisioner.template_add(config, params, template_type='system') cleanup(retcode, retdata) @@ -2369,8 +2387,14 @@ def provisioner_template_system_add(name, vcpus, vram, serial, vnc, vnc_bind, no is_flag=True, default=None, help='Autostart VM with their parent Node on first/next boot.' ) +@click.option( + '--migration-method', 'migration_method', + type=click.Choice(['none','live','shutdown'], case_sensitive=False), + default=None, # Use cluster default + help='The preferred migration method of the VM between nodes' +) @cluster_req -def provisioner_template_system_modify(name, vcpus, vram, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart): +def provisioner_template_system_modify(name, vcpus, vram, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, migration_method): """ Add a new system template NAME to the PVC cluster provisioner. """ @@ -2383,6 +2407,7 @@ def provisioner_template_system_modify(name, vcpus, vram, serial, vnc, vnc_bind, params['node_limit'] = node_limit params['node_selector'] = node_selector params['node_autostart'] = node_autostart + params['migration_method'] = migration_method retcode, retdata = pvc_provisioner.template_modify(config, params, name, template_type='system') cleanup(retcode, retdata) diff --git a/daemon-common/common.py b/daemon-common/common.py index 185ebd06..59cf333e 100644 --- a/daemon-common/common.py +++ b/daemon-common/common.py @@ -236,6 +236,10 @@ def getInformationFromXML(zk_conn, uuid): domain_node_autostart = zkhandler.readdata(zk_conn, '/domains/{}/node_autostart'.format(uuid)) except: domain_node_autostart = None + try: + domain_migration_method = zkhandler.readdata(zk_conn, '/domains/{}/migration_method'.format(uuid)) + except: + domain_migration_method = None if not domain_node_limit: domain_node_limit = None @@ -282,6 +286,7 @@ def getInformationFromXML(zk_conn, uuid): 'node_limit': domain_node_limit, 'node_selector': domain_node_selector, 'node_autostart': bool(strtobool(domain_node_autostart)), + 'migration_method': domain_migration_method, 'description': domain_description, 'profile': domain_profile, 'memory': int(domain_memory), diff --git a/daemon-common/vm.py b/daemon-common/vm.py index c621b40b..9e2ad262 100644 --- a/daemon-common/vm.py +++ b/daemon-common/vm.py @@ -157,7 +157,7 @@ def flush_locks(zk_conn, domain): return success, message -def define_vm(zk_conn, config_data, target_node, node_limit, node_selector, node_autostart, profile=None, initial_state='stop'): +def define_vm(zk_conn, config_data, target_node, node_limit, node_selector, node_autostart, migration_method=None, profile=None, initial_state='stop'): # Parse the XML data try: parsed_xml = lxml.objectify.fromstring(config_data) @@ -206,6 +206,7 @@ def define_vm(zk_conn, config_data, target_node, node_limit, node_selector, node '/domains/{}/node_limit'.format(dom_uuid): formatted_node_limit, '/domains/{}/node_selector'.format(dom_uuid): node_selector, '/domains/{}/node_autostart'.format(dom_uuid): node_autostart, + '/domains/{}/migration_method'.format(dom_uuid): migration_method, '/domains/{}/failedreason'.format(dom_uuid): '', '/domains/{}/consolelog'.format(dom_uuid): '', '/domains/{}/rbdlist'.format(dom_uuid): formatted_rbd_list, @@ -215,7 +216,7 @@ def define_vm(zk_conn, config_data, target_node, node_limit, node_selector, node return True, 'Added new VM with Name "{}" and UUID "{}" to database.'.format(dom_name, dom_uuid) -def modify_vm_metadata(zk_conn, domain, node_limit, node_selector, node_autostart, provisioner_profile): +def modify_vm_metadata(zk_conn, domain, node_limit, node_selector, node_autostart, provisioner_profile, migration_method): dom_uuid = getDomainUUID(zk_conn, domain) if not dom_uuid: return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) @@ -240,6 +241,11 @@ def modify_vm_metadata(zk_conn, domain, node_limit, node_selector, node_autostar '/domains/{}/profile'.format(dom_uuid): provisioner_profile }) + if migration_method is not None: + zkhandler.writedata(zk_conn, { + '/domains/{}/migration_method'.format(dom_uuid): migration_method + }) + return True, 'Successfully modified PVC metadata of VM "{}".'.format(domain) def modify_vm(zk_conn, domain, restart, new_vm_config): @@ -781,9 +787,15 @@ def format_info(zk_conn, domain_information, long_output): else: formatted_node_autostart = domain_information['node_autostart'] + if not domain_information['migration_method']: + formatted_migration_method = "False" + else: + formatted_migration_method = domain_information['migration_method'] + ainformation.append('{}Migration selector:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_selector)) ainformation.append('{}Node limit:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_limit)) ainformation.append('{}Autostart:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_autostart)) + ainformation.append('{}Migration Method:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_migration_method)) # Network list net_list = [] diff --git a/node-daemon/pvcnoded/VMInstance.py b/node-daemon/pvcnoded/VMInstance.py index d17b4c2a..c81deb30 100644 --- a/node-daemon/pvcnoded/VMInstance.py +++ b/node-daemon/pvcnoded/VMInstance.py @@ -113,7 +113,11 @@ class VMInstance(object): try: self.pinpolicy = zkhandler.readdata(self.zk_conn, '/domains/{}/pinpolicy'.format(self.domuuid)) except: - self.pinpolicy = "None" + self.pinpolicy = "none" + try: + self.migration_method = zkhandler.readdata(self.zk_conn, '/domains/{}/migration_method'.format(self.domuuid)) + except: + self.migration_method = 'none' # These will all be set later self.instart = False @@ -349,11 +353,16 @@ class VMInstance(object): zkhandler.writedata(self.zk_conn, { '/domains/{}/state'.format(self.domuuid): 'start' }) # Migrate the VM to a target host - def migrate_vm(self, force_live=False): + def migrate_vm(self, force_live=False, force_shutdown=False): # Wait for any previous migration while self.inmigrate: time.sleep(0.1) + if self.migration_method == 'live': + force_live = True + elif self.migration_method == 'shutdown': + force_shutdown = True + self.inmigrate = True self.logger.out('Migrating VM to node "{}"'.format(self.node), state='i', prefix='Domain {}'.format(self.domuuid)) @@ -466,17 +475,20 @@ class VMInstance(object): abort_migrate('Target node changed during preparation') return - # A live migrate is attemped 3 times in succession - ticks = 0 - while True: - ticks += 1 - self.logger.out('Attempting live migration try {}'.format(ticks), state='i', prefix='Domain {}'.format(self.domuuid)) - migrate_live_result = migrate_live() - if migrate_live_result: - break - time.sleep(0.5) - if ticks > 2: - break + if not force_shutdown: + # A live migrate is attemped 3 times in succession + ticks = 0 + while True: + ticks += 1 + self.logger.out('Attempting live migration try {}'.format(ticks), state='i', prefix='Domain {}'.format(self.domuuid)) + migrate_live_result = migrate_live() + if migrate_live_result: + break + time.sleep(0.5) + if ticks > 2: + break + else: + migrate_live_result = False if not migrate_live_result: if force_live: