Compare commits

...

5 Commits

Author SHA1 Message Date
6246b8dfb3 Fix help message output on root command 2021-04-08 14:27:55 -04:00
669338c22b Bump version to 0.9.15 2021-04-08 13:37:47 -04:00
629cf62385 Add confirmation flag to disruptive VM operations
Also add some additional output when --restart is not selected.

Closes #118
2021-04-08 13:33:10 -04:00
dfa3432601 Add unsafe envvar/flag option
Allows another way (beyond --yes) to avoid confirming "unsafe"
operations. While there is probably nearly zero usecase for this (at
least to any sane admin), it is provided to allow maximum flexibility.
2021-04-08 12:48:38 -04:00
62213fab99 Add description field to CLI clusters
Allow specifying a textual description of the cluster in the client for
ease of management.
2021-04-08 12:28:23 -04:00
6 changed files with 208 additions and 44 deletions

View File

@ -20,6 +20,12 @@ To get started with PVC, please see the [About](https://parallelvirtualcluster.r
## Changelog ## Changelog
#### v0.9.15
* [CLI] Adds additional verification (--yes) to several VM management commands
* [CLI] Adds a method to override --yes/confirmation requirements via envvar (PVC_UNSAFE)
* [CLI] Adds description fields to PVC clusters in CLI
#### v0.9.14 #### v0.9.14
* Fixes bugs around cloned volume provisioning * Fixes bugs around cloned volume provisioning

View File

@ -26,7 +26,7 @@ import pvcapid.flaskapi as pvc_api
########################################################## ##########################################################
# Version string for startup output # Version string for startup output
version = '0.9.14' version = '0.9.15'
if pvc_api.config['ssl_enabled']: if pvc_api.config['ssl_enabled']:
context = (pvc_api.config['ssl_cert_file'], pvc_api.config['ssl_key_file']) context = (pvc_api.config['ssl_cert_file'], pvc_api.config['ssl_key_file'])

View File

@ -46,7 +46,7 @@ myhostname = socket.gethostname().split('.')[0]
zk_host = '' zk_host = ''
default_store_data = { default_store_data = {
'cfgfile': '/etc/pvc/pvcapid.yaml' # pvc/api/listen_address, pvc/api/listen_port 'cfgfile': '/etc/pvc/pvcapid.yaml'
} }
@ -67,7 +67,7 @@ def read_from_yaml(cfgfile):
api_key = api_config['pvc']['api']['authentication']['tokens'][0]['token'] api_key = api_config['pvc']['api']['authentication']['tokens'][0]['token']
else: else:
api_key = 'N/A' api_key = 'N/A'
return host, port, scheme, api_key return cfgfile, host, port, scheme, api_key
def get_config(store_data, cluster=None): def get_config(store_data, cluster=None):
@ -84,7 +84,7 @@ def get_config(store_data, cluster=None):
# This is a reference to an API configuration; grab the details from its listen address # This is a reference to an API configuration; grab the details from its listen address
cfgfile = cluster_details.get('cfgfile') cfgfile = cluster_details.get('cfgfile')
if os.path.isfile(cfgfile): if os.path.isfile(cfgfile):
host, port, scheme, api_key = read_from_yaml(cfgfile) description, host, port, scheme, api_key = read_from_yaml(cfgfile)
else: else:
return {'badcfg': True} return {'badcfg': True}
# Handle an all-wildcard address # Handle an all-wildcard address
@ -92,6 +92,7 @@ def get_config(store_data, cluster=None):
host = '127.0.0.1' host = '127.0.0.1'
else: else:
# This is a static configuration, get the raw details # This is a static configuration, get the raw details
description = cluster_details['description']
host = cluster_details['host'] host = cluster_details['host']
port = cluster_details['port'] port = cluster_details['port']
scheme = cluster_details['scheme'] scheme = cluster_details['scheme']
@ -100,6 +101,7 @@ def get_config(store_data, cluster=None):
config = dict() config = dict()
config['debug'] = False config['debug'] = False
config['cluster'] = cluster config['cluster'] = cluster
config['desctription'] = description
config['api_host'] = '{}:{}'.format(host, port) config['api_host'] = '{}:{}'.format(host, port)
config['api_scheme'] = scheme config['api_scheme'] = scheme
config['api_key'] = api_key config['api_key'] = api_key
@ -175,6 +177,10 @@ def cli_cluster():
# pvc cluster add # pvc cluster add
############################################################################### ###############################################################################
@click.command(name='add', short_help='Add a new cluster to the client.') @click.command(name='add', short_help='Add a new cluster to the client.')
@click.option(
'-d', '--description', 'description', required=False, default="N/A",
help='A text description of the cluster.'
)
@click.option( @click.option(
'-a', '--address', 'address', required=True, '-a', '--address', 'address', required=True,
help='The IP address or hostname of the cluster API client.' help='The IP address or hostname of the cluster API client.'
@ -194,7 +200,7 @@ def cli_cluster():
@click.argument( @click.argument(
'name' 'name'
) )
def cluster_add(address, port, ssl, name, api_key): def cluster_add(description, address, port, ssl, name, api_key):
""" """
Add a new PVC cluster NAME, via its API connection details, to the configuration of the local CLI client. Replaces any existing cluster with this name. Add a new PVC cluster NAME, via its API connection details, to the configuration of the local CLI client. Replaces any existing cluster with this name.
""" """
@ -207,6 +213,7 @@ def cluster_add(address, port, ssl, name, api_key):
existing_config = get_store(store_path) existing_config = get_store(store_path)
# Append our new entry to the end # Append our new entry to the end
existing_config[name] = { existing_config[name] = {
'description': description,
'host': address, 'host': address,
'port': port, 'port': port,
'scheme': scheme, 'scheme': scheme,
@ -252,10 +259,11 @@ def cluster_list():
clusters = get_store(store_path) clusters = get_store(store_path)
# Find the lengths of each column # Find the lengths of each column
name_length = 5 name_length = 5
description_length = 12
address_length = 10 address_length = 10
port_length = 5 port_length = 5
scheme_length = 7 scheme_length = 7
api_key_length = 8 api_key_length = 32
for cluster in clusters: for cluster in clusters:
cluster_details = clusters[cluster] cluster_details = clusters[cluster]
@ -263,10 +271,11 @@ def cluster_list():
# This is a reference to an API configuration; grab the details from its listen address # This is a reference to an API configuration; grab the details from its listen address
cfgfile = cluster_details.get('cfgfile') cfgfile = cluster_details.get('cfgfile')
if os.path.isfile(cfgfile): if os.path.isfile(cfgfile):
address, port, scheme, api_key = read_from_yaml(cfgfile) description, address, port, scheme, api_key = read_from_yaml(cfgfile)
else: else:
address, port, scheme, api_key = 'N/A', 'N/A', 'N/A', 'N/A' description, address, port, scheme, api_key = 'N/A', 'N/A', 'N/A', 'N/A', 'N/A'
else: else:
description = cluster_details.get('description', '')
address = cluster_details.get('host', 'N/A') address = cluster_details.get('host', 'N/A')
port = cluster_details.get('port', 'N/A') port = cluster_details.get('port', 'N/A')
scheme = cluster_details.get('scheme', 'N/A') scheme = cluster_details.get('scheme', 'N/A')
@ -278,6 +287,9 @@ def cluster_list():
if _name_length > name_length: if _name_length > name_length:
name_length = _name_length name_length = _name_length
_address_length = len(address) + 1 _address_length = len(address) + 1
_description_length = len(description) + 1
if _description_length > description_length:
description_length = _description_length
if _address_length > address_length: if _address_length > address_length:
address_length = _address_length address_length = _address_length
_port_length = len(str(port)) + 1 _port_length = len(str(port)) + 1
@ -294,11 +306,13 @@ def cluster_list():
click.echo("Available clusters:") click.echo("Available clusters:")
click.echo() click.echo()
click.echo( click.echo(
'{bold}{name: <{name_length}} {address: <{address_length}} {port: <{port_length}} {scheme: <{scheme_length}} {api_key: <{api_key_length}}{end_bold}'.format( '{bold}{name: <{name_length}} {description: <{description_length}} {address: <{address_length}} {port: <{port_length}} {scheme: <{scheme_length}} {api_key: <{api_key_length}}{end_bold}'.format(
bold=ansiprint.bold(), bold=ansiprint.bold(),
end_bold=ansiprint.end(), end_bold=ansiprint.end(),
name="Name", name="Name",
name_length=name_length, name_length=name_length,
description="Description",
description_length=description_length,
address="Address", address="Address",
address_length=address_length, address_length=address_length,
port="Port", port="Port",
@ -315,14 +329,16 @@ def cluster_list():
if cluster_details.get('cfgfile', None): if cluster_details.get('cfgfile', None):
# This is a reference to an API configuration; grab the details from its listen address # This is a reference to an API configuration; grab the details from its listen address
if os.path.isfile(cfgfile): if os.path.isfile(cfgfile):
address, port, scheme, api_key = read_from_yaml(cfgfile) description, address, port, scheme, api_key = read_from_yaml(cfgfile)
else: else:
description = 'N/A'
address = 'N/A' address = 'N/A'
port = 'N/A' port = 'N/A'
scheme = 'N/A' scheme = 'N/A'
api_key = 'N/A' api_key = 'N/A'
else: else:
address = cluster_details.get('host', 'N/A') address = cluster_details.get('host', 'N/A')
description = cluster_details.get('description', 'N/A')
port = cluster_details.get('port', 'N/A') port = cluster_details.get('port', 'N/A')
scheme = cluster_details.get('scheme', 'N/A') scheme = cluster_details.get('scheme', 'N/A')
api_key = cluster_details.get('api_key', 'N/A') api_key = cluster_details.get('api_key', 'N/A')
@ -330,11 +346,13 @@ def cluster_list():
api_key = 'N/A' api_key = 'N/A'
click.echo( click.echo(
'{bold}{name: <{name_length}} {address: <{address_length}} {port: <{port_length}} {scheme: <{scheme_length}} {api_key: <{api_key_length}}{end_bold}'.format( '{bold}{name: <{name_length}} {description: <{description_length}} {address: <{address_length}} {port: <{port_length}} {scheme: <{scheme_length}} {api_key: <{api_key_length}}{end_bold}'.format(
bold='', bold='',
end_bold='', end_bold='',
name=cluster, name=cluster,
name_length=name_length, name_length=name_length,
description=description,
description_length=description_length,
address=address, address=address,
address_length=address_length, address_length=address_length,
port=port, port=port,
@ -694,13 +712,18 @@ def vm_meta(domain, node_limit, node_selector, node_autostart, migration_method,
'-r', '--restart', 'restart', is_flag=True, '-r', '--restart', 'restart', is_flag=True,
help='Immediately restart VM to apply new config.' help='Immediately restart VM to apply new config.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@click.argument( @click.argument(
'domain' 'domain'
) )
@click.argument( @click.argument(
'cfgfile', type=click.File(), default=None, required=False 'cfgfile', type=click.File(), default=None, required=False
) )
def vm_modify(domain, cfgfile, editor, restart): def vm_modify(domain, cfgfile, editor, restart, confirm_flag):
""" """
Modify existing virtual machine DOMAIN, either in-editor or with replacement CONFIG. DOMAIN may be a UUID or name. Modify existing virtual machine DOMAIN, either in-editor or with replacement CONFIG. DOMAIN may be a UUID or name.
""" """
@ -712,6 +735,12 @@ def vm_modify(domain, cfgfile, editor, restart):
if not retcode and not vm_information.get('name', None): if not retcode and not vm_information.get('name', None):
cleanup(False, 'ERROR: Could not find VM "{}"!'.format(domain)) cleanup(False, 'ERROR: Could not find VM "{}"!'.format(domain))
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
dom_name = vm_information.get('name') dom_name = vm_information.get('name')
if editor is True: if editor is True:
@ -768,6 +797,8 @@ def vm_modify(domain, cfgfile, editor, restart):
cleanup(False, 'Error: XML is malformed or invalid: {}'.format(e)) cleanup(False, 'Error: XML is malformed or invalid: {}'.format(e))
retcode, retmsg = pvc_vm.vm_modify(config, domain, new_cfg, restart) retcode, retmsg = pvc_vm.vm_modify(config, domain, new_cfg, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -788,7 +819,7 @@ def vm_undefine(domain, confirm_flag):
""" """
Stop virtual machine DOMAIN and remove it database, preserving disks. DOMAIN may be a UUID or name. Stop virtual machine DOMAIN and remove it database, preserving disks. DOMAIN may be a UUID or name.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Undefine VM {}'.format(domain), prompt_suffix='? ', abort=True) click.confirm('Undefine VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -815,7 +846,7 @@ def vm_remove(domain, confirm_flag):
""" """
Stop virtual machine DOMAIN and remove it, along with all disks,. DOMAIN may be a UUID or name. Stop virtual machine DOMAIN and remove it, along with all disks,. DOMAIN may be a UUID or name.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Undefine VM {} and remove all disks'.format(domain), prompt_suffix='? ', abort=True) click.confirm('Undefine VM {} and remove all disks'.format(domain), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -853,11 +884,21 @@ def vm_start(domain):
'-w', '--wait', 'wait', is_flag=True, default=False, '-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for restart to complete before returning.' help='Wait for restart to complete before returning.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@cluster_req @cluster_req
def vm_restart(domain, wait): def vm_restart(domain, wait, confirm_flag):
""" """
Restart running virtual machine DOMAIN. DOMAIN may be a UUID or name. Restart running virtual machine DOMAIN. DOMAIN may be a UUID or name.
""" """
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_state(config, domain, 'restart', wait=wait) retcode, retmsg = pvc_vm.vm_state(config, domain, 'restart', wait=wait)
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -874,11 +915,21 @@ def vm_restart(domain, wait):
'-w', '--wait', 'wait', is_flag=True, default=False, '-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for shutdown to complete before returning.' help='Wait for shutdown to complete before returning.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the shutdown'
)
@cluster_req @cluster_req
def vm_shutdown(domain, wait): def vm_shutdown(domain, wait, confirm_flag):
""" """
Gracefully shut down virtual machine DOMAIN. DOMAIN may be a UUID or name. Gracefully shut down virtual machine DOMAIN. DOMAIN may be a UUID or name.
""" """
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Shut down VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_state(config, domain, 'shutdown', wait=wait) retcode, retmsg = pvc_vm.vm_state(config, domain, 'shutdown', wait=wait)
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -891,11 +942,21 @@ def vm_shutdown(domain, wait):
@click.argument( @click.argument(
'domain' 'domain'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the stop'
)
@cluster_req @cluster_req
def vm_stop(domain): def vm_stop(domain, confirm_flag):
""" """
Forcibly halt (destroy) running virtual machine DOMAIN. DOMAIN may be a UUID or name. Forcibly halt (destroy) running virtual machine DOMAIN. DOMAIN may be a UUID or name.
""" """
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Forcibly stop VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_state(config, domain, 'stop') retcode, retmsg = pvc_vm.vm_state(config, domain, 'stop')
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1078,13 +1139,23 @@ def vm_vcpu_get(domain, raw):
'-r', '--restart', 'restart', is_flag=True, default=False, '-r', '--restart', 'restart', is_flag=True, default=False,
help='Immediately restart VM to apply new config.' help='Immediately restart VM to apply new config.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@cluster_req @cluster_req
def vm_vcpu_set(domain, vcpus, topology, restart): def vm_vcpu_set(domain, vcpus, topology, restart, confirm_flag):
""" """
Set the vCPU count of the virtual machine DOMAIN to VCPUS. Set the vCPU count of the virtual machine DOMAIN to VCPUS.
By default, the topology of the vCPus is 1 socket, VCPUS cores per socket, 1 thread per core. By default, the topology of the vCPus is 1 socket, VCPUS cores per socket, 1 thread per core.
""" """
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
if topology is not None: if topology is not None:
try: try:
@ -1098,6 +1169,8 @@ def vm_vcpu_set(domain, vcpus, topology, restart):
topology = (1, vcpus, 1) topology = (1, vcpus, 1)
retcode, retmsg = pvc_vm.vm_vcpus_set(config, domain, vcpus, topology, restart) retcode, retmsg = pvc_vm.vm_vcpus_set(config, domain, vcpus, topology, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1149,13 +1222,25 @@ def vm_memory_get(domain, raw):
'-r', '--restart', 'restart', is_flag=True, default=False, '-r', '--restart', 'restart', is_flag=True, default=False,
help='Immediately restart VM to apply new config.' help='Immediately restart VM to apply new config.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@cluster_req @cluster_req
def vm_memory_set(domain, memory, restart): def vm_memory_set(domain, memory, restart, confirm_flag):
""" """
Set the provisioned memory of the virtual machine DOMAIN to MEMORY; MEMORY must be an integer in MB. Set the provisioned memory of the virtual machine DOMAIN to MEMORY; MEMORY must be an integer in MB.
""" """
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_memory_set(config, domain, memory, restart) retcode, retmsg = pvc_vm.vm_memory_set(config, domain, memory, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1222,13 +1307,25 @@ def vm_network_get(domain, raw):
'-r', '--restart', 'restart', is_flag=True, default=False, '-r', '--restart', 'restart', is_flag=True, default=False,
help='Immediately restart VM to apply new config.' help='Immediately restart VM to apply new config.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@cluster_req @cluster_req
def vm_network_add(domain, vni, macaddr, model, restart): def vm_network_add(domain, vni, macaddr, model, restart, confirm_flag):
""" """
Add the network VNI to the virtual machine DOMAIN. Networks are always addded to the end of the current list of networks in the virtual machine. Add the network VNI to the virtual machine DOMAIN. Networks are always addded to the end of the current list of networks in the virtual machine.
""" """
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_networks_add(config, domain, vni, macaddr, model, restart) retcode, retmsg = pvc_vm.vm_networks_add(config, domain, vni, macaddr, model, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1246,13 +1343,25 @@ def vm_network_add(domain, vni, macaddr, model, restart):
'-r', '--restart', 'restart', is_flag=True, default=False, '-r', '--restart', 'restart', is_flag=True, default=False,
help='Immediately restart VM to apply new config.' help='Immediately restart VM to apply new config.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@cluster_req @cluster_req
def vm_network_remove(domain, vni, restart): def vm_network_remove(domain, vni, restart, confirm_flag):
""" """
Remove the network VNI to the virtual machine DOMAIN. Remove the network VNI to the virtual machine DOMAIN.
""" """
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_networks_remove(config, domain, vni, restart) retcode, retmsg = pvc_vm.vm_networks_remove(config, domain, vni, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1325,15 +1434,27 @@ def vm_volume_get(domain, raw):
'-r', '--restart', 'restart', is_flag=True, default=False, '-r', '--restart', 'restart', is_flag=True, default=False,
help='Immediately restart VM to apply new config.' help='Immediately restart VM to apply new config.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@cluster_req @cluster_req
def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart): def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart, confirm_flag):
""" """
Add the volume VOLUME to the virtual machine DOMAIN. Add the volume VOLUME to the virtual machine DOMAIN.
VOLUME may be either an absolute file path (for type 'file') or an RBD volume in the form "pool/volume" (for type 'rbd'). RBD volumes are verified against the cluster before adding and must exist. VOLUME may be either an absolute file path (for type 'file') or an RBD volume in the form "pool/volume" (for type 'rbd'). RBD volumes are verified against the cluster before adding and must exist.
""" """
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_volumes_add(config, domain, volume, disk_id, bus, disk_type, restart) retcode, retmsg = pvc_vm.vm_volumes_add(config, domain, volume, disk_id, bus, disk_type, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1351,13 +1472,25 @@ def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart):
'-r', '--restart', 'restart', is_flag=True, default=False, '-r', '--restart', 'restart', is_flag=True, default=False,
help='Immediately restart VM to apply new config.' help='Immediately restart VM to apply new config.'
) )
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@cluster_req @cluster_req
def vm_volume_remove(domain, vni, restart): def vm_volume_remove(domain, vni, restart, confirm_flag):
""" """
Remove the volume VNI to the virtual machine DOMAIN. Remove the volume VNI to the virtual machine DOMAIN.
""" """
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_volumes_remove(config, domain, vni, restart) retcode, retmsg = pvc_vm.vm_volumes_remove(config, domain, vni, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg) cleanup(retcode, retmsg)
@ -1669,7 +1802,7 @@ def net_remove(net, confirm_flag):
WARNING: PVC does not verify whether clients are still present in this network. Before removing, ensure WARNING: PVC does not verify whether clients are still present in this network. Before removing, ensure
that all client VMs have been removed from the network or undefined behaviour may occur. that all client VMs have been removed from the network or undefined behaviour may occur.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove network {}'.format(net), prompt_suffix='? ', abort=True) click.confirm('Remove network {}'.format(net), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -1778,7 +1911,7 @@ def net_dhcp_remove(net, macaddr, confirm_flag):
""" """
Remove a DHCP lease for MACADDR from virtual network NET; NET must be a VNI. Remove a DHCP lease for MACADDR from virtual network NET; NET must be a VNI.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove DHCP lease for {} in network {}'.format(macaddr, net), prompt_suffix='? ', abort=True) click.confirm('Remove DHCP lease for {} in network {}'.format(macaddr, net), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -1897,7 +2030,7 @@ def net_acl_remove(net, rule, confirm_flag):
""" """
Remove an NFT firewall rule RULE from network NET; RULE must be a description; NET must be a VNI. Remove an NFT firewall rule RULE from network NET; RULE must be a description; NET must be a VNI.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove ACL {} in network {}'.format(rule, net), prompt_suffix='? ', abort=True) click.confirm('Remove ACL {} in network {}'.format(rule, net), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -2096,7 +2229,7 @@ def ceph_osd_add(node, device, weight, confirm_flag):
""" """
Add a new Ceph OSD on node NODE with block device DEVICE. Add a new Ceph OSD on node NODE with block device DEVICE.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Destroy all data and create a new OSD on {}:{}'.format(node, device), prompt_suffix='? ', abort=True) click.confirm('Destroy all data and create a new OSD on {}:{}'.format(node, device), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -2125,7 +2258,7 @@ def ceph_osd_remove(osdid, confirm_flag):
DANGER: This will completely remove the OSD from the cluster. OSDs will rebalance which may negatively affect performance or available space. DANGER: This will completely remove the OSD from the cluster. OSDs will rebalance which may negatively affect performance or available space.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove OSD {}'.format(osdid), prompt_suffix='? ', abort=True) click.confirm('Remove OSD {}'.format(osdid), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -2289,7 +2422,7 @@ def ceph_pool_remove(name, confirm_flag):
DANGER: This will completely remove the pool and all volumes contained in it from the cluster. DANGER: This will completely remove the pool and all volumes contained in it from the cluster.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove RBD pool {}'.format(name), prompt_suffix='? ', abort=True) click.confirm('Remove RBD pool {}'.format(name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -2410,7 +2543,7 @@ def ceph_volume_remove(pool, name, confirm_flag):
DANGER: This will completely remove the volume and all data contained in it. DANGER: This will completely remove the volume and all data contained in it.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove volume {}/{}'.format(pool, name), prompt_suffix='? ', abort=True) click.confirm('Remove volume {}/{}'.format(pool, name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -2594,7 +2727,7 @@ def ceph_volume_snapshot_remove(pool, volume, name, confirm_flag):
DANGER: This will completely remove the snapshot. DANGER: This will completely remove the snapshot.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove snapshot {} for volume {}/{}'.format(name, pool, volume), prompt_suffix='? ', abort=True) click.confirm('Remove snapshot {} for volume {}/{}'.format(name, pool, volume), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -2870,7 +3003,7 @@ def provisioner_template_system_remove(name, confirm_flag):
""" """
Remove system template NAME from the PVC cluster provisioner. Remove system template NAME from the PVC cluster provisioner.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove system template {}'.format(name), prompt_suffix='? ', abort=True) click.confirm('Remove system template {}'.format(name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -2977,7 +3110,7 @@ def provisioner_template_network_remove(name, confirm_flag):
""" """
Remove network template MAME from the PVC cluster provisioner. Remove network template MAME from the PVC cluster provisioner.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove network template {}'.format(name), prompt_suffix='? ', abort=True) click.confirm('Remove network template {}'.format(name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -3041,7 +3174,7 @@ def provisioner_template_network_vni_remove(name, vni, confirm_flag):
""" """
Remove network VNI from network template NAME. Remove network VNI from network template NAME.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove VNI {} from network template {}'.format(vni, name), prompt_suffix='? ', abort=True) click.confirm('Remove VNI {} from network template {}'.format(vni, name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -3116,7 +3249,7 @@ def provisioner_template_storage_remove(name, confirm_flag):
""" """
Remove storage template NAME from the PVC cluster provisioner. Remove storage template NAME from the PVC cluster provisioner.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove storage template {}'.format(name), prompt_suffix='? ', abort=True) click.confirm('Remove storage template {}'.format(name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -3235,7 +3368,7 @@ def provisioner_template_storage_disk_remove(name, disk, confirm_flag):
DISK must be a Linux-style disk identifier such as "sda" or "vdb". DISK must be a Linux-style disk identifier such as "sda" or "vdb".
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove disk {} from storage template {}'.format(disk, name), prompt_suffix='? ', abort=True) click.confirm('Remove disk {} from storage template {}'.format(disk, name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -3425,7 +3558,7 @@ def provisioner_userdata_remove(name, confirm_flag):
""" """
Remove userdata document NAME from the PVC cluster provisioner. Remove userdata document NAME from the PVC cluster provisioner.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove userdata document {}'.format(name), prompt_suffix='? ', abort=True) click.confirm('Remove userdata document {}'.format(name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -3604,7 +3737,7 @@ def provisioner_script_remove(name, confirm_flag):
""" """
Remove script NAME from the PVC cluster provisioner. Remove script NAME from the PVC cluster provisioner.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove provisioning script {}'.format(name), prompt_suffix='? ', abort=True) click.confirm('Remove provisioning script {}'.format(name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -3700,7 +3833,7 @@ def provisioner_ova_remove(name, confirm_flag):
""" """
Remove OVA image NAME from the PVC cluster provisioner. Remove OVA image NAME from the PVC cluster provisioner.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove OVA image {}'.format(name), prompt_suffix='? ', abort=True) click.confirm('Remove OVA image {}'.format(name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -3885,7 +4018,7 @@ def provisioner_profile_remove(name, confirm_flag):
""" """
Remove profile NAME from the PVC cluster provisioner. Remove profile NAME from the PVC cluster provisioner.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove profile {}'.format(name), prompt_suffix='? ', abort=True) click.confirm('Remove profile {}'.format(name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -4131,7 +4264,7 @@ def task_restore(filename, confirm_flag):
Restore the JSON backup data from a file to the cluster. Restore the JSON backup data from a file to the cluster.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Replace all existing cluster data from coordinators with backup file "{}"'.format(filename.name), prompt_suffix='? ', abort=True) click.confirm('Replace all existing cluster data from coordinators with backup file "{}"'.format(filename.name), prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -4157,7 +4290,7 @@ def task_init(confirm_flag):
Perform initialization of a new PVC cluster. Perform initialization of a new PVC cluster.
""" """
if not confirm_flag: if not confirm_flag and not config['unsafe']:
try: try:
click.confirm('Remove all existing cluster data from coordinators and initialize a new cluster', prompt_suffix='? ', abort=True) click.confirm('Remove all existing cluster data from coordinators and initialize a new cluster', prompt_suffix='? ', abort=True)
except Exception: except Exception:
@ -4186,7 +4319,11 @@ def task_init(confirm_flag):
'-q', '--quiet', '_quiet', envvar='PVC_QUIET', is_flag=True, default=False, '-q', '--quiet', '_quiet', envvar='PVC_QUIET', is_flag=True, default=False,
help='Suppress cluster connection information.' help='Suppress cluster connection information.'
) )
def cli(_cluster, _debug, _quiet): @click.option(
'-u', '--unsafe', '_unsafe', envvar='PVC_UNSAFE', is_flag=True, default=False,
help='Allow unsafe operations without confirmation/"--yes" argument.'
)
def cli(_cluster, _debug, _quiet, _unsafe):
""" """
Parallel Virtual Cluster CLI management tool Parallel Virtual Cluster CLI management tool
@ -4194,6 +4331,12 @@ def cli(_cluster, _debug, _quiet):
"PVC_CLUSTER": Set the cluster to access instead of using --cluster/-c "PVC_CLUSTER": Set the cluster to access instead of using --cluster/-c
"PVC_DEBUG": Enable additional debugging details instead of using --debug/-v
"PVC_QUIET": Suppress stderr connection output from client instead of using --quiet/-q
"PVC_UNSAFE": Suppress confirmation requirements instead of using --unsafe/-u or --yes/-y; USE WITH EXTREME CARE
If no PVC_CLUSTER/--cluster is specified, attempts first to load the "local" cluster, checking If no PVC_CLUSTER/--cluster is specified, attempts first to load the "local" cluster, checking
for an API configuration in "/etc/pvc/pvcapid.yaml". If this is also not found, abort. for an API configuration in "/etc/pvc/pvcapid.yaml". If this is also not found, abort.
""" """
@ -4203,6 +4346,7 @@ def cli(_cluster, _debug, _quiet):
config = get_config(store_data, _cluster) config = get_config(store_data, _cluster)
if not config.get('badcfg', None): if not config.get('badcfg', None):
config['debug'] = _debug config['debug'] = _debug
config['unsafe'] = _unsafe
if not _quiet: if not _quiet:
if config['api_scheme'] == 'https' and not config['verify_ssl']: if config['api_scheme'] == 'https' and not config['verify_ssl']:

8
debian/changelog vendored
View File

@ -1,3 +1,11 @@
pvc (0.9.15-0) unstable; urgency=high
* [CLI] Adds additional verification (--yes) to several VM management commands
* [CLI] Adds a method to override --yes/confirmation requirements via envvar (PVC_UNSAFE)
* [CLI] Adds description fields to PVC clusters in CLI
-- Joshua M. Boniface <joshua@boniface.me> Thu, 08 Apr 2021 13:37:47 -0400
pvc (0.9.14-0) unstable; urgency=high pvc (0.9.14-0) unstable; urgency=high
* Fixes bugs around cloned volume provisioning * Fixes bugs around cloned volume provisioning

View File

@ -18,6 +18,12 @@ To get started with PVC, please see the [About](https://parallelvirtualcluster.r
## Changelog ## Changelog
#### v0.9.15
* [CLI] Adds additional verification (--yes) to several VM management commands
* [CLI] Adds a method to override --yes/confirmation requirements via envvar (PVC_UNSAFE)
* [CLI] Adds description fields to PVC clusters in CLI
#### v0.9.14 #### v0.9.14
* Fixes bugs around cloned volume provisioning * Fixes bugs around cloned volume provisioning

View File

@ -53,7 +53,7 @@ import pvcnoded.CephInstance as CephInstance
import pvcnoded.MetadataAPIInstance as MetadataAPIInstance import pvcnoded.MetadataAPIInstance as MetadataAPIInstance
# Version string for startup output # Version string for startup output
version = '0.9.14' version = '0.9.15'
############################################################################### ###############################################################################
# PVCD - node daemon startup program # PVCD - node daemon startup program