Compare commits

...

11 Commits

Author SHA1 Message Date
fe15bdb854 Bump version to 0.9.16 2021-05-10 01:13:21 -04:00
b851a6209c Catch all other exceptions in subprocess run
Found a rare glitch where the subprocess pipes would not engage, causing
a daemon crash. Catch these exceptions with a retcode of 255 instead of
bailing out.

Closes #124
2021-05-10 01:07:25 -04:00
5ceb57e540 Handle emptying corrupted console log files
Libvirt will someones write junk out to console log files, which breaks
the log parser deque with a UnicodeDecodeError.

If this happens, clear the log and re-open the deque again for newer
updates.

Closes #123
2021-05-10 01:03:04 -04:00
62c84664fc Handle restart confirmation for VMs more nicely
For the "vm modify", revamp the way confirmations are presented. Do the
edits/load, show changes, verify XML, then prompt to write and the
restart. The previous order didn't make much sense.

For any of these `--restart` triggered VM modifications, also alter how
the confirmation works. If the user declines the restart, do not abort;
instead, just set restart=False and continue with the modification.
2021-04-13 10:35:26 -04:00
66f1ac35ab Skip an empty local cluster in cluster list 2021-04-13 10:01:49 -04:00
529f99841d Improve formatting of some help messages 2021-04-12 15:55:59 -04:00
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
8 changed files with 272 additions and 78 deletions

View File

@ -20,6 +20,20 @@ To get started with PVC, please see the [About](https://parallelvirtualcluster.r
## Changelog
#### v0.9.16
* Improves some CLI help messages
* Skips empty local cluster in CLI
* Adjusts how confirmations happen during VM modify restarts
* Fixes bug around corrupted VM log files
* Fixes bug around subprocess pipe exceptions
#### 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
* Fixes bugs around cloned volume provisioning

View File

@ -26,7 +26,7 @@ import pvcapid.flaskapi as pvc_api
##########################################################
# Version string for startup output
version = '0.9.14'
version = '0.9.16'
if pvc_api.config['ssl_enabled']:
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 = ''
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']
else:
api_key = 'N/A'
return host, port, scheme, api_key
return cfgfile, host, port, scheme, api_key
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
cfgfile = cluster_details.get('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:
return {'badcfg': True}
# Handle an all-wildcard address
@ -92,6 +92,7 @@ def get_config(store_data, cluster=None):
host = '127.0.0.1'
else:
# This is a static configuration, get the raw details
description = cluster_details['description']
host = cluster_details['host']
port = cluster_details['port']
scheme = cluster_details['scheme']
@ -100,6 +101,7 @@ def get_config(store_data, cluster=None):
config = dict()
config['debug'] = False
config['cluster'] = cluster
config['desctription'] = description
config['api_host'] = '{}:{}'.format(host, port)
config['api_scheme'] = scheme
config['api_key'] = api_key
@ -175,6 +177,10 @@ def cli_cluster():
# pvc cluster add
###############################################################################
@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(
'-a', '--address', 'address', required=True,
help='The IP address or hostname of the cluster API client.'
@ -194,7 +200,7 @@ def cli_cluster():
@click.argument(
'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.
"""
@ -207,6 +213,7 @@ def cluster_add(address, port, ssl, name, api_key):
existing_config = get_store(store_path)
# Append our new entry to the end
existing_config[name] = {
'description': description,
'host': address,
'port': port,
'scheme': scheme,
@ -252,10 +259,11 @@ def cluster_list():
clusters = get_store(store_path)
# Find the lengths of each column
name_length = 5
description_length = 12
address_length = 10
port_length = 5
scheme_length = 7
api_key_length = 8
api_key_length = 32
for cluster in clusters:
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
cfgfile = cluster_details.get('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:
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:
description = cluster_details.get('description', '')
address = cluster_details.get('host', 'N/A')
port = cluster_details.get('port', 'N/A')
scheme = cluster_details.get('scheme', 'N/A')
@ -278,6 +287,9 @@ def cluster_list():
if _name_length > name_length:
name_length = _name_length
_address_length = len(address) + 1
_description_length = len(description) + 1
if _description_length > description_length:
description_length = _description_length
if _address_length > address_length:
address_length = _address_length
_port_length = len(str(port)) + 1
@ -294,11 +306,13 @@ def cluster_list():
click.echo("Available clusters:")
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(),
end_bold=ansiprint.end(),
name="Name",
name_length=name_length,
description="Description",
description_length=description_length,
address="Address",
address_length=address_length,
port="Port",
@ -315,14 +329,12 @@ def cluster_list():
if cluster_details.get('cfgfile', None):
# This is a reference to an API configuration; grab the details from its listen address
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:
address = 'N/A'
port = 'N/A'
scheme = 'N/A'
api_key = 'N/A'
continue
else:
address = cluster_details.get('host', 'N/A')
description = cluster_details.get('description', 'N/A')
port = cluster_details.get('port', 'N/A')
scheme = cluster_details.get('scheme', 'N/A')
api_key = cluster_details.get('api_key', 'N/A')
@ -330,11 +342,13 @@ def cluster_list():
api_key = 'N/A'
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='',
end_bold='',
name=cluster,
name_length=name_length,
description=description,
description_length=description_length,
address=address,
address_length=address_length,
port=port,
@ -694,13 +708,18 @@ def vm_meta(domain, node_limit, node_selector, node_autostart, migration_method,
'-r', '--restart', 'restart', is_flag=True,
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(
'domain'
)
@click.argument(
'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.
"""
@ -727,38 +746,28 @@ def vm_modify(domain, cfgfile, editor, restart):
else:
new_vm_cfgfile = new_vm_cfgfile.strip()
# Show a diff and confirm
click.echo('Pending modifications:')
click.echo('')
diff = list(difflib.unified_diff(current_vm_cfgfile.split('\n'), new_vm_cfgfile.split('\n'), fromfile='current', tofile='modified', fromfiledate='', tofiledate='', n=3, lineterm=''))
for line in diff:
if re.match(r'^\+', line) is not None:
click.echo(colorama.Fore.GREEN + line + colorama.Fore.RESET)
elif re.match(r'^\-', line) is not None:
click.echo(colorama.Fore.RED + line + colorama.Fore.RESET)
elif re.match(r'^\^', line) is not None:
click.echo(colorama.Fore.BLUE + line + colorama.Fore.RESET)
else:
click.echo(line)
click.echo('')
click.confirm('Write modifications to cluster?', abort=True)
if restart:
click.echo('Writing modified configuration of VM "{}" and restarting.'.format(dom_name))
else:
click.echo('Writing modified configuration of VM "{}".'.format(dom_name))
# We're operating in replace mode
else:
# Open the XML file
new_vm_cfgfile = cfgfile.read()
cfgfile.close()
if restart:
click.echo('Replacing configuration of VM "{}" with file "{}" and restarting.'.format(dom_name, cfgfile.name))
click.echo('Replacing configuration of VM "{}" with file "{}".'.format(dom_name, cfgfile.name))
# Show a diff and confirm
click.echo('Pending modifications:')
click.echo('')
diff = list(difflib.unified_diff(current_vm_cfgfile.split('\n'), new_vm_cfgfile.split('\n'), fromfile='current', tofile='modified', fromfiledate='', tofiledate='', n=3, lineterm=''))
for line in diff:
if re.match(r'^\+', line) is not None:
click.echo(colorama.Fore.GREEN + line + colorama.Fore.RESET)
elif re.match(r'^\-', line) is not None:
click.echo(colorama.Fore.RED + line + colorama.Fore.RESET)
elif re.match(r'^\^', line) is not None:
click.echo(colorama.Fore.BLUE + line + colorama.Fore.RESET)
else:
click.echo('Replacing configuration of VM "{}" with file "{}".'.format(dom_name, cfgfile.name))
click.echo(line)
click.echo('')
# Verify our XML is sensible
try:
@ -767,7 +776,17 @@ def vm_modify(domain, cfgfile, editor, restart):
except Exception as e:
cleanup(False, 'Error: XML is malformed or invalid: {}'.format(e))
click.confirm('Write modifications to cluster?', abort=True)
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
restart = False
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)
@ -788,7 +807,7 @@ def vm_undefine(domain, confirm_flag):
"""
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:
click.confirm('Undefine VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
@ -815,7 +834,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.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Undefine VM {} and remove all disks'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
@ -853,11 +872,21 @@ def vm_start(domain):
'-w', '--wait', 'wait', is_flag=True, default=False,
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
def vm_restart(domain, wait):
def vm_restart(domain, wait, confirm_flag):
"""
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)
cleanup(retcode, retmsg)
@ -874,11 +903,21 @@ def vm_restart(domain, wait):
'-w', '--wait', 'wait', is_flag=True, default=False,
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
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.
"""
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)
cleanup(retcode, retmsg)
@ -891,11 +930,21 @@ def vm_shutdown(domain, wait):
@click.argument(
'domain'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the stop'
)
@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.
"""
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')
cleanup(retcode, retmsg)
@ -1078,26 +1127,38 @@ def vm_vcpu_get(domain, raw):
'-r', '--restart', 'restart', is_flag=True, default=False,
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
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.
By default, the topology of the vCPus is 1 socket, VCPUS cores per socket, 1 thread per core.
"""
if topology is not None:
try:
sockets, cores, threads = topology.split(',')
if sockets * cores * threads != vcpus:
raise
except Exception:
cleanup(False, "The topology specified is not valid.")
cleanup(False, "The specified topology is not valid.")
topology = (sockets, cores, threads)
else:
topology = (1, vcpus, 1)
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
restart = False
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)
@ -1149,13 +1210,25 @@ def vm_memory_get(domain, raw):
'-r', '--restart', 'restart', is_flag=True, default=False,
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
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.
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
restart = False
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)
@ -1222,13 +1295,25 @@ def vm_network_get(domain, raw):
'-r', '--restart', 'restart', is_flag=True, default=False,
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
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.
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
restart = False
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)
@ -1246,13 +1331,25 @@ def vm_network_add(domain, vni, macaddr, model, restart):
'-r', '--restart', 'restart', is_flag=True, default=False,
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
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.
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
restart = False
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)
@ -1325,15 +1422,27 @@ def vm_volume_get(domain, raw):
'-r', '--restart', 'restart', is_flag=True, default=False,
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
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.
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 {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
restart = False
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)
@ -1351,13 +1460,25 @@ def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart):
'-r', '--restart', 'restart', is_flag=True, default=False,
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
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.
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
restart = False
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)
@ -1642,6 +1763,7 @@ def net_modify(vni, description, domain, name_servers, ip6_network, ip6_gateway,
Modify details of virtual network VNI. All fields optional; only specified fields will be updated.
Example:
pvc network modify 1001 --gateway 10.1.1.1 --dhcp
"""
@ -1669,7 +1791,7 @@ def net_remove(net, confirm_flag):
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.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove network {}'.format(net), prompt_suffix='? ', abort=True)
except Exception:
@ -1778,7 +1900,7 @@ def net_dhcp_remove(net, macaddr, confirm_flag):
"""
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:
click.confirm('Remove DHCP lease for {} in network {}'.format(macaddr, net), prompt_suffix='? ', abort=True)
except Exception:
@ -1897,7 +2019,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.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove ACL {} in network {}'.format(rule, net), prompt_suffix='? ', abort=True)
except Exception:
@ -2096,7 +2218,7 @@ def ceph_osd_add(node, device, weight, confirm_flag):
"""
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:
click.confirm('Destroy all data and create a new OSD on {}:{}'.format(node, device), prompt_suffix='? ', abort=True)
except Exception:
@ -2125,7 +2247,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.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove OSD {}'.format(osdid), prompt_suffix='? ', abort=True)
except Exception:
@ -2256,14 +2378,16 @@ def ceph_pool():
default='copies=3,mincopies=2', show_default=True, required=False,
help="""
The replication configuration, specifying both a "copies" and "mincopies" value, separated by a
comma, e.g. "copies=3,mincopies=2". The "copies" value specifies the total number of replicas and should not exceed the total number of nodes; the "mincopies" value specifies the minimum number of available copies to allow writes. For additional details please see the Cluster Architecture documentation.
comma, e.g. "copies=3,mincopies=2". The "copies" value specifies the total number of replicas
and should not exceed the total number of nodes; the "mincopies" value specifies the minimum
number of available copies to allow writes. For additional details please see the Cluster
Architecture documentation.
"""
)
@cluster_req
def ceph_pool_add(name, pgs, replcfg):
"""
Add a new Ceph RBD pool with name NAME and PGS placement groups.
"""
retcode, retmsg = pvc_ceph.ceph_pool_add(config, name, pgs, replcfg)
@ -2289,7 +2413,7 @@ def ceph_pool_remove(name, confirm_flag):
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:
click.confirm('Remove RBD pool {}'.format(name), prompt_suffix='? ', abort=True)
except Exception:
@ -2410,7 +2534,7 @@ def ceph_volume_remove(pool, name, confirm_flag):
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:
click.confirm('Remove volume {}/{}'.format(pool, name), prompt_suffix='? ', abort=True)
except Exception:
@ -2594,7 +2718,7 @@ def ceph_volume_snapshot_remove(pool, volume, name, confirm_flag):
DANGER: This will completely remove the snapshot.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove snapshot {} for volume {}/{}'.format(name, pool, volume), prompt_suffix='? ', abort=True)
except Exception:
@ -2870,7 +2994,7 @@ def provisioner_template_system_remove(name, confirm_flag):
"""
Remove system template NAME from the PVC cluster provisioner.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove system template {}'.format(name), prompt_suffix='? ', abort=True)
except Exception:
@ -2977,7 +3101,7 @@ def provisioner_template_network_remove(name, confirm_flag):
"""
Remove network template MAME from the PVC cluster provisioner.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove network template {}'.format(name), prompt_suffix='? ', abort=True)
except Exception:
@ -3041,7 +3165,7 @@ def provisioner_template_network_vni_remove(name, vni, confirm_flag):
"""
Remove network VNI from network template NAME.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove VNI {} from network template {}'.format(vni, name), prompt_suffix='? ', abort=True)
except Exception:
@ -3116,7 +3240,7 @@ def provisioner_template_storage_remove(name, confirm_flag):
"""
Remove storage template NAME from the PVC cluster provisioner.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove storage template {}'.format(name), prompt_suffix='? ', abort=True)
except Exception:
@ -3235,7 +3359,7 @@ def provisioner_template_storage_disk_remove(name, disk, confirm_flag):
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:
click.confirm('Remove disk {} from storage template {}'.format(disk, name), prompt_suffix='? ', abort=True)
except Exception:
@ -3425,7 +3549,7 @@ def provisioner_userdata_remove(name, confirm_flag):
"""
Remove userdata document NAME from the PVC cluster provisioner.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove userdata document {}'.format(name), prompt_suffix='? ', abort=True)
except Exception:
@ -3604,7 +3728,7 @@ def provisioner_script_remove(name, confirm_flag):
"""
Remove script NAME from the PVC cluster provisioner.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove provisioning script {}'.format(name), prompt_suffix='? ', abort=True)
except Exception:
@ -3700,7 +3824,7 @@ def provisioner_ova_remove(name, confirm_flag):
"""
Remove OVA image NAME from the PVC cluster provisioner.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove OVA image {}'.format(name), prompt_suffix='? ', abort=True)
except Exception:
@ -3885,7 +4009,7 @@ def provisioner_profile_remove(name, confirm_flag):
"""
Remove profile NAME from the PVC cluster provisioner.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove profile {}'.format(name), prompt_suffix='? ', abort=True)
except Exception:
@ -4065,9 +4189,13 @@ def status_cluster(oformat):
Show basic information and health for the active PVC cluster.
Output formats:
plain: Full text, full colour output for human-readability.
short: Health-only, full colour output for human-readability.
json: Compact JSON representation for machine parsing.
json-pretty: Pretty-printed JSON representation for machine parsing or human-readability.
"""
@ -4131,7 +4259,7 @@ def task_restore(filename, confirm_flag):
Restore the JSON backup data from a file to the cluster.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Replace all existing cluster data from coordinators with backup file "{}"'.format(filename.name), prompt_suffix='? ', abort=True)
except Exception:
@ -4157,7 +4285,7 @@ def task_init(confirm_flag):
Perform initialization of a new PVC cluster.
"""
if not confirm_flag:
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove all existing cluster data from coordinators and initialize a new cluster', prompt_suffix='? ', abort=True)
except Exception:
@ -4186,7 +4314,11 @@ def task_init(confirm_flag):
'-q', '--quiet', '_quiet', envvar='PVC_QUIET', is_flag=True, default=False,
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
@ -4194,6 +4326,12 @@ def cli(_cluster, _debug, _quiet):
"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
for an API configuration in "/etc/pvc/pvcapid.yaml". If this is also not found, abort.
"""
@ -4203,6 +4341,7 @@ def cli(_cluster, _debug, _quiet):
config = get_config(store_data, _cluster)
if not config.get('badcfg', None):
config['debug'] = _debug
config['unsafe'] = _unsafe
if not _quiet:
if config['api_scheme'] == 'https' and not config['verify_ssl']:

18
debian/changelog vendored
View File

@ -1,3 +1,21 @@
pvc (0.9.16-0) unstable; urgency=high
* Improves some CLI help messages
* Skips empty local cluster in CLI
* Adjusts how confirmations happen during VM modify restarts
* Fixes bug around corrupted VM log files
* Fixes bug around subprocess pipe exceptions
-- Joshua M. Boniface <joshua@boniface.me> Mon, 10 May 2021 01:13:21 -0400
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
* Fixes bugs around cloned volume provisioning

View File

@ -18,6 +18,20 @@ To get started with PVC, please see the [About](https://parallelvirtualcluster.r
## Changelog
#### v0.9.16
* Improves some CLI help messages
* Skips empty local cluster in CLI
* Adjusts how confirmations happen during VM modify restarts
* Fixes bug around corrupted VM log files
* Fixes bug around subprocess pipe exceptions
#### 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
* Fixes bugs around cloned volume provisioning

View File

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

View File

@ -44,7 +44,14 @@ class VMConsoleWatcherInstance(object):
open(self.logfile, 'a').close()
os.chmod(self.logfile, 0o600)
self.logdeque = deque(open(self.logfile), self.console_log_lines)
try:
self.logdeque = deque(open(self.logfile), self.console_log_lines)
except UnicodeDecodeError:
# There is corruption in the log file; overwrite it
self.logger.out('Failed to decode console log file; clearing existing file', state='w', prefix='Domain {}'.format(self.domuuid))
with open(self.logfile, 'w') as lfh:
lfh.write('\n')
self.logdeque = deque(open(self.logfile), self.console_log_lines)
self.stamp = None
self.cached_stamp = None

View File

@ -91,6 +91,8 @@ def run_os_command(command_string, background=False, environment=None, timeout=N
retcode = command_output.returncode
except subprocess.TimeoutExpired:
retcode = 128
except Exception:
retcode = 255
try:
stdout = command_output.stdout.decode('ascii')