pvc/cli-client/pvc.py

568 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
# pvcd.py - PVC client command-line interface
# Part of the Parallel Virtual Cluster (PVC) system
#
# Copyright (C) 2018 Joshua M. Boniface <joshua@boniface.me>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
###############################################################################
import socket
import click
import client_lib.common as common
import client_lib.node as node
import client_lib.vm as vm
myhostname = socket.gethostname()
zk_host = ''
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=120)
def cleanup(retcode, retmsg, zk_conn):
common.stopZKConnection(zk_conn)
if retcode == True:
if retmsg != '':
click.echo(retmsg)
exit(0)
else:
if retmsg != '':
click.echo(retmsg)
exit(1)
###############################################################################
# pvc node
###############################################################################
@click.group(name='node', short_help='Manage a PVC hypervisor node.', context_settings=CONTEXT_SETTINGS)
def cli_node():
"""
Manage the state of a node in the PVC cluster.
"""
pass
###############################################################################
# pvc node flush
###############################################################################
@click.command(name='flush', short_help='Take a node out of service.')
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for migrations to complete before returning.'
)
@click.argument(
'node', default=myhostname
)
def node_flush(node, wait):
"""
Take NODE out of active service and migrate away all VMs. If unspecified, defaults to this host.
"""
zk_conn = common.startZKConnection(zk_host)
retstate, retmsg = node.flush_node(node, wait)
cleanup(retstate, retmsg, zk_conn)
###############################################################################
# pvc node ready/unflush
###############################################################################
@click.command(name='ready', short_help='Restore node to service.')
@click.argument(
'node', default=myhostname
)
def node_ready(node):
"""
Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host.
"""
zk_conn = common.startZKConnection(zk_host)
retstate, retmsg = node.ready_node(zk_conn, node)
cleanup(retstate, retcode, zk_conn)
@click.command(name='unflush', short_help='Restore node to service.')
@click.argument(
'node', default=myhostname
)
def node_unflush(node):
"""
Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host.
"""
zk_conn = common.startZKConnection(zk_host)
retstate, retmsg = node.ready_node(zk_conn, node)
cleanup(retstate, retcode, zk_conn)
###############################################################################
# pvc node info
###############################################################################
@click.command(name='info', short_help='Show details of a node object.')
@click.argument(
'node', default=myhostname
)
@click.option(
'-l', '--long', 'long_output', is_flag=True, default=False,
help='Display more detailed information.'
)
def node_info(node, long_output):
"""
Show information about node NODE. If unspecified, defaults to this host.
"""
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = node.get_info(node, long_output)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc node list
###############################################################################
@click.command(name='list', short_help='List all node objects.')
@click.argument(
'limit', default=None, required=False
)
def node_list(limit):
"""
List all hypervisor nodes in the cluster; optionally only match names matching regex LIMIT.
"""
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = node.get_list(zk_conn, limit)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm
###############################################################################
@click.group(name='vm', short_help='Manage a PVC virtual machine.', context_settings=CONTEXT_SETTINGS)
def cli_vm():
"""
Manage the state of a virtual machine in the PVC cluster.
"""
pass
###############################################################################
# pvc vm define
###############################################################################
@click.command(name='define', short_help='Define a new virtual machine from a Libvirt XML file.')
@click.option(
'-t', '--hypervisor', 'target_hypervisor',
help='Home hypervisor for this domain; autodetect if unspecified.'
)
@click.option(
'-s', '--selector', 'selector', default='mem', show_default=True,
type=click.Choice(['mem','load','vcpus','vms']),
help='Method to determine optimal target hypervisor during autodetect.'
)
@click.argument(
'config', type=click.File()
)
def vm_define(config, target_hypervisor, selector):
"""
Define a new virtual machine from Libvirt XML configuration file CONFIG.
"""
# Open the XML file
config_data = config.read()
config.close()
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.define_vm(zk_conn, config_data, target_hypervisor, selector)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm modify
###############################################################################
@click.command(name='modify', short_help='Modify an existing VM configuration.')
@click.option(
'-e', '--editor', 'editor', is_flag=True,
help='Use local editor to modify existing config.'
)
@click.option(
'-r', '--restart', 'restart', is_flag=True,
help='Immediately restart VM to apply new config.'
)
@click.argument(
'domain'
)
@click.argument(
'config', type=click.File(), default=None, required=False
)
def vm_modify(domain, config, editor, restart):
"""
Modify existing virtual machine DOMAIN, either in-editor or with replacement CONFIG. DOMAIN may be a UUID or name.
"""
if editor == False and config == None:
cleanup(False, 'Either an XML config file or the "--editor" option must be specified.')
zk_conn = common.startZKConnection(zk_host)
if editor == True:
dom_uuid = vm.getDomainUUID(zk_conn, domain)
if dom_uuid == None:
cleanup(False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain))
# Grab the current config
current_vm_config = zk_conn.get('/domains/{}/xml'.format(dom_uuid))[0].decode('ascii')
# Write it to a tempfile
fd, path = tempfile.mkstemp()
fw = os.fdopen(fd, 'w')
fw.write(current_vm_config)
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_vm_config = fr.read()
fr.close()
# Delete the tempfile
os.unlink(path)
# Show a diff and confirm
diff = list(difflib.unified_diff(current_vm_config.split('\n'), new_vm_config.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 Zookeeper?', abort=True)
click.echo('Writing modified config of VM "{}".'.format(dom_name))
# We're operating in replace mode
else:
# Open the XML file
new_vm_config = config.read()
config.close()
click.echo('Replacing config of VM "{}".'.format(dom_name, config))
retcode, retmsg = vm.modify_vm(zk_conn, domain, restart)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm undefine
###############################################################################
@click.command(name='undefine', short_help='Undefine and stop a virtual machine.')
@click.argument(
'domain'
)
def vm_undefine(domain):
"""
Stop virtual machine DOMAIN and remove it from the cluster database. DOMAIN may be a UUID or name.
"""
# Ensure at least one search method is set
if domain == None:
click.echo("ERROR: You must specify either a name or UUID value.")
exit(1)
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.undefine_vm(zk_conn, domain)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm start
###############################################################################
@click.command(name='start', short_help='Start up a defined virtual machine.')
@click.argument(
'domain'
)
def vm_start(domain):
"""
Start virtual machine DOMAIN on its configured hypervisor. DOMAIN may be a UUID or name.
"""
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.start_vm(zk_conn, domain)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm restart
###############################################################################
@click.command(name='restart', short_help='Restart a running virtual machine.')
@click.argument(
'domain'
)
def vm_restart(domain):
"""
Restart running virtual machine DOMAIN. DOMAIN may be a UUID or name.
"""
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.restart_vm(zk_conn, domain)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm shutdown
###############################################################################
@click.command(name='shutdown', short_help='Gracefully shut down a running virtual machine.')
@click.argument(
'domain'
)
def vm_shutdown(domain):
"""
Gracefully shut down virtual machine DOMAIN. DOMAIN may be a UUID or name.
"""
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.shutdown_vm(zk_conn, domain)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm stop
###############################################################################
@click.command(name='stop', short_help='Forcibly halt a running virtual machine.')
@click.argument(
'domain'
)
def vm_stop(domain):
"""
Forcibly halt (destroy) running virtual machine DOMAIN. DOMAIN may be a UUID or name.
"""
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.stop_vm(zk_conn, domain)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm move
###############################################################################
@click.command(name='move', short_help='Permanently move a virtual machine to another node.')
@click.argument(
'domain'
)
@click.option(
'-t', '--hypervisor', 'target_hypervisor', default=None,
help='Target hypervisor to migrate to; autodetect if unspecified.'
)
@click.option(
'-s', '--selector', 'selector', default='mem', show_default=True,
type=click.Choice(['mem','load','vcpus','vms']),
help='Method to determine optimal target hypervisor during autodetect.'
)
def vm_move(domain, target_hypervisor, selector):
"""
Permanently move virtual machine DOMAIN, via live migration if running and possible, to another hypervisor node. DOMAIN may be a UUID or name.
"""
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.move_vm(zk_conn, domain, target_hypervisor, selector)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm migrate
###############################################################################
@click.command(name='migrate', short_help='Temporarily migrate a virtual machine to another node.')
@click.argument(
'domain'
)
@click.option(
'-t', '--hypervisor', 'target_hypervisor', default=None,
help='Target hypervisor to migrate to; autodetect if unspecified.'
)
@click.option(
'-s', '--selector', 'selector', default='mem', show_default=True,
type=click.Choice(['mem','load','vcpus','vms']),
help='Method to determine optimal target hypervisor during autodetect.'
)
@click.option(
'-f', '--force', 'force_migrate', is_flag=True, default=False,
help='Force migrate an already migrated VM.'
)
def vm_migrate(domain, target_hypervisor, selector, force_migrate):
"""
Temporarily migrate running virtual machine DOMAIN, via live migration if possible, to another hypervisor node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node.
"""
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.migrate_vm(zk_conn, domain, target_hypervisor, selector, force_migrate)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm unmigrate
###############################################################################
@click.command(name='unmigrate', short_help='Restore a migrated virtual machine to its original node.')
@click.argument(
'domain'
)
def vm_unmigrate(domain):
"""
Restore previously migrated virtual machine DOMAIN, via live migration if possible, to its original hypervisor node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node.
"""
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.unmigrate_vm(zk_conn, domain)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm info
###############################################################################
@click.command(name='info', short_help='Show details of a VM object.')
@click.argument(
'domain'
)
@click.option(
'-l', '--long', 'long_output', is_flag=True, default=False,
help='Display more detailed information.'
)
def vm_info(domain, long_output):
"""
Show information about virtual machine DOMAIN. DOMAIN may be a UUID or name.
"""
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.get_info(zk_conn, domain, long_output)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc vm list
###############################################################################
@click.command(name='list', short_help='List all VM objects.')
@click.argument(
'limit', default=None, required=False
)
@click.option(
'-t', '--hypervisor', 'hypervisor', default=None,
help='Limit list to this hypervisor.'
)
def vm_list(hypervisor, limit):
"""
List all virtual machines in the cluster; optionally only match names matching regex LIMIT.
"""
zk_conn = common.startZKConnection(zk_host)
retcode, retmsg = vm.get_list(zk_conn, hypervisor, limit)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc init
###############################################################################
@click.command(name='init', short_help='Initialize a new cluster.')
@click.option('--yes', is_flag=True,
expose_value=False,
prompt='DANGER: This command will destroy any existing cluster data. Do you want to continue?')
def init_cluster():
"""
Perform initialization of Zookeeper to act as a PVC cluster.
DANGER: This command will overwrite any existing cluster data and provision a new cluster at the specified Zookeeper connection string. Do not run this against a cluster unless you are sure this is what you want.
"""
click.echo('Initializing a new cluster with Zookeeper address "{}".'.format(zk_host))
# Open a Zookeeper connection
zk_conn = common.startZKConnection(zk_host)
# Destroy the existing data
try:
zk_conn.delete('/domains', recursive=True)
zk_conn.delete('nodes', recursive=True)
except:
pass
# Create the root keys
transaction = zk_conn.transaction()
transaction.create('/domains', ''.encode('ascii'))
transaction.create('/nodes', ''.encode('ascii'))
transaction.create('/networks', ''.encode('ascii'))
transaction.commit()
# Close the Zookeeper connection
common.stopZKConnection(zk_conn)
click.echo('Successfully initialized new cluster. Any running PVC daemons will need to be restarted.')
###############################################################################
# pvc
###############################################################################
@click.group(context_settings=CONTEXT_SETTINGS)
@click.option(
'-z', '--zookeeper', '_zk_host', envvar='PVC_ZOOKEEPER', default='{}:2181'.format(myhostname), show_default=True,
help='Zookeeper connection string.'
)
def cli(_zk_host):
"""
Parallel Virtual Cluster CLI management tool
Environment variables:
"PVC_ZOOKEEPER": Set the cluster Zookeeper address instead of using "--zookeeper".
"""
global zk_host
zk_host = _zk_host
#
# Click command tree
#
cli_node.add_command(node_flush)
cli_node.add_command(node_ready)
cli_node.add_command(node_unflush)
cli_node.add_command(node_info)
cli_node.add_command(node_list)
cli_vm.add_command(vm_define)
cli_vm.add_command(vm_modify)
cli_vm.add_command(vm_undefine)
cli_vm.add_command(vm_start)
cli_vm.add_command(vm_restart)
cli_vm.add_command(vm_shutdown)
cli_vm.add_command(vm_stop)
cli_vm.add_command(vm_move)
cli_vm.add_command(vm_migrate)
cli_vm.add_command(vm_unmigrate)
cli_vm.add_command(vm_info)
cli_vm.add_command(vm_list)
cli.add_command(cli_node)
cli.add_command(cli_vm)
cli.add_command(init_cluster)
#
# Main entry point
#
def main():
return cli(obj={})
if __name__ == '__main__':
main()