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.
This commit is contained in:
Joshua Boniface 2021-07-13 19:04:56 -04:00
parent 27f1758791
commit 9ea9ac3b8a
8 changed files with 432 additions and 62 deletions

View File

@ -892,6 +892,22 @@ class API_VM_Root(Resource):
migration_method: migration_method:
type: string type: string
description: The preferred migration method (live, shutdown, none) 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: description:
type: string type: string
description: The description of the VM 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': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms', 'none'), 'helptext': "A valid selector must be specified"},
{'name': 'autostart'}, {'name': 'autostart'},
{'name': 'migration_method', 'choices': ('live', 'shutdown', 'none'), 'helptext': "A valid migration_method must be specified"}, {'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"}, {'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified"},
]) ])
@Authenticator @Authenticator
@ -1160,10 +1177,17 @@ class API_VM_Root(Resource):
- shutdown - shutdown
- none - none
- in: query - in: query
name: tags name: user_tags
type: array type: array
required: false 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: items:
type: string type: string
responses: responses:
@ -1178,6 +1202,13 @@ class API_VM_Root(Resource):
type: object type: object
id: Message 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( return api_helper.vm_define(
reqargs.get('xml'), reqargs.get('xml'),
reqargs.get('node', None), reqargs.get('node', None),
@ -1185,7 +1216,8 @@ class API_VM_Root(Resource):
reqargs.get('selector', 'none'), reqargs.get('selector', 'none'),
bool(strtobool(reqargs.get('autostart', 'false'))), bool(strtobool(reqargs.get('autostart', 'false'))),
reqargs.get('migration_method', 'none'), 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': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms', 'none'), 'helptext': "A valid selector must be specified"},
{'name': 'autostart'}, {'name': 'autostart'},
{'name': 'migration_method', 'choices': ('live', 'shutdown', 'none'), 'helptext': "A valid migration_method must be specified"}, {'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"}, {'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified"},
]) ])
@Authenticator @Authenticator
@ -1276,10 +1309,17 @@ class API_VM_Element(Resource):
- shutdown - shutdown
- none - none
- in: query - in: query
name: tags name: user_tags
type: array type: array
required: false 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: items:
type: string type: string
responses: responses:
@ -1294,6 +1334,13 @@ class API_VM_Element(Resource):
type: object type: object
id: Message 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( return api_helper.vm_define(
reqargs.get('xml'), reqargs.get('xml'),
reqargs.get('node', None), reqargs.get('node', None),
@ -1301,7 +1348,8 @@ class API_VM_Element(Resource):
reqargs.get('selector', 'none'), reqargs.get('selector', 'none'),
bool(strtobool(reqargs.get('autostart', 'false'))), bool(strtobool(reqargs.get('autostart', 'false'))),
reqargs.get('migration_method', 'none'), reqargs.get('migration_method', 'none'),
reqargs.get('tags', []) user_tags,
protected_tags
) )
@RequestParser([ @RequestParser([
@ -1529,7 +1577,8 @@ class API_VM_Tags(Resource):
type: array type: array
description: The tag(s) of the VM description: The tag(s) of the VM
items: items:
type: string type: object
id: VMTag
404: 404:
description: VM not found description: VM not found
schema: schema:
@ -1539,8 +1588,9 @@ class API_VM_Tags(Resource):
return api_helper.get_vm_tags(vm) return api_helper.get_vm_tags(vm)
@RequestParser([ @RequestParser([
{'name': 'action', 'choices': ('add', 'remove', 'replace'), 'helptext': "A valid action must be specified"}, {'name': 'action', 'choices': ('add', 'remove'), 'helptext': "A valid action must be specified"},
{'name': 'tags'}, {'name': 'tag'},
{'name': 'protected'}
]) ])
@Authenticator @Authenticator
def post(self, vm, reqargs): def post(self, vm, reqargs):
@ -1554,18 +1604,21 @@ class API_VM_Tags(Resource):
name: action name: action
type: string type: string
required: true 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: enum:
- add - add
- remove - remove
- replace
- in: query - in: query
name: tags name: tag
type: array
required: true
description: The list of text tags to add/remove/replace-with
items:
type: string type: string
required: true
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: responses:
200: 200:
description: OK description: OK
@ -1583,10 +1636,11 @@ class API_VM_Tags(Resource):
type: object type: object
id: Message id: Message
""" """
return api_helper.update_vm_tags( return api_helper.update_vm_tag(
vm, vm,
reqargs.get('action'), reqargs.get('action'),
reqargs.get('tags') reqargs.get('tag'),
reqargs.get('protected', False)
) )

View File

@ -433,7 +433,7 @@ def vm_list(zkhandler, node=None, state=None, limit=None, is_fuzzy=True):
@ZKConnection(config) @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. 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: except Exception as e:
return {'message': 'XML is malformed or incorrect: {}'.format(e)}, 400 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) retflag, retdata = pvc_vm.define_vm(zkhandler, new_cfg, node, limit, selector, autostart, migration_method, profile=None, tags=tags)
if retflag: if retflag:
@ -530,18 +536,18 @@ def get_vm_tags(zkhandler, vm):
@ZKConnection(config) @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']: if action not in ['add', 'remove']:
return {"message": "Tag action must be one of 'add', 'remove', 'replace'."}, 400 return {"message": "Tag action must be one of 'add', 'remove'."}, 400
dom_uuid = pvc_vm.getDomainUUID(zkhandler, vm) dom_uuid = pvc_vm.getDomainUUID(zkhandler, vm)
if not dom_uuid: if not dom_uuid:
return {"message": "VM not found."}, 404 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: if retflag:
retcode = 200 retcode = 200

View File

@ -78,12 +78,12 @@ def vm_list(config, limit, target_node, target_state):
return False, response.json().get('message', '') 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 Define a new VM on the cluster
API endpoint: POST /vm 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}"} API schema: {"message":"{data}"}
""" """
params = { params = {
@ -91,7 +91,9 @@ def vm_define(config, xml, node, node_limit, node_selector, node_autostart, migr
'limit': node_limit, 'limit': node_limit,
'selector': node_selector, 'selector': node_selector,
'autostart': node_autostart, 'autostart': node_autostart,
'migration_method': migration_method 'migration_method': migration_method,
'user_tags': user_tags,
'protected_tags': protected_tags
} }
data = { data = {
'xml': xml 'xml': xml
@ -155,7 +157,7 @@ def vm_metadata(config, vm, node_limit, node_selector, node_autostart, migration
""" """
Modify PVC metadata of a VM 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 arguments: limit={node_limit}, selector={node_selector}, autostart={node_autostart}, migration_method={migration_method} profile={provisioner_profile}
API schema: {"message":"{data}"} 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', '') 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): def vm_remove(config, vm, delete_disks=False):
""" """
Remove a VM 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('{}Autostart:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_autostart))
ainformation.append('{}Migration Method:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_migration_method)) 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 # Network list
net_list = [] net_list = []
cluster_net_list = call_api(config, 'get', '/network').json() 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']) net_list.append(net['vni'])
return net_list 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 # Handle raw mode since it just lists the names
if raw: if raw:
ainformation = list() ainformation = list()
@ -1344,6 +1507,7 @@ def format_list(config, vm_list, raw):
# Dynamic columns: node_name, node, migrated # Dynamic columns: node_name, node, migrated
vm_name_length = 5 vm_name_length = 5
vm_state_length = 6 vm_state_length = 6
vm_tags_length = 5
vm_nets_length = 9 vm_nets_length = 9
vm_ram_length = 8 vm_ram_length = 8
vm_vcpu_length = 6 vm_vcpu_length = 6
@ -1351,6 +1515,7 @@ def format_list(config, vm_list, raw):
vm_migrated_length = 9 vm_migrated_length = 9
for domain_information in vm_list: for domain_information in vm_list:
net_list = getNiceNetID(domain_information) net_list = getNiceNetID(domain_information)
tag_list = getNiceTagName(domain_information)
# vm_name column # vm_name column
_vm_name_length = len(domain_information['name']) + 1 _vm_name_length = len(domain_information['name']) + 1
if _vm_name_length > vm_name_length: 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 _vm_state_length = len(domain_information['state']) + 1
if _vm_state_length > vm_state_length: if _vm_state_length > vm_state_length:
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 column
_vm_nets_length = len(','.join(net_list)) + 1 _vm_nets_length = len(','.join(net_list)) + 1
if _vm_nets_length > vm_nets_length: if _vm_nets_length > vm_nets_length:
@ -1375,12 +1544,12 @@ def format_list(config, vm_list, raw):
# Format the string (header) # Format the string (header)
vm_list_output.append( vm_list_output.append(
'{bold}{vm_header: <{vm_header_length}} {resource_header: <{resource_header_length}} {node_header: <{node_header_length}}{end_bold}'.format( '{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, resource_header_length=vm_nets_length + vm_ram_length + vm_vcpu_length + 2,
node_header_length=vm_node_length + vm_migrated_length + 1, node_header_length=vm_node_length + vm_migrated_length + 1,
bold=ansiprint.bold(), bold=ansiprint.bold(),
end_bold=ansiprint.end(), 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)]), 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)]) 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( vm_list_output.append(
'{bold}{vm_name: <{vm_name_length}} \ '{bold}{vm_name: <{vm_name_length}} \
{vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \ {vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \
{vm_tags: <{vm_tags_length}} \
{vm_networks: <{vm_nets_length}} \ {vm_networks: <{vm_nets_length}} \
{vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \ {vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \
{vm_node: <{vm_node_length}} \ {vm_node: <{vm_node_length}} \
{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( {vm_migrated: <{vm_migrated_length}}{end_bold}'.format(
vm_name_length=vm_name_length, vm_name_length=vm_name_length,
vm_state_length=vm_state_length, vm_state_length=vm_state_length,
vm_tags_length=vm_tags_length,
vm_nets_length=vm_nets_length, vm_nets_length=vm_nets_length,
vm_ram_length=vm_ram_length, vm_ram_length=vm_ram_length,
vm_vcpu_length=vm_vcpu_length, vm_vcpu_length=vm_vcpu_length,
@ -1406,6 +1577,7 @@ def format_list(config, vm_list, raw):
end_colour='', end_colour='',
vm_name='Name', vm_name='Name',
vm_state='State', vm_state='State',
vm_tags='Tags',
vm_networks='Networks', vm_networks='Networks',
vm_memory='RAM (M)', vm_memory='RAM (M)',
vm_vcpu='vCPUs', vm_vcpu='vCPUs',
@ -1434,6 +1606,9 @@ def format_list(config, vm_list, raw):
# Handle colouring for an invalid network config # Handle colouring for an invalid network config
net_list = getNiceNetID(domain_information) net_list = getNiceNetID(domain_information)
tag_list = getNiceTagName(domain_information)
if len(tag_list) < 1:
tag_list = ['N/A']
vm_net_colour = '' vm_net_colour = ''
for net_vni in net_list: 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): 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( vm_list_output.append(
'{bold}{vm_name: <{vm_name_length}} \ '{bold}{vm_name: <{vm_name_length}} \
{vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \ {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_net_colour}{vm_networks: <{vm_nets_length}}{end_colour} \
{vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \ {vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \
{vm_node: <{vm_node_length}} \ {vm_node: <{vm_node_length}} \
{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( {vm_migrated: <{vm_migrated_length}}{end_bold}'.format(
vm_name_length=vm_name_length, vm_name_length=vm_name_length,
vm_state_length=vm_state_length, vm_state_length=vm_state_length,
vm_tags_length=vm_tags_length,
vm_nets_length=vm_nets_length, vm_nets_length=vm_nets_length,
vm_ram_length=vm_ram_length, vm_ram_length=vm_ram_length,
vm_vcpu_length=vm_vcpu_length, vm_vcpu_length=vm_vcpu_length,
@ -1460,6 +1637,7 @@ def format_list(config, vm_list, raw):
end_colour=ansiprint.end(), end_colour=ansiprint.end(),
vm_name=domain_information['name'], vm_name=domain_information['name'],
vm_state=domain_information['state'], vm_state=domain_information['state'],
vm_tags=','.join(tag_list),
vm_net_colour=vm_net_colour, vm_net_colour=vm_net_colour,
vm_networks=','.join(net_list), vm_networks=','.join(net_list),
vm_memory=domain_information['memory'], vm_memory=domain_information['memory'],

View File

@ -638,11 +638,21 @@ def cli_vm():
type=click.Choice(['none', 'live', 'shutdown']), type=click.Choice(['none', 'live', 'shutdown']),
help='The preferred migration method of the VM between nodes; saved with VM.' 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( @click.argument(
'vmconfig', type=click.File() 'vmconfig', type=click.File()
) )
@cluster_req @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. 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: except Exception:
cleanup(False, 'Error: XML is malformed or invalid') 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) cleanup(retcode, retmsg)
@ -1111,6 +1121,90 @@ def vm_flush_locks(domain):
cleanup(retcode, retmsg) 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 # pvc vm vcpu
############################################################################### ###############################################################################
@ -4612,6 +4706,10 @@ cli_node.add_command(node_unflush)
cli_node.add_command(node_info) cli_node.add_command(node_info)
cli_node.add_command(node_list) 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_get)
vm_vcpu.add_command(vm_vcpu_set) 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_migrate)
cli_vm.add_command(vm_unmigrate) cli_vm.add_command(vm_unmigrate)
cli_vm.add_command(vm_flush_locks) cli_vm.add_command(vm_flush_locks)
cli_vm.add_command(vm_tags)
cli_vm.add_command(vm_vcpu) cli_vm.add_command(vm_vcpu)
cli_vm.add_command(vm_memory) cli_vm.add_command(vm_memory)
cli_vm.add_command(vm_network) cli_vm.add_command(vm_network)

View File

@ -310,7 +310,18 @@ def getDomainDiskList(zkhandler, dom_uuid):
# Get a list of domain tags # Get a list of domain tags
# #
def getDomainTags(zkhandler, dom_uuid): 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 return tags
@ -318,10 +329,15 @@ def getDomainTags(zkhandler, dom_uuid):
# Get a set of domain metadata # Get a set of domain metadata
# #
def getDomainMetadata(zkhandler, dom_uuid): 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)) Get the domain metadata for domain dom_uuid
domain_node_autostart = zkhandler.read(('domain.meta.autostart', uuid))
domain_migration_method = zkhandler.read(('domain.meta.migrate_method', 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: if not domain_node_limit:
domain_node_limit = None domain_node_limit = None
@ -348,7 +364,7 @@ def getInformationFromXML(zkhandler, uuid):
domain_failedreason = zkhandler.read(('domain.failed_reason', 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_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_profile = zkhandler.read(('domain.profile', uuid))
domain_vnc = zkhandler.read(('domain.console.vnc', uuid)) domain_vnc = zkhandler.read(('domain.console.vnc', uuid))
@ -369,8 +385,6 @@ def getInformationFromXML(zkhandler, uuid):
else: else:
stats_data = {} stats_data = {}
domain_tags = getDomainTags(zkhandler, uuid)
domain_uuid, domain_name, domain_description, domain_memory, domain_vcpu, domain_vcputopo = getDomainMainDetails(parsed_xml) domain_uuid, domain_name, domain_description, domain_memory, domain_vcpu, domain_vcputopo = getDomainMainDetails(parsed_xml)
domain_networks = getDomainNetworks(parsed_xml, stats_data) domain_networks = getDomainNetworks(parsed_xml, stats_data)

View File

@ -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"}}

View File

@ -24,6 +24,7 @@ import re
import lxml.objectify import lxml.objectify
import lxml.etree import lxml.etree
from distutils.util import strtobool
from uuid import UUID from uuid import UUID
from concurrent.futures import ThreadPoolExecutor 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.migrate_method', dom_uuid), migration_method),
(('domain.meta.node_limit', dom_uuid), formatted_node_limit), (('domain.meta.node_limit', dom_uuid), formatted_node_limit),
(('domain.meta.node_selector', dom_uuid), node_selector), (('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), ''), (('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) 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) 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) dom_uuid = getDomainUUID(zkhandler, domain)
if not dom_uuid: if not dom_uuid:
return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain)
if action in ['replace']: if action == 'add':
zkhandler.write([ 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(',') return True, 'Successfully added tag "{}" to VM "{}".'.format(tag, domain)
updated_tags = current_tags + tags elif action == 'remove':
zkhandler.write([ if not zkhandler.exists(('domain.meta.tags', dom_uuid, 'tag', tag)):
(('domain.meta.tags', dom_uuid), ','.join(updated_tags)) return False, 'The tag "{}" does not exist.'.format(tag)
])
elif action in ['remove']: if zkhandler.read(('domain.meta.tags', dom_uuid, 'tag.type', tag)) != 'user':
current_tags = zkhandler.read(('domain.meta.tags', dom_uuid)).split(',') return False, 'The tag "{}" is not a user tag and cannot be removed.'.format(tag)
for tag in tags:
if tag in current_tags: if bool(strtobool(zkhandler.read(('domain.meta.tags', dom_uuid, 'tag.protected', tag)))):
current_tags.remove(tag) return False, 'The tag "{}" is protected and cannot be removed.'.format(tag)
zkhandler.write([
(('domain.meta.tags', dom_uuid), ','.join(current_tags)) zkhandler.delete([
(('domain.meta.tags', dom_uuid, 'tag', tag))
]) ])
return True, 'Successfully removed tag "{}" from VM "{}".'.format(tag, domain)
else: else:
return False, 'Specified tag action is not available.' 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): def modify_vm(zkhandler, domain, restart, new_vm_config):
dom_uuid = getDomainUUID(zkhandler, domain) dom_uuid = getDomainUUID(zkhandler, domain)
@ -433,7 +445,7 @@ def rename_vm(zkhandler, domain, new_domain):
undefine_vm(zkhandler, dom_uuid) undefine_vm(zkhandler, dom_uuid)
# Define the new VM # 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 the VM is migrated, store that
if dom_info['migrated'] != 'no': if dom_info['migrated'] != 'no':

View File

@ -579,6 +579,12 @@ class ZKSchema(object):
'meta.tags': '/tags', 'meta.tags': '/tags',
'migrate.sync_lock': '/migrate_sync_lock' '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}) # The schema of an individual network entry (/networks/{vni})
'network': { 'network': {
'vni': '', # The root key 'vni': '', # The root key