Compare commits

...

10 Commits

Author SHA1 Message Date
0c75a127b2 Bump version to 0.9.18 2021-05-23 17:23:10 -04:00
f46c2e7f6a Implement VM rename functionality
Closes #125
2021-05-23 17:21:19 -04:00
9de14c46fb Bump version to 0.9.17 2021-05-19 17:06:29 -04:00
1b8b101b64 Fix bugs in log follow command 2021-05-19 16:22:48 -04:00
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
13 changed files with 323 additions and 60 deletions

View File

@ -20,6 +20,22 @@ To get started with PVC, please see the [About](https://parallelvirtualcluster.r
## Changelog
#### v0.9.18
* Adds VM rename functionality to API and CLI client
#### v0.9.17
* [CLI] Fixes bugs in log follow output
#### 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

View File

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

View File

@ -1804,6 +1804,45 @@ class API_VM_Console(Resource):
api.add_resource(API_VM_Console, '/vm/<vm>/console')
# /vm/<vm>/rename
class API_VM_Rename(Resource):
@RequestParser([
{'name': 'new_name'}
])
@Authenticator
def post(self, vm, reqargs):
"""
Rename VM {vm}, and all connected disk volumes which include this name, to {new_name}
---
tags:
- vm
parameters:
- in: query
name: new_name
type: string
required: true
description: The new name of the VM
responses:
200:
description: OK
schema:
type: object
id: Message
400:
description: Bad request
schema:
type: object
id: Message
"""
return api_helper.vm_rename(
vm,
reqargs.get('new_name', None)
)
api.add_resource(API_VM_Rename, '/vm/<vm>/rename')
##########################################################
# Client API - Network
##########################################################

View File

@ -601,6 +601,37 @@ def vm_modify(name, restart, xml):
return output, retcode
def vm_rename(name, new_name):
"""
Rename a VM in the PVC cluster.
"""
if new_name is None:
output = {
'message': 'A new VM name must be specified'
}
return 400, output
zk_conn = pvc_common.startZKConnection(config['coordinators'])
if pvc_vm.searchClusterByName(zk_conn, new_name) is not None:
output = {
'message': 'A VM named \'{}\' is already present in the cluster'.format(new_name)
}
return 400, output
retflag, retdata = pvc_vm.rename_vm(zk_conn, name, new_name)
pvc_common.stopZKConnection(zk_conn)
if retflag:
retcode = 200
else:
retcode = 400
output = {
'message': retdata.replace('\"', '\'')
}
return output, retcode
def vm_undefine(name):
"""
Undefine a VM from the PVC cluster.

View File

@ -130,6 +130,27 @@ def vm_modify(config, vm, xml, restart):
return retstatus, response.json().get('message', '')
def vm_rename(config, vm, new_name):
"""
Rename VM to new name
API endpoint: POST /vm/{vm}/rename
API arguments: new_name={new_name}
API schema: {"message":"{data}"}
"""
params = {
'new_name': new_name
}
response = call_api(config, 'post', '/vm/{vm}/rename'.format(vm=vm), params=params)
if response.status_code == 200:
retstatus = True
else:
retstatus = False
return retstatus, response.json().get('message', '')
def vm_metadata(config, vm, node_limit, node_selector, node_autostart, migration_method, provisioner_profile):
"""
Modify PVC metadata of a VM
@ -1002,8 +1023,9 @@ def follow_console_log(config, vm, lines=10):
API arguments: lines={lines}
API schema: {"name":"{vmname}","data":"{console_log}"}
"""
# We always grab 500 to match the follow call, but only _show_ `lines` number
params = {
'lines': lines
'lines': 500
}
response = call_api(config, 'get', '/vm/{vm}/console'.format(vm=vm), params=params)
@ -1012,7 +1034,7 @@ def follow_console_log(config, vm, lines=10):
# Shrink the log buffer to length lines
console_log = response.json()['data']
shrunk_log = console_log.split('\n')[-lines:]
shrunk_log = console_log.split('\n')[-int(lines):]
loglines = '\n'.join(shrunk_log)
# Print the initial data and begin following

View File

@ -331,11 +331,7 @@ def cluster_list():
if os.path.isfile(cfgfile):
description, address, port, scheme, api_key = read_from_yaml(cfgfile)
else:
description = 'N/A'
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')
@ -735,12 +731,6 @@ def vm_modify(domain, cfgfile, editor, restart, confirm_flag):
if not retcode and not vm_information.get('name', None):
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')
if editor is True:
@ -756,38 +746,28 @@ def vm_modify(domain, cfgfile, editor, restart, confirm_flag):
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:
@ -796,12 +776,50 @@ def vm_modify(domain, cfgfile, editor, restart, confirm_flag):
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)
###############################################################################
# pvc vm rename
###############################################################################
@click.command(name='rename', short_help='Rename a virtual machine.')
@click.argument(
'domain'
)
@click.argument(
'new_name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the rename'
)
@cluster_req
def vm_rename(domain, new_name, confirm_flag):
"""
Rename virtual machine DOMAIN, and all its connected disk volumes, to NEW_NAME. DOMAIN may be a UUID or name.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Rename VM {} to {}'.format(domain, new_name), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
retcode, retmsg = pvc_vm.vm_rename(config, domain, new_name)
cleanup(retcode, retmsg)
###############################################################################
# pvc vm undefine
###############################################################################
@ -1151,23 +1169,23 @@ def vm_vcpu_set(domain, vcpus, topology, restart, confirm_flag):
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:
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."
@ -1234,9 +1252,9 @@ def vm_memory_set(domain, memory, restart, confirm_flag):
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
restart = False
retcode, retmsg = pvc_vm.vm_memory_set(config, domain, memory, restart)
if retcode and not restart:
@ -1319,9 +1337,9 @@ def vm_network_add(domain, vni, macaddr, model, restart, confirm_flag):
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
restart = False
retcode, retmsg = pvc_vm.vm_networks_add(config, domain, vni, macaddr, model, restart)
if retcode and not restart:
@ -1355,9 +1373,9 @@ def vm_network_remove(domain, vni, restart, confirm_flag):
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
restart = False
retcode, retmsg = pvc_vm.vm_networks_remove(config, domain, vni, restart)
if retcode and not restart:
@ -1448,9 +1466,9 @@ def vm_volume_add(domain, volume, disk_id, bus, disk_type, restart, confirm_flag
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
restart = False
retcode, retmsg = pvc_vm.vm_volumes_add(config, domain, volume, disk_id, bus, disk_type, restart)
if retcode and not restart:
@ -1484,9 +1502,9 @@ def vm_volume_remove(domain, vni, restart, confirm_flag):
"""
if restart and not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {} after applying change'.format(domain), prompt_suffix='? ', abort=True)
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
restart = False
retcode, retmsg = pvc_vm.vm_volumes_remove(config, domain, vni, restart)
if retcode and not restart:
@ -1775,6 +1793,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
"""
@ -2389,14 +2408,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)
@ -4198,9 +4219,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.
"""
@ -4400,6 +4425,7 @@ vm_volume.add_command(vm_volume_remove)
cli_vm.add_command(vm_define)
cli_vm.add_command(vm_meta)
cli_vm.add_command(vm_modify)
cli_vm.add_command(vm_rename)
cli_vm.add_command(vm_undefine)
cli_vm.add_command(vm_remove)
cli_vm.add_command(vm_dump)

View File

@ -22,6 +22,7 @@
import time
import re
import lxml.objectify
import lxml.etree
import daemon_lib.zkhandler as zkhandler
import daemon_lib.common as common
@ -299,6 +300,55 @@ def dump_vm(zk_conn, domain):
return True, vm_xml
def rename_vm(zk_conn, domain, new_domain):
dom_uuid = getDomainUUID(zk_conn, domain)
if not dom_uuid:
return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain)
# Verify that the VM is in a stopped state; renaming is not supported otherwise
state = zkhandler.readdata(zk_conn, '/domains/{}/state'.format(dom_uuid))
if state != 'stop':
return False, 'ERROR: VM "{}" is not in stopped state; VMs cannot be renamed while running.'.format(domain)
# Parse and valiate the XML
vm_config = common.getDomainXML(zk_conn, dom_uuid)
# Obtain the RBD disk list using the common functions
ddisks = common.getDomainDisks(vm_config, {})
pool_list = []
rbd_list = []
for disk in ddisks:
if disk['type'] == 'rbd':
pool_list.append(disk['name'].split('/')[0])
rbd_list.append(disk['name'].split('/')[1])
# Rename each volume in turn
for idx, rbd in enumerate(rbd_list):
rbd_new = re.sub(r"{}".format(domain), new_domain, rbd)
# Skip renaming if nothing changed
if rbd_new == rbd:
continue
ceph.rename_volume(zk_conn, pool_list[idx], rbd, rbd_new)
# Replace the name in the config
vm_config_new = lxml.etree.tostring(vm_config, encoding='ascii', method='xml').decode().replace(domain, new_domain)
# Get VM information
_b, dom_info = get_info(zk_conn, dom_uuid)
# Undefine the old VM
undefine_vm(zk_conn, dom_uuid)
# Define the new VM
define_vm(zk_conn, vm_config_new, dom_info['node'], dom_info['node_limit'], dom_info['node_selector'], dom_info['node_autostart'], migration_method=dom_info['migration_method'], profile=dom_info['profile'], initial_state='stop')
# If the VM is migrated, store that
if dom_info['migrated'] != 'no':
zkhandler.writedata(zk_conn, {'/domains/{}/lastnode'.format(dom_uuid): dom_info['last_node']})
return True, 'Successfully renamed VM "{}" to "{}".'.format(domain, new_domain)
def undefine_vm(zk_conn, domain):
# Validate that VM exists in cluster
dom_uuid = getDomainUUID(zk_conn, domain)

22
debian/changelog vendored
View File

@ -1,3 +1,25 @@
pvc (0.9.18-0) unstable; urgency=high
* Adds VM rename functionality to API and CLI client
-- Joshua M. Boniface <joshua@boniface.me> Sun, 23 May 2021 17:23:10 -0400
pvc (0.9.17-0) unstable; urgency=high
* [CLI] Fixes bugs in log follow output
-- Joshua M. Boniface <joshua@boniface.me> Wed, 19 May 2021 17:06:29 -0400
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

View File

@ -18,6 +18,22 @@ To get started with PVC, please see the [About](https://parallelvirtualcluster.r
## Changelog
#### v0.9.18
* Adds VM rename functionality to API and CLI client
#### v0.9.17
* [CLI] Fixes bugs in log follow output
#### 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

View File

@ -6035,6 +6035,38 @@
]
}
},
"/api/v1/vm/{vm}/rename": {
"post": {
"description": "",
"parameters": [
{
"description": "The new name of the VM",
"in": "query",
"name": "new_name",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Message"
}
},
"400": {
"description": "Bad request",
"schema": {
"$ref": "#/definitions/Message"
}
}
},
"summary": "Rename VM {vm}, and all connected disk volumes which include this name, to {new_name}",
"tags": [
"vm"
]
}
},
"/api/v1/vm/{vm}/state": {
"get": {
"description": "",

View File

@ -53,7 +53,7 @@ import pvcnoded.CephInstance as CephInstance
import pvcnoded.MetadataAPIInstance as MetadataAPIInstance
# Version string for startup output
version = '0.9.15'
version = '0.9.18'
###############################################################################
# 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')