From e5d38ec6bd882ccf48bb93885f78d070d34bf4d5 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 4 Jan 2020 11:58:30 -0500 Subject: [PATCH] Implement template CLI functions --- client-cli/cli_lib/provisioner.py | 925 ++++++++---------------------- client-cli/pvc.py | 440 +++++++++++++- 2 files changed, 679 insertions(+), 686 deletions(-) diff --git a/client-cli/cli_lib/provisioner.py b/client-cli/cli_lib/provisioner.py index b3068698..2114dbd3 100644 --- a/client-cli/cli_lib/provisioner.py +++ b/client-cli/cli_lib/provisioner.py @@ -97,6 +97,113 @@ def template_list(config, limit, template_type=None): else: return False, response.json()['message'] +def template_add(config, params, template_type=None): + """ + Add a new template of {template_type} with {params} + + API endpoint: POST /api/v1/provisioner/template/{template_type} + API_arguments: args + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/template/{template_type}'.format(template_type=template_type)) + response = requests.post( + request_uri, + params=params + ) + + if config['debug']: + print('API endpoint: POST {}'.format(request_uri)) + print('Response code: {}'.format(response.status_code)) + print('Response headers: {}'.format(response.headers)) + + if response.status_code == 200: + retvalue = True + else: + retvalue = False + + return retvalue, response.json()['message'] + +def template_remove(config, name, template_type=None): + """ + Remove a template {name} of {template_type} + + API endpoint: DELETE /api/v1/provisioner/template/{template_type}/{name} + API_arguments: + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/template/{template_type}/{name}'.format(template_type=template_type, name=name)) + response = requests.delete( + request_uri + ) + + if config['debug']: + print('API endpoint: DELETE {}'.format(request_uri)) + print('Response code: {}'.format(response.status_code)) + print('Response headers: {}'.format(response.headers)) + + if response.status_code == 200: + retvalue = True + else: + retvalue = False + + return retvalue, response.json()['message'] + +def template_element_add(config, name, element_id, params, element_type=None, template_type=None): + """ + Add a new template element of {element_type} with {params} to template {name} of {template_type} + + API endpoint: POST /api/v1/provisioner/template/{template_type}/{name}/{element_type}/{element_id} + API_arguments: args + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/template/{template_type}/{name}/{element_type}/{element_id}'.format(template_type=template_type, name=name, element_type=element_type, element_id=element_id)) + response = requests.post( + request_uri, + params=params + ) + + if config['debug']: + print('API endpoint: POST {}'.format(request_uri)) + print('Response code: {}'.format(response.status_code)) + print('Response headers: {}'.format(response.headers)) + + if response.status_code == 200: + retvalue = True + else: + retvalue = False + + return retvalue, response.json()['message'] + +def template_element_remove(config, name, element_id, element_type=None, template_type=None): + """ + Remove a template element {element_id} of {element_type} from template {name} of {template_type} + + API endpoint: DELETE /api/v1/provisioner/template/{template_type}/{name}/{element_type}/{element_id} + API_arguments: + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/template/{template_type}/{name}/{element_type}/{element_id}'.format(template_type=template_type, name=name, element_type=element_type, element_id=element_id)) + response = requests.delete( + request_uri + ) + + if config['debug']: + print('API endpoint: DELETE {}'.format(request_uri)) + print('Response code: {}'.format(response.status_code)) + print('Response headers: {}'.format(response.headers)) + + if response.status_code == 200: + retvalue = True + else: + retvalue = False + + return retvalue, response.json()['message'] + + + +# +# Format functions +# def format_list_template(template_data, template_type=None): """ Format the returned template data @@ -105,28 +212,33 @@ def format_list_template(template_data, template_type=None): reuse with more limited output options. """ template_types = [ 'system', 'network', 'storage' ] + normalized_template_data = dict() if template_type in template_types: template_types = [ template_type ] + template_data_type = '{}_templates'.format(template_type) + normalized_template_data[template_data_type] = template_data + else: + normalized_template_data = template_data if 'system' in template_types: click.echo('System templates:') click.echo() - format_list_template_system(template_data['system_templates']) - - if len(template_types) > 1: - click.echo() + format_list_template_system(normalized_template_data['system_templates']) + if len(template_types) > 1: + click.echo() if 'network' in template_types: click.echo('Network templates:') click.echo() - format_list_template_network(template_data['network_templates']) + format_list_template_network(normalized_template_data['network_templates']) + if len(template_types) > 1: + click.echo() - if len(template_types) > 1: + if 'storage' in template_types: + click.echo('Storage templates:') click.echo() - -# if 'storage' in template_types: -# format_list_template_storage(template_data['storage_templates']) + format_list_template_storage(normalized_template_data['storage_templates']) def format_list_template_system(template_data): if isinstance(template_data, dict): @@ -189,8 +301,7 @@ def format_list_template_system(template_data): template_node_autostart_length = _template_node_autostart_length # Format the string (header) - template_list_output.append( - '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ + 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}} \ @@ -224,13 +335,12 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ template_node_selector='Selector', template_node_autostart='Autostart' ) - ) # Keep track of nets we found to be valid to cut down on duplicate API hits valid_net_list = [] # Format the string (elements) - for template in template_data: + for template in sorted(template_data, key=lambda i: i.get('name', None)): template_list_output.append( '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ {template_vcpu: <{template_vcpu_length}} \ @@ -266,7 +376,7 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ ) ) - click.echo('\n'.join(sorted(template_list_output))) + click.echo('\n'.join([template_list_output_header] + template_list_output)) return True, '' @@ -308,8 +418,7 @@ def format_list_template_network(template_data): template_networks_length = _template_networks_length # Format the string (header) - template_list_output.append( - '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ + template_list_output_header = '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ {template_mac_template: <{template_mac_template_length}} \ {template_networks: <{template_networks_length}}{end_bold}'.format( template_name_length=template_name_length, @@ -318,18 +427,14 @@ def format_list_template_network(template_data): template_networks_length=template_networks_length, bold=ansiprint.bold(), end_bold=ansiprint.end(), - template_state_colour='', - end_colour='', template_name='Name', template_id='ID', template_mac_template='MAC template', template_networks='Network VNIs' ) - ) # Format the string (elements) - - for template in template_data: + for template in sorted(template_data, key=lambda i: i.get('name', None)): template_list_output.append( '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ {template_mac_template: <{template_mac_template_length}} \ @@ -340,8 +445,6 @@ def format_list_template_network(template_data): template_networks_length=template_networks_length, bold='', end_bold='', - template_state_colour='', - end_colour='', template_name=str(template['name']), template_id=str(template['id']), template_mac_template=str(template['mac_template']), @@ -349,683 +452,139 @@ def format_list_template_network(template_data): ) ) - click.echo('\n'.join(sorted(template_list_output))) + click.echo('\n'.join([template_list_output_header] + template_list_output)) return True, '' +def format_list_template_storage(template_data): + if isinstance(template_data, dict): + template_data = [ template_data ] - - - - - -def vm_info(config, vm): - """ - Get information about VM - - API endpoint: GET /api/v1/vm/{vm} - API arguments: - API schema: {json_data_object} - """ - request_uri = get_request_uri(config, '/vm/{vm}'.format(vm=vm)) - response = requests.get( - request_uri - ) - - if config['debug']: - print('API endpoint: POST {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - return True, response.json() - else: - return False, response.json()['message'] - -def vm_list(config, limit, target_node, target_state): - """ - Get list information about nodes (limited by {limit}, {target_node}, or {target_state}) - - API endpoint: GET /api/v1/vm - API arguments: limit={limit}, node={target_node}, state={target_state} - API schema: [{json_data_object},{json_data_object},etc.] - """ - params = dict() - if limit: - params['limit'] = limit - if target_node: - params['node'] = target_node - if target_state: - params['state'] = target_state - - request_uri = get_request_uri(config, '/vm') - response = requests.get( - request_uri, - params=params - ) - - if config['debug']: - print('API endpoint: POST {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - return True, response.json() - else: - return False, response.json()['message'] - -def vm_define(config, xml, node, node_limit, node_selector, node_autostart): - """ - 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 schema: {"message":"{data}"} - """ - request_uri = get_request_uri(config, '/vm') - response = requests.post( - request_uri, - params={ - 'xml': xml, - 'node': node, - 'limit': node_limit, - 'selector': node_selector, - 'autostart': node_autostart - } - ) - - if config['debug']: - print('API endpoint: POST {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - retstatus = True - else: - retstatus = False - - return retstatus, response.json()['message'] - -def vm_modify(config, vm, xml, restart): - """ - Modify the configuration of VM - - API endpoint: POST /vm/{vm} - API arguments: xml={xml}, restart={restart} - API schema: {"message":"{data}"} - """ - request_uri = get_request_uri(config, '/vm/{vm}'.format(vm=vm)) - response = requests.post( - request_uri, - params={ - 'xml': xml, - 'restart': restart - } - ) - - if config['debug']: - print('API endpoint: POST {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - retstatus = True - else: - retstatus = False - - return retstatus, response.json()['message'] - -def vm_metadata(config, vm, node_limit, node_selector, node_autostart): - """ - 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} - API schema: {"message":"{data}"} - """ - request_uri = get_request_uri(config, '/vm/{vm}/meta'.format(vm=vm)) - - # Get the existing metadata so we can perform a fully dynamic update - response = requests.get( - request_uri - ) - - if config['debug']: - print('API endpoint: GET {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - metadata = response.json() - - # Update any params that we've sent - if node_limit is not None: - metadata['node_limit'] = node_limit - else: - # Collapse the existing list back down to a CSV - metadata['node_limit'] = ','.join(metadata['node_limit']) - - if node_selector is not None: - metadata['node_selector'] = node_selector - - if node_autostart is not None: - metadata['node_autostart'] = node_autostart - - # Write the new metadata - print(metadata['node_limit']) - response = requests.post( - request_uri, - params={ - 'limit': metadata['node_limit'], - 'selector': metadata['node_selector'], - 'autostart': metadata['node_autostart'] - } - ) - - if config['debug']: - print('API endpoint: POST {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - retstatus = True - else: - retstatus = False - - return retstatus, response.json()['message'] - -def vm_remove(config, vm, delete_disks=False): - """ - Remove a VM - - API endpoint: DELETE /vm/{vm} - API arguments: delete_disks={delete_disks} - API schema: {"message":"{data}"} - """ - request_uri = get_request_uri(config, '/vm/{vm}'.format(vm=vm)) - response = requests.delete( - request_uri, - params={ - 'delete_disks': delete_disks - } - ) - - if config['debug']: - print('API endpoint: DELETE {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - retstatus = True - else: - retstatus = False - - return retstatus, response.json()['message'] - -def vm_state(config, vm, target_state): - """ - Modify the current state of VM - - API endpoint: POST /vm/{vm}/state - API arguments: state={state} - API schema: {"message":"{data}"} - """ - request_uri = get_request_uri(config, '/vm/{vm}/state'.format(vm=vm)) - response = requests.post( - request_uri, - params={ - 'state': target_state, - } - ) - - if config['debug']: - print('API endpoint: POST {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - retstatus = True - else: - retstatus = False - - return retstatus, response.json()['message'] - -def vm_node(config, vm, target_node, action, force=False): - """ - Modify the current node of VM via {action} - - API endpoint: POST /vm/{vm}/node - API arguments: node={target_node}, action={action}, force={force} - API schema: {"message":"{data}"} - """ - request_uri = get_request_uri(config, '/vm/{vm}/node'.format(vm=vm)) - response = requests.post( - request_uri, - params={ - 'node': target_node, - 'action': action, - 'force': force - } - ) - - if config['debug']: - print('API endpoint: POST {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - retstatus = True - else: - retstatus = False - - return retstatus, response.json()['message'] - -def vm_locks(config, vm): - """ - Flush RBD locks of (stopped) VM - - API endpoint: POST /vm/{vm}/locks - API arguments: - API schema: {"message":"{data}"} - """ - request_uri = get_request_uri(config, '/vm/{vm}/locks'.format(vm=vm)) - response = requests.post( - request_uri - ) - - if config['debug']: - print('API endpoint: POST {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - if response.status_code == 200: - retstatus = True - else: - retstatus = False - - return retstatus, response.json()['message'] - -def view_console_log(config, vm, lines=100): - """ - Return console log lines from the API and display them in a pager - - API endpoint: GET /vm/{vm}/console - API arguments: lines={lines} - API schema: {"name":"{vmname}","data":"{console_log}"} - """ - request_uri = get_request_uri(config, '/vm/{vm}/console'.format(vm=vm)) - response = requests.get( - request_uri, - params={'lines': lines} - ) - - if config['debug']: - print('API endpoint: GET {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - console_log = response.json()['data'] - - # Shrink the log buffer to length lines - shrunk_log = console_log.split('\n')[-lines:] - loglines = '\n'.join(shrunk_log) - - # Show it in the pager (less) - try: - pager = subprocess.Popen(['less', '-R'], stdin=subprocess.PIPE) - pager.communicate(input=loglines.encode('utf8')) - except FileNotFoundError: - click.echo("Error: `less` pager not found, dumping log ({} lines) to stdout".format(lines)) - return True, loglines - - return True, '' - -def follow_console_log(config, vm, lines=10): - """ - Return and follow console log lines from the API - - API endpoint: GET /vm/{vm}/console - API arguments: lines={lines} - API schema: {"name":"{vmname}","data":"{console_log}"} - """ - request_uri = get_request_uri(config, '/vm/{vm}/console'.format(vm=vm)) - response = requests.get( - request_uri, - params={'lines': lines} - ) - - if config['debug']: - print('API endpoint: GET {}'.format(request_uri)) - print('Response code: {}'.format(response.status_code)) - print('Response headers: {}'.format(response.headers)) - - console_log = response.json()['data'] - - # Shrink the log buffer to length lines - shrunk_log = console_log.split('\n')[-lines:] - loglines = '\n'.join(shrunk_log) - - # Print the initial data and begin following - print(loglines, end='') - - while True: - # Grab the next line set - # Get the (initial) data from the API - response = requests.get( - '{}://{}{}{}'.format( - config['api_scheme'], - config['api_host'], - config['api_prefix'], - '/vm/{}/console'.format(vm) - ), - params={'lines': lines} - ) - - if config['debug']: - print( - 'Response code: {}'.format( - response.status_code - ) - ) - print( - 'Response headers: {}'.format( - response.headers - ) - ) - - new_console_log = response.json()['data'] - # Split the new and old log strings into constitutent lines - old_console_loglines = console_log.split('\n') - new_console_loglines = new_console_log.split('\n') - # Set the console log to the new log value for the next iteration - console_log = new_console_log - # Remove the lines from the old log until we hit the first line of the new log; this - # ensures that the old log is a string that we can remove from the new log entirely - for index, line in enumerate(old_console_loglines, start=0): - if line == new_console_loglines[0]: - del old_console_loglines[0:index] - break - # Rejoin the log lines into strings - old_console_log = '\n'.join(old_console_loglines) - new_console_log = '\n'.join(new_console_loglines) - # Remove the old lines from the new log - diff_console_log = new_console_log.replace(old_console_log, "") - # If there's a difference, print it out - if diff_console_log: - print(diff_console_log, end='') - # Wait a second - time.sleep(1) - - return True, '' - -# -# Output display functions -# -def format_info(config, domain_information, long_output): - # Format a nice output; do this line-by-line then concat the elements at the end - ainformation = [] - ainformation.append('{}Virtual machine information:{}'.format(ansiprint.bold(), ansiprint.end())) - ainformation.append('') - # Basic information - ainformation.append('{}UUID:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['uuid'])) - ainformation.append('{}Name:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['name'])) - ainformation.append('{}Description:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['description'])) - ainformation.append('{}Profile:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['profile'])) - ainformation.append('{}Memory (M):{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['memory'])) - ainformation.append('{}vCPUs:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['vcpu'])) - ainformation.append('{}Topology (S/C/T):{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['vcpu_topology'])) - - if long_output == True: - # Virtualization information - ainformation.append('') - ainformation.append('{}Emulator:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['emulator'])) - ainformation.append('{}Type:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['type'])) - ainformation.append('{}Arch:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['arch'])) - ainformation.append('{}Machine:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['machine'])) - ainformation.append('{}Features:{} {}'.format(ansiprint.purple(), ansiprint.end(), ' '.join(domain_information['features']))) - - # PVC cluster information - ainformation.append('') - dstate_colour = { - 'start': ansiprint.green(), - 'restart': ansiprint.yellow(), - 'shutdown': ansiprint.yellow(), - 'stop': ansiprint.red(), - 'disable': ansiprint.blue(), - 'fail': ansiprint.red(), - 'migrate': ansiprint.blue(), - 'unmigrate': ansiprint.blue() - } - ainformation.append('{}State:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), dstate_colour[domain_information['state']], domain_information['state'], ansiprint.end())) - ainformation.append('{}Current Node:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['node'])) - if not domain_information['last_node']: - domain_information['last_node'] = "N/A" - ainformation.append('{}Previous Node:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['last_node'])) - - # Get a failure reason if applicable - if domain_information['failed_reason']: - ainformation.append('') - ainformation.append('{}Failure reason:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['failed_reason'])) - - if not domain_information['node_selector']: - formatted_node_selector = "False" - else: - formatted_node_selector = domain_information['node_selector'] - - if not domain_information['node_limit']: - formatted_node_limit = "False" - else: - formatted_node_limit = ', '.join(domain_information['node_limit']) - - if not domain_information['node_autostart']: - formatted_node_autostart = "False" - else: - formatted_node_autostart = domain_information['node_autostart'] - - 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)) - - # Network list - net_list = [] - for net in domain_information['networks']: - # Split out just the numerical (VNI) part of the brXXXX name - net_vnis = re.findall(r'\d+', net['source']) - if net_vnis: - net_vni = net_vnis[0] - else: - net_vni = re.sub('br', '', net['source']) - - request_uri = get_request_uri(config, '/network/{net}'.format(net=net_vni)) - response = requests.get( - request_uri - ) - if response.status_code != 200 and net_vni != 'cluster': - net_list.append(ansiprint.red() + net_vni + ansiprint.end() + ' [invalid]') - else: - net_list.append(net_vni) - - ainformation.append('') - ainformation.append('{}Networks:{} {}'.format(ansiprint.purple(), ansiprint.end(), ', '.join(net_list))) - - if long_output == True: - # Disk list - ainformation.append('') - name_length = 0 - for disk in domain_information['disks']: - _name_length = len(disk['name']) + 1 - if _name_length > name_length: - name_length = _name_length - ainformation.append('{0}Disks:{1} {2}ID Type {3: <{width}} Dev Bus{4}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), 'Name', ansiprint.end(), width=name_length)) - for disk in domain_information['disks']: - ainformation.append(' {0: <3} {1: <5} {2: <{width}} {3: <4} {4: <5}'.format(domain_information['disks'].index(disk), disk['type'], disk['name'], disk['dev'], disk['bus'], width=name_length)) - ainformation.append('') - ainformation.append('{}Interfaces:{} {}ID Type Source Model MAC{}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), ansiprint.end())) - for net in domain_information['networks']: - ainformation.append(' {0: <3} {1: <8} {2: <10} {3: <8} {4}'.format(domain_information['networks'].index(net), net['type'], net['source'], net['model'], net['mac'])) - # Controller list - ainformation.append('') - ainformation.append('{}Controllers:{} {}ID Type Model{}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), ansiprint.end())) - for controller in domain_information['controllers']: - ainformation.append(' {0: <3} {1: <14} {2: <8}'.format(domain_information['controllers'].index(controller), controller['type'], controller['model'])) - - # Join it all together - information = '\n'.join(ainformation) - click.echo(information) - - click.echo('') - -def format_list(config, vm_list, raw): - # Function to strip the "br" off of nets and return a nicer list - def getNiceNetID(domain_information): - # Network list - net_list = [] - for net in domain_information['networks']: - # Split out just the numerical (VNI) part of the brXXXX name - net_vnis = re.findall(r'\d+', net['source']) - if net_vnis: - net_vni = net_vnis[0] - else: - net_vni = re.sub('br', '', net['source']) - net_list.append(net_vni) - return net_list - - # Handle raw mode since it just lists the names - if raw: - for vm in sorted(item['name'] for item in vm_list): - click.echo(vm) - return True, '' - - vm_list_output = [] + template_list_output = [] # Determine optimal column widths - # Dynamic columns: node_name, node, migrated - vm_name_length = 5 - vm_uuid_length = 37 - vm_state_length = 6 - vm_nets_length = 9 - vm_ram_length = 8 - vm_vcpu_length = 6 - vm_node_length = 8 - vm_migrated_length = 10 - for domain_information in vm_list: - net_list = getNiceNetID(domain_information) - # vm_name column - _vm_name_length = len(domain_information['name']) + 1 - if _vm_name_length > vm_name_length: - vm_name_length = _vm_name_length - # vm_state column - _vm_state_length = len(domain_information['state']) + 1 - if _vm_state_length > vm_state_length: - vm_state_length = _vm_state_length - # vm_nets column - _vm_nets_length = len(','.join(net_list)) + 1 - if _vm_nets_length > vm_nets_length: - vm_nets_length = _vm_nets_length - # vm_node column - _vm_node_length = len(domain_information['node']) + 1 - if _vm_node_length > vm_node_length: - vm_node_length = _vm_node_length - # vm_migrated column - _vm_migrated_length = len(domain_information['migrated']) + 1 - if _vm_migrated_length > vm_migrated_length: - vm_migrated_length = _vm_migrated_length + template_name_length = 5 + template_id_length = 4 + template_disk_id_length = 8 + template_disk_pool_length = 8 + template_disk_size_length = 10 + template_disk_filesystem_length = 11 + template_disk_fsargs_length = 10 + template_disk_mountpoint_length = 10 + + for template in template_data: + # template_name column + _template_name_length = len(str(template['name'])) + 1 + if _template_name_length > template_name_length: + template_name_length = _template_name_length + # template_id column + _template_id_length = len(str(template['id'])) + 1 + if _template_id_length > template_id_length: + template_id_length = _template_id_length + + for disk in template['disks']: + # template_disk_id column + _template_disk_id_length = len(str(disk['disk_id'])) + 1 + if _template_disk_id_length > template_disk_id_length: + template_disk_id_length = _template_disk_id_length + # template_disk_pool column + _template_disk_pool_length = len(str(disk['pool'])) + 1 + if _template_disk_pool_length > template_disk_pool_length: + template_disk_pool_length = _template_disk_pool_length + # template_disk_size column + _template_disk_size_length = len(str(disk['disk_size_gb'])) + 1 + if _template_disk_size_length > template_disk_size_length: + template_disk_size_length = _template_disk_size_length + # template_disk_filesystem column + _template_disk_filesystem_length = len(str(disk['filesystem'])) + 1 + if _template_disk_filesystem_length > template_disk_filesystem_length: + template_disk_filesystem_length = _template_disk_filesystem_length + # template_disk_fsargs column + _template_disk_fsargs_length = len(str(disk['filesystem_args'])) + 1 + if _template_disk_fsargs_length > template_disk_fsargs_length: + template_disk_fsargs_length = _template_disk_fsargs_length + # template_disk_mountpoint column + _template_disk_mountpoint_length = len(str(disk['mountpoint'])) + 1 + if _template_disk_mountpoint_length > template_disk_mountpoint_length: + template_disk_mountpoint_length = _template_disk_mountpoint_length # Format the string (header) - vm_list_output.append( - '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <{vm_uuid_length}} \ -{vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \ -{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_uuid_length=vm_uuid_length, - vm_state_length=vm_state_length, - vm_nets_length=vm_nets_length, - vm_ram_length=vm_ram_length, - vm_vcpu_length=vm_vcpu_length, - vm_node_length=vm_node_length, - vm_migrated_length=vm_migrated_length, + template_list_output_header = '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ +{template_disk_id: <{template_disk_id_length}} \ +{template_disk_pool: <{template_disk_pool_length}} \ +{template_disk_size: <{template_disk_size_length}} \ +{template_disk_filesystem: <{template_disk_filesystem_length}} \ +{template_disk_fsargs: <{template_disk_fsargs_length}} \ +{template_disk_mountpoint: <{template_disk_mountpoint_length}}{end_bold}'.format( + template_name_length=template_name_length, + template_id_length=template_id_length, + template_disk_id_length=template_disk_id_length, + template_disk_pool_length=template_disk_pool_length, + template_disk_size_length=template_disk_size_length, + template_disk_filesystem_length=template_disk_filesystem_length, + template_disk_fsargs_length=template_disk_fsargs_length, + template_disk_mountpoint_length=template_disk_mountpoint_length, bold=ansiprint.bold(), end_bold=ansiprint.end(), - vm_state_colour='', - end_colour='', - vm_name='Name', - vm_uuid='UUID', - vm_state='State', - vm_networks='Networks', - vm_memory='RAM (M)', - vm_vcpu='vCPUs', - vm_node='Node', - vm_migrated='Migrated' + template_name='Name', + template_id='ID', + template_disk_id='Disk ID', + template_disk_pool='Pool', + template_disk_size='Size [GB]', + template_disk_filesystem='Filesystem', + template_disk_fsargs='Arguments', + template_disk_mountpoint='Mountpoint' ) - ) - # Keep track of nets we found to be valid to cut down on duplicate API hits - valid_net_list = [] # Format the string (elements) - for domain_information in vm_list: - if domain_information['state'] == 'start': - vm_state_colour = ansiprint.green() - elif domain_information['state'] == 'restart': - vm_state_colour = ansiprint.yellow() - elif domain_information['state'] == 'shutdown': - vm_state_colour = ansiprint.yellow() - elif domain_information['state'] == 'stop': - vm_state_colour = ansiprint.red() - elif domain_information['state'] == 'fail': - vm_state_colour = ansiprint.red() - else: - vm_state_colour = ansiprint.blue() - - # Handle colouring for an invalid network config - raw_net_list = getNiceNetID(domain_information) - net_list = [] - vm_net_colour = '' - for net_vni in raw_net_list: - if not net_vni in valid_net_list: - request_uri = get_request_uri(config, '/network/{net}'.format(net=net_vni)) - response = requests.get( - request_uri - ) - if response.status_code != 200 and net_vni != 'cluster': - vm_net_colour = ansiprint.red() - else: - valid_net_list.append(net_vni) - - net_list.append(net_vni) - - vm_list_output.append( - '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <{vm_uuid_length}} \ -{vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \ -{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_uuid_length=vm_uuid_length, - vm_state_length=vm_state_length, - vm_nets_length=vm_nets_length, - vm_ram_length=vm_ram_length, - vm_vcpu_length=vm_vcpu_length, - vm_node_length=vm_node_length, - vm_migrated_length=vm_migrated_length, + for template in sorted(template_data, key=lambda i: i.get('name', None)): + template_list_output.append( + '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}}{end_bold}'.format( + template_name_length=template_name_length, + template_id_length=template_id_length, + template_disk_id_length=template_disk_id_length, + template_disk_pool_length=template_disk_pool_length, + template_disk_size_length=template_disk_size_length, + template_disk_filesystem_length=template_disk_filesystem_length, + template_disk_fsargs_length=template_disk_fsargs_length, + template_disk_mountpoint_length=template_disk_mountpoint_length, bold='', end_bold='', - vm_state_colour=vm_state_colour, - end_colour=ansiprint.end(), - vm_name=domain_information['name'], - vm_uuid=domain_information['uuid'], - vm_state=domain_information['state'], - vm_net_colour=vm_net_colour, - vm_networks=','.join(net_list), - vm_memory=domain_information['memory'], - vm_vcpu=domain_information['vcpu'], - vm_node=domain_information['node'], - vm_migrated=domain_information['migrated'] + template_name=str(template['name']), + template_id=str(template['id']) ) ) + for disk in sorted(template['disks'], key=lambda i: i.get('disk_id', None)): + template_list_output.append( + '{bold}{template_name: <{template_name_length}} {template_id: <{template_id_length}} \ +{template_disk_id: <{template_disk_id_length}} \ +{template_disk_pool: <{template_disk_pool_length}} \ +{template_disk_size: <{template_disk_size_length}} \ +{template_disk_filesystem: <{template_disk_filesystem_length}} \ +{template_disk_fsargs: <{template_disk_fsargs_length}} \ +{template_disk_mountpoint: <{template_disk_mountpoint_length}}{end_bold}'.format( + template_name_length=template_name_length, + template_id_length=template_id_length, + template_disk_id_length=template_disk_id_length, + template_disk_pool_length=template_disk_pool_length, + template_disk_size_length=template_disk_size_length, + template_disk_filesystem_length=template_disk_filesystem_length, + template_disk_fsargs_length=template_disk_fsargs_length, + template_disk_mountpoint_length=template_disk_mountpoint_length, + bold='', + end_bold='', + template_name='', + template_id='', + template_disk_id=str(disk['disk_id']), + template_disk_pool=str(disk['pool']), + template_disk_size=str(disk['disk_size_gb']), + template_disk_filesystem=str(disk['filesystem']), + template_disk_fsargs=str(disk['filesystem_args']), + template_disk_mountpoint=str(disk['mountpoint']) + ) + ) - click.echo('\n'.join(sorted(vm_list_output))) + click.echo('\n'.join([template_list_output_header] + template_list_output)) return True, '' + diff --git a/client-cli/pvc.py b/client-cli/pvc.py index 2043b0b1..165156f8 100755 --- a/client-cli/pvc.py +++ b/client-cli/pvc.py @@ -1854,7 +1854,7 @@ def ceph_volume_snapshot_remove(pool, volume, name, yes): help='Show snapshots from this pool only.' ) @click.option( - '-p', '--volume', 'volume', + '-o', '--volume', 'volume', default=None, show_default=True, help='Show snapshots from this volume only.' ) @@ -1902,7 +1902,7 @@ def provisioner_template(): @click.argument( 'limit', default=None, required=False ) -def template_list(limit): +def provisioner_template_list(limit): """ List all templates in the PVC cluster provisioner. """ @@ -1912,6 +1912,417 @@ def template_list(limit): retdata = '' cleanup(retcode, retdata) +############################################################################### +# pvc provisioner template system +############################################################################### +@click.group(name='system', short_help='Manage PVC provisioner system templates.', context_settings=CONTEXT_SETTINGS) +def provisioner_template_system(): + """ + Manage the PVC provisioner system templates. + """ + # Abort commands under this group if config is bad + if config.get('badcfg', None): + exit(1) + +############################################################################### +# pvc provisioner template system list +############################################################################### +@click.command(name='list', short_help='List all system templates in the cluster.') +@click.argument( + 'limit', default=None, required=False +) +def provisioner_template_system_list(limit): + """ + List all system templates in the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.template_list(config, limit, template_type='system') + if retcode: + pvc_provisioner.format_list_template(retdata, template_type='system') + retdata = '' + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template system add +############################################################################### +@click.command(name='add', short_help='Add new system template to the cluster.') +@click.argument( + 'name' +) +@click.option( + '-u', '--vcpus', 'vcpus', + required=True, type=int, + help='The number of vCPUs.' +) +@click.option( + '-m', '--vram', 'vram', + required=True, type=int, + help='The amount of vRAM (in MB).' +) +@click.option( + '-s', '--serial', 'serial', + is_flag=True, default=False, + help='Enable the virtual serial console.' +) +@click.option( + '-n', '--vnc', 'vnc', + is_flag=True, default=False, + help='Enable the VNC console.' +) +@click.option( + '-b', '--vnc-bind', 'vnc_bind', + default=None, + help='Bind VNC to this IP address instead of localhost.' +) +@click.option( + '--node-limit', 'node_limit', + default=None, + help='Limit VM operation to this CSV list of node(s).' +) +@click.option( + '--node-selector', 'node_selector', + type=click.Choice(['mem', 'vcpus', 'vms', 'load'], case_sensitive=False), + default=None, # Use cluster default + help='Use this selector to determine the optimal node during migrations.' +) +@click.option( + '--node-autostart', 'node_autostart', + is_flag=True, default=False, + help='Autostart VM with their parent Node on first/next boot.' +) +def provisioner_template_system_add(name, vcpus, vram, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart): + """ + Add a new system template NAME to the PVC cluster provisioner. + """ + params = dict() + params['name'] = name + params['vcpus'] = vcpus + params['vram'] = vram + params['serial'] = serial + params['vnc'] = vnc + if vnc: + params['vnc_bind'] = vnc_bind + if node_limit: + params['node_limit'] = node_limit + if node_selector: + params['node_selector'] = node_selector + if node_autostart: + params['node_autostart'] = node_autostart + + retcode, retdata = pvc_provisioner.template_add(config, params, template_type='system') + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template system remove +############################################################################### +@click.command(name='remove', short_help='Remove system template from the cluster.') +@click.argument( + 'name' +) +def provisioner_template_system_remove(name): + """ + Remove a system template from the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='system') + cleanup(retcode, retdata) + + +############################################################################### +# pvc provisioner template network +############################################################################### +@click.group(name='network', short_help='Manage PVC provisioner network templates.', context_settings=CONTEXT_SETTINGS) +def provisioner_template_network(): + """ + Manage the PVC provisioner network templates. + """ + # Abort commands under this group if config is bad + if config.get('badcfg', None): + exit(1) + +############################################################################### +# pvc provisioner template network list +############################################################################### +@click.command(name='list', short_help='List all network templates in the cluster.') +@click.argument( + 'limit', default=None, required=False +) +def provisioner_template_network_list(limit): + """ + List all network templates in the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.template_list(config, limit, template_type='network') + if retcode: + pvc_provisioner.format_list_template(retdata, template_type='network') + retdata = '' + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template network add +############################################################################### +@click.command(name='add', short_help='Add new network template to the cluster.') +@click.argument( + 'name' +) +@click.option( + '-m', '--mac-template', 'mac_template', + default=None, + help='Use this template for MAC addresses.' +) +def provisioner_template_network_add(name, mac_template): + """ + Add a new network template to the PVC cluster provisioner. + + MAC address templates are used to provide predictable MAC addresses for provisioned VMs. + The normal format of a MAC template is: + + {prefix}:XX:XX:{vmid}{netid} + + The {prefix} variable is replaced by the provisioner with a standard prefix ("52:54:01"), + which is different from the randomly-generated MAC prefix ("52:54:00") to avoid accidental + overlap of MAC addresses. + + The {vmid} variable is replaced by a single hexidecimal digit representing the VM's ID, + the numerical suffix portion of its name; VMs without a suffix numeral have ID 0. VMs with + IDs greater than 15 (hexidecimal "f") will wrap back to 0. + + The {netid} variable is replaced by the sequential identifier, starting at 0, of the + network VNI of the interface; for example, the first interface is 0, the second is 1, etc. + + The four X digits are use-configurable. Use these digits to uniquely define the MAC + address. + + Example: pvc provisioner template network add --mac-template "{prefix}:2f:1f:{vmid}{netid}" test-template + + The location of the two per-VM variables can be adjusted at the administrator's discretion, + or removed if not required (e.g. a single-network template, or template for a single VM). + In such situations, be careful to avoid accidental overlap with other templates' variable + portions. + """ + params = dict() + params['name'] = name + params['mac_template'] = mac_template + + retcode, retdata = pvc_provisioner.template_add(config, params, template_type='network') + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template network remove +############################################################################### +@click.command(name='remove', short_help='Remove network template from the cluster.') +@click.argument( + 'name' +) +def provisioner_template_network_remove(name): + """ + Remove a network template from the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='network') + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template network vni +############################################################################### +@click.group(name='vni', short_help='Manage PVC provisioner network template VNIs.', context_settings=CONTEXT_SETTINGS) +def provisioner_template_network_vni(): + """ + Manage the network VNIs in PVC provisioner network templates. + """ + # Abort commands under this group if config is bad + if config.get('badcfg', None): + exit(1) + +############################################################################### +# pvc provisioner template network vni add +############################################################################### +@click.command(name='add', short_help='Add network VNI to network template.') +@click.argument( + 'name' +) +@click.argument( + 'vni' +) +def provisioner_template_network_vni_add(name, vni): + """ + Add a new network VNI to network template NAME. + """ + params = dict() + + retcode, retdata = pvc_provisioner.template_element_add(config, name, vni, params, element_type='net', template_type='network') + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template network vni remove +############################################################################### +@click.command(name='remove', short_help='Remove network VNI from network template.') +@click.argument( + 'name' +) +@click.argument( + 'vni' +) +def provisioner_template_network_vni_remove(name, vni): + """ + Remove a network VNI from network template NAME. + """ + params = dict() + + retcode, retdata = pvc_provisioner.template_element_remove(config, name, vni, element_type='net', template_type='network') + cleanup(retcode, retdata) + + +############################################################################### +# pvc provisioner template storage +############################################################################### +@click.group(name='storage', short_help='Manage PVC provisioner storage templates.', context_settings=CONTEXT_SETTINGS) +def provisioner_template_storage(): + """ + Manage the PVC provisioner storage templates. + """ + # Abort commands under this group if config is bad + if config.get('badcfg', None): + exit(1) + +############################################################################### +# pvc provisioner template storage list +############################################################################### +@click.command(name='list', short_help='List all storage templates in the cluster.') +@click.argument( + 'limit', default=None, required=False +) +def provisioner_template_storage_list(limit): + """ + List all storage templates in the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.template_list(config, limit, template_type='storage') + if retcode: + pvc_provisioner.format_list_template(retdata, template_type='storage') + retdata = '' + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template storage add +############################################################################### +@click.command(name='add', short_help='Add new storage template to the cluster.') +@click.argument( + 'name' +) +def provisioner_template_storage_add(name): + """ + Add a new storage template to the PVC cluster provisioner. + """ + params = dict() + params['name'] = name + + retcode, retdata = pvc_provisioner.template_add(config, params, template_type='storage') + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template storage remove +############################################################################### +@click.command(name='remove', short_help='Remove storage template from the cluster.') +@click.argument( + 'name' +) +def provisioner_template_storage_remove(name): + """ + Remove a storage template from the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='storage') + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template storage disk +############################################################################### +@click.group(name='disk', short_help='Manage PVC provisioner storage template disks.', context_settings=CONTEXT_SETTINGS) +def provisioner_template_storage_disk(): + """ + Manage the disks in PVC provisioner storage templates. + """ + # Abort commands under this group if config is bad + if config.get('badcfg', None): + exit(1) + +############################################################################### +# pvc provisioner template storage disk add +############################################################################### +@click.command(name='add', short_help='Add disk to storage template.') +@click.argument( + 'name' +) +@click.argument( + 'disk' +) +@click.option( + '-p', '--pool', 'pool', + required=True, + help='The storage pool for the disk.' +) +@click.option( + '-s', '--size', 'size', + required=True, type=int, + help='The size of the disk (in GB).' +) +@click.option( + '-f', '--filesystem', 'filesystem', + default=None, + help='The filesystem of the disk.' +) +@click.option( + '--fsarg', 'fsargs', + default=None, multiple=True, + help='Additional argument for filesystem creation, in arg=value format without leading dashes.' +) +@click.option( + '-m', '--mountpoint', 'mountpoint', + default=None, + help='The target Linux mountpoint of the disk; requires a filesystem.' +) +def provisioner_template_storage_disk_add(name, disk, pool, size, filesystem, fsargs, mountpoint): + """ + Add a new DISK to storage template NAME. + + DISK must be a Linux-style disk identifier such as "sda" or "vdb". + """ + params = dict() + params['pool'] = pool + params['disk_size'] = size + if filesystem: + params['filesystem'] = filesystem + if filesystem and fsargs: + dash_fsargs = list() + for arg in fsargs: + arg_len = len(arg.split('=')[0]) + if arg_len == 1: + dash_fsargs.append('-' + arg) + else: + dash_fsargs.append('--' + arg) + params['filesystem_arg'] = dash_fsargs + if filesystem and mountpoint: + params['mountpoint'] = mountpoint + + retcode, retdata = pvc_provisioner.template_element_add(config, name, disk, params, element_type='disk', template_type='storage') + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner template storage disk remove +############################################################################### +@click.command(name='remove', short_help='Remove disk from storage template.') +@click.argument( + 'name' +) +@click.argument( + 'disk' +) +def provisioner_template_storage_disk_remove(name, disk): + """ + Remove a DISK from storage template NAME. + """ + params = dict() + + retcode, retdata = pvc_provisioner.template_element_remove(config, name, disk, element_type='disk', template_type='storage') + cleanup(retcode, retdata) + + + @@ -2099,7 +2510,30 @@ cli_ceph.add_command(ceph_volume) cli_storage.add_command(cli_ceph) -provisioner_template.add_command(template_list) +provisioner_template_system.add_command(provisioner_template_system_list) +provisioner_template_system.add_command(provisioner_template_system_add) +provisioner_template_system.add_command(provisioner_template_system_remove) + +provisioner_template_network.add_command(provisioner_template_network_list) +provisioner_template_network.add_command(provisioner_template_network_add) +provisioner_template_network.add_command(provisioner_template_network_remove) +provisioner_template_network.add_command(provisioner_template_network_vni) + +provisioner_template_network_vni.add_command(provisioner_template_network_vni_add) +provisioner_template_network_vni.add_command(provisioner_template_network_vni_remove) + +provisioner_template_storage.add_command(provisioner_template_storage_list) +provisioner_template_storage.add_command(provisioner_template_storage_add) +provisioner_template_storage.add_command(provisioner_template_storage_remove) +provisioner_template_storage.add_command(provisioner_template_storage_disk) + +provisioner_template_storage_disk.add_command(provisioner_template_storage_disk_add) +provisioner_template_storage_disk.add_command(provisioner_template_storage_disk_remove) + +provisioner_template.add_command(provisioner_template_system) +provisioner_template.add_command(provisioner_template_network) +provisioner_template.add_command(provisioner_template_storage) +provisioner_template.add_command(provisioner_template_list) cli_provisioner.add_command(provisioner_template)