From a4ad58e64c61b5e9319b268fadac7bbf2672ae44 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sat, 4 Jan 2020 13:04:01 -0500 Subject: [PATCH] Add userdata and script options --- client-cli/cli_lib/provisioner.py | 468 ++++++++++++++++++++++++++++-- client-cli/pvc.py | 348 +++++++++++++++++++++- 2 files changed, 780 insertions(+), 36 deletions(-) diff --git a/client-cli/cli_lib/provisioner.py b/client-cli/cli_lib/provisioner.py index 2114dbd3..2210a0f0 100644 --- a/client-cli/cli_lib/provisioner.py +++ b/client-cli/cli_lib/provisioner.py @@ -49,7 +49,7 @@ def template_info(config, template, template_type): API endpoint: GET /api/v1/provisioner/template/{template_type}/{template} API arguments: - API schema: {json_data_object} + API schema: {json_template_object} """ request_uri = get_request_uri(config, '/provisioner/template/{template_type}/{template}'.format(template_type=template_type, template=template)) response = requests.get( @@ -72,7 +72,7 @@ def template_list(config, limit, template_type=None): API endpoint: GET /api/v1/provisioner/template/{template_type} API arguments: limit={limit} - API schema: [{json_data_object},{json_data_object},etc.] + API schema: [{json_template_object},{json_template_object},etc.] """ params = dict() if limit: @@ -125,7 +125,7 @@ def template_add(config, params, template_type=None): def template_remove(config, name, template_type=None): """ - Remove a template {name} of {template_type} + Remove template {name} of {template_type} API endpoint: DELETE /api/v1/provisioner/template/{template_type}/{name} API_arguments: @@ -176,7 +176,7 @@ def template_element_add(config, name, element_id, params, element_type=None, te 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} + Remove 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: @@ -199,50 +199,304 @@ def template_element_remove(config, name, element_id, element_type=None, templat return retvalue, response.json()['message'] +def userdata_info(config, userdata): + """ + Get information about userdata + API endpoint: GET /api/v1/provisioner/userdata/{userdata} + API arguments: + API schema: {json_data_object} + """ + request_uri = get_request_uri(config, '/provisioner/userdata/{userdata}'.format(userdata=userdata)) + 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)) + + if response.status_code == 200: + return True, response.json()[0] + else: + return False, response.json()['message'] + +def userdata_list(config, limit): + """ + Get list information about userdatas (limited by {limit}) + + API endpoint: GET /api/v1/provisioner/userdata/{userdata_type} + API arguments: limit={limit} + API schema: [{json_data_object},{json_data_object},etc.] + """ + params = dict() + if limit: + params['limit'] = limit + + request_uri = get_request_uri(config, '/provisioner/userdata') + response = requests.get( + request_uri, + params=params + ) + + if config['debug']: + print('API endpoint: GET {}'.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 userdata_add(config, params): + """ + Add a new userdata with {params} + + API endpoint: POST /api/v1/provisioner/userdata + API_arguments: args + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/userdata') + 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 userdata_modify(config, name, params): + """ + Modify userdata {name} with {params} + + API endpoint: PUT /api/v1/provisioner/userdata/{name} + API_arguments: args + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/userdata/{name}'.format(name=name)) + response = requests.put( + request_uri, + params=params + ) + + if config['debug']: + print('API endpoint: PUT {}'.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 userdata_remove(config, name): + """ + Remove userdata {name} + + API endpoint: DELETE /api/v1/provisioner/userdata/{name} + API_arguments: + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/userdata/{name}'.format(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 script_info(config, script): + """ + Get information about script + + API endpoint: GET /api/v1/provisioner/script/{script} + API arguments: + API schema: {json_data_object} + """ + request_uri = get_request_uri(config, '/provisioner/script/{script}'.format(script=script)) + 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)) + + if response.status_code == 200: + return True, response.json()[0] + else: + return False, response.json()['message'] + +def script_list(config, limit): + """ + Get list information about scripts (limited by {limit}) + + API endpoint: GET /api/v1/provisioner/script/{script_type} + API arguments: limit={limit} + API schema: [{json_data_object},{json_data_object},etc.] + """ + params = dict() + if limit: + params['limit'] = limit + + request_uri = get_request_uri(config, '/provisioner/script') + response = requests.get( + request_uri, + params=params + ) + + if config['debug']: + print('API endpoint: GET {}'.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 script_add(config, params): + """ + Add a new script with {params} + + API endpoint: POST /api/v1/provisioner/script + API_arguments: args + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/script') + 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 script_modify(config, name, params): + """ + Modify script {name} with {params} + + API endpoint: PUT /api/v1/provisioner/script/{name} + API_arguments: args + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/script/{name}'.format(name=name)) + response = requests.put( + request_uri, + params=params + ) + + if config['debug']: + print('API endpoint: PUT {}'.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 script_remove(config, name): + """ + Remove script {name} + + API endpoint: DELETE /api/v1/provisioner/script/{name} + API_arguments: + API schema: {message} + """ + request_uri = get_request_uri(config, '/provisioner/script/{name}'.format(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'] # # Format functions # -def format_list_template(template_data, template_type=None): +def format_list_template(template_template, template_type=None): """ - Format the returned template data + Format the returned template template template_type can be used to only display part of the full list, allowing function reuse with more limited output options. """ template_types = [ 'system', 'network', 'storage' ] - normalized_template_data = dict() + normalized_template_template = 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 + template_template_type = '{}_templates'.format(template_type) + normalized_template_template[template_template_type] = template_template else: - normalized_template_data = template_data + normalized_template_template = template_template if 'system' in template_types: click.echo('System templates:') click.echo() - format_list_template_system(normalized_template_data['system_templates']) + format_list_template_system(normalized_template_template['system_templates']) if len(template_types) > 1: click.echo() if 'network' in template_types: click.echo('Network templates:') click.echo() - format_list_template_network(normalized_template_data['network_templates']) + format_list_template_network(normalized_template_template['network_templates']) if len(template_types) > 1: click.echo() if 'storage' in template_types: click.echo('Storage templates:') click.echo() - format_list_template_storage(normalized_template_data['storage_templates']) + format_list_template_storage(normalized_template_template['storage_templates']) -def format_list_template_system(template_data): - if isinstance(template_data, dict): - template_data = [ template_data ] +def format_list_template_system(template_template): + if isinstance(template_template, dict): + template_template = [ template_template ] template_list_output = [] @@ -258,7 +512,7 @@ def format_list_template_system(template_data): template_node_selector_length = 11 template_node_autostart_length = 11 - for template in template_data: + for template in template_template: # template_name column _template_name_length = len(str(template['name'])) + 1 if _template_name_length > template_name_length: @@ -307,7 +561,7 @@ def format_list_template_system(template_data): Consoles: {template_serial: <{template_serial_length}} \ {template_vnc: <{template_vnc_length}} \ {template_vnc_bind: <{template_vnc_bind_length}} \ -Metadata: {template_node_limit: <{template_node_limit_length}} \ +Metatemplate: {template_node_limit: <{template_node_limit_length}} \ {template_node_selector: <{template_node_selector_length}} \ {template_node_autostart: <{template_node_autostart_length}}{end_bold}'.format( template_name_length=template_name_length, @@ -340,7 +594,7 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ valid_net_list = [] # Format the string (elements) - for template in sorted(template_data, key=lambda i: i.get('name', None)): + for template in sorted(template_template, 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}} \ @@ -380,9 +634,9 @@ Metadata: {template_node_limit: <{template_node_limit_length}} \ return True, '' -def format_list_template_network(template_data): - if isinstance(template_data, dict): - template_data = [ template_data ] +def format_list_template_network(template_template): + if isinstance(template_template, dict): + template_template = [ template_template ] template_list_output = [] @@ -392,14 +646,14 @@ def format_list_template_network(template_data): template_mac_template_length = 13 template_networks_length = 10 - for template in template_data: + for template in template_template: # Join the networks elements into a single list of VNIs network_list = list() for network in template['networks']: network_list.append(str(network['vni'])) template['networks_csv'] = ','.join(network_list) - for template in template_data: + for template in template_template: # template_name column _template_name_length = len(str(template['name'])) + 1 if _template_name_length > template_name_length: @@ -434,7 +688,7 @@ def format_list_template_network(template_data): ) # Format the string (elements) - for template in sorted(template_data, key=lambda i: i.get('name', None)): + for template in sorted(template_template, 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}} \ @@ -456,9 +710,9 @@ def format_list_template_network(template_data): return True, '' -def format_list_template_storage(template_data): - if isinstance(template_data, dict): - template_data = [ template_data ] +def format_list_template_storage(template_template): + if isinstance(template_template, dict): + template_template = [ template_template ] template_list_output = [] @@ -472,7 +726,7 @@ def format_list_template_storage(template_data): template_disk_fsargs_length = 10 template_disk_mountpoint_length = 10 - for template in template_data: + for template in template_template: # template_name column _template_name_length = len(str(template['name'])) + 1 if _template_name_length > template_name_length: @@ -537,7 +791,7 @@ def format_list_template_storage(template_data): ) # Format the string (elements) - for template in sorted(template_data, key=lambda i: i.get('name', None)): + for template in sorted(template_template, 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, @@ -588,3 +842,157 @@ def format_list_template_storage(template_data): return True, '' +def format_list_userdata(userdata, lines=None): + if isinstance(userdata, dict): + userdata = [ userdata ] + + data_list_output = [] + + # Determine optimal column widths + data_name_length = 5 + data_id_length = 4 + data_userdata_length = 8 + + for data in userdata: + # data_name column + _data_name_length = len(str(data['name'])) + 1 + if _data_name_length > data_name_length: + data_name_length = _data_name_length + # data_id column + _data_id_length = len(str(data['id'])) + 1 + if _data_id_length > data_id_length: + data_id_length = _data_id_length + + # Format the string (header) + data_list_output_header = '{bold}{data_name: <{data_name_length}} {data_id: <{data_id_length}} \ +{data_userdata}{end_bold}'.format( + data_name_length=data_name_length, + data_id_length=data_id_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + data_name='Name', + data_id='ID', + data_userdata='Document' + ) + + # Format the string (elements) + for data in sorted(userdata, key=lambda i: i.get('name', None)): + line_count = 0 + for line in data['userdata'].split('\n'): + if line_count < 1: + data_name = data['name'] + data_id = data['id'] + else: + data_name = '' + data_id = '' + line_count += 1 + + if lines and line_count > lines: + data_list_output.append( + '{bold}{data_name: <{data_name_length}} {data_id: <{data_id_length}} \ +{data_script}{end_bold}'.format( + data_name_length=data_name_length, + data_id_length=data_id_length, + bold='', + end_bold='', + data_name=data_name, + data_id=data_id, + data_script='[...]' + ) + ) + break + + data_list_output.append( + '{bold}{data_name: <{data_name_length}} {data_id: <{data_id_length}} \ +{data_userdata}{end_bold}'.format( + data_name_length=data_name_length, + data_id_length=data_id_length, + bold='', + end_bold='', + data_name=data_name, + data_id=data_id, + data_userdata=str(line) + ) + ) + + click.echo('\n'.join([data_list_output_header] + data_list_output)) + + return True, '' + +def format_list_script(script, lines=None): + if isinstance(script, dict): + script = [ script ] + + data_list_output = [] + + # Determine optimal column widths + data_name_length = 5 + data_id_length = 4 + data_script_length = 8 + + for data in script: + # data_name column + _data_name_length = len(str(data['name'])) + 1 + if _data_name_length > data_name_length: + data_name_length = _data_name_length + # data_id column + _data_id_length = len(str(data['id'])) + 1 + if _data_id_length > data_id_length: + data_id_length = _data_id_length + + # Format the string (header) + data_list_output_header = '{bold}{data_name: <{data_name_length}} {data_id: <{data_id_length}} \ +{data_script}{end_bold}'.format( + data_name_length=data_name_length, + data_id_length=data_id_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + data_name='Name', + data_id='ID', + data_script='Script' + ) + + # Format the string (elements) + for data in sorted(script, key=lambda i: i.get('name', None)): + line_count = 0 + for line in data['script'].split('\n'): + if line_count < 1: + data_name = data['name'] + data_id = data['id'] + else: + data_name = '' + data_id = '' + line_count += 1 + + if lines and line_count > lines: + data_list_output.append( + '{bold}{data_name: <{data_name_length}} {data_id: <{data_id_length}} \ +{data_script}{end_bold}'.format( + data_name_length=data_name_length, + data_id_length=data_id_length, + bold='', + end_bold='', + data_name=data_name, + data_id=data_id, + data_script='[...]' + ) + ) + break + + data_list_output.append( + '{bold}{data_name: <{data_name_length}} {data_id: <{data_id_length}} \ +{data_script}{end_bold}'.format( + data_name_length=data_name_length, + data_id_length=data_id_length, + bold='', + end_bold='', + data_name=data_name, + data_id=data_id, + data_script=str(line) + ) + ) + + click.echo('\n'.join([data_list_output_header] + data_list_output)) + + return True, '' + diff --git a/client-cli/pvc.py b/client-cli/pvc.py index 165156f8..df938a20 100755 --- a/client-cli/pvc.py +++ b/client-cli/pvc.py @@ -582,7 +582,7 @@ def vm_modify(domain, cfgfile, editor, restart): click.echo(line) click.echo('') - click.confirm('Write modifications to Zookeeper?', abort=True) + click.confirm('Write modifications to cluster?', abort=True) if restart: click.echo('Writing modified configuration of VM "{}" and restarting.'.format(dom_name)) @@ -2020,7 +2020,7 @@ def provisioner_template_system_add(name, vcpus, vram, serial, vnc, vnc_bind, no ) def provisioner_template_system_remove(name): """ - Remove a system template from the PVC cluster provisioner. + Remove system template NAME from the PVC cluster provisioner. """ retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='system') cleanup(retcode, retdata) @@ -2113,7 +2113,7 @@ def provisioner_template_network_add(name, mac_template): ) def provisioner_template_network_remove(name): """ - Remove a network template from the PVC cluster provisioner. + Remove network template MAME from the PVC cluster provisioner. """ retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='network') cleanup(retcode, retdata) @@ -2161,7 +2161,7 @@ def provisioner_template_network_vni_add(name, vni): ) def provisioner_template_network_vni_remove(name, vni): """ - Remove a network VNI from network template NAME. + Remove network VNI from network template NAME. """ params = dict() @@ -2224,7 +2224,7 @@ def provisioner_template_storage_add(name): ) def provisioner_template_storage_remove(name): """ - Remove a storage template from the PVC cluster provisioner. + Remove storage template NAME from the PVC cluster provisioner. """ retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='storage') cleanup(retcode, retdata) @@ -2314,7 +2314,9 @@ def provisioner_template_storage_disk_add(name, disk, pool, size, filesystem, fs ) def provisioner_template_storage_disk_remove(name, disk): """ - Remove a DISK from storage template NAME. + Remove DISK from storage template NAME. + + DISK must be a Linux-style disk identifier such as "sda" or "vdb". """ params = dict() @@ -2322,6 +2324,328 @@ def provisioner_template_storage_disk_remove(name, disk): cleanup(retcode, retdata) +############################################################################### +# pvc provisioner userdata +############################################################################### +@click.group(name='userdata', short_help='Manage PVC provisioner userdata documents.', context_settings=CONTEXT_SETTINGS) +def provisioner_userdata(): + """ + Manage userdata documents in the PVC provisioner. + """ + # Abort commands under this group if config is bad + if config.get('badcfg', None): + exit(1) + +############################################################################### +# pvc provisioner userdata list +############################################################################### +@click.command(name='list', short_help='List all userdata documents in the cluster.') +@click.argument( + 'limit', default=None, required=False +) +@click.option( + '-f', '--full', 'full', + is_flag=True, default=False, + help='Show all lines of the document instead of first 4.' +) +def provisioner_userdata_list(limit, full): + """ + List all userdata documents in the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.userdata_list(config, limit) + if retcode: + if not full: + lines = 4 + else: + lines = None + pvc_provisioner.format_list_userdata(retdata, lines) + retdata = '' + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner userdata add +############################################################################### +@click.command(name='add', short_help='Define userdata document from file.') +@click.argument( + 'name' +) +@click.argument( + 'filename', type=click.File() +) +def provisioner_userdata_add(name, filename): + """ + Add a new userdata document NAME from file FILENAME. + """ + + # Open the XML file + userdata = filename.read() + filename.close() + + params = dict() + params['name'] = name + params['data'] = userdata + + retcode, retmsg = pvc_provisioner.userdata_add(config, params) + cleanup(retcode, retmsg) + +############################################################################### +# pvc provisioner userdata modify +############################################################################### +@click.command(name='modify', short_help='Modify existing userdata document.') +@click.option( + '-e', '--editor', 'editor', is_flag=True, + help='Use local editor to modify existing document.' +) +@click.argument( + 'name' +) +@click.argument( + 'filename', type=click.File(), default=None, required=False +) +def provisioner_userdata_modify(name, filename, editor): + """ + Modify existing userdata document NAME, either in-editor or with replacement FILE. + """ + + if editor == False and filename == None: + cleanup(False, 'Either a file or the "--editor" option must be specified.') + + if editor == True: + # Grab the current config + retcode, retdata = pvc_provisioner.userdata_info(config, name) + if not retcode: + click.echo(retdata) + exit(1) + current_userdata = retdata['userdata'] + + # Write it to a tempfile + fd, path = tempfile.mkstemp() + fw = os.fdopen(fd, 'w') + fw.write(current_userdata.strip()) + fw.close() + + # Edit it + editor = os.getenv('EDITOR', 'vi') + subprocess.call('%s %s' % (editor, path), shell=True) + + # Open the tempfile to read + with open(path, 'r') as fr: + new_userdata = fr.read().strip() + fr.close() + + # Delete the tempfile + os.unlink(path) + + # Show a diff and confirm + diff = list(difflib.unified_diff(current_userdata.split('\n'), new_userdata.split('\n'), fromfile='current', tofile='modified', fromfiledate='', tofiledate='', n=3, lineterm='')) + if len(diff) < 1: + click.echo('Aborting with no modifications.') + exit(0) + + click.echo('Pending modifications:') + click.echo('') + for line in diff: + if re.match('^\+', line) != None: + click.echo(colorama.Fore.GREEN + line + colorama.Fore.RESET) + elif re.match('^\-', line) != None: + click.echo(colorama.Fore.RED + line + colorama.Fore.RESET) + elif re.match('^\^', line) != None: + click.echo(colorama.Fore.BLUE + line + colorama.Fore.RESET) + else: + click.echo(line) + click.echo('') + + click.confirm('Write modifications to cluster?', abort=True) + + userdata = new_userdata + + # We're operating in replace mode + else: + # Open the new file + userdata = filename.read().strip() + filename.close() + + params = dict() + params['data'] = userdata + + retcode, retmsg = pvc_provisioner.userdata_modify(config, name, params) + cleanup(retcode, retmsg) + +############################################################################### +# pvc provisioner userdata remove +############################################################################### +@click.command(name='remove', short_help='Remove userdata document from the cluster.') +@click.argument( + 'name' +) +def provisioner_userdata_remove(name): + """ + Remove userdata document NAME from the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.userdata_remove(config, name) + cleanup(retcode, retdata) + + +############################################################################### +# pvc provisioner script +############################################################################### +@click.group(name='script', short_help='Manage PVC provisioner scripts.', context_settings=CONTEXT_SETTINGS) +def provisioner_script(): + """ + Manage scripts in the PVC provisioner. + """ + # Abort commands under this group if config is bad + if config.get('badcfg', None): + exit(1) + +############################################################################### +# pvc provisioner script list +############################################################################### +@click.command(name='list', short_help='List all scripts in the cluster.') +@click.argument( + 'limit', default=None, required=False +) +@click.option( + '-f', '--full', 'full', + is_flag=True, default=False, + help='Show all lines of the document instead of first 4.' +) +def provisioner_script_list(limit, full): + """ + List all scripts in the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.script_list(config, limit) + if retcode: + if not full: + lines = 4 + else: + lines = None + pvc_provisioner.format_list_script(retdata, lines) + retdata = '' + cleanup(retcode, retdata) + +############################################################################### +# pvc provisioner script add +############################################################################### +@click.command(name='add', short_help='Define script from file.') +@click.argument( + 'name' +) +@click.argument( + 'filename', type=click.File() +) +def provisioner_script_add(name, filename): + """ + Add a new script NAME from file FILENAME. + """ + + # Open the XML file + script = filename.read() + filename.close() + + params = dict() + params['name'] = name + params['data'] = script + + retcode, retmsg = pvc_provisioner.script_add(config, params) + cleanup(retcode, retmsg) + +############################################################################### +# pvc provisioner script modify +############################################################################### +@click.command(name='modify', short_help='Modify existing script.') +@click.option( + '-e', '--editor', 'editor', is_flag=True, + help='Use local editor to modify existing document.' +) +@click.argument( + 'name' +) +@click.argument( + 'filename', type=click.File(), default=None, required=False +) +def provisioner_script_modify(name, filename, editor): + """ + Modify existing script NAME, either in-editor or with replacement FILE. + """ + + if editor == False and filename == None: + cleanup(False, 'Either a file or the "--editor" option must be specified.') + + if editor == True: + # Grab the current config + retcode, retdata = pvc_provisioner.script_info(config, name) + if not retcode: + click.echo(retdata) + exit(1) + current_script = retdata['script'] + + # Write it to a tempfile + fd, path = tempfile.mkstemp() + fw = os.fdopen(fd, 'w') + fw.write(current_script.strip()) + fw.close() + + # Edit it + editor = os.getenv('EDITOR', 'vi') + subprocess.call('%s %s' % (editor, path), shell=True) + + # Open the tempfile to read + with open(path, 'r') as fr: + new_script = fr.read().strip() + fr.close() + + # Delete the tempfile + os.unlink(path) + + # Show a diff and confirm + diff = list(difflib.unified_diff(current_script.split('\n'), new_script.split('\n'), fromfile='current', tofile='modified', fromfiledate='', tofiledate='', n=3, lineterm='')) + if len(diff) < 1: + click.echo('Aborting with no modifications.') + exit(0) + + click.echo('Pending modifications:') + click.echo('') + for line in diff: + if re.match('^\+', line) != None: + click.echo(colorama.Fore.GREEN + line + colorama.Fore.RESET) + elif re.match('^\-', line) != None: + click.echo(colorama.Fore.RED + line + colorama.Fore.RESET) + elif re.match('^\^', line) != None: + click.echo(colorama.Fore.BLUE + line + colorama.Fore.RESET) + else: + click.echo(line) + click.echo('') + + click.confirm('Write modifications to cluster?', abort=True) + + script = new_script + + # We're operating in replace mode + else: + # Open the new file + script = filename.read().strip() + filename.close() + + params = dict() + params['data'] = script + + retcode, retmsg = pvc_provisioner.script_modify(config, name, params) + cleanup(retcode, retmsg) + +############################################################################### +# pvc provisioner script remove +############################################################################### +@click.command(name='remove', short_help='Remove script from the cluster.') +@click.argument( + 'name' +) +def provisioner_script_remove(name): + """ + Remove script NAME from the PVC cluster provisioner. + """ + retcode, retdata = pvc_provisioner.script_remove(config, name) + cleanup(retcode, retdata) @@ -2535,7 +2859,19 @@ provisioner_template.add_command(provisioner_template_network) provisioner_template.add_command(provisioner_template_storage) provisioner_template.add_command(provisioner_template_list) +provisioner_userdata.add_command(provisioner_userdata_list) +provisioner_userdata.add_command(provisioner_userdata_add) +provisioner_userdata.add_command(provisioner_userdata_modify) +provisioner_userdata.add_command(provisioner_userdata_remove) + +provisioner_script.add_command(provisioner_script_list) +provisioner_script.add_command(provisioner_script_add) +provisioner_script.add_command(provisioner_script_modify) +provisioner_script.add_command(provisioner_script_remove) + cli_provisioner.add_command(provisioner_template) +cli_provisioner.add_command(provisioner_userdata) +cli_provisioner.add_command(provisioner_script) cli.add_command(cli_cluster) cli.add_command(cli_node)