From f46c2e7f6a2f472f2ddf6946479f263fa89d9308 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 23 May 2021 16:41:42 -0400 Subject: [PATCH] Implement VM rename functionality Closes #125 --- api-daemon/pvcapid/flaskapi.py | 39 ++++++++++++++++++++++++++ api-daemon/pvcapid/helper.py | 31 +++++++++++++++++++++ client-cli/cli_lib/vm.py | 21 ++++++++++++++ client-cli/pvc.py | 31 +++++++++++++++++++++ daemon-common/vm.py | 50 ++++++++++++++++++++++++++++++++++ docs/manuals/swagger.json | 32 ++++++++++++++++++++++ 6 files changed, 204 insertions(+) diff --git a/api-daemon/pvcapid/flaskapi.py b/api-daemon/pvcapid/flaskapi.py index 72ca2f9f..e0b5f329 100755 --- a/api-daemon/pvcapid/flaskapi.py +++ b/api-daemon/pvcapid/flaskapi.py @@ -1804,6 +1804,45 @@ class API_VM_Console(Resource): api.add_resource(API_VM_Console, '/vm//console') +# /vm//rename +class API_VM_Rename(Resource): + @RequestParser([ + {'name': 'new_name'} + ]) + @Authenticator + def post(self, vm, reqargs): + """ + Rename VM {vm}, and all connected disk volumes which include this name, to {new_name} + --- + tags: + - vm + parameters: + - in: query + name: new_name + type: string + required: true + description: The new name of the VM + responses: + 200: + description: OK + schema: + type: object + id: Message + 400: + description: Bad request + schema: + type: object + id: Message + """ + return api_helper.vm_rename( + vm, + reqargs.get('new_name', None) + ) + + +api.add_resource(API_VM_Rename, '/vm//rename') + + ########################################################## # Client API - Network ########################################################## diff --git a/api-daemon/pvcapid/helper.py b/api-daemon/pvcapid/helper.py index 685f014e..5aa94e5d 100755 --- a/api-daemon/pvcapid/helper.py +++ b/api-daemon/pvcapid/helper.py @@ -601,6 +601,37 @@ def vm_modify(name, restart, xml): return output, retcode +def vm_rename(name, new_name): + """ + Rename a VM in the PVC cluster. + """ + if new_name is None: + output = { + 'message': 'A new VM name must be specified' + } + return 400, output + + zk_conn = pvc_common.startZKConnection(config['coordinators']) + if pvc_vm.searchClusterByName(zk_conn, new_name) is not None: + output = { + 'message': 'A VM named \'{}\' is already present in the cluster'.format(new_name) + } + return 400, output + + retflag, retdata = pvc_vm.rename_vm(zk_conn, name, new_name) + pvc_common.stopZKConnection(zk_conn) + + if retflag: + retcode = 200 + else: + retcode = 400 + + output = { + 'message': retdata.replace('\"', '\'') + } + return output, retcode + + def vm_undefine(name): """ Undefine a VM from the PVC cluster. diff --git a/client-cli/cli_lib/vm.py b/client-cli/cli_lib/vm.py index 10a1a332..e316995e 100644 --- a/client-cli/cli_lib/vm.py +++ b/client-cli/cli_lib/vm.py @@ -130,6 +130,27 @@ def vm_modify(config, vm, xml, restart): return retstatus, response.json().get('message', '') +def vm_rename(config, vm, new_name): + """ + Rename VM to new name + + API endpoint: POST /vm/{vm}/rename + API arguments: new_name={new_name} + API schema: {"message":"{data}"} + """ + params = { + 'new_name': new_name + } + response = call_api(config, 'post', '/vm/{vm}/rename'.format(vm=vm), params=params) + + if response.status_code == 200: + retstatus = True + else: + retstatus = False + + return retstatus, response.json().get('message', '') + + def vm_metadata(config, vm, node_limit, node_selector, node_autostart, migration_method, provisioner_profile): """ Modify PVC metadata of a VM diff --git a/client-cli/pvc.py b/client-cli/pvc.py index 0f66bc64..e68df9af 100755 --- a/client-cli/pvc.py +++ b/client-cli/pvc.py @@ -790,6 +790,36 @@ def vm_modify(domain, cfgfile, editor, restart, confirm_flag): cleanup(retcode, retmsg) +############################################################################### +# pvc vm rename +############################################################################### +@click.command(name='rename', short_help='Rename a virtual machine.') +@click.argument( + 'domain' +) +@click.argument( + 'new_name' +) +@click.option( + '-y', '--yes', 'confirm_flag', + is_flag=True, default=False, + help='Confirm the rename' +) +@cluster_req +def vm_rename(domain, new_name, confirm_flag): + """ + Rename virtual machine DOMAIN, and all its connected disk volumes, to NEW_NAME. DOMAIN may be a UUID or name. + """ + if not confirm_flag and not config['unsafe']: + try: + click.confirm('Rename VM {} to {}'.format(domain, new_name), prompt_suffix='? ', abort=True) + except Exception: + exit(0) + + retcode, retmsg = pvc_vm.vm_rename(config, domain, new_name) + cleanup(retcode, retmsg) + + ############################################################################### # pvc vm undefine ############################################################################### @@ -4395,6 +4425,7 @@ 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) +cli_vm.add_command(vm_rename) cli_vm.add_command(vm_undefine) cli_vm.add_command(vm_remove) cli_vm.add_command(vm_dump) diff --git a/daemon-common/vm.py b/daemon-common/vm.py index 3658882f..fec7f5a6 100644 --- a/daemon-common/vm.py +++ b/daemon-common/vm.py @@ -22,6 +22,7 @@ import time import re import lxml.objectify +import lxml.etree import daemon_lib.zkhandler as zkhandler import daemon_lib.common as common @@ -299,6 +300,55 @@ def dump_vm(zk_conn, domain): return True, vm_xml +def rename_vm(zk_conn, domain, new_domain): + dom_uuid = getDomainUUID(zk_conn, 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; renaming is not supported otherwise + state = zkhandler.readdata(zk_conn, '/domains/{}/state'.format(dom_uuid)) + if state != 'stop': + return False, 'ERROR: VM "{}" is not in stopped state; VMs cannot be renamed while running.'.format(domain) + + # Parse and valiate the XML + vm_config = common.getDomainXML(zk_conn, dom_uuid) + + # Obtain the RBD disk list using the common functions + ddisks = common.getDomainDisks(vm_config, {}) + pool_list = [] + rbd_list = [] + for disk in ddisks: + if disk['type'] == 'rbd': + pool_list.append(disk['name'].split('/')[0]) + rbd_list.append(disk['name'].split('/')[1]) + + # Rename each volume in turn + for idx, rbd in enumerate(rbd_list): + rbd_new = re.sub(r"{}".format(domain), new_domain, rbd) + # Skip renaming if nothing changed + if rbd_new == rbd: + continue + ceph.rename_volume(zk_conn, pool_list[idx], rbd, rbd_new) + + # Replace the name in the config + vm_config_new = lxml.etree.tostring(vm_config, encoding='ascii', method='xml').decode().replace(domain, new_domain) + + # Get VM information + _b, dom_info = get_info(zk_conn, dom_uuid) + + # Undefine the old VM + undefine_vm(zk_conn, dom_uuid) + + # Define the new VM + define_vm(zk_conn, vm_config_new, dom_info['node'], dom_info['node_limit'], dom_info['node_selector'], dom_info['node_autostart'], migration_method=dom_info['migration_method'], profile=dom_info['profile'], initial_state='stop') + + # If the VM is migrated, store that + if dom_info['migrated'] != 'no': + zkhandler.writedata(zk_conn, {'/domains/{}/lastnode'.format(dom_uuid): dom_info['last_node']}) + + return True, 'Successfully renamed VM "{}" to "{}".'.format(domain, new_domain) + + def undefine_vm(zk_conn, domain): # Validate that VM exists in cluster dom_uuid = getDomainUUID(zk_conn, domain) diff --git a/docs/manuals/swagger.json b/docs/manuals/swagger.json index 9d0481c4..c52cfc92 100644 --- a/docs/manuals/swagger.json +++ b/docs/manuals/swagger.json @@ -6035,6 +6035,38 @@ ] } }, + "/api/v1/vm/{vm}/rename": { + "post": { + "description": "", + "parameters": [ + { + "description": "The new name of the VM", + "in": "query", + "name": "new_name", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Message" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Message" + } + } + }, + "summary": "Rename VM {vm}, and all connected disk volumes which include this name, to {new_name}", + "tags": [ + "vm" + ] + } + }, "/api/v1/vm/{vm}/state": { "get": { "description": "",