From 9ea9ac3b8a0d75af0e5e42d5b0d7864db1a0e2d8 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Tue, 13 Jul 2021 19:04:56 -0400 Subject: [PATCH] Revamp tag handling and display Add an additional protected class, limit manipulation to one at a time, and ensure future flexibility. Also makes display consistent with other VM elements. --- api-daemon/pvcapid/flaskapi.py | 94 ++++++++--- api-daemon/pvcapid/helper.py | 18 ++- client-cli/pvc/cli_lib/vm.py | 190 ++++++++++++++++++++++- client-cli/pvc/pvc.py | 103 +++++++++++- daemon-common/common.py | 30 +++- daemon-common/migrations/versions/3.json | 1 + daemon-common/vm.py | 52 ++++--- daemon-common/zkhandler.py | 6 + 8 files changed, 432 insertions(+), 62 deletions(-) create mode 100644 daemon-common/migrations/versions/3.json diff --git a/api-daemon/pvcapid/flaskapi.py b/api-daemon/pvcapid/flaskapi.py index d3115ff3..c88fed0c 100755 --- a/api-daemon/pvcapid/flaskapi.py +++ b/api-daemon/pvcapid/flaskapi.py @@ -892,6 +892,22 @@ class API_VM_Root(Resource): migration_method: type: string description: The preferred migration method (live, shutdown, none) + tags: + type: array + description: The tag(s) of the VM + items: + type: object + id: VMTag + properties: + name: + type: string + description: The name of the tag + type: + type: string + description: The type of the tag (user, system) + protected: + type: boolean + description: Whether the tag is protected or not description: type: string description: The description of the VM @@ -1107,7 +1123,8 @@ class API_VM_Root(Resource): {'name': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms', 'none'), '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': 'tags'}, + {'name': 'user_tags', 'action': 'append'}, + {'name': 'protected_tags', 'action': 'append'}, {'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified"}, ]) @Authenticator @@ -1160,10 +1177,17 @@ class API_VM_Root(Resource): - shutdown - none - in: query - name: tags + name: user_tags type: array required: false - description: The tag(s) of the VM + description: The user tag(s) of the VM + items: + type: string + - in: query + name: protected_tags + type: array + required: false + description: The protected user tag(s) of the VM items: type: string responses: @@ -1178,6 +1202,13 @@ class API_VM_Root(Resource): type: object id: Message """ + user_tags = reqargs.get('user_tags', None) + if user_tags is None: + user_tags = [] + protected_tags = reqargs.get('protected_tags', None) + if protected_tags is None: + protected_tags = [] + return api_helper.vm_define( reqargs.get('xml'), reqargs.get('node', None), @@ -1185,7 +1216,8 @@ class API_VM_Root(Resource): reqargs.get('selector', 'none'), bool(strtobool(reqargs.get('autostart', 'false'))), reqargs.get('migration_method', 'none'), - reqargs.get('tags', []) + user_tags, + protected_tags ) @@ -1220,7 +1252,8 @@ class API_VM_Element(Resource): {'name': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms', 'none'), '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': 'tags'}, + {'name': 'user_tags', 'action': 'append'}, + {'name': 'protected_tags', 'action': 'append'}, {'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified"}, ]) @Authenticator @@ -1276,10 +1309,17 @@ class API_VM_Element(Resource): - shutdown - none - in: query - name: tags + name: user_tags type: array required: false - description: The tag(s) of the VM + description: The user tag(s) of the VM + items: + type: string + - in: query + name: protected_tags + type: array + required: false + description: The protected user tag(s) of the VM items: type: string responses: @@ -1294,6 +1334,13 @@ class API_VM_Element(Resource): type: object id: Message """ + user_tags = reqargs.get('user_tags', None) + if user_tags is None: + user_tags = [] + protected_tags = reqargs.get('protected_tags', None) + if protected_tags is None: + protected_tags = [] + return api_helper.vm_define( reqargs.get('xml'), reqargs.get('node', None), @@ -1301,7 +1348,8 @@ class API_VM_Element(Resource): reqargs.get('selector', 'none'), bool(strtobool(reqargs.get('autostart', 'false'))), reqargs.get('migration_method', 'none'), - reqargs.get('tags', []) + user_tags, + protected_tags ) @RequestParser([ @@ -1529,7 +1577,8 @@ class API_VM_Tags(Resource): type: array description: The tag(s) of the VM items: - type: string + type: object + id: VMTag 404: description: VM not found schema: @@ -1539,8 +1588,9 @@ class API_VM_Tags(Resource): return api_helper.get_vm_tags(vm) @RequestParser([ - {'name': 'action', 'choices': ('add', 'remove', 'replace'), 'helptext': "A valid action must be specified"}, - {'name': 'tags'}, + {'name': 'action', 'choices': ('add', 'remove'), 'helptext': "A valid action must be specified"}, + {'name': 'tag'}, + {'name': 'protected'} ]) @Authenticator def post(self, vm, reqargs): @@ -1554,18 +1604,21 @@ class API_VM_Tags(Resource): name: action type: string required: true - description: The action to perform with the tags, either "add" to existing, "remove" from existing, or "replace" all existing + description: The action to perform with the tag enum: - add - remove - - replace - in: query - name: tags - type: array + name: tag + type: string required: true - description: The list of text tags to add/remove/replace-with - items: - type: string + description: The text value of the tag + - in: query + name: protected + type: boolean + required: false + default: false + description: Set the protected state of the tag responses: 200: description: OK @@ -1583,10 +1636,11 @@ class API_VM_Tags(Resource): type: object id: Message """ - return api_helper.update_vm_tags( + return api_helper.update_vm_tag( vm, reqargs.get('action'), - reqargs.get('tags') + reqargs.get('tag'), + reqargs.get('protected', False) ) diff --git a/api-daemon/pvcapid/helper.py b/api-daemon/pvcapid/helper.py index 62212d90..d32de022 100755 --- a/api-daemon/pvcapid/helper.py +++ b/api-daemon/pvcapid/helper.py @@ -433,7 +433,7 @@ def vm_list(zkhandler, node=None, state=None, limit=None, is_fuzzy=True): @ZKConnection(config) -def vm_define(zkhandler, xml, node, limit, selector, autostart, migration_method, tags=[]): +def vm_define(zkhandler, xml, node, limit, selector, autostart, migration_method, user_tags=[], protected_tags=[]): """ Define a VM from Libvirt XML in the PVC cluster. """ @@ -444,6 +444,12 @@ def vm_define(zkhandler, xml, node, limit, selector, autostart, migration_method except Exception as e: return {'message': 'XML is malformed or incorrect: {}'.format(e)}, 400 + tags = list() + for tag in user_tags: + tags.append({'name': tag, 'type': 'user', 'protected': False}) + for tag in protected_tags: + tags.append({'name': tag, 'type': 'user', 'protected': True}) + retflag, retdata = pvc_vm.define_vm(zkhandler, new_cfg, node, limit, selector, autostart, migration_method, profile=None, tags=tags) if retflag: @@ -530,18 +536,18 @@ def get_vm_tags(zkhandler, vm): @ZKConnection(config) -def update_vm_tags(zkhandler, vm, action, tags): +def update_vm_tag(zkhandler, vm, action, tag, protected=False): """ - Update the tags of a VM. + Update a tag of a VM. """ - if action not in ['add', 'remove', 'replace']: - return {"message": "Tag action must be one of 'add', 'remove', 'replace'."}, 400 + if action not in ['add', 'remove']: + return {"message": "Tag action must be one of 'add', 'remove'."}, 400 dom_uuid = pvc_vm.getDomainUUID(zkhandler, vm) if not dom_uuid: return {"message": "VM not found."}, 404 - retflag, retdata = pvc_vm.modify_vm_tags(zkhandler, vm, action, tags) + retflag, retdata = pvc_vm.modify_vm_tag(zkhandler, vm, action, tag, protected=protected) if retflag: retcode = 200 diff --git a/client-cli/pvc/cli_lib/vm.py b/client-cli/pvc/cli_lib/vm.py index 029c7092..5d85dd00 100644 --- a/client-cli/pvc/cli_lib/vm.py +++ b/client-cli/pvc/cli_lib/vm.py @@ -78,12 +78,12 @@ def vm_list(config, limit, target_node, target_state): return False, response.json().get('message', '') -def vm_define(config, xml, node, node_limit, node_selector, node_autostart, migration_method): +def vm_define(config, xml, node, node_limit, node_selector, node_autostart, migration_method, user_tags, protected_tags): """ 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}, migration_method={migration_method} + API arguments: xml={xml}, node={node}, limit={node_limit}, selector={node_selector}, autostart={node_autostart}, migration_method={migration_method}, user_tags={user_tags}, protected_tags={protected_tags} API schema: {"message":"{data}"} """ params = { @@ -91,7 +91,9 @@ def vm_define(config, xml, node, node_limit, node_selector, node_autostart, migr 'limit': node_limit, 'selector': node_selector, 'autostart': node_autostart, - 'migration_method': migration_method + 'migration_method': migration_method, + 'user_tags': user_tags, + 'protected_tags': protected_tags } data = { 'xml': xml @@ -155,7 +157,7 @@ def vm_metadata(config, vm, node_limit, node_selector, node_autostart, migration """ Modify PVC metadata of a VM - API endpoint: GET /vm/{vm}/meta, POST /vm/{vm}/meta + API endpoint: POST /vm/{vm}/meta API arguments: limit={node_limit}, selector={node_selector}, autostart={node_autostart}, migration_method={migration_method} profile={provisioner_profile} API schema: {"message":"{data}"} """ @@ -188,6 +190,119 @@ def vm_metadata(config, vm, node_limit, node_selector, node_autostart, migration return retstatus, response.json().get('message', '') +def vm_tags_get(config, vm): + """ + Get PVC tags of a VM + + API endpoint: GET /vm/{vm}/tags + API arguments: + API schema: {{"name": "{name}", "type": "{type}"},...} + """ + + response = call_api(config, 'get', '/vm/{vm}/tags'.format(vm=vm)) + + if response.status_code == 200: + retstatus = True + retdata = response.json() + else: + retstatus = False + retdata = response.json().get('message', '') + + return retstatus, retdata + + +def vm_tag_set(config, vm, action, tag, protected=False): + """ + Modify PVC tags of a VM + + API endpoint: POST /vm/{vm}/tags + API arguments: action={action}, tag={tag}, protected={protected} + API schema: {"message":"{data}"} + """ + + params = { + 'action': action, + 'tag': tag, + 'protected': protected + } + + # Update the tags + response = call_api(config, 'post', '/vm/{vm}/tags'.format(vm=vm), params=params) + + if response.status_code == 200: + retstatus = True + else: + retstatus = False + + return retstatus, response.json().get('message', '') + + +def format_vm_tags(config, name, tags): + """ + Format the output of a tags dictionary in a nice table + """ + if len(tags) < 1: + return "No tags found." + + output_list = [] + + name_length = 5 + _name_length = len(name) + 1 + if _name_length > name_length: + name_length = _name_length + + tags_name_length = 4 + tags_type_length = 5 + tags_protected_length = 10 + for tag in tags: + _tags_name_length = len(tag['name']) + 1 + if _tags_name_length > tags_name_length: + tags_name_length = _tags_name_length + + _tags_type_length = len(tag['type']) + 1 + if _tags_type_length > tags_type_length: + tags_type_length = _tags_type_length + + _tags_protected_length = len(str(tag['protected'])) + 1 + if _tags_protected_length > tags_protected_length: + tags_protected_length = _tags_protected_length + + output_list.append( + '{bold}{tags_name: <{tags_name_length}} \ +{tags_type: <{tags_type_length}} \ +{tags_protected: <{tags_protected_length}}{end_bold}'.format( + name_length=name_length, + tags_name_length=tags_name_length, + tags_type_length=tags_type_length, + tags_protected_length=tags_protected_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + tags_name='Name', + tags_type='Type', + tags_protected='Protected' + ) + ) + + for tag in sorted(tags, key=lambda t: t['name']): + output_list.append( + '{bold}{tags_name: <{tags_name_length}} \ +{tags_type: <{tags_type_length}} \ +{tags_protected: <{tags_protected_length}}{end_bold}'.format( + name_length=name_length, + tags_type_length=tags_type_length, + tags_name_length=tags_name_length, + tags_protected_length=tags_protected_length, + bold='', + end_bold='', + tags_name=tag['name'], + tags_type=tag['type'], + tags_protected=str(tag['protected']) + ) + ) + + return '\n'.join(output_list) + + def vm_remove(config, vm, delete_disks=False): """ Remove a VM @@ -1248,6 +1363,46 @@ def format_info(config, domain_information, long_output): ainformation.append('{}Autostart:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_autostart)) ainformation.append('{}Migration Method:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_migration_method)) + # Tag list + tags_name_length = 5 + tags_type_length = 5 + tags_protected_length = 10 + for tag in domain_information['tags']: + _tags_name_length = len(tag['name']) + 1 + if _tags_name_length > tags_name_length: + tags_name_length = _tags_name_length + + _tags_type_length = len(tag['type']) + 1 + if _tags_type_length > tags_type_length: + tags_type_length = _tags_type_length + + _tags_protected_length = len(str(tag['protected'])) + 1 + if _tags_protected_length > tags_protected_length: + tags_protected_length = _tags_protected_length + + ainformation.append('') + ainformation.append('{purple}Tags:{end} {bold}{tags_name: <{tags_name_length}} {tags_type: <{tags_type_length}} {tags_protected: <{tags_protected_length}}{end}'.format( + purple=ansiprint.purple(), + bold=ansiprint.bold(), + end=ansiprint.end(), + tags_name_length=tags_name_length, + tags_type_length=tags_type_length, + tags_protected_length=tags_protected_length, + tags_name='Name', + tags_type='Type', + tags_protected='Protected' + )) + + for tag in sorted(domain_information['tags'], key=lambda t: t['type'] + t['name']): + ainformation.append(' {tags_name: <{tags_name_length}} {tags_type: <{tags_type_length}} {tags_protected: <{tags_protected_length}}'.format( + tags_name_length=tags_name_length, + tags_type_length=tags_type_length, + tags_protected_length=tags_protected_length, + tags_name=tag['name'], + tags_type=tag['type'], + tags_protected=str(tag['protected']) + )) + # Network list net_list = [] cluster_net_list = call_api(config, 'get', '/network').json() @@ -1331,6 +1486,14 @@ def format_list(config, vm_list, raw): net_list.append(net['vni']) return net_list + # Function to get tag names and returna nicer list + def getNiceTagName(domain_information): + # Tag list + tag_list = [] + for tag in sorted(domain_information['tags'], key=lambda t: t['type'] + t['name']): + tag_list.append(tag['name']) + return tag_list + # Handle raw mode since it just lists the names if raw: ainformation = list() @@ -1344,6 +1507,7 @@ def format_list(config, vm_list, raw): # Dynamic columns: node_name, node, migrated vm_name_length = 5 vm_state_length = 6 + vm_tags_length = 5 vm_nets_length = 9 vm_ram_length = 8 vm_vcpu_length = 6 @@ -1351,6 +1515,7 @@ def format_list(config, vm_list, raw): vm_migrated_length = 9 for domain_information in vm_list: net_list = getNiceNetID(domain_information) + tag_list = getNiceTagName(domain_information) # vm_name column _vm_name_length = len(domain_information['name']) + 1 if _vm_name_length > vm_name_length: @@ -1359,6 +1524,10 @@ def format_list(config, vm_list, raw): _vm_state_length = len(domain_information['state']) + 1 if _vm_state_length > vm_state_length: vm_state_length = _vm_state_length + # vm_tags column + _vm_tags_length = len(','.join(tag_list)) + 1 + if _vm_tags_length > vm_tags_length: + vm_tags_length = _vm_tags_length # vm_nets column _vm_nets_length = len(','.join(net_list)) + 1 if _vm_nets_length > vm_nets_length: @@ -1375,12 +1544,12 @@ def format_list(config, vm_list, raw): # Format the string (header) vm_list_output.append( '{bold}{vm_header: <{vm_header_length}} {resource_header: <{resource_header_length}} {node_header: <{node_header_length}}{end_bold}'.format( - vm_header_length=vm_name_length + vm_state_length + 1, + vm_header_length=vm_name_length + vm_state_length + vm_tags_length + 2, resource_header_length=vm_nets_length + vm_ram_length + vm_vcpu_length + 2, node_header_length=vm_node_length + vm_migrated_length + 1, bold=ansiprint.bold(), end_bold=ansiprint.end(), - vm_header='VMs ' + ''.join(['-' for _ in range(4, vm_name_length + vm_state_length)]), + vm_header='VMs ' + ''.join(['-' for _ in range(4, vm_name_length + vm_state_length + vm_tags_length + 1)]), resource_header='Resources ' + ''.join(['-' for _ in range(10, vm_nets_length + vm_ram_length + vm_vcpu_length + 1)]), node_header='Node ' + ''.join(['-' for _ in range(5, vm_node_length + vm_migrated_length)]) ) @@ -1389,12 +1558,14 @@ def format_list(config, vm_list, raw): vm_list_output.append( '{bold}{vm_name: <{vm_name_length}} \ {vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \ +{vm_tags: <{vm_tags_length}} \ {vm_networks: <{vm_nets_length}} \ {vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \ {vm_node: <{vm_node_length}} \ {vm_migrated: <{vm_migrated_length}}{end_bold}'.format( vm_name_length=vm_name_length, vm_state_length=vm_state_length, + vm_tags_length=vm_tags_length, vm_nets_length=vm_nets_length, vm_ram_length=vm_ram_length, vm_vcpu_length=vm_vcpu_length, @@ -1406,6 +1577,7 @@ def format_list(config, vm_list, raw): end_colour='', vm_name='Name', vm_state='State', + vm_tags='Tags', vm_networks='Networks', vm_memory='RAM (M)', vm_vcpu='vCPUs', @@ -1434,6 +1606,9 @@ def format_list(config, vm_list, raw): # Handle colouring for an invalid network config net_list = getNiceNetID(domain_information) + tag_list = getNiceTagName(domain_information) + if len(tag_list) < 1: + tag_list = ['N/A'] vm_net_colour = '' for net_vni in net_list: if net_vni not in ['cluster', 'storage', 'upstream'] and not re.match(r'^macvtap:.*', net_vni) and not re.match(r'^hostdev:.*', net_vni): @@ -1443,12 +1618,14 @@ def format_list(config, vm_list, raw): vm_list_output.append( '{bold}{vm_name: <{vm_name_length}} \ {vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \ +{vm_tags: <{vm_tags_length}} \ {vm_net_colour}{vm_networks: <{vm_nets_length}}{end_colour} \ {vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \ {vm_node: <{vm_node_length}} \ {vm_migrated: <{vm_migrated_length}}{end_bold}'.format( vm_name_length=vm_name_length, vm_state_length=vm_state_length, + vm_tags_length=vm_tags_length, vm_nets_length=vm_nets_length, vm_ram_length=vm_ram_length, vm_vcpu_length=vm_vcpu_length, @@ -1460,6 +1637,7 @@ def format_list(config, vm_list, raw): end_colour=ansiprint.end(), vm_name=domain_information['name'], vm_state=domain_information['state'], + vm_tags=','.join(tag_list), vm_net_colour=vm_net_colour, vm_networks=','.join(net_list), vm_memory=domain_information['memory'], diff --git a/client-cli/pvc/pvc.py b/client-cli/pvc/pvc.py index 49dec3f8..87cc2ba6 100755 --- a/client-cli/pvc/pvc.py +++ b/client-cli/pvc/pvc.py @@ -638,11 +638,21 @@ def cli_vm(): type=click.Choice(['none', 'live', 'shutdown']), help='The preferred migration method of the VM between nodes; saved with VM.' ) +@click.option( + '-g', '--tag', 'user_tags', + default=[], multiple=True, + help='User tag(s) for the VM.' +) +@click.option( + '-G', '--protected-tag', 'protected_tags', + default=[], multiple=True, + help='Protected user tag(s) for the VM.' +) @click.argument( 'vmconfig', type=click.File() ) @cluster_req -def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart, migration_method): +def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart, migration_method, user_tags, protected_tags): """ Define a new virtual machine from Libvirt XML configuration file VMCONFIG. """ @@ -658,7 +668,7 @@ def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart, except Exception: 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, migration_method) + retcode, retmsg = pvc_vm.vm_define(config, new_cfg, target_node, node_limit, node_selector, node_autostart, migration_method, user_tags, protected_tags) cleanup(retcode, retmsg) @@ -1111,6 +1121,90 @@ def vm_flush_locks(domain): cleanup(retcode, retmsg) +############################################################################### +# pvc vm tag +############################################################################### +@click.group(name='tag', short_help='Manage tags of a virtual machine.', context_settings=CONTEXT_SETTINGS) +def vm_tags(): + """ + Manage the tags of a virtual machine in the PVC cluster." + """ + pass + + +############################################################################### +# pvc vm tag get +############################################################################### +@click.command(name='get', short_help='Get the current tags 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_tags_get(domain, raw): + """ + Get the current tags of the virtual machine DOMAIN. + """ + + retcode, retdata = pvc_vm.vm_tags_get(config, domain) + if retcode: + if not raw: + retdata = pvc_vm.format_vm_tags(config, domain, retdata['tags']) + else: + if len(retdata['tags']) > 0: + retdata = '\n'.join([tag['name'] for tag in retdata['tags']]) + else: + retdata = 'No tags found.' + cleanup(retcode, retdata) + + +############################################################################### +# pvc vm tag add +############################################################################### +@click.command(name='add', short_help='Add new tags to a virtual machine.') +@click.argument( + 'domain' +) +@click.argument( + 'tag' +) +@click.option( + '-p', '--protected', 'protected', is_flag=True, required=False, default=False, + help="Set this tag as protected; protected tags cannot be removed." +) +@cluster_req +def vm_tags_add(domain, tag, protected): + """ + Add TAG to the virtual machine DOMAIN. + """ + + retcode, retmsg = pvc_vm.vm_tag_set(config, domain, 'add', tag, protected) + cleanup(retcode, retmsg) + + +############################################################################### +# pvc vm tag remove +############################################################################### +@click.command(name='remove', short_help='Remove tags from a virtual machine.') +@click.argument( + 'domain' +) +@click.argument( + 'tag' +) +@cluster_req +def vm_tags_remove(domain, tag): + """ + Remove TAG from the virtual machine DOMAIN. + """ + + retcode, retmsg = pvc_vm.vm_tag_set(config, domain, 'remove', tag) + cleanup(retcode, retmsg) + + ############################################################################### # pvc vm vcpu ############################################################################### @@ -4612,6 +4706,10 @@ cli_node.add_command(node_unflush) cli_node.add_command(node_info) cli_node.add_command(node_list) +vm_tags.add_command(vm_tags_get) +vm_tags.add_command(vm_tags_add) +vm_tags.add_command(vm_tags_remove) + vm_vcpu.add_command(vm_vcpu_get) vm_vcpu.add_command(vm_vcpu_set) @@ -4642,6 +4740,7 @@ 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_tags) cli_vm.add_command(vm_vcpu) cli_vm.add_command(vm_memory) cli_vm.add_command(vm_network) diff --git a/daemon-common/common.py b/daemon-common/common.py index 14d1a17c..ad60d04f 100644 --- a/daemon-common/common.py +++ b/daemon-common/common.py @@ -310,7 +310,18 @@ def getDomainDiskList(zkhandler, dom_uuid): # Get a list of domain tags # def getDomainTags(zkhandler, dom_uuid): - tags = zkhandler.read(('domain.meta.tags', dom_uuid)).split(',') + """ + Get a list of tags for domain dom_uuid + + The UUID must be validated before calling this function! + """ + tags = list() + + for tag in zkhandler.children(('domain.meta.tags', dom_uuid)): + tag_type = zkhandler.read(('domain.meta.tags', dom_uuid, 'tag.type', tag)) + protected = bool(strtobool(zkhandler.read(('domain.meta.tags', dom_uuid, 'tag.protected', tag)))) + tags.append({'name': tag, 'type': tag_type, 'protected': protected}) + return tags @@ -318,10 +329,15 @@ def getDomainTags(zkhandler, dom_uuid): # Get a set of domain metadata # def getDomainMetadata(zkhandler, dom_uuid): - domain_node_limit = zkhandler.read(('domain.meta.node_limit', uuid)) - domain_node_selector = zkhandler.read(('domain.meta.node_selector', uuid)) - domain_node_autostart = zkhandler.read(('domain.meta.autostart', uuid)) - domain_migration_method = zkhandler.read(('domain.meta.migrate_method', uuid)) + """ + Get the domain metadata for domain dom_uuid + + The UUID must be validated before calling this function! + """ + domain_node_limit = zkhandler.read(('domain.meta.node_limit', dom_uuid)) + domain_node_selector = zkhandler.read(('domain.meta.node_selector', dom_uuid)) + domain_node_autostart = zkhandler.read(('domain.meta.autostart', dom_uuid)) + domain_migration_method = zkhandler.read(('domain.meta.migrate_method', dom_uuid)) if not domain_node_limit: domain_node_limit = None @@ -348,7 +364,7 @@ def getInformationFromXML(zkhandler, uuid): domain_failedreason = zkhandler.read(('domain.failed_reason', uuid)) domain_node_limit, domain_node_selector, domain_node_autostart, domain_migration_method = getDomainMetadata(zkhandler, uuid) - + domain_tags = getDomainTags(zkhandler, uuid) domain_profile = zkhandler.read(('domain.profile', uuid)) domain_vnc = zkhandler.read(('domain.console.vnc', uuid)) @@ -369,8 +385,6 @@ def getInformationFromXML(zkhandler, uuid): else: stats_data = {} - domain_tags = getDomainTags(zkhandler, uuid) - domain_uuid, domain_name, domain_description, domain_memory, domain_vcpu, domain_vcputopo = getDomainMainDetails(parsed_xml) domain_networks = getDomainNetworks(parsed_xml, stats_data) diff --git a/daemon-common/migrations/versions/3.json b/daemon-common/migrations/versions/3.json new file mode 100644 index 00000000..75bdf32d --- /dev/null +++ b/daemon-common/migrations/versions/3.json @@ -0,0 +1 @@ +{"version": "3", "root": "", "base": {"root": "", "schema": "/schema", "schema.version": "/schema/version", "config": "/config", "config.maintenance": "/config/maintenance", "config.primary_node": "/config/primary_node", "config.primary_node.sync_lock": "/config/primary_node/sync_lock", "config.upstream_ip": "/config/upstream_ip", "config.migration_target_selector": "/config/migration_target_selector", "cmd": "/cmd", "cmd.node": "/cmd/nodes", "cmd.domain": "/cmd/domains", "cmd.ceph": "/cmd/ceph", "node": "/nodes", "domain": "/domains", "network": "/networks", "storage": "/ceph", "storage.util": "/ceph/util", "osd": "/ceph/osds", "pool": "/ceph/pools", "volume": "/ceph/volumes", "snapshot": "/ceph/snapshots"}, "node": {"name": "", "keepalive": "/keepalive", "mode": "/daemonmode", "data.active_schema": "/activeschema", "data.latest_schema": "/latestschema", "data.static": "/staticdata", "data.pvc_version": "/pvcversion", "running_domains": "/runningdomains", "count.provisioned_domains": "/domainscount", "count.networks": "/networkscount", "state.daemon": "/daemonstate", "state.router": "/routerstate", "state.domain": "/domainstate", "cpu.load": "/cpuload", "vcpu.allocated": "/vcpualloc", "memory.total": "/memtotal", "memory.used": "/memused", "memory.free": "/memfree", "memory.allocated": "/memalloc", "memory.provisioned": "/memprov", "ipmi.hostname": "/ipmihostname", "ipmi.username": "/ipmiusername", "ipmi.password": "/ipmipassword", "sriov": "/sriov", "sriov.pf": "/sriov/pf", "sriov.vf": "/sriov/vf"}, "sriov_pf": {"phy": "", "mtu": "/mtu", "vfcount": "/vfcount"}, "sriov_vf": {"phy": "", "pf": "/pf", "mtu": "/mtu", "mac": "/mac", "phy_mac": "/phy_mac", "config": "/config", "config.vlan_id": "/config/vlan_id", "config.vlan_qos": "/config/vlan_qos", "config.tx_rate_min": "/config/tx_rate_min", "config.tx_rate_max": "/config/tx_rate_max", "config.spoof_check": "/config/spoof_check", "config.link_state": "/config/link_state", "config.trust": "/config/trust", "config.query_rss": "/config/query_rss", "pci": "/pci", "pci.domain": "/pci/domain", "pci.bus": "/pci/bus", "pci.slot": "/pci/slot", "pci.function": "/pci/function", "used": "/used", "used_by": "/used_by"}, "domain": {"name": "", "xml": "/xml", "state": "/state", "profile": "/profile", "stats": "/stats", "node": "/node", "last_node": "/lastnode", "failed_reason": "/failedreason", "storage.volumes": "/rbdlist", "console.log": "/consolelog", "console.vnc": "/vnc", "meta.autostart": "/node_autostart", "meta.migrate_method": "/migration_method", "meta.node_selector": "/node_selector", "meta.node_limit": "/node_limit", "meta.tags": "/tags", "migrate.sync_lock": "/migrate_sync_lock"}, "tag": {"name": "", "type": "/type", "protected": "/protected"}, "network": {"vni": "", "type": "/nettype", "rule": "/firewall_rules", "rule.in": "/firewall_rules/in", "rule.out": "/firewall_rules/out", "nameservers": "/name_servers", "domain": "/domain", "reservation": "/dhcp4_reservations", "lease": "/dhcp4_leases", "ip4.gateway": "/ip4_gateway", "ip4.network": "/ip4_network", "ip4.dhcp": "/dhcp4_flag", "ip4.dhcp_start": "/dhcp4_start", "ip4.dhcp_end": "/dhcp4_end", "ip6.gateway": "/ip6_gateway", "ip6.network": "/ip6_network", "ip6.dhcp": "/dhcp6_flag"}, "reservation": {"mac": "", "ip": "/ipaddr", "hostname": "/hostname"}, "lease": {"mac": "", "ip": "/ipaddr", "hostname": "/hostname", "expiry": "/expiry", "client_id": "/clientid"}, "rule": {"description": "", "rule": "/rule", "order": "/order"}, "osd": {"id": "", "node": "/node", "device": "/device", "stats": "/stats"}, "pool": {"name": "", "pgs": "/pgs", "stats": "/stats"}, "volume": {"name": "", "stats": "/stats"}, "snapshot": {"name": "", "stats": "/stats"}} \ No newline at end of file diff --git a/daemon-common/vm.py b/daemon-common/vm.py index 60be1b6d..a97c5e93 100644 --- a/daemon-common/vm.py +++ b/daemon-common/vm.py @@ -24,6 +24,7 @@ import re import lxml.objectify import lxml.etree +from distutils.util import strtobool from uuid import UUID from concurrent.futures import ThreadPoolExecutor @@ -246,10 +247,18 @@ def define_vm(zkhandler, config_data, target_node, node_limit, node_selector, no (('domain.meta.migrate_method', dom_uuid), migration_method), (('domain.meta.node_limit', dom_uuid), formatted_node_limit), (('domain.meta.node_selector', dom_uuid), node_selector), - (('domain.meta.tags', dom_uuid), ','.join(tags)), + (('domain.meta.tags', dom_uuid), ''), (('domain.migrate.sync_lock', dom_uuid), ''), ]) + for tag in tags: + tag_name = tag['name'] + zkhandler.write([ + (('domain.meta.tags', dom_uuid, 'tag.name', tag_name), tag['name']), + (('domain.meta.tags', dom_uuid, 'tag.type', tag_name), tag['type']), + (('domain.meta.tags', dom_uuid, 'tag.protected', tag_name), tag['protected']), + ]) + return True, 'Added new VM with Name "{}" and UUID "{}" to database.'.format(dom_name, dom_uuid) @@ -283,34 +292,37 @@ def modify_vm_metadata(zkhandler, domain, node_limit, node_selector, node_autost return True, 'Successfully modified PVC metadata of VM "{}".'.format(domain) -def modify_vm_tags(zkhandler, domain, action, tags): +def modify_vm_tag(zkhandler, domain, action, tag, protected=False): dom_uuid = getDomainUUID(zkhandler, domain) if not dom_uuid: return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) - if action in ['replace']: + if action == 'add': zkhandler.write([ - (('domain.meta.tags', dom_uuid), ','.join(tags)) + (('domain.meta.tags', dom_uuid, 'tag.name', tag), tag), + (('domain.meta.tags', dom_uuid, 'tag.type', tag), 'user'), + (('domain.meta.tags', dom_uuid, 'tag.protected', tag), protected), ]) - elif action in ['add']: - current_tags = zkhandler.read(('domain.meta.tags', dom_uuid)).split(',') - updated_tags = current_tags + tags - zkhandler.write([ - (('domain.meta.tags', dom_uuid), ','.join(updated_tags)) - ]) - elif action in ['remove']: - current_tags = zkhandler.read(('domain.meta.tags', dom_uuid)).split(',') - for tag in tags: - if tag in current_tags: - current_tags.remove(tag) - zkhandler.write([ - (('domain.meta.tags', dom_uuid), ','.join(current_tags)) + + return True, 'Successfully added tag "{}" to VM "{}".'.format(tag, domain) + elif action == 'remove': + if not zkhandler.exists(('domain.meta.tags', dom_uuid, 'tag', tag)): + return False, 'The tag "{}" does not exist.'.format(tag) + + if zkhandler.read(('domain.meta.tags', dom_uuid, 'tag.type', tag)) != 'user': + return False, 'The tag "{}" is not a user tag and cannot be removed.'.format(tag) + + if bool(strtobool(zkhandler.read(('domain.meta.tags', dom_uuid, 'tag.protected', tag)))): + return False, 'The tag "{}" is protected and cannot be removed.'.format(tag) + + zkhandler.delete([ + (('domain.meta.tags', dom_uuid, 'tag', tag)) ]) + + return True, 'Successfully removed tag "{}" from VM "{}".'.format(tag, domain) else: return False, 'Specified tag action is not available.' - return True, 'Successfully modified tags of VM "{}".'.format(domain) - def modify_vm(zkhandler, domain, restart, new_vm_config): dom_uuid = getDomainUUID(zkhandler, domain) @@ -433,7 +445,7 @@ def rename_vm(zkhandler, domain, new_domain): undefine_vm(zkhandler, dom_uuid) # Define the new VM - define_vm(zkhandler, 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') + define_vm(zkhandler, 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'], tags=dom_info['tags'], initial_state='stop') # If the VM is migrated, store that if dom_info['migrated'] != 'no': diff --git a/daemon-common/zkhandler.py b/daemon-common/zkhandler.py index 80e155aa..c0af11cf 100644 --- a/daemon-common/zkhandler.py +++ b/daemon-common/zkhandler.py @@ -579,6 +579,12 @@ class ZKSchema(object): 'meta.tags': '/tags', 'migrate.sync_lock': '/migrate_sync_lock' }, + # The schema of an individual domain tag entry (/domains/{domain}/tags/{tag}) + 'tag': { + 'name': '', # The root key + 'type': '/type', + 'protected': '/protected' + }, # The schema of an individual network entry (/networks/{vni}) 'network': { 'vni': '', # The root key