pvc/client-cli/pvc.py

4788 lines
161 KiB
Python
Raw Normal View History

2018-05-31 20:26:44 -04:00
#!/usr/bin/env python3
2019-12-25 13:35:31 -05:00
# pvc.py - PVC client command-line interface
# Part of the Parallel Virtual Cluster (PVC) system
#
2021-03-25 17:01:55 -04:00
# Copyright (C) 2018-2021 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, version 3.
#
# 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 os
import difflib
import re
import time
import colorama
import yaml
import json
2019-12-27 09:52:16 -05:00
import lxml.etree as etree
2019-12-25 13:35:31 -05:00
from distutils.util import strtobool
from functools import wraps
2019-12-25 14:10:23 -05:00
import cli_lib.ansiprint as ansiprint
import cli_lib.cluster as pvc_cluster
import cli_lib.node as pvc_node
import cli_lib.vm as pvc_vm
import cli_lib.network as pvc_network
import cli_lib.ceph as pvc_ceph
2020-01-02 11:18:46 -05:00
import cli_lib.provisioner as pvc_provisioner
2018-06-05 01:39:59 -04:00
myhostname = socket.gethostname().split('.')[0]
zk_host = ''
default_store_data = {
'cfgfile': '/etc/pvc/pvcapid.yaml'
}
#
# Data store handling functions
#
def read_from_yaml(cfgfile):
with open(cfgfile, 'r') as fh:
api_config = yaml.load(fh, Loader=yaml.BaseLoader)
host = api_config['pvc']['api']['listen_address']
port = api_config['pvc']['api']['listen_port']
if strtobool(api_config['pvc']['api']['ssl']['enabled']):
scheme = 'https'
else:
scheme = 'http'
if strtobool(api_config['pvc']['api']['authentication']['enabled']):
# Always use the first token
api_key = api_config['pvc']['api']['authentication']['tokens'][0]['token']
else:
api_key = 'N/A'
return cfgfile, host, port, scheme, api_key
2019-12-30 13:29:07 -05:00
def get_config(store_data, cluster=None):
# This is generally static
prefix = '/api/v1'
cluster_details = store_data.get(cluster)
if not cluster_details:
cluster_details = default_store_data
cluster = 'local'
if cluster_details.get('cfgfile', 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):
description, host, port, scheme, api_key = read_from_yaml(cfgfile)
else:
2020-11-07 12:57:42 -05:00
return {'badcfg': True}
# Handle an all-wildcard address
if host == '0.0.0.0':
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']
api_key = cluster_details['api_key']
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
config['api_prefix'] = prefix
if cluster == 'local':
config['verify_ssl'] = False
else:
config['verify_ssl'] = bool(strtobool(os.environ.get('PVC_CLIENT_VERIFY_SSL', 'True')))
return config
def get_store(store_path):
store_file = '{}/pvc-cli.json'.format(store_path)
with open(store_file, 'r') as fh:
store_data = json.loads(fh.read())
return store_data
def update_store(store_path, store_data):
store_file = '{}/pvc-cli.json'.format(store_path)
if not os.path.exists(store_file):
with open(store_file, 'w') as fh:
fh.write(json.dumps(store_data, sort_keys=True, indent=4))
# Ensure file has sensible permissions due to API key storage, but only when created!
2020-08-28 01:08:48 -04:00
os.chmod(store_file, int(os.environ.get('PVC_CLIENT_DB_PERMS', '600'), 8))
else:
with open(store_file, 'w') as fh:
fh.write(json.dumps(store_data, sort_keys=True, indent=4))
pvc_client_dir = os.environ.get('PVC_CLIENT_DIR', None)
home_dir = os.environ.get('HOME', None)
if pvc_client_dir:
store_path = '{}'.format(pvc_client_dir)
elif home_dir:
store_path = '{}/.config/pvc'.format(home_dir)
else:
print('WARNING: No client or home config dir found, using /tmp instead')
store_path = '/tmp/pvc'
if not os.path.isdir(store_path):
os.makedirs(store_path)
if not os.path.isfile(store_path + '/pvc-cli.json'):
update_store(store_path, {"local": default_store_data})
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=120)
def cleanup(retcode, retmsg):
if retmsg != '':
click.echo(retmsg)
if retcode is True:
exit(0)
else:
exit(1)
###############################################################################
# pvc cluster
###############################################################################
@click.group(name='cluster', short_help='Manage PVC cluster connections.', context_settings=CONTEXT_SETTINGS)
def cli_cluster():
"""
Manage the PVC clusters this CLI can connect to.
"""
pass
###############################################################################
# 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.'
)
@click.option(
'-p', '--port', 'port', required=False, default=7370, show_default=True,
help='The cluster API client port.'
)
@click.option(
'-s/-S', '--ssl/--no-ssl', 'ssl', is_flag=True, default=False, show_default=True,
help='Whether to use SSL or not.'
)
@click.option(
'-k', '--api-key', 'api_key', required=False, default=None,
help='An API key to authenticate against the cluster.'
)
@click.argument(
'name'
)
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.
"""
if ssl:
scheme = 'https'
else:
scheme = 'http'
# Get the existing data
existing_config = get_store(store_path)
# Append our new entry to the end
existing_config[name] = {
'description': description,
'host': address,
'port': port,
'scheme': scheme,
'api_key': api_key
}
# Update the store
update_store(store_path, existing_config)
click.echo('Added new cluster "{}" at host "{}" to local database'.format(name, address))
###############################################################################
# pvc cluster remove
###############################################################################
@click.command(name='remove', short_help='Remove a cluster from the client.')
@click.argument(
'name'
)
def cluster_remove(name):
"""
Remove a PVC cluster from the configuration of the local CLI client.
"""
# Get the existing data
existing_config = get_store(store_path)
# Remove the entry matching the name
try:
existing_config.pop(name)
except KeyError:
print('No cluster with name "{}" found'.format(name))
# Update the store
update_store(store_path, existing_config)
click.echo('Removed cluster "{}" from local database'.format(name))
###############################################################################
# pvc cluster list
###############################################################################
@click.command(name='list', short_help='List all available clusters.')
def cluster_list():
"""
List all the available PVC clusters configured in this CLI instance.
"""
# Get the existing data
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 = 32
for cluster in clusters:
cluster_details = clusters[cluster]
if cluster_details.get('cfgfile', 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):
description, address, port, scheme, api_key = read_from_yaml(cfgfile)
2020-01-02 12:18:41 -05:00
else:
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')
api_key = cluster_details.get('api_key', 'N/A')
if not api_key:
api_key = 'N/A'
_name_length = len(cluster) + 1
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
if _port_length > port_length:
port_length = _port_length
_scheme_length = len(scheme) + 1
if _scheme_length > scheme_length:
scheme_length = _scheme_length
_api_key_length = len(api_key) + 1
if _api_key_length > api_key_length:
api_key_length = _api_key_length
# Display the data nicely
click.echo("Available clusters:")
click.echo()
click.echo(
'{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",
port_length=port_length,
scheme="Scheme",
scheme_length=scheme_length,
api_key="API Key",
api_key_length=api_key_length
)
)
for cluster in clusters:
cluster_details = clusters[cluster]
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):
description, address, port, scheme, api_key = read_from_yaml(cfgfile)
else:
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')
if not api_key:
api_key = 'N/A'
click.echo(
'{bold}{name: <{name_length}} {description: <{description_length}} {address: <{address_length}} {port: <{port_length}} {scheme: <{scheme_length}} {api_key: <{api_key_length}}{end_bold}'.format(
2019-12-30 14:19:17 -05:00
bold='',
end_bold='',
name=cluster,
name_length=name_length,
description=description,
description_length=description_length,
address=address,
address_length=address_length,
port=port,
port_length=port_length,
scheme=scheme,
scheme_length=scheme_length,
api_key=api_key,
api_key_length=api_key_length
)
)
2019-12-30 13:29:07 -05:00
# Validate that the cluster is set for a given command
def cluster_req(function):
@wraps(function)
def validate_cluster(*args, **kwargs):
if config.get('badcfg', None):
click.echo('No cluster specified and no local pvcapid.yaml configuration found. Use "pvc cluster" to add a cluster API to connect to.')
exit(1)
2020-06-05 15:50:43 -04:00
return function(*args, **kwargs)
return validate_cluster
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc node
###############################################################################
@click.group(name='node', short_help='Manage a PVC node.', context_settings=CONTEXT_SETTINGS)
def cli_node():
2018-06-05 01:39:59 -04:00
"""
Manage the state of a node in the PVC cluster.
"""
pass
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc node secondary
###############################################################################
@click.command(name='secondary', short_help='Set a node in secondary node status.')
@click.argument(
'node'
)
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for transition to complete before returning.'
)
@cluster_req
def node_secondary(node, wait):
"""
Take NODE out of primary router mode.
"""
2019-12-30 13:29:07 -05:00
task_retcode, task_retdata = pvc_provisioner.task_status(config, None)
if len(task_retdata) > 0:
2020-01-12 16:06:18 -05:00
click.echo("Note: There are currently {} active or queued provisioner jobs on the current primary node.".format(len(task_retdata)))
click.echo(" These jobs will continue executing, but status will not be visible until the current")
click.echo(" node returns to primary state.")
click.echo()
retcode, retmsg = pvc_node.node_coordinator_state(config, node, 'secondary')
if not retcode:
cleanup(retcode, retmsg)
else:
2020-02-19 16:22:29 -05:00
if wait:
click.echo(retmsg)
click.echo("Waiting for state transition... ", nl=False)
# Every half-second, check if the API is reachable and the node is in secondary state
while True:
try:
_retcode, _retmsg = pvc_node.node_info(config, node)
if _retmsg['coordinator_state'] == 'secondary':
retmsg = "done."
break
else:
time.sleep(0.5)
2020-11-06 18:55:10 -05:00
except Exception:
time.sleep(0.5)
2020-02-19 16:22:29 -05:00
cleanup(retcode, retmsg)
###############################################################################
# pvc node primary
###############################################################################
@click.command(name='primary', short_help='Set a node in primary status.')
@click.argument(
'node'
)
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for transition to complete before returning.'
)
@cluster_req
def node_primary(node, wait):
"""
Put NODE into primary router mode.
"""
task_retcode, task_retdata = pvc_provisioner.task_status(config, None)
if len(task_retdata) > 0:
2020-01-12 16:06:18 -05:00
click.echo("Note: There are currently {} active or queued provisioner jobs on the current primary node.".format(len(task_retdata)))
click.echo(" These jobs will continue executing, but status will not be visible until the current")
click.echo(" node returns to primary state.")
click.echo()
retcode, retmsg = pvc_node.node_coordinator_state(config, node, 'primary')
if not retcode:
cleanup(retcode, retmsg)
else:
2020-02-19 16:22:29 -05:00
if wait:
click.echo(retmsg)
click.echo("Waiting for state transition... ", nl=False)
# Every half-second, check if the API is reachable and the node is in secondary state
while True:
try:
_retcode, _retmsg = pvc_node.node_info(config, node)
if _retmsg['coordinator_state'] == 'primary':
retmsg = "done."
break
else:
time.sleep(0.5)
2020-11-06 18:55:10 -05:00
except Exception:
time.sleep(0.5)
2020-02-19 16:22:29 -05:00
cleanup(retcode, retmsg)
2018-06-05 01:39:59 -04:00
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc node flush
###############################################################################
@click.command(name='flush', short_help='Take a node out of service.')
2018-07-17 01:48:15 -04:00
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for migrations to complete before returning.'
)
@click.argument(
'node', default=myhostname
2018-06-06 20:49:07 -04:00
)
@cluster_req
def node_flush(node, wait):
2018-06-05 01:39:59 -04:00
"""
Take NODE out of active service and migrate away all VMs. If unspecified, defaults to this host.
2018-06-05 01:39:59 -04:00
"""
2019-12-30 13:29:07 -05:00
2019-12-25 20:18:53 -05:00
retcode, retmsg = pvc_node.node_domain_state(config, node, 'flush', wait)
cleanup(retcode, retmsg)
2018-06-05 01:39:59 -04:00
2018-06-05 01:39:59 -04:00
###############################################################################
2018-06-26 23:46:03 -04:00
# pvc node ready/unflush
2018-06-05 01:39:59 -04:00
###############################################################################
@click.command(name='ready', short_help='Restore node to service.')
@click.argument(
'node', default=myhostname
2018-06-06 20:49:07 -04:00
)
2019-05-11 00:16:38 -04:00
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for migrations to complete before returning.'
)
@cluster_req
2019-05-11 00:16:38 -04:00
def node_ready(node, wait):
2018-07-18 22:58:41 -04:00
"""
Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host.
"""
2019-12-25 20:18:53 -05:00
retcode, retmsg = pvc_node.node_domain_state(config, node, 'ready', wait)
cleanup(retcode, retmsg)
2018-06-26 23:46:03 -04:00
@click.command(name='unflush', short_help='Restore node to service.')
2018-06-26 23:46:03 -04:00
@click.argument(
'node', default=myhostname
)
2019-05-11 00:16:38 -04:00
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for migrations to complete before returning.'
)
def node_unflush(node, wait):
2018-07-18 22:58:41 -04:00
"""
Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host.
"""
2019-12-25 20:18:53 -05:00
retcode, retmsg = pvc_node.node_domain_state(config, node, 'ready', wait)
cleanup(retcode, retmsg)
2018-06-05 01:39:59 -04:00
###############################################################################
# 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.'
)
@cluster_req
def node_info(node, long_output):
"""
Show information about node NODE. If unspecified, defaults to this host.
"""
2019-12-26 11:20:57 -05:00
retcode, retdata = pvc_node.node_info(config, node)
if retcode:
retdata = pvc_node.format_info(retdata, long_output)
2019-12-26 11:20:57 -05:00
cleanup(retcode, retdata)
###############################################################################
# pvc node list
###############################################################################
@click.command(name='list', short_help='List all node objects.')
@click.argument(
'limit', default=None, required=False
)
@click.option(
'-ds', '--daemon-state', 'target_daemon_state', default=None,
help='Limit list to nodes in the specified daemon state.'
)
@click.option(
'-cs', '--coordinator-state', 'target_coordinator_state', default=None,
help='Limit list to nodes in the specified coordinator state.'
)
@click.option(
'-vs', '--domain-state', 'target_domain_state', default=None,
help='Limit list to nodes in the specified domain state.'
)
@click.option(
'-r', '--raw', 'raw', is_flag=True, default=False,
help='Display the raw list of node names only.'
)
@cluster_req
def node_list(limit, target_daemon_state, target_coordinator_state, target_domain_state, raw):
"""
2020-01-04 14:06:36 -05:00
List all nodes; optionally only match names matching regex LIMIT.
"""
retcode, retdata = pvc_node.node_list(config, limit, target_daemon_state, target_coordinator_state, target_domain_state)
if retcode:
retdata = pvc_node.format_list(retdata, raw)
else:
if raw:
retdata = ""
2019-12-26 17:52:57 -05:00
cleanup(retcode, retdata)
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc vm
###############################################################################
2018-07-18 22:30:23 -04:00
@click.group(name='vm', short_help='Manage a PVC virtual machine.', context_settings=CONTEXT_SETTINGS)
def cli_vm():
2018-06-05 01:39:59 -04:00
"""
Manage the state of a virtual machine in the PVC cluster.
"""
pass
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc vm define
###############################################################################
@click.command(name='define', short_help='Define a new virtual machine from a Libvirt XML file.')
@click.option(
2019-03-12 23:17:31 -04:00
'-t', '--target', 'target_node',
help='Home node for this domain; autoselect if unspecified.'
)
@click.option(
'-l', '--limit', 'node_limit', default=None, show_default=False,
help='Comma-separated list of nodes to limit VM operation to; saved with VM.'
)
@click.option(
'-s', '--selector', 'node_selector', default='mem', show_default=True,
type=click.Choice(['mem', 'load', 'vcpus', 'vms', 'none']),
help='Method to determine optimal target node during autoselect; "none" will use the default for the cluster.'
)
@click.option(
2019-10-12 01:36:50 -04:00
'-a/-A', '--autostart/--no-autostart', 'node_autostart', is_flag=True, default=False,
help='Start VM automatically on next unflush/ready state of home node; unset by daemon once used.'
2018-06-05 01:39:59 -04:00
)
@click.option(
'-m', '--method', 'migration_method', default='none', show_default=True,
type=click.Choice(['none', 'live', 'shutdown']),
help='The preferred migration method of the VM between nodes; saved with VM.'
)
@click.argument(
'vmconfig', type=click.File()
)
@cluster_req
def vm_define(vmconfig, target_node, node_limit, node_selector, node_autostart, migration_method):
2018-06-05 01:39:59 -04:00
"""
Define a new virtual machine from Libvirt XML configuration file VMCONFIG.
2018-06-05 01:39:59 -04:00
"""
# Open the XML file
vmconfig_data = vmconfig.read()
vmconfig.close()
2020-01-24 13:17:48 -05:00
# Verify our XML is sensible
try:
xml_data = etree.fromstring(vmconfig_data)
new_cfg = etree.tostring(xml_data, pretty_print=True).decode('utf8')
2020-11-06 18:55:10 -05:00
except Exception:
2020-01-24 13:17:48 -05:00
cleanup(False, 'Error: XML is malformed or invalid')
retcode, retmsg = pvc_vm.vm_define(config, new_cfg, target_node, node_limit, node_selector, node_autostart, migration_method)
2019-12-27 09:52:16 -05:00
cleanup(retcode, retmsg)
###############################################################################
# pvc vm meta
###############################################################################
@click.command(name='meta', short_help='Modify PVC metadata of an existing VM.')
@click.option(
'-l', '--limit', 'node_limit', default=None, show_default=False,
2019-10-12 02:08:52 -04:00
help='Comma-separated list of nodes to limit VM operation to; set to an empty string to remove.'
)
@click.option(
'-s', '--selector', 'node_selector', default=None, show_default=False,
type=click.Choice(['mem', 'load', 'vcpus', 'vms', 'none']),
help='Method to determine optimal target node during autoselect; "none" will use the default for the cluster.'
)
@click.option(
2019-10-12 01:36:50 -04:00
'-a/-A', '--autostart/--no-autostart', 'node_autostart', is_flag=True, default=None,
help='Start VM automatically on next unflush/ready state of home node; unset by daemon once used.'
)
@click.option(
'-m', '--method', 'migration_method', default='none', show_default=True,
type=click.Choice(['none', 'live', 'shutdown']),
2021-06-22 03:40:21 -04:00
help='The preferred migration method of the VM between nodes.'
)
@click.option(
'-p', '--profile', 'provisioner_profile', default=None, show_default=False,
help='PVC provisioner profile name for VM.'
)
@click.argument(
'domain'
)
@cluster_req
def vm_meta(domain, node_limit, node_selector, node_autostart, migration_method, provisioner_profile):
"""
Modify the PVC metadata of existing virtual machine DOMAIN. At least one option to update must be specified. DOMAIN may be a UUID or name.
"""
if node_limit is None and node_selector is None and node_autostart is None and migration_method is None and provisioner_profile is None:
cleanup(False, 'At least one metadata option must be specified to update.')
retcode, retmsg = pvc_vm.vm_metadata(config, domain, node_limit, node_selector, node_autostart, migration_method, provisioner_profile)
2019-12-27 09:52:16 -05:00
cleanup(retcode, retmsg)
###############################################################################
# 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.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restart'
)
@click.argument(
'domain'
)
@click.argument(
2019-12-27 09:52:16 -05:00
'cfgfile', type=click.File(), default=None, required=False
)
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.
"""
if editor is False and cfgfile is None:
cleanup(False, 'Either an XML config file or the "--editor" option must be specified.')
2019-12-27 09:52:16 -05:00
retcode, vm_information = pvc_vm.vm_info(config, domain)
if not retcode or not vm_information.get('name', None):
2020-01-04 14:06:36 -05:00
cleanup(False, 'ERROR: Could not find VM "{}"!'.format(domain))
2019-12-27 09:52:16 -05:00
dom_name = vm_information.get('name')
if editor is True:
# Grab the current config
2019-12-27 09:52:16 -05:00
current_vm_cfg_raw = vm_information.get('xml')
xml_data = etree.fromstring(current_vm_cfg_raw)
current_vm_cfgfile = etree.tostring(xml_data, pretty_print=True).decode('utf8').strip()
new_vm_cfgfile = click.edit(text=current_vm_cfgfile, require_save=True, extension='.xml')
if new_vm_cfgfile is None:
click.echo('Aborting with no modifications.')
exit(0)
else:
new_vm_cfgfile = new_vm_cfgfile.strip()
# We're operating in replace mode
else:
# Open the XML file
2019-12-27 09:52:16 -05:00
new_vm_cfgfile = cfgfile.read()
cfgfile.close()
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(line)
click.echo('')
2020-01-24 13:17:48 -05:00
# Verify our XML is sensible
try:
xml_data = etree.fromstring(new_vm_cfgfile)
new_cfg = etree.tostring(xml_data, pretty_print=True).decode('utf8')
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
2020-01-24 13:17:48 -05:00
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."
2019-12-27 09:52:16 -05:00
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
###############################################################################
@click.command(name='undefine', short_help='Undefine a virtual machine.')
@click.argument(
'domain'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def vm_undefine(domain, confirm_flag):
"""
2020-01-04 14:06:36 -05:00
Stop virtual machine DOMAIN and remove it database, preserving disks. DOMAIN may be a UUID or name.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Undefine VM {}'.format(domain), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2019-12-27 09:52:16 -05:00
retcode, retmsg = pvc_vm.vm_remove(config, domain, delete_disks=False)
cleanup(retcode, retmsg)
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc vm remove
###############################################################################
@click.command(name='remove', short_help='Remove a virtual machine.')
@click.argument(
'domain'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def vm_remove(domain, confirm_flag):
"""
2020-01-04 14:06:36 -05:00
Stop virtual machine DOMAIN and remove it, along with all disks,. DOMAIN may be a UUID or name.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Undefine VM {} and remove all disks'.format(domain), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2019-12-27 09:52:16 -05:00
retcode, retmsg = pvc_vm.vm_remove(config, domain, delete_disks=True)
cleanup(retcode, retmsg)
2019-03-12 21:09:54 -04:00
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc vm start
###############################################################################
@click.command(name='start', short_help='Start up a defined virtual machine.')
@click.argument(
'domain'
2018-06-05 01:39:59 -04:00
)
@cluster_req
def vm_start(domain):
2018-06-05 01:39:59 -04:00
"""
Start virtual machine DOMAIN on its configured node. DOMAIN may be a UUID or name.
2018-06-05 01:39:59 -04:00
"""
2019-12-27 09:52:16 -05:00
retcode, retmsg = pvc_vm.vm_state(config, domain, 'start')
cleanup(retcode, retmsg)
2018-06-05 01:39:59 -04:00
2018-06-13 12:49:51 -04:00
###############################################################################
# pvc vm restart
###############################################################################
2018-06-17 02:24:06 -04:00
@click.command(name='restart', short_help='Restart a running virtual machine.')
@click.argument(
'domain'
2018-06-13 12:49:51 -04:00
)
@click.option(
'-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, confirm_flag):
2018-06-13 12:49:51 -04:00
"""
2018-06-17 02:24:06 -04:00
Restart running virtual machine DOMAIN. DOMAIN may be a UUID or name.
2018-06-13 12:49:51 -04:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Restart VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
2018-06-13 12:49:51 -04:00
retcode, retmsg = pvc_vm.vm_state(config, domain, 'restart', wait=wait)
2019-12-27 09:52:16 -05:00
cleanup(retcode, retmsg)
2018-06-13 12:49:51 -04:00
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc vm shutdown
###############################################################################
@click.command(name='shutdown', short_help='Gracefully shut down a running virtual machine.')
@click.argument(
'domain'
2018-06-05 01:39:59 -04:00
)
@click.option(
'-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, confirm_flag):
2018-06-05 01:39:59 -04:00
"""
Gracefully shut down virtual machine DOMAIN. DOMAIN may be a UUID or name.
2018-06-05 01:39:59 -04:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Shut down VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
2018-06-05 01:39:59 -04:00
retcode, retmsg = pvc_vm.vm_state(config, domain, 'shutdown', wait=wait)
2019-12-27 09:52:16 -05:00
cleanup(retcode, retmsg)
2018-06-05 01:39:59 -04:00
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc vm stop
###############################################################################
@click.command(name='stop', short_help='Forcibly halt a running virtual machine.')
@click.argument(
'domain'
2018-06-05 01:39:59 -04:00
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the stop'
)
@cluster_req
def vm_stop(domain, confirm_flag):
2018-06-05 01:39:59 -04:00
"""
Forcibly halt (destroy) running virtual machine DOMAIN. DOMAIN may be a UUID or name.
2018-06-05 01:39:59 -04:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Forcibly stop VM {}'.format(domain), prompt_suffix='? ', abort=True)
except Exception:
exit(0)
2018-06-05 01:39:59 -04:00
2019-12-27 09:52:16 -05:00
retcode, retmsg = pvc_vm.vm_state(config, domain, 'stop')
cleanup(retcode, retmsg)
2018-06-05 01:39:59 -04:00
2019-10-23 23:37:42 -04:00
###############################################################################
# pvc vm disable
###############################################################################
@click.command(name='disable', short_help='Mark a virtual machine as disabled.')
@click.argument(
'domain'
)
@cluster_req
2019-10-23 23:37:42 -04:00
def vm_disable(domain):
"""
Prevent stopped virtual machine DOMAIN from being counted towards cluster health status. DOMAIN may be a UUID or name.
Use this option for VM that are stopped intentionally or long-term and which should not impact cluster health if stopped. A VM can be started directly from disable state.
"""
2019-12-27 09:52:16 -05:00
retcode, retmsg = pvc_vm.vm_state(config, domain, 'disable')
cleanup(retcode, retmsg)
2019-10-23 23:37:42 -04:00
2018-06-10 21:03:41 -04:00
###############################################################################
# pvc vm move
###############################################################################
@click.command(name='move', short_help='Permanently move a virtual machine to another node.')
@click.argument(
'domain'
2018-06-10 21:03:41 -04:00
)
@click.option(
2019-03-12 23:17:31 -04:00
'-t', '--target', 'target_node', default=None,
help='Target node to migrate to; autodetect if unspecified.'
2018-06-10 21:03:41 -04:00
)
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for migration to complete before returning.'
)
@click.option(
'--force-live', 'force_live', is_flag=True, default=False,
help='Do not fall back to shutdown-based migration if live migration fails.'
)
@cluster_req
def vm_move(domain, target_node, wait, force_live):
2018-06-10 21:03:41 -04:00
"""
Permanently move virtual machine DOMAIN, via live migration if running and possible, to another node. DOMAIN may be a UUID or name.
2018-06-10 21:03:41 -04:00
"""
retcode, retmsg = pvc_vm.vm_node(config, domain, target_node, 'move', force=False, wait=wait, force_live=force_live)
2019-12-27 09:52:16 -05:00
cleanup(retcode, retmsg)
2018-06-10 21:03:41 -04:00
###############################################################################
# pvc vm migrate
###############################################################################
@click.command(name='migrate', short_help='Temporarily migrate a virtual machine to another node.')
@click.argument(
'domain'
)
@click.option(
2019-03-12 23:17:31 -04:00
'-t', '--target', 'target_node', default=None,
help='Target node to migrate to; autodetect if unspecified.'
)
@click.option(
'-f', '--force', 'force_migrate', is_flag=True, default=False,
help='Force migrate an already migrated VM; does not replace an existing previous node value.'
)
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for migration to complete before returning.'
)
@click.option(
'--force-live', 'force_live', is_flag=True, default=False,
help='Do not fall back to shutdown-based migration if live migration fails.'
)
@cluster_req
def vm_migrate(domain, target_node, force_migrate, wait, force_live):
2018-06-05 01:39:59 -04:00
"""
Temporarily migrate running virtual machine DOMAIN, via live migration if possible, to another node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node.
2018-06-05 01:39:59 -04:00
"""
retcode, retmsg = pvc_vm.vm_node(config, domain, target_node, 'migrate', force=force_migrate, wait=wait, force_live=force_live)
2019-12-27 09:52:16 -05:00
cleanup(retcode, retmsg)
###############################################################################
# pvc vm unmigrate
###############################################################################
2018-06-05 01:39:59 -04:00
@click.command(name='unmigrate', short_help='Restore a migrated virtual machine to its original node.')
@click.argument(
'domain'
)
@click.option(
'-w', '--wait', 'wait', is_flag=True, default=False,
help='Wait for migration to complete before returning.'
)
@click.option(
'--force-live', 'force_live', is_flag=True, default=False,
help='Do not fall back to shutdown-based migration if live migration fails.'
)
@cluster_req
def vm_unmigrate(domain, wait, force_live):
2018-06-05 01:39:59 -04:00
"""
Restore previously migrated virtual machine DOMAIN, via live migration if possible, to its original node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node.
2018-06-05 01:39:59 -04:00
"""
retcode, retmsg = pvc_vm.vm_node(config, domain, None, 'unmigrate', force=False, wait=wait, force_live=force_live)
2019-12-27 09:52:16 -05:00
cleanup(retcode, retmsg)
2019-08-07 13:42:01 -04:00
###############################################################################
2019-08-07 17:50:25 -04:00
# pvc vm flush-locks
2019-08-07 13:42:01 -04:00
###############################################################################
2019-08-07 17:50:25 -04:00
@click.command(name='flush-locks', short_help='Flush stale RBD locks for a virtual machine.')
2019-08-07 13:42:01 -04:00
@click.argument(
'domain'
)
@cluster_req
2019-08-07 13:42:01 -04:00
def vm_flush_locks(domain):
"""
Flush stale RBD locks for virtual machine DOMAIN. DOMAIN may be a UUID or name. DOMAIN must be in a stopped state before flushing locks.
"""
2019-12-27 09:52:16 -05:00
retcode, retmsg = pvc_vm.vm_locks(config, domain)
cleanup(retcode, retmsg)
###############################################################################
# pvc vm vcpu
###############################################################################
@click.group(name='vcpu', short_help='Manage vCPU counts of a virtual machine.', context_settings=CONTEXT_SETTINGS)
def vm_vcpu():
"""
Manage the vCPU counts of a virtual machine in the PVC cluster."
"""
pass
###############################################################################
# pvc vm vcpu get
###############################################################################
@click.command(name='get', short_help='Get the current vCPU count of a virtual machine.')
@click.argument(
'domain'
)
@click.option(
'-r', '--raw', 'raw', is_flag=True, default=False,
help='Display the raw value only without formatting.'
)
@cluster_req
def vm_vcpu_get(domain, raw):
"""
Get the current vCPU count of the virtual machine DOMAIN.
"""
retcode, retmsg = pvc_vm.vm_vcpus_get(config, domain)
if not raw:
retmsg = pvc_vm.format_vm_vcpus(config, domain, retmsg)
else:
retmsg = retmsg[0] # Get only the first part of the tuple (vm_vcpus)
cleanup(retcode, retmsg)
###############################################################################
# pvc vm vcpu set
###############################################################################
@click.command(name='set', short_help='Set the vCPU count of a virtual machine.')
@click.argument(
'domain'
)
@click.argument(
'vcpus'
)
@click.option(
'-t', '--topology', 'topology', default=None,
help='Use an alternative topology for the vCPUs in the CSV form <sockets>,<cores>,<threads>. SxCxT must equal VCPUS.'
)
@click.option(
'-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, 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 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)
###############################################################################
# pvc vm memory
###############################################################################
@click.group(name='memory', short_help='Manage provisioned memory of a virtual machine.', context_settings=CONTEXT_SETTINGS)
def vm_memory():
"""
Manage the provisioned memory of a virtual machine in the PVC cluster."
"""
pass
###############################################################################
# pvc vm memory get
###############################################################################
@click.command(name='get', short_help='Get the current provisioned memory of a virtual machine.')
@click.argument(
'domain'
)
@click.option(
'-r', '--raw', 'raw', is_flag=True, default=False,
help='Display the raw value only without formatting.'
)
@cluster_req
def vm_memory_get(domain, raw):
"""
Get the current provisioned memory of the virtual machine DOMAIN.
"""
retcode, retmsg = pvc_vm.vm_memory_get(config, domain)
if not raw:
retmsg = pvc_vm.format_vm_memory(config, domain, retmsg)
cleanup(retcode, retmsg)
###############################################################################
# pvc vm memory set
###############################################################################
@click.command(name='set', short_help='Set the provisioned memory of a virtual machine.')
@click.argument(
'domain'
)
@click.argument(
'memory'
)
@click.option(
'-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, 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)
###############################################################################
# pvc vm network
###############################################################################
@click.group(name='network', short_help='Manage attached networks of a virtual machine.', context_settings=CONTEXT_SETTINGS)
def vm_network():
"""
Manage the attached networks of a virtual machine in the PVC cluster.
Network details cannot be modified here. To modify a network, first remove it, then readd it with the correct settings. Unless the '-r'/'--reboot' flag is provided, this will not affect the running VM until it is restarted.
"""
pass
###############################################################################
# pvc vm network get
###############################################################################
@click.command(name='get', short_help='Get the networks of a virtual machine.')
@click.argument(
'domain'
)
@click.option(
'-r', '--raw', 'raw', is_flag=True, default=False,
help='Display the raw values only without formatting.'
)
@cluster_req
def vm_network_get(domain, raw):
"""
Get the networks of the virtual machine DOMAIN.
"""
retcode, retdata = pvc_vm.vm_networks_get(config, domain)
if not raw:
retmsg = pvc_vm.format_vm_networks(config, domain, retdata)
else:
network_vnis = list()
for network in retdata:
network_vnis.append(network[0])
retmsg = ','.join(network_vnis)
cleanup(retcode, retmsg)
###############################################################################
# pvc vm network add
###############################################################################
@click.command(name='add', short_help='Add network to a virtual machine.')
@click.argument(
'domain'
)
@click.argument(
2021-06-21 22:21:54 -04:00
'net'
)
@click.option(
'-a', '--macaddr', 'macaddr', default=None,
2021-06-21 22:21:54 -04:00
help='Use this MAC address instead of random generation; must be a valid MAC address in colon-delimited format.'
)
@click.option(
2021-06-22 03:40:21 -04:00
'-m', '--model', 'model', default='virtio', show_default=True,
help='The model for the interface; must be a valid libvirt model. Not used for "netdev" SR-IOV NETs.'
2021-06-21 22:21:54 -04:00
)
@click.option(
'-s', '--sriov', 'sriov', is_flag=True, default=False,
help='Identify that NET is an SR-IOV device name and not a VNI. Required for adding SR-IOV NETs.'
)
@click.option(
2021-06-22 03:40:21 -04:00
'-d', '--sriov-mode', 'sriov_mode', default='macvtap', show_default=True,
2021-06-21 22:21:54 -04:00
type=click.Choice(['hostdev', 'macvtap']),
help='For SR-IOV NETs, the SR-IOV network device mode.'
)
@click.option(
'-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
2021-06-21 22:21:54 -04:00
def vm_network_add(domain, net, macaddr, model, sriov, sriov_mode, restart, confirm_flag):
"""
2021-06-21 22:21:54 -04:00
Add the network NET to the virtual machine DOMAIN. Networks are always addded to the end of the current list of networks in the virtual machine.
NET may be a PVC network VNI, which is added as a bridged device, or a SR-IOV VF device connected in the given mode.
NOTE: Adding a SR-IOV network device in the "hostdev" mode has the following caveats:
1. The VM will not be able to be live migrated; it must be shut down to migrate between nodes. The VM metadata will be updated to force this.
2. If an identical SR-IOV VF device is not present on the target node, post-migration startup will fail. It may be prudent to use a node limit here.
"""
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
2021-06-21 22:21:54 -04:00
retcode, retmsg = pvc_vm.vm_networks_add(config, domain, net, macaddr, model, sriov, sriov_mode, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg)
###############################################################################
# pvc vm network remove
###############################################################################
@click.command(name='remove', short_help='Remove network from a virtual machine.')
@click.argument(
'domain'
)
@click.argument(
2021-06-21 22:21:54 -04:00
'net'
)
@click.option(
'-s', '--sriov', 'sriov', is_flag=True, default=False,
help='Identify that NET is an SR-IOV device name and not a VNI. Required for removing SR-IOV NETs.'
)
@click.option(
'-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
2021-06-21 22:21:54 -04:00
def vm_network_remove(domain, net, sriov, restart, confirm_flag):
"""
2021-06-21 22:21:54 -04:00
Remove the network NET from the virtual machine DOMAIN.
NET may be a PVC network VNI, which is added as a bridged device, or a SR-IOV VF device connected in the given mode.
"""
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
2021-06-21 22:21:54 -04:00
retcode, retmsg = pvc_vm.vm_networks_remove(config, domain, net, sriov, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg)
###############################################################################
# pvc vm volume
###############################################################################
@click.group(name='volume', short_help='Manage attached volumes of a virtual machine.', context_settings=CONTEXT_SETTINGS)
def vm_volume():
"""
Manage the attached volumes of a virtual machine in the PVC cluster.
Volume details cannot be modified here. To modify a volume, first remove it, then readd it with the correct settings. Unless the '-r'/'--reboot' flag is provided, this will not affect the running VM until it is restarted.
"""
pass
###############################################################################
# pvc vm volume get
###############################################################################
@click.command(name='get', short_help='Get the volumes of a virtual machine.')
@click.argument(
'domain'
)
@click.option(
'-r', '--raw', 'raw', is_flag=True, default=False,
help='Display the raw values only without formatting.'
)
@cluster_req
def vm_volume_get(domain, raw):
"""
Get the volumes of the virtual machine DOMAIN.
"""
retcode, retdata = pvc_vm.vm_volumes_get(config, domain)
if not raw:
retmsg = pvc_vm.format_vm_volumes(config, domain, retdata)
else:
volume_paths = list()
for volume in retdata:
volume_paths.append("{}:{}".format(volume[2], volume[0]))
retmsg = ','.join(volume_paths)
cleanup(retcode, retmsg)
###############################################################################
# pvc vm volume add
###############################################################################
@click.command(name='add', short_help='Add volume to a virtual machine.')
@click.argument(
'domain'
)
@click.argument(
'volume'
)
@click.option(
'-d', '--disk-id', 'disk_id', default=None,
help='The disk ID in sdX/vdX/hdX format; if not specified, the next available will be used.'
)
@click.option(
'-b', '--bus', 'bus', default='scsi', show_default=True,
type=click.Choice(['scsi', 'ide', 'usb', 'virtio']),
help='The bus to attach the disk to; must be present in the VM.'
)
@click.option(
'-t', '--type', 'disk_type', default='rbd', show_default=True,
type=click.Choice(['rbd', 'file']),
help='The type of volume to add.'
)
@click.option(
'-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, 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)
###############################################################################
# pvc vm volume remove
###############################################################################
@click.command(name='remove', short_help='Remove volume from a virtual machine.')
@click.argument(
'domain'
)
@click.argument(
2021-06-22 04:31:02 -04:00
'volume'
)
@click.option(
'-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
2021-06-22 04:31:02 -04:00
def vm_volume_remove(domain, volume, restart, confirm_flag):
"""
2021-06-22 04:31:02 -04:00
Remove VOLUME from the virtual machine DOMAIN; VOLUME must be a file path or RBD path in 'pool/volume' format.
"""
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
2021-06-22 04:31:02 -04:00
retcode, retmsg = pvc_vm.vm_volumes_remove(config, domain, volume, restart)
if retcode and not restart:
retmsg = retmsg + " Changes will be applied on next VM start/restart."
cleanup(retcode, retmsg)
2019-12-27 09:52:16 -05:00
###############################################################################
# pvc vm log
###############################################################################
@click.command(name='log', short_help='Show console logs of a VM object.')
@click.argument(
'domain'
)
@click.option(
2020-11-03 11:14:49 -05:00
'-l', '--lines', 'lines', default=None, show_default=False,
2020-11-04 23:53:37 -05:00
help='Display this many log lines from the end of the log buffer. [default: 1000; with follow: 10]'
2019-12-27 09:52:16 -05:00
)
@click.option(
'-f', '--follow', 'follow', is_flag=True, default=False,
help='Follow the log buffer; output may be delayed by a few seconds relative to the live system. The --lines value defaults to 10 for the initial output.'
)
@cluster_req
2019-12-27 09:52:16 -05:00
def vm_log(domain, lines, follow):
"""
Show console logs of virtual machine DOMAIN on its current node in a pager or continuously. DOMAIN may be a UUID or name. Note that migrating a VM to a different node will cause the log buffer to be overwritten by entries from the new node.
2019-12-27 09:52:16 -05:00
"""
2020-11-03 11:14:49 -05:00
# Set the default here so we can handle it
if lines is None:
if follow:
lines = 10
else:
lines = 1000
2019-12-27 09:52:16 -05:00
if follow:
retcode, retmsg = pvc_vm.follow_console_log(config, domain, lines)
else:
retcode, retmsg = pvc_vm.view_console_log(config, domain, lines)
click.echo_via_pager(retmsg)
retmsg = ''
2019-12-27 09:52:16 -05:00
cleanup(retcode, retmsg)
2019-08-07 13:42:01 -04:00
###############################################################################
# pvc vm info
###############################################################################
@click.command(name='info', short_help='Show details of a VM object.')
@click.argument(
'domain'
2018-06-05 01:39:59 -04:00
)
2018-06-05 18:45:54 -04:00
@click.option(
'-l', '--long', 'long_output', is_flag=True, default=False,
help='Display more detailed information.'
)
@cluster_req
def vm_info(domain, long_output):
2018-06-05 01:39:59 -04:00
"""
Show information about virtual machine DOMAIN. DOMAIN may be a UUID or name.
2018-06-05 01:39:59 -04:00
"""
2019-12-27 09:52:16 -05:00
retcode, retdata = pvc_vm.vm_info(config, domain)
2019-05-20 22:15:28 -04:00
if retcode:
retdata = pvc_vm.format_info(config, retdata, long_output)
2019-12-27 09:52:16 -05:00
cleanup(retcode, retdata)
2018-06-05 01:39:59 -04:00
###############################################################################
2019-12-27 09:52:16 -05:00
# pvc vm dump
###############################################################################
2019-12-27 09:52:16 -05:00
@click.command(name='dump', short_help='Dump a virtual machine XML to stdout.')
@click.option(
'-f', '--file', 'filename',
default=None, type=click.File(mode='w'),
help='Write VM XML to this file.'
)
@click.argument(
'domain'
)
@cluster_req
def vm_dump(filename, domain):
"""
2019-12-27 09:52:16 -05:00
Dump the Libvirt XML definition of virtual machine DOMAIN to stdout. DOMAIN may be a UUID or name.
"""
retcode, retdata = pvc_vm.vm_info(config, domain)
if not retcode or not retdata.get('name', None):
2020-01-04 14:06:36 -05:00
cleanup(False, 'ERROR: Could not find VM "{}"!'.format(domain))
2019-12-27 09:52:16 -05:00
current_vm_cfg_raw = retdata.get('xml')
2019-12-27 09:52:16 -05:00
xml_data = etree.fromstring(current_vm_cfg_raw)
current_vm_cfgfile = etree.tostring(xml_data, pretty_print=True).decode('utf8')
xml = current_vm_cfgfile.strip()
if filename is not None:
filename.write(xml)
cleanup(retcode, 'VM XML written to "{}".'.format(filename.name))
else:
cleanup(retcode, xml)
2018-06-10 20:21:00 -04:00
###############################################################################
# pvc vm list
2018-06-10 20:21:00 -04:00
###############################################################################
@click.command(name='list', short_help='List all VM objects.')
@click.argument(
'limit', default=None, required=False
)
@click.option(
2019-03-12 23:17:31 -04:00
'-t', '--target', 'target_node', default=None,
2019-03-20 11:31:54 -04:00
help='Limit list to VMs on the specified node.'
)
@click.option(
'-s', '--state', 'target_state', default=None,
help='Limit list to VMs in the specified state.'
)
2019-03-12 21:30:01 -04:00
@click.option(
'-r', '--raw', 'raw', is_flag=True, default=False,
2019-03-20 11:31:54 -04:00
help='Display the raw list of VM names only.'
2019-03-12 21:30:01 -04:00
)
@cluster_req
2019-03-20 11:31:54 -04:00
def vm_list(target_node, target_state, limit, raw):
2018-07-18 22:58:41 -04:00
"""
2020-01-04 14:06:36 -05:00
List all virtual machines; optionally only match names matching regex LIMIT.
NOTE: Red-coloured network lists indicate one or more configured networks are missing/invalid.
2018-07-18 22:58:41 -04:00
"""
2019-12-27 09:52:16 -05:00
retcode, retdata = pvc_vm.vm_list(config, limit, target_node, target_state)
2019-05-20 22:15:28 -04:00
if retcode:
retdata = pvc_vm.format_list(config, retdata, raw)
else:
if raw:
retdata = ""
2019-12-27 09:52:16 -05:00
cleanup(retcode, retdata)
2018-06-10 20:21:00 -04:00
2018-09-21 23:43:30 -04:00
###############################################################################
# pvc network
###############################################################################
@click.group(name='network', short_help='Manage a PVC virtual network.', context_settings=CONTEXT_SETTINGS)
def cli_network():
"""
Manage the state of a VXLAN network in the PVC cluster.
"""
pass
2018-09-21 23:43:30 -04:00
###############################################################################
# pvc network add
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='add', short_help='Add a new virtual network.')
2018-09-21 23:43:30 -04:00
@click.option(
'-d', '--description', 'description',
2018-10-17 00:23:27 -04:00
required=True,
help='Description of the network; must be unique and not contain whitespace.'
2018-09-28 20:42:24 -04:00
)
@click.option(
2019-03-15 11:28:49 -04:00
'-p', '--type', 'nettype',
2018-09-28 20:42:24 -04:00
required=True,
2019-03-15 11:28:49 -04:00
type=click.Choice(['managed', 'bridged']),
help='Network type; managed networks control IP addressing; bridged networks are simple vLAN bridges. All subsequent options are unused for bridged networks.'
)
@click.option(
'-n', '--domain', 'domain',
default=None,
2018-09-28 20:42:24 -04:00
help='Domain name of the network.'
2018-09-21 23:43:30 -04:00
)
2019-12-08 23:32:03 -05:00
@click.option(
'--dns-server', 'name_servers',
multiple=True,
2019-12-08 23:59:17 -05:00
help='DNS nameserver for network; multiple entries may be specified.'
2019-12-08 23:32:03 -05:00
)
2018-09-21 23:43:30 -04:00
@click.option(
'-i', '--ipnet', 'ip_network',
default=None,
help='CIDR-format IPv4 network address for subnet.'
)
@click.option(
'-i6', '--ipnet6', 'ip6_network',
default=None,
help='CIDR-format IPv6 network address for subnet; should be /64 or larger ending "::/YY".'
2018-09-21 23:43:30 -04:00
)
@click.option(
'-g', '--gateway', 'ip_gateway',
default=None,
help='Default IPv4 gateway address for subnet.'
)
@click.option(
'-g6', '--gateway6', 'ip6_gateway',
default=None,
help='Default IPv6 gateway address for subnet. [default: "X::1"]'
2018-09-21 23:43:30 -04:00
)
@click.option(
2018-09-23 15:26:20 -04:00
'--dhcp/--no-dhcp', 'dhcp_flag',
2018-09-21 23:43:30 -04:00
is_flag=True,
2019-03-15 11:28:49 -04:00
default=False,
help='Enable/disable IPv4 DHCP for clients on subnet.'
2018-09-21 23:43:30 -04:00
)
@click.option(
'--dhcp-start', 'dhcp_start',
default=None,
help='IPv4 DHCP range start address.'
)
@click.option(
'--dhcp-end', 'dhcp_end',
default=None,
help='IPv4 DHCP range end address.'
)
2018-09-21 23:43:30 -04:00
@click.argument(
'vni'
)
@cluster_req
2019-12-08 23:32:03 -05:00
def net_add(vni, description, nettype, domain, ip_network, ip_gateway, ip6_network, ip6_gateway, dhcp_flag, dhcp_start, dhcp_end, name_servers):
2018-09-21 23:43:30 -04:00
"""
2020-01-04 14:06:36 -05:00
Add a new virtual network with VXLAN identifier VNI.
2018-09-21 23:43:30 -04:00
2019-03-15 11:28:49 -04:00
Examples:
pvc network add 101 --description my-bridged-net --type bridged
2019-03-15 11:28:49 -04:00
> Creates vLAN 101 and a simple bridge on the VNI dev interface.
2019-12-30 13:29:07 -05:00
pvc network add 1001 --description my-managed-net --type managed --domain test.local --ipnet 10.1.1.0/24 --gateway 10.1.1.1
2019-03-15 11:28:49 -04:00
> Creates a VXLAN with ID 1001 on the VNI dev interface, with IPv4 managed networking.
IPv6 is fully supported with --ipnet6 and --gateway6 in addition to or instead of IPv4. PVC will configure DHCPv6 in a semi-managed configuration for the network if set.
2018-09-21 23:43:30 -04:00
"""
2019-12-29 16:13:32 -05:00
retcode, retmsg = pvc_network.net_add(config, vni, description, nettype, domain, name_servers, ip_network, ip_gateway, ip6_network, ip6_gateway, dhcp_flag, dhcp_start, dhcp_end)
cleanup(retcode, retmsg)
2018-09-21 23:43:30 -04:00
2018-09-23 15:26:20 -04:00
###############################################################################
# pvc network modify
###############################################################################
@click.command(name='modify', short_help='Modify an existing virtual network.')
@click.option(
'-d', '--description', 'description',
default=None,
2018-10-17 00:23:27 -04:00
help='Description of the network; must be unique and not contain whitespace.'
2018-09-23 15:26:20 -04:00
)
@click.option(
'-n', '--domain', 'domain',
default=None,
help='Domain name of the network.'
)
2019-12-08 23:32:03 -05:00
@click.option(
'--dns-server', 'name_servers',
multiple=True,
2019-12-08 23:59:17 -05:00
help='DNS nameserver for network; multiple entries may be specified (will overwrite all previous entries).'
2019-12-08 23:32:03 -05:00
)
2018-09-23 15:26:20 -04:00
@click.option(
2018-11-14 00:19:43 -05:00
'-i', '--ipnet', 'ip4_network',
2018-09-23 15:26:20 -04:00
default=None,
help='CIDR-format IPv4 network address for subnet; disable with "".'
)
@click.option(
'-i6', '--ipnet6', 'ip6_network',
default=None,
help='CIDR-format IPv6 network address for subnet; disable with "".'
2018-09-23 15:26:20 -04:00
)
@click.option(
2018-11-14 00:19:43 -05:00
'-g', '--gateway', 'ip4_gateway',
2018-09-23 15:26:20 -04:00
default=None,
help='Default IPv4 gateway address for subnet; disable with "".'
)
@click.option(
'-g6', '--gateway6', 'ip6_gateway',
default=None,
help='Default IPv6 gateway address for subnet; disable with "".'
2018-09-23 15:26:20 -04:00
)
@click.option(
'--dhcp/--no-dhcp', 'dhcp_flag',
is_flag=True,
2018-10-17 00:23:27 -04:00
default=None,
help='Enable/disable DHCPv4 for clients on subnet (DHCPv6 is always enabled if DHCPv6 network is set).'
2018-09-23 15:26:20 -04:00
)
@click.option(
'--dhcp-start', 'dhcp_start',
default=None,
help='DHCPvr range start address.'
)
@click.option(
'--dhcp-end', 'dhcp_end',
default=None,
help='DHCPv4 range end address.'
)
2018-09-23 15:26:20 -04:00
@click.argument(
'vni'
)
@cluster_req
2019-12-08 23:32:03 -05:00
def net_modify(vni, description, domain, name_servers, ip6_network, ip6_gateway, ip4_network, ip4_gateway, dhcp_flag, dhcp_start, dhcp_end):
2018-09-23 15:26:20 -04:00
"""
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
2018-09-23 15:26:20 -04:00
"""
2019-12-29 16:13:32 -05:00
retcode, retmsg = pvc_network.net_modify(config, vni, description, domain, name_servers, ip4_network, ip4_gateway, ip6_network, ip6_gateway, dhcp_flag, dhcp_start, dhcp_end)
cleanup(retcode, retmsg)
2018-09-23 15:26:20 -04:00
2018-09-21 23:43:30 -04:00
###############################################################################
# pvc network remove
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='remove', short_help='Remove a virtual network.')
2018-09-21 23:43:30 -04:00
@click.argument(
'net'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def net_remove(net, confirm_flag):
2018-09-21 23:43:30 -04:00
"""
2020-01-04 14:06:36 -05:00
Remove an existing virtual network NET; NET must be a VNI.
2018-09-21 23:43:30 -04:00
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 and not config['unsafe']:
try:
click.confirm('Remove network {}'.format(net), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2018-09-21 23:43:30 -04:00
2019-12-29 16:13:32 -05:00
retcode, retmsg = pvc_network.net_remove(config, net)
cleanup(retcode, retmsg)
2018-09-21 23:43:30 -04:00
2018-09-21 23:43:30 -04:00
###############################################################################
# pvc network info
###############################################################################
@click.command(name='info', short_help='Show details of a network.')
@click.argument(
'vni'
)
@click.option(
'-l', '--long', 'long_output', is_flag=True, default=False,
help='Display more detailed information.'
)
@cluster_req
def net_info(vni, long_output):
2018-09-21 23:43:30 -04:00
"""
Show information about virtual network VNI.
2018-09-21 23:43:30 -04:00
"""
2019-12-29 16:13:32 -05:00
retcode, retdata = pvc_network.net_info(config, vni)
2019-07-04 23:01:22 -04:00
if retcode:
retdata = pvc_network.format_info(config, retdata, long_output)
2019-12-29 16:13:32 -05:00
cleanup(retcode, retdata)
2018-09-21 23:43:30 -04:00
2018-09-21 23:43:30 -04:00
###############################################################################
# pvc network list
###############################################################################
@click.command(name='list', short_help='List all VM objects.')
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2018-09-21 23:43:30 -04:00
def net_list(limit):
"""
2020-01-04 14:06:36 -05:00
List all virtual networks; optionally only match VNIs or Descriptions matching regex LIMIT.
2018-09-21 23:43:30 -04:00
"""
2019-12-29 16:13:32 -05:00
retcode, retdata = pvc_network.net_list(config, limit)
2019-07-04 23:01:22 -04:00
if retcode:
retdata = pvc_network.format_list(config, retdata)
2019-12-29 16:13:32 -05:00
cleanup(retcode, retdata)
2018-09-21 23:43:30 -04:00
###############################################################################
# pvc network dhcp
###############################################################################
@click.group(name='dhcp', short_help='Manage IPv4 DHCP leases in a PVC virtual network.', context_settings=CONTEXT_SETTINGS)
def net_dhcp():
"""
Manage host IPv4 DHCP leases of a VXLAN network in the PVC cluster.
"""
pass
###############################################################################
2019-12-29 16:13:32 -05:00
# pvc network dhcp add
###############################################################################
@click.command(name='add', short_help='Add a DHCP static reservation.')
@click.argument(
'net'
)
@click.argument(
'ipaddr'
)
@click.argument(
'hostname'
)
@click.argument(
'macaddr'
)
@cluster_req
2019-12-29 16:13:32 -05:00
def net_dhcp_add(net, ipaddr, macaddr, hostname):
"""
2019-12-29 16:13:32 -05:00
Add a new DHCP static reservation of IP address IPADDR with hostname HOSTNAME for MAC address MACADDR to virtual network NET; NET must be a VNI.
"""
2019-12-29 16:13:32 -05:00
retcode, retmsg = pvc_network.net_dhcp_add(config, net, ipaddr, macaddr, hostname)
cleanup(retcode, retmsg)
###############################################################################
2019-12-29 16:13:32 -05:00
# pvc network dhcp remove
###############################################################################
@click.command(name='remove', short_help='Remove a DHCP static reservation.')
@click.argument(
'net'
)
@click.argument(
2019-12-29 16:13:32 -05:00
'macaddr'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
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 and not config['unsafe']:
try:
click.confirm('Remove DHCP lease for {} in network {}'.format(macaddr, net), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-05 00:49:50 -05:00
retcode, retmsg = pvc_network.net_dhcp_remove(config, net, macaddr)
2019-12-29 16:13:32 -05:00
cleanup(retcode, retmsg)
###############################################################################
2019-12-29 16:13:32 -05:00
# pvc network dhcp list
###############################################################################
2019-12-29 16:13:32 -05:00
@click.command(name='list', short_help='List active DHCP leases.')
@click.argument(
'net'
)
@click.argument(
'limit', default=None, required=False
)
2019-12-29 16:13:32 -05:00
@click.option(
'-s', '--static', 'only_static', is_flag=True, default=False,
help='Show only static leases.'
)
@cluster_req
2019-12-29 16:13:32 -05:00
def net_dhcp_list(net, limit, only_static):
"""
2019-12-29 16:13:32 -05:00
List all DHCP leases in virtual network NET; optionally only match elements matching regex LIMIT; NET must be a VNI.
"""
2019-12-29 16:13:32 -05:00
retcode, retdata = pvc_network.net_dhcp_list(config, net, limit, only_static)
2019-07-04 23:01:22 -04:00
if retcode:
retdata = pvc_network.format_list_dhcp(retdata)
2019-12-29 16:13:32 -05:00
cleanup(retcode, retdata)
###############################################################################
# pvc network acl
###############################################################################
@click.group(name='acl', short_help='Manage a PVC virtual network firewall ACL rule.', context_settings=CONTEXT_SETTINGS)
def net_acl():
"""
Manage firewall ACLs of a VXLAN network in the PVC cluster.
"""
pass
2018-09-21 23:43:30 -04:00
2018-10-17 00:23:27 -04:00
###############################################################################
# pvc network acl add
###############################################################################
@click.command(name='add', short_help='Add firewall ACL.')
@click.option(
'--in/--out', 'direction',
is_flag=True,
default=True, # inbound
2018-10-17 00:23:27 -04:00
help='Inbound or outbound ruleset.'
)
@click.option(
'-d', '--description', 'description',
required=True,
help='Description of the ACL; must be unique and not contain whitespace.'
)
@click.option(
'-r', '--rule', 'rule',
required=True,
help='NFT firewall rule.'
)
@click.option(
'-o', '--order', 'order',
default=None,
help='Order of rule in the chain (see "list"); defaults to last.'
)
@click.argument(
'net'
)
@cluster_req
2018-10-17 00:23:27 -04:00
def net_acl_add(net, direction, description, rule, order):
"""
2019-12-29 16:13:32 -05:00
Add a new NFT firewall rule to network NET; the rule is a literal NFT rule belonging to the forward table for the client network; NET must be a VNI.
2018-10-17 00:23:27 -04:00
NOTE: All client networks are default-allow in both directions; deny rules MUST be added here at the end of the sequence for a default-deny setup.
NOTE: Ordering places the rule at the specified ID, not before it; the old rule of that ID and all subsequent rules will be moved down.
2019-12-29 16:13:32 -05:00
NOTE: Descriptions are used as names, and must be unique within a network (both directions).
2018-10-17 00:23:27 -04:00
Example:
pvc network acl add 1001 --in --rule "tcp dport 22 ct state new accept" --description "ssh-in" --order 3
"""
2019-12-29 16:13:32 -05:00
if direction:
direction = 'in'
else:
direction = 'out'
2018-10-17 00:23:27 -04:00
2019-12-29 16:13:32 -05:00
retcode, retmsg = pvc_network.net_acl_add(config, net, direction, description, rule, order)
cleanup(retcode, retmsg)
2018-10-17 00:23:27 -04:00
2018-10-17 00:23:27 -04:00
###############################################################################
# pvc network acl remove
###############################################################################
@click.command(name='remove', short_help='Remove firewall ACL.')
@click.argument(
'net'
)
@click.argument(
'rule',
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def net_acl_remove(net, rule, confirm_flag):
2018-10-17 00:23:27 -04:00
"""
2019-12-29 16:13:32 -05:00
Remove an NFT firewall rule RULE from network NET; RULE must be a description; NET must be a VNI.
2018-10-17 00:23:27 -04:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove ACL {} in network {}'.format(rule, net), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2018-10-17 00:23:27 -04:00
2019-12-29 16:13:32 -05:00
retcode, retmsg = pvc_network.net_acl_remove(config, net, rule)
cleanup(retcode, retmsg)
2018-10-17 00:23:27 -04:00
###############################################################################
# pvc network acl list
###############################################################################
@click.command(name='list', short_help='List firewall ACLs.')
@click.option(
'--in/--out', 'direction',
is_flag=True,
2018-10-17 20:05:22 -04:00
required=False,
2018-10-17 00:23:27 -04:00
default=None,
2018-10-17 20:05:22 -04:00
help='Inbound or outbound rule set only.'
2018-10-17 00:23:27 -04:00
)
@click.argument(
'net'
)
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2018-10-17 00:23:27 -04:00
def net_acl_list(net, limit, direction):
"""
List all NFT firewall rules in network NET; optionally only match elements matching description regex LIMIT; NET can be either a VNI or description.
"""
2019-12-29 16:13:32 -05:00
if direction is not None:
if direction:
direction = 'in'
else:
direction = 'out'
2018-09-21 23:43:30 -04:00
2019-12-29 16:13:32 -05:00
retcode, retdata = pvc_network.net_acl_list(config, net, limit, direction)
2019-07-04 23:01:22 -04:00
if retcode:
retdata = pvc_network.format_list_acl(retdata)
2019-12-29 16:13:32 -05:00
cleanup(retcode, retdata)
2018-09-21 23:43:30 -04:00
2021-06-21 17:12:53 -04:00
###############################################################################
# pvc network sriov
###############################################################################
@click.group(name='sriov', short_help='Manage SR-IOV network resources.', context_settings=CONTEXT_SETTINGS)
def net_sriov():
"""
Manage SR-IOV network resources on nodes (PFs and VFs).
"""
pass
###############################################################################
# pvc network sriov pf
###############################################################################
@click.group(name='pf', short_help='Manage PF devices.', context_settings=CONTEXT_SETTINGS)
def net_sriov_pf():
"""
Manage SR-IOV PF devices on nodes.
"""
pass
###############################################################################
# pvc network sriov pf list
###############################################################################
@click.command(name='list', short_help='List PF devices.')
@click.argument(
'node'
)
@cluster_req
def net_sriov_pf_list(node):
"""
List all SR-IOV PFs on NODE.
"""
retcode, retdata = pvc_network.net_sriov_pf_list(config, node)
if retcode:
retdata = pvc_network.format_list_sriov_pf(retdata)
cleanup(retcode, retdata)
###############################################################################
# pvc network sriov vf
###############################################################################
@click.group(name='vf', short_help='Manage VF devices.', context_settings=CONTEXT_SETTINGS)
def net_sriov_vf():
"""
Manage SR-IOV VF devices on nodes.
"""
pass
###############################################################################
# pvc network sriov vf set
###############################################################################
@click.command(name='set', short_help='Set VF device properties.')
@click.option(
'--vlan-id', 'vlan_id', default=None, show_default=False,
help='The vLAN ID for vLAN tagging.'
)
@click.option(
'--qos-prio', 'vlan_qos', default=None, show_default=False,
help='The vLAN QOS priority.'
)
@click.option(
'--tx-min', 'tx_rate_min', default=None, show_default=False,
help='The minimum TX rate.'
)
@click.option(
'--tx-max', 'tx_rate_max', default=None, show_default=False,
help='The maximum TX rate.'
)
@click.option(
'--link-state', 'link_state', default=None, show_default=False,
type=click.Choice(['auto', 'enable', 'disable']),
help='The administrative link state.'
)
@click.option(
'--spoof-check/--no-spoof-check', 'spoof_check', is_flag=True, default=None, show_default=False,
help='Enable or disable spoof checking.'
)
@click.option(
'--trust/--no-trust', 'trust', is_flag=True, default=None, show_default=False,
help='Enable or disable VF user trust.'
)
@click.option(
'--query-rss/--no-query-rss', 'query_rss', is_flag=True, default=None, show_default=False,
help='Enable or disable query RSS support.'
)
2021-06-21 17:12:53 -04:00
@click.argument(
'node'
)
@click.argument(
'vf'
)
@cluster_req
def net_sriov_vf_set(node, vf, vlan_id, vlan_qos, tx_rate_min, tx_rate_max, link_state, spoof_check, trust, query_rss):
2021-06-21 17:12:53 -04:00
"""
Set a property of SR-IOV VF on NODE.
"""
if vlan_id is None and vlan_qos is None and tx_rate_min is None and tx_rate_max is None and link_state is None and spoof_check is None and trust is None and query_rss is None:
cleanup(False, 'At least one configuration property must be specified to update.')
retcode, retmsg = pvc_network.net_sriov_vf_set(config, node, vf, vlan_id, vlan_qos, tx_rate_min, tx_rate_max, link_state, spoof_check, trust, query_rss)
cleanup(retcode, retmsg)
2021-06-21 17:12:53 -04:00
###############################################################################
# pvc network sriov vf list
###############################################################################
@click.command(name='list', short_help='List VF devices.')
@click.argument(
'node'
)
@click.argument(
'pf', default=None, required=False
)
@cluster_req
def net_sriov_vf_list(node, pf):
"""
List all SR-IOV VFs on NODE, optionally limited to device PF.
"""
retcode, retdata = pvc_network.net_sriov_vf_list(config, node, pf)
if retcode:
retdata = pvc_network.format_list_sriov_vf(retdata)
cleanup(retcode, retdata)
###############################################################################
# pvc network sriov vf info
###############################################################################
@click.command(name='info', short_help='List VF devices.')
@click.argument(
'node'
)
@click.argument(
'vf'
)
@cluster_req
def net_sriov_vf_info(node, vf):
"""
Show details of the SR-IOV VF on NODE.
"""
retcode, retdata = pvc_network.net_sriov_vf_info(config, node, vf)
if retcode:
retdata = pvc_network.format_info_sriov_vf(config, retdata, node)
cleanup(retcode, retdata)
2018-10-27 18:11:58 -04:00
###############################################################################
# pvc storage
###############################################################################
# Note: The prefix `storage` allows future potential storage subsystems.
# Since Ceph is the only section not abstracted by PVC directly
# (i.e. it references Ceph-specific concepts), this makes more
# sense in the long-term.
###############################################################################
@click.group(name='storage', short_help='Manage the PVC storage cluster.', context_settings=CONTEXT_SETTINGS)
def cli_storage():
"""
Manage the storage of the PVC cluster.
"""
pass
###############################################################################
# pvc storage status
2018-10-27 18:11:58 -04:00
###############################################################################
@click.command(name='status', short_help='Show storage cluster status.')
@cluster_req
2018-10-27 18:11:58 -04:00
def ceph_status():
"""
Show detailed status of the storage cluster.
"""
2019-12-29 20:33:51 -05:00
retcode, retdata = pvc_ceph.ceph_status(config)
2019-07-05 00:44:40 -04:00
if retcode:
retdata = pvc_ceph.format_raw_output(retdata)
2019-12-29 20:33:51 -05:00
cleanup(retcode, retdata)
2019-07-08 10:56:33 -04:00
2019-07-08 10:56:33 -04:00
###############################################################################
# pvc storage util
2019-07-08 10:56:33 -04:00
###############################################################################
2019-12-29 20:33:51 -05:00
@click.command(name='util', short_help='Show storage cluster utilization.')
@cluster_req
2019-12-29 20:33:51 -05:00
def ceph_util():
2019-07-08 10:56:33 -04:00
"""
Show utilization of the storage cluster.
"""
2019-12-29 20:33:51 -05:00
retcode, retdata = pvc_ceph.ceph_util(config)
2019-07-08 10:56:33 -04:00
if retcode:
retdata = pvc_ceph.format_raw_output(retdata)
2019-12-29 20:33:51 -05:00
cleanup(retcode, retdata)
2018-10-27 18:11:58 -04:00
2020-08-24 14:57:52 -04:00
###############################################################################
# pvc storage benchmark
###############################################################################
@click.group(name='benchmark', short_help='Run or view cluster storage benchmarks.')
@cluster_req
def ceph_benchmark():
"""
Run or view benchmarks of the storage cluster.
"""
pass
2020-08-24 14:57:52 -04:00
###############################################################################
# pvc storage benchmark run
###############################################################################
@click.command(name='run', short_help='Run a storage benchmark.')
@click.argument(
'pool'
)
@cluster_req
def ceph_benchmark_run(pool):
"""
Run a storage benchmark on POOL in the background.
"""
2020-08-25 12:16:23 -04:00
try:
click.confirm('NOTE: Storage benchmarks generate significant load on the cluster and can take a very long time to complete on slow storage. They should be run sparingly. Continue', prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
2020-08-25 12:16:23 -04:00
exit(0)
2020-08-24 14:57:52 -04:00
retcode, retmsg = pvc_ceph.ceph_benchmark_run(config, pool)
cleanup(retcode, retmsg)
2020-08-25 12:16:23 -04:00
###############################################################################
# pvc storage benchmark info
###############################################################################
@click.command(name='info', short_help='Show detailed storage benchmark results.')
@click.argument(
'job', required=True
)
@cluster_req
def ceph_benchmark_info(job):
"""
Show full details of storage benchmark JOB.
"""
retcode, retdata = pvc_ceph.ceph_benchmark_list(config, job)
if retcode:
retdata = pvc_ceph.format_info_benchmark(config, retdata)
cleanup(retcode, retdata)
2020-08-24 14:57:52 -04:00
###############################################################################
# pvc storage benchmark list
###############################################################################
@click.command(name='list', short_help='List storage benchmark results.')
@click.argument(
'job', default=None, required=False
)
@cluster_req
def ceph_benchmark_list(job):
"""
List all Ceph storage benchmarks; optionally only match JOB.
"""
retcode, retdata = pvc_ceph.ceph_benchmark_list(config, job)
if retcode:
2020-08-25 12:16:23 -04:00
retdata = pvc_ceph.format_list_benchmark(config, retdata)
2020-08-24 14:57:52 -04:00
cleanup(retcode, retdata)
2018-10-27 18:11:58 -04:00
###############################################################################
# pvc storage osd
2018-10-27 18:11:58 -04:00
###############################################################################
@click.group(name='osd', short_help='Manage OSDs in the PVC storage cluster.', context_settings=CONTEXT_SETTINGS)
def ceph_osd():
"""
Manage the Ceph OSDs of the PVC cluster.
"""
pass
2018-10-27 18:11:58 -04:00
2018-10-28 22:15:25 -04:00
###############################################################################
# pvc storage osd add
2018-10-28 22:15:25 -04:00
###############################################################################
@click.command(name='add', short_help='Add new OSD.')
@click.argument(
'node'
)
@click.argument(
'device'
)
@click.option(
'-w', '--weight', 'weight',
default=1.0, show_default=True,
help='Weight of the OSD within the CRUSH map.'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def ceph_osd_add(node, device, weight, confirm_flag):
2018-10-28 22:15:25 -04:00
"""
2020-01-04 14:06:36 -05:00
Add a new Ceph OSD on node NODE with block device DEVICE.
2018-10-28 22:15:25 -04:00
"""
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)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_osd_add(config, node, device, weight)
cleanup(retcode, retmsg)
2018-10-28 22:15:25 -04:00
2018-10-28 22:15:25 -04:00
###############################################################################
# pvc storage osd remove
2018-10-28 22:15:25 -04:00
###############################################################################
@click.command(name='remove', short_help='Remove OSD.')
@click.argument(
'osdid'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def ceph_osd_remove(osdid, confirm_flag):
2018-10-28 22:15:25 -04:00
"""
2020-01-04 14:06:36 -05:00
Remove a Ceph OSD with ID OSDID.
DANGER: This will completely remove the OSD from the cluster. OSDs will rebalance which may negatively affect performance or available space.
2018-10-28 22:15:25 -04:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove OSD {}'.format(osdid), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_osd_remove(config, osdid)
cleanup(retcode, retmsg)
2018-10-28 22:15:25 -04:00
2018-11-01 22:00:59 -04:00
###############################################################################
# pvc storage osd in
2018-11-01 22:00:59 -04:00
###############################################################################
@click.command(name='in', short_help='Online OSD.')
@click.argument(
'osdid'
)
@cluster_req
2018-11-01 22:00:59 -04:00
def ceph_osd_in(osdid):
"""
2020-01-04 14:06:36 -05:00
Set a Ceph OSD with ID OSDID online.
2018-11-01 22:00:59 -04:00
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_osd_state(config, osdid, 'in')
cleanup(retcode, retmsg)
2018-11-01 22:00:59 -04:00
2018-11-01 22:00:59 -04:00
###############################################################################
# pvc storage osd out
2018-11-01 22:00:59 -04:00
###############################################################################
@click.command(name='out', short_help='Offline OSD.')
@click.argument(
'osdid'
)
@cluster_req
2018-11-01 22:00:59 -04:00
def ceph_osd_out(osdid):
"""
2020-01-04 14:06:36 -05:00
Set a Ceph OSD with ID OSDID offline.
2018-11-01 22:00:59 -04:00
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_osd_state(config, osdid, 'out')
cleanup(retcode, retmsg)
2018-11-01 22:00:59 -04:00
2018-11-01 22:00:59 -04:00
###############################################################################
# pvc storage osd set
2018-11-01 22:00:59 -04:00
###############################################################################
@click.command(name='set', short_help='Set property.')
@click.argument(
'osd_property'
)
@cluster_req
2018-11-01 22:00:59 -04:00
def ceph_osd_set(osd_property):
"""
Set a Ceph OSD property OSD_PROPERTY on the cluster.
Valid properties are:
2020-11-06 19:44:14 -05:00
full|pause|noup|nodown|noout|noin|nobackfill|norebalance|norecover|noscrub|nodeep-scrub|notieragent|sortbitwise|recovery_deletes|require_jewel_osds|require_kraken_osds
2018-11-01 22:00:59 -04:00
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_osd_option(config, osd_property, 'set')
cleanup(retcode, retmsg)
2018-11-01 22:00:59 -04:00
2018-11-01 22:00:59 -04:00
###############################################################################
# pvc storage osd unset
2018-11-01 22:00:59 -04:00
###############################################################################
@click.command(name='unset', short_help='Unset property.')
@click.argument(
'osd_property'
)
@cluster_req
2018-11-01 22:00:59 -04:00
def ceph_osd_unset(osd_property):
"""
Unset a Ceph OSD property OSD_PROPERTY on the cluster.
Valid properties are:
2020-11-06 19:44:14 -05:00
full|pause|noup|nodown|noout|noin|nobackfill|norebalance|norecover|noscrub|nodeep-scrub|notieragent|sortbitwise|recovery_deletes|require_jewel_osds|require_kraken_osds
2018-11-01 22:00:59 -04:00
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_osd_option(config, osd_property, 'unset')
cleanup(retcode, retmsg)
2018-11-01 22:00:59 -04:00
2018-10-30 09:17:32 -04:00
###############################################################################
# pvc storage osd list
2018-10-30 09:17:32 -04:00
###############################################################################
@click.command(name='list', short_help='List cluster OSDs.')
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2018-10-30 09:17:32 -04:00
def ceph_osd_list(limit):
"""
2020-01-04 14:06:36 -05:00
List all Ceph OSDs; optionally only match elements matching ID regex LIMIT.
2018-10-30 09:17:32 -04:00
"""
2019-12-29 20:33:51 -05:00
retcode, retdata = pvc_ceph.ceph_osd_list(config, limit)
2019-07-05 00:29:47 -04:00
if retcode:
retdata = pvc_ceph.format_list_osd(retdata)
2019-12-29 20:33:51 -05:00
cleanup(retcode, retdata)
2018-10-30 09:17:32 -04:00
2018-10-27 18:11:58 -04:00
###############################################################################
# pvc storage pool
2018-10-27 18:11:58 -04:00
###############################################################################
@click.group(name='pool', short_help='Manage RBD pools in the PVC storage cluster.', context_settings=CONTEXT_SETTINGS)
def ceph_pool():
"""
Manage the Ceph RBD pools of the PVC cluster.
"""
pass
2018-09-21 23:43:30 -04:00
2018-10-31 23:38:17 -04:00
###############################################################################
# pvc storage pool add
2018-10-31 23:38:17 -04:00
###############################################################################
@click.command(name='add', short_help='Add new RBD pool.')
@click.argument(
'name'
)
@click.argument(
'pgs'
)
@click.option(
'--replcfg', 'replcfg',
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.
"""
)
@cluster_req
def ceph_pool_add(name, pgs, replcfg):
2018-10-31 23:38:17 -04:00
"""
Add a new Ceph RBD pool with name NAME and PGS placement groups.
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_pool_add(config, name, pgs, replcfg)
cleanup(retcode, retmsg)
2018-10-31 23:38:17 -04:00
2018-10-31 23:38:17 -04:00
###############################################################################
# pvc storage pool remove
2018-10-31 23:38:17 -04:00
###############################################################################
@click.command(name='remove', short_help='Remove RBD pool.')
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def ceph_pool_remove(name, confirm_flag):
2018-10-31 23:38:17 -04:00
"""
Remove a Ceph RBD pool with name NAME and all volumes on it.
DANGER: This will completely remove the pool and all volumes contained in it from the cluster.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove RBD pool {}'.format(name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_pool_remove(config, name)
cleanup(retcode, retmsg)
2018-10-31 23:38:17 -04:00
2018-10-31 23:38:17 -04:00
###############################################################################
# pvc storage pool list
2018-10-31 23:38:17 -04:00
###############################################################################
@click.command(name='list', short_help='List cluster RBD pools.')
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2018-10-31 23:38:17 -04:00
def ceph_pool_list(limit):
"""
2020-01-04 14:06:36 -05:00
List all Ceph RBD pools; optionally only match elements matching name regex LIMIT.
2018-10-31 23:38:17 -04:00
"""
2019-12-29 20:33:51 -05:00
retcode, retdata = pvc_ceph.ceph_pool_list(config, limit)
2019-07-05 00:29:47 -04:00
if retcode:
retdata = pvc_ceph.format_list_pool(retdata)
2019-12-29 20:33:51 -05:00
cleanup(retcode, retdata)
2018-10-31 23:38:17 -04:00
2019-06-19 00:23:14 -04:00
###############################################################################
# pvc storage volume
2019-06-19 00:23:14 -04:00
###############################################################################
@click.group(name='volume', short_help='Manage RBD volumes in the PVC storage cluster.', context_settings=CONTEXT_SETTINGS)
def ceph_volume():
"""
Manage the Ceph RBD volumes of the PVC cluster.
"""
pass
2019-06-19 00:23:14 -04:00
###############################################################################
# pvc storage volume add
###############################################################################
@click.command(name='add', short_help='Add new RBD volume.')
@click.argument(
'pool'
)
@click.argument(
'name'
)
@click.argument(
'size'
)
@cluster_req
def ceph_volume_add(pool, name, size):
"""
Add a new Ceph RBD volume with name NAME and size SIZE [in human units, e.g. 1024M, 20G, etc.] to pool POOL.
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_volume_add(config, pool, name, size)
cleanup(retcode, retmsg)
###############################################################################
# pvc storage volume upload
###############################################################################
@click.command(name='upload', short_help='Upload a local image file to RBD volume.')
@click.argument(
'pool'
)
@click.argument(
'name'
)
@click.argument(
'image_file'
)
@click.option(
'-f', '--format', 'image_format',
default='raw', show_default=True,
help='The format of the source image.'
)
@cluster_req
def ceph_volume_upload(pool, name, image_format, image_file):
"""
Upload a disk image file IMAGE_FILE to the RBD volume NAME in pool POOL.
The volume NAME must exist in the pool before uploading to it, and must be large enough to fit the disk image in raw format.
If the image format is "raw", the image is uploaded directly to the target volume without modification. Otherwise, it will be converted into raw format by "qemu-img convert" on the remote side before writing using a temporary volume. The image format must be a valid format recognized by "qemu-img", such as "vmdk" or "qcow2".
"""
if not os.path.exists(image_file):
click.echo("ERROR: File '{}' does not exist!".format(image_file))
exit(1)
retcode, retmsg = pvc_ceph.ceph_volume_upload(config, pool, name, image_format, image_file)
cleanup(retcode, retmsg)
###############################################################################
# pvc storage volume remove
###############################################################################
@click.command(name='remove', short_help='Remove RBD volume.')
@click.argument(
'pool'
)
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def ceph_volume_remove(pool, name, confirm_flag):
"""
Remove a Ceph RBD volume with name NAME from pool POOL.
DANGER: This will completely remove the volume and all data contained in it.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove volume {}/{}'.format(pool, name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_volume_remove(config, pool, name)
cleanup(retcode, retmsg)
###############################################################################
# pvc storage volume resize
###############################################################################
@click.command(name='resize', short_help='Resize RBD volume.')
@click.argument(
'pool'
)
@click.argument(
'name'
)
@click.argument(
'size'
)
@cluster_req
def ceph_volume_resize(pool, name, size):
"""
Resize an existing Ceph RBD volume with name NAME in pool POOL to size SIZE [in human units, e.g. 1024M, 20G, etc.].
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_volume_modify(config, pool, name, new_size=size)
cleanup(retcode, retmsg)
###############################################################################
# pvc storage volume rename
###############################################################################
@click.command(name='rename', short_help='Rename RBD volume.')
@click.argument(
'pool'
)
@click.argument(
'name'
)
@click.argument(
'new_name'
)
@cluster_req
def ceph_volume_rename(pool, name, new_name):
"""
Rename an existing Ceph RBD volume with name NAME in pool POOL to name NEW_NAME.
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_volume_modify(config, pool, name, new_name=new_name)
cleanup(retcode, retmsg)
2019-10-10 14:11:13 -04:00
###############################################################################
# pvc storage volume clone
2019-10-10 14:11:13 -04:00
###############################################################################
2019-12-29 20:33:51 -05:00
@click.command(name='clone', short_help='Clone RBD volume.')
2019-10-10 14:11:13 -04:00
@click.argument(
'pool'
)
@click.argument(
'name'
)
@click.argument(
'new_name'
)
@cluster_req
2019-10-10 14:11:13 -04:00
def ceph_volume_clone(pool, name, new_name):
"""
Clone a Ceph RBD volume with name NAME in pool POOL to name NEW_NAME in pool POOL.
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_volume_clone(config, pool, name, new_name)
cleanup(retcode, retmsg)
2019-10-10 14:11:13 -04:00
###############################################################################
# pvc storage volume list
###############################################################################
@click.command(name='list', short_help='List cluster RBD volumes.')
@click.argument(
'limit', default=None, required=False
)
2019-06-19 14:11:03 -04:00
@click.option(
'-p', '--pool', 'pool',
default=None, show_default=True,
2019-06-19 14:11:03 -04:00
help='Show volumes from this pool only.'
)
@cluster_req
2019-06-19 14:11:03 -04:00
def ceph_volume_list(limit, pool):
"""
2020-01-04 14:06:36 -05:00
List all Ceph RBD volumes; optionally only match elements matching name regex LIMIT.
"""
2019-12-29 20:33:51 -05:00
retcode, retdata = pvc_ceph.ceph_volume_list(config, limit, pool)
2019-07-05 00:29:47 -04:00
if retcode:
retdata = pvc_ceph.format_list_volume(retdata)
2019-12-29 20:33:51 -05:00
cleanup(retcode, retdata)
2019-06-19 00:23:14 -04:00
###############################################################################
# pvc storage volume snapshot
2019-06-19 00:23:14 -04:00
###############################################################################
@click.group(name='snapshot', short_help='Manage RBD volume snapshots in the PVC storage cluster.', context_settings=CONTEXT_SETTINGS)
def ceph_volume_snapshot():
"""
Manage the Ceph RBD volume snapshots of the PVC cluster.
"""
pass
2019-06-19 00:23:14 -04:00
###############################################################################
# pvc storage volume snapshot add
###############################################################################
@click.command(name='add', short_help='Add new RBD volume snapshot.')
@click.argument(
'pool'
)
@click.argument(
'volume'
)
@click.argument(
'name'
)
@cluster_req
2019-06-19 00:23:14 -04:00
def ceph_volume_snapshot_add(pool, volume, name):
"""
Add a snapshot with name NAME of Ceph RBD volume VOLUME in pool POOL.
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_snapshot_add(config, pool, volume, name)
cleanup(retcode, retmsg)
###############################################################################
# pvc storage volume snapshot rename
###############################################################################
@click.command(name='rename', short_help='Rename RBD volume snapshot.')
@click.argument(
'pool'
)
@click.argument(
'volume'
)
@click.argument(
'name'
)
@click.argument(
'new_name'
)
@cluster_req
def ceph_volume_snapshot_rename(pool, volume, name, new_name):
"""
Rename an existing Ceph RBD volume snapshot with name NAME to name NEW_NAME for volume VOLUME in pool POOL.
"""
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_snapshot_modify(config, pool, volume, name, new_name=new_name)
cleanup(retcode, retmsg)
###############################################################################
# pvc storage volume snapshot remove
###############################################################################
@click.command(name='remove', short_help='Remove RBD volume snapshot.')
@click.argument(
'pool'
)
@click.argument(
'volume'
)
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def ceph_volume_snapshot_remove(pool, volume, name, confirm_flag):
"""
Remove a Ceph RBD volume snapshot with name NAME from volume VOLUME in pool POOL.
DANGER: This will completely remove the snapshot.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove snapshot {} for volume {}/{}'.format(name, pool, volume), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2019-12-29 20:33:51 -05:00
retcode, retmsg = pvc_ceph.ceph_snapshot_remove(config, pool, volume, name)
cleanup(retcode, retmsg)
###############################################################################
# pvc storage volume snapshot list
###############################################################################
@click.command(name='list', short_help='List cluster RBD volume shapshots.')
@click.argument(
2019-06-19 15:22:44 -04:00
'limit', default=None, required=False
)
2019-06-19 15:22:44 -04:00
@click.option(
'-p', '--pool', 'pool',
default=None, show_default=True,
2019-06-19 15:22:44 -04:00
help='Show snapshots from this pool only.'
)
2019-06-19 15:22:44 -04:00
@click.option(
2020-01-04 11:58:30 -05:00
'-o', '--volume', 'volume',
default=None, show_default=True,
2019-06-19 15:22:44 -04:00
help='Show snapshots from this volume only.'
)
@cluster_req
2019-06-19 00:23:14 -04:00
def ceph_volume_snapshot_list(pool, volume, limit):
"""
2019-06-19 15:22:44 -04:00
List all Ceph RBD volume snapshots; optionally only match elements matching name regex LIMIT.
"""
2019-12-29 20:33:51 -05:00
retcode, retdata = pvc_ceph.ceph_snapshot_list(config, limit, volume, pool)
2019-07-05 00:29:47 -04:00
if retcode:
retdata = pvc_ceph.format_list_snapshot(retdata)
2019-12-29 20:33:51 -05:00
cleanup(retcode, retdata)
2020-01-02 11:18:46 -05:00
###############################################################################
# pvc provisioner
###############################################################################
@click.group(name='provisioner', short_help='Manage PVC provisioner.', context_settings=CONTEXT_SETTINGS)
def cli_provisioner():
"""
Manage the PVC provisioner.
"""
pass
2020-01-02 11:18:46 -05:00
###############################################################################
# pvc provisioner template
###############################################################################
@click.group(name='template', short_help='Manage PVC provisioner templates.', context_settings=CONTEXT_SETTINGS)
def provisioner_template():
"""
Manage the PVC provisioner template system.
"""
pass
2020-01-02 11:18:46 -05:00
###############################################################################
# pvc provisioner template list
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='list', short_help='List all templates.')
2020-01-02 11:18:46 -05:00
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2020-01-04 11:58:30 -05:00
def provisioner_template_list(limit):
2020-01-02 11:18:46 -05:00
"""
List all templates in the PVC cluster provisioner.
"""
retcode, retdata = pvc_provisioner.template_list(config, limit)
if retcode:
retdata = pvc_provisioner.format_list_template(retdata)
2020-01-02 11:18:46 -05:00
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template system
###############################################################################
@click.group(name='system', short_help='Manage PVC provisioner system templates.', context_settings=CONTEXT_SETTINGS)
def provisioner_template_system():
"""
Manage the PVC provisioner system templates.
"""
pass
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template system list
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='list', short_help='List all system templates.')
2020-01-04 11:58:30 -05:00
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2020-01-04 11:58:30 -05:00
def provisioner_template_system_list(limit):
"""
List all system templates in the PVC cluster provisioner.
"""
retcode, retdata = pvc_provisioner.template_list(config, limit, template_type='system')
if retcode:
retdata = pvc_provisioner.format_list_template(retdata, template_type='system')
2020-01-04 11:58:30 -05:00
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template system add
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='add', short_help='Add new system template.')
2020-01-04 11:58:30 -05:00
@click.argument(
'name'
)
@click.option(
'-u', '--vcpus', 'vcpus',
required=True, type=int,
help='The number of vCPUs.'
)
@click.option(
'-m', '--vram', 'vram',
required=True, type=int,
help='The amount of vRAM (in MB).'
)
@click.option(
'-s/-S', '--serial/--no-serial', 'serial',
2020-01-04 11:58:30 -05:00
is_flag=True, default=False,
help='Enable the virtual serial console.'
)
@click.option(
'-n/-N', '--vnc/--no-vnc', 'vnc',
2020-01-04 11:58:30 -05:00
is_flag=True, default=False,
help='Enable/disable the VNC console.'
2020-01-04 11:58:30 -05:00
)
@click.option(
'-b', '--vnc-bind', 'vnc_bind',
default=None,
help='Bind VNC to this IP address instead of localhost.'
)
@click.option(
'--node-limit', 'node_limit',
default=None,
help='Limit VM operation to this CSV list of node(s).'
)
@click.option(
'--node-selector', 'node_selector',
type=click.Choice(['mem', 'vcpus', 'vms', 'load', 'none'], case_sensitive=False),
default='none',
help='Method to determine optimal target node during autoselect; "none" will use the default for the cluster.'
2020-01-04 11:58:30 -05:00
)
@click.option(
'--node-autostart', 'node_autostart',
is_flag=True, default=False,
help='Autostart VM with their parent Node on first/next boot.'
)
@click.option(
'--migration-method', 'migration_method',
type=click.Choice(['none', 'live', 'shutdown'], case_sensitive=False),
default=None, # Use cluster default
help='The preferred migration method of the VM between nodes'
)
@cluster_req
def provisioner_template_system_add(name, vcpus, vram, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, migration_method):
2020-01-04 11:58:30 -05:00
"""
Add a new system template NAME to the PVC cluster provisioner.
"""
params = dict()
params['name'] = name
params['vcpus'] = vcpus
params['vram'] = vram
2020-01-04 11:58:30 -05:00
params['serial'] = serial
params['vnc'] = vnc
if vnc:
params['vnc_bind'] = vnc_bind
if node_limit:
params['node_limit'] = node_limit
if node_selector:
params['node_selector'] = node_selector
if node_autostart:
params['node_autostart'] = node_autostart
if migration_method:
params['migration_method'] = migration_method
2020-01-04 11:58:30 -05:00
retcode, retdata = pvc_provisioner.template_add(config, params, template_type='system')
cleanup(retcode, retdata)
###############################################################################
# pvc provisioner template system modify
###############################################################################
@click.command(name='modify', short_help='Modify an existing system template.')
@click.argument(
'name'
)
@click.option(
'-u', '--vcpus', 'vcpus',
type=int,
help='The number of vCPUs.'
)
@click.option(
'-m', '--vram', 'vram',
type=int,
help='The amount of vRAM (in MB).'
)
@click.option(
'-s/-S', '--serial/--no-serial', 'serial',
is_flag=True, default=None,
help='Enable the virtual serial console.'
)
@click.option(
'-n/-N', '--vnc/--no-vnc', 'vnc',
is_flag=True, default=None,
help='Enable/disable the VNC console.'
)
@click.option(
'-b', '--vnc-bind', 'vnc_bind',
help='Bind VNC to this IP address instead of localhost.'
)
@click.option(
'--node-limit', 'node_limit',
help='Limit VM operation to this CSV list of node(s).'
)
@click.option(
'--node-selector', 'node_selector',
type=click.Choice(['mem', 'vcpus', 'vms', 'load', 'none'], case_sensitive=False),
help='Method to determine optimal target node during autoselect; "none" will use the default for the cluster.'
)
@click.option(
'--node-autostart', 'node_autostart',
is_flag=True, default=None,
help='Autostart VM with their parent Node on first/next boot.'
)
@click.option(
'--migration-method', 'migration_method',
type=click.Choice(['none', 'live', 'shutdown'], case_sensitive=False),
default=None, # Use cluster default
help='The preferred migration method of the VM between nodes'
)
@cluster_req
def provisioner_template_system_modify(name, vcpus, vram, serial, vnc, vnc_bind, node_limit, node_selector, node_autostart, migration_method):
"""
Add a new system template NAME to the PVC cluster provisioner.
"""
params = dict()
params['vcpus'] = vcpus
params['vram'] = vram
params['serial'] = serial
params['vnc'] = vnc
params['vnc_bind'] = vnc_bind
params['node_limit'] = node_limit
params['node_selector'] = node_selector
params['node_autostart'] = node_autostart
params['migration_method'] = migration_method
retcode, retdata = pvc_provisioner.template_modify(config, params, name, template_type='system')
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template system remove
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='remove', short_help='Remove system template.')
2020-01-04 11:58:30 -05:00
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_template_system_remove(name, confirm_flag):
2020-01-04 11:58:30 -05:00
"""
2020-01-04 13:04:01 -05:00
Remove system template NAME from the PVC cluster provisioner.
2020-01-04 11:58:30 -05:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove system template {}'.format(name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-04 11:58:30 -05:00
retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='system')
cleanup(retcode, retdata)
###############################################################################
# pvc provisioner template network
###############################################################################
@click.group(name='network', short_help='Manage PVC provisioner network templates.', context_settings=CONTEXT_SETTINGS)
def provisioner_template_network():
"""
Manage the PVC provisioner network templates.
"""
pass
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template network list
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='list', short_help='List all network templates.')
2020-01-04 11:58:30 -05:00
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2020-01-04 11:58:30 -05:00
def provisioner_template_network_list(limit):
"""
List all network templates in the PVC cluster provisioner.
"""
retcode, retdata = pvc_provisioner.template_list(config, limit, template_type='network')
if retcode:
retdata = pvc_provisioner.format_list_template(retdata, template_type='network')
2020-01-04 11:58:30 -05:00
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template network add
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='add', short_help='Add new network template.')
2020-01-04 11:58:30 -05:00
@click.argument(
'name'
)
@click.option(
'-m', '--mac-template', 'mac_template',
default=None,
help='Use this template for MAC addresses.'
)
@cluster_req
2020-01-04 11:58:30 -05:00
def provisioner_template_network_add(name, mac_template):
"""
Add a new network template to the PVC cluster provisioner.
MAC address templates are used to provide predictable MAC addresses for provisioned VMs.
The normal format of a MAC template is:
{prefix}:XX:XX:{vmid}{netid}
The {prefix} variable is replaced by the provisioner with a standard prefix ("52:54:01"),
which is different from the randomly-generated MAC prefix ("52:54:00") to avoid accidental
overlap of MAC addresses.
The {vmid} variable is replaced by a single hexidecimal digit representing the VM's ID,
the numerical suffix portion of its name; VMs without a suffix numeral have ID 0. VMs with
IDs greater than 15 (hexidecimal "f") will wrap back to 0.
The {netid} variable is replaced by the sequential identifier, starting at 0, of the
network VNI of the interface; for example, the first interface is 0, the second is 1, etc.
The four X digits are use-configurable. Use these digits to uniquely define the MAC
address.
Example: pvc provisioner template network add --mac-template "{prefix}:2f:1f:{vmid}{netid}" test-template
The location of the two per-VM variables can be adjusted at the administrator's discretion,
or removed if not required (e.g. a single-network template, or template for a single VM).
In such situations, be careful to avoid accidental overlap with other templates' variable
portions.
"""
params = dict()
params['name'] = name
params['mac_template'] = mac_template
retcode, retdata = pvc_provisioner.template_add(config, params, template_type='network')
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template network remove
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='remove', short_help='Remove network template.')
2020-01-04 11:58:30 -05:00
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_template_network_remove(name, confirm_flag):
2020-01-04 11:58:30 -05:00
"""
2020-01-04 13:04:01 -05:00
Remove network template MAME from the PVC cluster provisioner.
2020-01-04 11:58:30 -05:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove network template {}'.format(name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-04 11:58:30 -05:00
retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='network')
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template network vni
###############################################################################
@click.group(name='vni', short_help='Manage PVC provisioner network template VNIs.', context_settings=CONTEXT_SETTINGS)
def provisioner_template_network_vni():
"""
Manage the network VNIs in PVC provisioner network templates.
"""
pass
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template network vni add
###############################################################################
@click.command(name='add', short_help='Add network VNI to network template.')
@click.argument(
'name'
)
@click.argument(
'vni'
)
@cluster_req
2020-01-04 11:58:30 -05:00
def provisioner_template_network_vni_add(name, vni):
"""
Add a new network VNI to network template NAME.
Networks will be added to VMs in the order they are added and displayed within the template.
2020-01-04 11:58:30 -05:00
"""
params = dict()
retcode, retdata = pvc_provisioner.template_element_add(config, name, vni, params, element_type='net', template_type='network')
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template network vni remove
###############################################################################
@click.command(name='remove', short_help='Remove network VNI from network template.')
@click.argument(
'name'
)
@click.argument(
'vni'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_template_network_vni_remove(name, vni, confirm_flag):
2020-01-04 11:58:30 -05:00
"""
2020-01-04 13:04:01 -05:00
Remove network VNI from network template NAME.
2020-01-04 11:58:30 -05:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove VNI {} from network template {}'.format(vni, name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-04 11:58:30 -05:00
retcode, retdata = pvc_provisioner.template_element_remove(config, name, vni, element_type='net', template_type='network')
cleanup(retcode, retdata)
###############################################################################
# pvc provisioner template storage
###############################################################################
@click.group(name='storage', short_help='Manage PVC provisioner storage templates.', context_settings=CONTEXT_SETTINGS)
def provisioner_template_storage():
"""
Manage the PVC provisioner storage templates.
"""
pass
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template storage list
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='list', short_help='List all storage templates.')
2020-01-04 11:58:30 -05:00
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2020-01-04 11:58:30 -05:00
def provisioner_template_storage_list(limit):
"""
List all storage templates in the PVC cluster provisioner.
"""
retcode, retdata = pvc_provisioner.template_list(config, limit, template_type='storage')
if retcode:
retdata = pvc_provisioner.format_list_template(retdata, template_type='storage')
2020-01-04 11:58:30 -05:00
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template storage add
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='add', short_help='Add new storage template.')
2020-01-04 11:58:30 -05:00
@click.argument(
'name'
)
@cluster_req
2020-01-04 11:58:30 -05:00
def provisioner_template_storage_add(name):
"""
Add a new storage template to the PVC cluster provisioner.
"""
params = dict()
params['name'] = name
retcode, retdata = pvc_provisioner.template_add(config, params, template_type='storage')
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template storage remove
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='remove', short_help='Remove storage template.')
2020-01-04 11:58:30 -05:00
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_template_storage_remove(name, confirm_flag):
2020-01-04 11:58:30 -05:00
"""
2020-01-04 13:04:01 -05:00
Remove storage template NAME from the PVC cluster provisioner.
2020-01-04 11:58:30 -05:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove storage template {}'.format(name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-04 11:58:30 -05:00
retcode, retdata = pvc_provisioner.template_remove(config, name, template_type='storage')
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template storage disk
###############################################################################
@click.group(name='disk', short_help='Manage PVC provisioner storage template disks.', context_settings=CONTEXT_SETTINGS)
def provisioner_template_storage_disk():
"""
Manage the disks in PVC provisioner storage templates.
"""
pass
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template storage disk add
###############################################################################
@click.command(name='add', short_help='Add disk to storage template.')
@click.argument(
'name'
)
@click.argument(
'disk'
)
@click.option(
'-p', '--pool', 'pool',
required=True,
help='The storage pool for the disk.'
)
@click.option(
2020-01-05 19:11:52 -05:00
'-i', '--source-volume', 'source_volume',
default=None,
help='The source volume to clone'
)
@click.option(
'-s', '--size', 'size', type=int,
default=None,
2020-01-04 11:58:30 -05:00
help='The size of the disk (in GB).'
)
@click.option(
'-f', '--filesystem', 'filesystem',
default=None,
help='The filesystem of the disk.'
)
@click.option(
'--fsarg', 'fsargs',
default=None, multiple=True,
help='Additional argument for filesystem creation, in arg=value format without leading dashes.'
)
@click.option(
'-m', '--mountpoint', 'mountpoint',
default=None,
help='The target Linux mountpoint of the disk; requires a filesystem.'
)
@cluster_req
2020-01-05 19:11:52 -05:00
def provisioner_template_storage_disk_add(name, disk, pool, source_volume, size, filesystem, fsargs, mountpoint):
2020-01-04 11:58:30 -05:00
"""
Add a new DISK to storage template NAME.
DISK must be a Linux-style sdX/vdX disk identifier, such as "sda" or "vdb". All disks in a template must use the same identifier format.
Disks will be added to VMs in sdX/vdX order. For disks with mountpoints, ensure this order is sensible.
2020-01-04 11:58:30 -05:00
"""
2020-01-05 19:11:52 -05:00
if source_volume and (size or filesystem or mountpoint):
click.echo('The "--source-volume" option is not compatible with the "--size", "--filesystem", or "--mountpoint" options.')
exit(1)
2020-01-04 11:58:30 -05:00
params = dict()
params['pool'] = pool
2020-01-05 19:11:52 -05:00
params['source_volume'] = source_volume
2020-01-04 11:58:30 -05:00
params['disk_size'] = size
if filesystem:
params['filesystem'] = filesystem
if filesystem and fsargs:
dash_fsargs = list()
for arg in fsargs:
arg_len = len(arg.split('=')[0])
if arg_len == 1:
dash_fsargs.append('-' + arg)
else:
dash_fsargs.append('--' + arg)
params['filesystem_arg'] = dash_fsargs
if filesystem and mountpoint:
params['mountpoint'] = mountpoint
retcode, retdata = pvc_provisioner.template_element_add(config, name, disk, params, element_type='disk', template_type='storage')
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
###############################################################################
# pvc provisioner template storage disk remove
###############################################################################
@click.command(name='remove', short_help='Remove disk from storage template.')
@click.argument(
'name'
)
@click.argument(
'disk'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_template_storage_disk_remove(name, disk, confirm_flag):
2020-01-04 11:58:30 -05:00
"""
2020-01-04 13:04:01 -05:00
Remove DISK from storage template NAME.
DISK must be a Linux-style disk identifier such as "sda" or "vdb".
2020-01-04 11:58:30 -05:00
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove disk {} from storage template {}'.format(disk, name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-04 11:58:30 -05:00
retcode, retdata = pvc_provisioner.template_element_remove(config, name, disk, element_type='disk', template_type='storage')
cleanup(retcode, retdata)
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner userdata
###############################################################################
@click.group(name='userdata', short_help='Manage PVC provisioner userdata documents.', context_settings=CONTEXT_SETTINGS)
def provisioner_userdata():
"""
Manage userdata documents in the PVC provisioner.
"""
pass
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner userdata list
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='list', short_help='List all userdata documents.')
2020-01-04 13:04:01 -05:00
@click.argument(
'limit', default=None, required=False
)
@click.option(
'-f', '--full', 'full',
is_flag=True, default=False,
help='Show all lines of the document instead of first 4.'
)
@cluster_req
2020-01-04 13:04:01 -05:00
def provisioner_userdata_list(limit, full):
"""
List all userdata documents in the PVC cluster provisioner.
"""
retcode, retdata = pvc_provisioner.userdata_list(config, limit)
if retcode:
if not full:
lines = 4
else:
lines = None
retdata = pvc_provisioner.format_list_userdata(retdata, lines)
2020-01-04 13:04:01 -05:00
cleanup(retcode, retdata)
###############################################################################
# pvc provisioner userdata show
###############################################################################
@click.command(name='show', short_help='Show contents of userdata documents.')
@click.argument(
'name'
)
@cluster_req
def provisioner_userdata_show(name):
"""
Show the full contents of userdata document NAME.
"""
retcode, retdata = pvc_provisioner.userdata_show(config, name)
cleanup(retcode, retdata)
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner userdata add
###############################################################################
@click.command(name='add', short_help='Define userdata document from file.')
@click.argument(
'name'
)
@click.argument(
'filename', type=click.File()
)
@cluster_req
2020-01-04 13:04:01 -05:00
def provisioner_userdata_add(name, filename):
"""
Add a new userdata document NAME from file FILENAME.
"""
2020-08-12 14:09:56 -04:00
# Open the YAML file
2020-01-04 13:04:01 -05:00
userdata = filename.read()
filename.close()
2020-08-12 14:09:56 -04:00
try:
yaml.load(userdata, Loader=yaml.SafeLoader)
2020-08-12 14:09:56 -04:00
except Exception as e:
click.echo("Error: Userdata document is malformed")
cleanup(False, e)
2020-01-04 13:04:01 -05:00
params = dict()
params['name'] = name
params['data'] = userdata.strip()
2020-01-04 13:04:01 -05:00
retcode, retmsg = pvc_provisioner.userdata_add(config, params)
cleanup(retcode, retmsg)
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner userdata modify
###############################################################################
@click.command(name='modify', short_help='Modify existing userdata document.')
@click.option(
'-e', '--editor', 'editor', is_flag=True,
help='Use local editor to modify existing document.'
)
@click.argument(
'name'
)
@click.argument(
'filename', type=click.File(), default=None, required=False
)
@cluster_req
2020-01-04 13:04:01 -05:00
def provisioner_userdata_modify(name, filename, editor):
"""
Modify existing userdata document NAME, either in-editor or with replacement FILE.
"""
if editor is False and filename is None:
2020-01-04 13:04:01 -05:00
cleanup(False, 'Either a file or the "--editor" option must be specified.')
if editor is True:
2020-01-04 13:04:01 -05:00
# Grab the current config
retcode, retdata = pvc_provisioner.userdata_info(config, name)
if not retcode:
click.echo(retdata)
exit(1)
current_userdata = retdata['userdata'].strip()
2020-01-04 13:04:01 -05:00
new_userdata = click.edit(text=current_userdata, require_save=True, extension='.yaml')
if new_userdata is None:
2020-01-04 13:04:01 -05:00
click.echo('Aborting with no modifications.')
exit(0)
else:
new_userdata = new_userdata.strip()
2020-01-04 13:04:01 -05:00
# Show a diff and confirm
2020-01-04 13:04:01 -05:00
click.echo('Pending modifications:')
click.echo('')
diff = list(difflib.unified_diff(current_userdata.split('\n'), new_userdata.split('\n'), fromfile='current', tofile='modified', fromfiledate='', tofiledate='', n=3, lineterm=''))
2020-01-04 13:04:01 -05:00
for line in diff:
if re.match(r'^\+', line) is not None:
2020-01-04 13:04:01 -05:00
click.echo(colorama.Fore.GREEN + line + colorama.Fore.RESET)
elif re.match(r'^\-', line) is not None:
2020-01-04 13:04:01 -05:00
click.echo(colorama.Fore.RED + line + colorama.Fore.RESET)
elif re.match(r'^\^', line) is not None:
2020-01-04 13:04:01 -05:00
click.echo(colorama.Fore.BLUE + line + colorama.Fore.RESET)
else:
click.echo(line)
click.echo('')
click.confirm('Write modifications to cluster?', abort=True)
userdata = new_userdata
# We're operating in replace mode
else:
# Open the new file
userdata = filename.read().strip()
filename.close()
2020-08-12 14:09:56 -04:00
try:
yaml.load(userdata, Loader=yaml.SafeLoader)
2020-08-12 14:09:56 -04:00
except Exception as e:
click.echo("Error: Userdata document is malformed")
cleanup(False, e)
2020-01-04 13:04:01 -05:00
params = dict()
params['data'] = userdata
retcode, retmsg = pvc_provisioner.userdata_modify(config, name, params)
cleanup(retcode, retmsg)
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner userdata remove
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='remove', short_help='Remove userdata document.')
2020-01-04 13:04:01 -05:00
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_userdata_remove(name, confirm_flag):
2020-01-04 13:04:01 -05:00
"""
Remove userdata document NAME from the PVC cluster provisioner.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove userdata document {}'.format(name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-04 13:04:01 -05:00
retcode, retdata = pvc_provisioner.userdata_remove(config, name)
cleanup(retcode, retdata)
###############################################################################
# pvc provisioner script
###############################################################################
@click.group(name='script', short_help='Manage PVC provisioner scripts.', context_settings=CONTEXT_SETTINGS)
def provisioner_script():
"""
Manage scripts in the PVC provisioner.
"""
pass
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner script list
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='list', short_help='List all scripts.')
2020-01-04 13:04:01 -05:00
@click.argument(
'limit', default=None, required=False
)
@click.option(
'-f', '--full', 'full',
is_flag=True, default=False,
help='Show all lines of the document instead of first 4.'
)
@cluster_req
2020-01-04 13:04:01 -05:00
def provisioner_script_list(limit, full):
"""
List all scripts in the PVC cluster provisioner.
"""
retcode, retdata = pvc_provisioner.script_list(config, limit)
if retcode:
if not full:
lines = 4
else:
lines = None
retdata = pvc_provisioner.format_list_script(retdata, lines)
2020-01-04 13:04:01 -05:00
cleanup(retcode, retdata)
###############################################################################
# pvc provisioner script show
###############################################################################
@click.command(name='show', short_help='Show contents of script documents.')
@click.argument(
'name'
)
@cluster_req
def provisioner_script_show(name):
"""
Show the full contents of script document NAME.
"""
retcode, retdata = pvc_provisioner.script_show(config, name)
cleanup(retcode, retdata)
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner script add
###############################################################################
@click.command(name='add', short_help='Define script from file.')
@click.argument(
'name'
)
@click.argument(
'filename', type=click.File()
)
@cluster_req
2020-01-04 13:04:01 -05:00
def provisioner_script_add(name, filename):
"""
Add a new script NAME from file FILENAME.
"""
# Open the XML file
script = filename.read()
filename.close()
params = dict()
params['name'] = name
params['data'] = script.strip()
2020-01-04 13:04:01 -05:00
retcode, retmsg = pvc_provisioner.script_add(config, params)
cleanup(retcode, retmsg)
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner script modify
###############################################################################
@click.command(name='modify', short_help='Modify existing script.')
@click.option(
'-e', '--editor', 'editor', is_flag=True,
help='Use local editor to modify existing document.'
)
@click.argument(
'name'
)
@click.argument(
'filename', type=click.File(), default=None, required=False
)
@cluster_req
2020-01-04 13:04:01 -05:00
def provisioner_script_modify(name, filename, editor):
"""
Modify existing script NAME, either in-editor or with replacement FILE.
"""
if editor is False and filename is None:
2020-01-04 13:04:01 -05:00
cleanup(False, 'Either a file or the "--editor" option must be specified.')
if editor is True:
2020-01-04 13:04:01 -05:00
# Grab the current config
retcode, retdata = pvc_provisioner.script_info(config, name)
if not retcode:
click.echo(retdata)
exit(1)
current_script = retdata['script'].strip()
2020-01-04 13:04:01 -05:00
new_script = click.edit(text=current_script, require_save=True, extension='.py')
if new_script is None:
2020-01-04 13:04:01 -05:00
click.echo('Aborting with no modifications.')
exit(0)
else:
new_script = new_script.strip()
2020-01-04 13:04:01 -05:00
# Show a diff and confirm
2020-01-04 13:04:01 -05:00
click.echo('Pending modifications:')
click.echo('')
diff = list(difflib.unified_diff(current_script.split('\n'), new_script.split('\n'), fromfile='current', tofile='modified', fromfiledate='', tofiledate='', n=3, lineterm=''))
2020-01-04 13:04:01 -05:00
for line in diff:
if re.match(r'^\+', line) is not None:
2020-01-04 13:04:01 -05:00
click.echo(colorama.Fore.GREEN + line + colorama.Fore.RESET)
elif re.match(r'^\-', line) is not None:
2020-01-04 13:04:01 -05:00
click.echo(colorama.Fore.RED + line + colorama.Fore.RESET)
elif re.match(r'^\^', line) is not None:
2020-01-04 13:04:01 -05:00
click.echo(colorama.Fore.BLUE + line + colorama.Fore.RESET)
else:
click.echo(line)
click.echo('')
click.confirm('Write modifications to cluster?', abort=True)
script = new_script
# We're operating in replace mode
else:
# Open the new file
script = filename.read().strip()
filename.close()
params = dict()
params['data'] = script
retcode, retmsg = pvc_provisioner.script_modify(config, name, params)
cleanup(retcode, retmsg)
2020-01-04 13:04:01 -05:00
###############################################################################
# pvc provisioner script remove
###############################################################################
2020-01-04 14:06:36 -05:00
@click.command(name='remove', short_help='Remove script.')
2020-01-04 13:04:01 -05:00
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_script_remove(name, confirm_flag):
2020-01-04 13:04:01 -05:00
"""
Remove script NAME from the PVC cluster provisioner.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove provisioning script {}'.format(name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-04 13:04:01 -05:00
retcode, retdata = pvc_provisioner.script_remove(config, name)
cleanup(retcode, retdata)
2020-01-04 11:58:30 -05:00
2020-01-02 11:18:46 -05:00
###############################################################################
# pvc provisioner ova
###############################################################################
@click.group(name='ova', short_help='Manage PVC provisioner OVA images.', context_settings=CONTEXT_SETTINGS)
def provisioner_ova():
"""
Manage ovas in the PVC provisioner.
"""
pass
###############################################################################
# pvc provisioner ova list
###############################################################################
@click.command(name='list', short_help='List all OVA images.')
@click.argument(
'limit', default=None, required=False
)
@cluster_req
def provisioner_ova_list(limit):
"""
List all OVA images in the PVC cluster provisioner.
"""
retcode, retdata = pvc_provisioner.ova_list(config, limit)
if retcode:
retdata = pvc_provisioner.format_list_ova(retdata)
cleanup(retcode, retdata)
###############################################################################
# pvc provisioner ova upload
###############################################################################
@click.command(name='upload', short_help='Upload OVA file.')
@click.argument(
'name'
)
@click.argument(
'filename'
)
@click.option(
'-p', '--pool', 'pool',
required=True,
help='The storage pool for the OVA images.'
)
@cluster_req
def provisioner_ova_upload(name, filename, pool):
"""
Upload a new OVA image NAME from FILENAME.
Only single-file (.ova) OVA/OVF images are supported. For multi-file (.ovf + .vmdk) OVF images, concatenate them with "tar" then upload the resulting file.
Once uploaded, a provisioner system template and OVA-type profile, each named NAME, will be created to store the configuration of the OVA.
Note that the provisioner profile for the OVA will not contain any network template definitions, and will ignore network definitions from the OVA itself. The administrator must modify the profile's network template as appropriate to set the desired network configuration.
Storage templates, provisioning scripts, and arguments for OVA-type profiles will be ignored and should not be set.
"""
if not os.path.exists(filename):
click.echo("ERROR: File '{}' does not exist!".format(filename))
exit(1)
params = dict()
params['pool'] = pool
params['ova_size'] = os.path.getsize(filename)
retcode, retdata = pvc_provisioner.ova_upload(config, name, filename, params)
cleanup(retcode, retdata)
###############################################################################
# pvc provisioner ova remove
###############################################################################
@click.command(name='remove', short_help='Remove OVA image.')
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_ova_remove(name, confirm_flag):
"""
Remove OVA image NAME from the PVC cluster provisioner.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove OVA image {}'.format(name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
retcode, retdata = pvc_provisioner.ova_remove(config, name)
cleanup(retcode, retdata)
2020-01-04 14:06:36 -05:00
###############################################################################
# pvc provisioner profile
###############################################################################
@click.group(name='profile', short_help='Manage PVC provisioner profiless.', context_settings=CONTEXT_SETTINGS)
def provisioner_profile():
"""
Manage profiles in the PVC provisioner.
"""
pass
2020-01-04 14:06:36 -05:00
###############################################################################
# pvc provisioner profile list
###############################################################################
@click.command(name='list', short_help='List all profiles.')
@click.argument(
'limit', default=None, required=False
)
@cluster_req
2020-01-04 14:06:36 -05:00
def provisioner_profile_list(limit):
"""
List all profiles in the PVC cluster provisioner.
"""
retcode, retdata = pvc_provisioner.profile_list(config, limit)
if retcode:
retdata = pvc_provisioner.format_list_profile(retdata)
2020-01-04 14:06:36 -05:00
cleanup(retcode, retdata)
2020-01-04 14:06:36 -05:00
###############################################################################
# pvc provisioner profile add
###############################################################################
@click.command(name='add', short_help='Add provisioner profile.')
@click.argument(
'name'
)
@click.option(
'-p', '--profile-type', 'profile_type',
default='provisioner', show_default=True,
type=click.Choice(['provisioner', 'ova'], case_sensitive=False),
help='The type of profile.'
)
2020-01-04 14:06:36 -05:00
@click.option(
'-s', '--system-template', 'system_template',
help='The system template for the profile.'
)
@click.option(
'-n', '--network-template', 'network_template',
help='The network template for the profile.'
)
@click.option(
'-t', '--storage-template', 'storage_template',
help='The storage template for the profile.'
)
@click.option(
'-u', '--userdata', 'userdata',
help='The userdata document for the profile.'
)
@click.option(
'-x', '--script', 'script',
help='The script for the profile.'
)
@click.option(
'-o', '--ova', 'ova',
help='The OVA image for the profile.'
)
2020-01-04 14:06:36 -05:00
@click.option(
'-a', '--script-arg', 'script_args',
default=[], multiple=True,
help='Additional argument to the script install() function in key=value format.'
)
@cluster_req
def provisioner_profile_add(name, profile_type, system_template, network_template, storage_template, userdata, script, ova, script_args):
2020-01-04 14:06:36 -05:00
"""
Add a new provisioner profile NAME.
"""
params = dict()
params['name'] = name
params['profile_type'] = profile_type
2020-01-04 14:06:36 -05:00
params['system_template'] = system_template
params['network_template'] = network_template
params['storage_template'] = storage_template
params['userdata'] = userdata
params['script'] = script
params['ova'] = ova
2020-01-04 14:06:36 -05:00
params['arg'] = script_args
retcode, retdata = pvc_provisioner.profile_add(config, params)
cleanup(retcode, retdata)
2020-01-04 14:06:36 -05:00
###############################################################################
# pvc provisioner profile modify
###############################################################################
@click.command(name='modify', short_help='Modify provisioner profile.')
@click.argument(
'name'
)
@click.option(
'-s', '--system-template', 'system_template',
default=None,
help='The system template for the profile.'
)
@click.option(
'-n', '--network-template', 'network_template',
default=None,
help='The network template for the profile.'
)
@click.option(
'-t', '--storage-template', 'storage_template',
default=None,
help='The storage template for the profile.'
)
@click.option(
'-u', '--userdata', 'userdata',
default=None,
help='The userdata document for the profile.'
)
@click.option(
'-x', '--script', 'script',
default=None,
help='The script for the profile.'
)
@click.option(
'-d', '--delete-script-args', 'delete_script_args',
default=False, is_flag=True,
help="Delete any existing script arguments."
)
@click.option(
'-a', '--script-arg', 'script_args',
default=None, multiple=True,
help='Additional argument to the script install() function in key=value format.'
)
@cluster_req
2020-01-04 14:06:36 -05:00
def provisioner_profile_modify(name, system_template, network_template, storage_template, userdata, script, delete_script_args, script_args):
"""
Modify existing provisioner profile NAME.
"""
params = dict()
if system_template is not None:
params['system_template'] = system_template
if network_template is not None:
params['network_template'] = network_template
if storage_template is not None:
params['storage_template'] = storage_template
if userdata is not None:
params['userdata'] = userdata
if script is not None:
params['script'] = script
if delete_script_args:
params['arg'] = []
if script_args is not None:
params['arg'] = script_args
retcode, retdata = pvc_provisioner.profile_modify(config, name, params)
cleanup(retcode, retdata)
2020-01-04 14:06:36 -05:00
###############################################################################
# pvc provisioner profile remove
###############################################################################
@click.command(name='remove', short_help='Remove profile.')
@click.argument(
'name'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the removal'
)
@cluster_req
def provisioner_profile_remove(name, confirm_flag):
2020-01-04 14:06:36 -05:00
"""
Remove profile NAME from the PVC cluster provisioner.
"""
if not confirm_flag and not config['unsafe']:
try:
click.confirm('Remove profile {}'.format(name), prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
exit(0)
2020-01-04 14:06:36 -05:00
retcode, retdata = pvc_provisioner.profile_remove(config, name)
cleanup(retcode, retdata)
2020-01-02 11:18:46 -05:00
2020-01-04 14:31:22 -05:00
###############################################################################
# pvc provisioner create
###############################################################################
@click.command(name='create', short_help='Create new VM.')
@click.argument(
'name'
)
@click.argument(
'profile'
)
@click.option(
'-a', '--script-arg', 'script_args',
default=[], multiple=True,
help='Additional argument to the script install() function in key=value format.'
)
@click.option(
'-d/-D', '--define/--no-define', 'define_flag',
is_flag=True, default=True, show_default=True,
help='Define the VM automatically during provisioning.'
2020-01-09 09:46:58 -05:00
)
@click.option(
'-s/-S', '--start/--no-start', 'start_flag',
is_flag=True, default=True, show_default=True,
help='Start the VM automatically upon completion of provisioning.'
2020-01-09 09:46:58 -05:00
)
@click.option(
'-w', '--wait', 'wait_flag',
is_flag=True, default=False,
help='Wait for provisioning to complete, showing progress'
)
@cluster_req
def provisioner_create(name, profile, wait_flag, define_flag, start_flag, script_args):
2020-01-04 14:31:22 -05:00
"""
Create a new VM NAME with profile PROFILE.
The "--no-start" flag can be used to prevent automatic startup of the VM once provisioning
is completed. This can be useful for the administrator to preform additional actions to
the VM after provisioning is completed. Note that the VM will remain in "provision" state
until its state is explicitly changed (e.g. with "pvc vm start").
The "--no-define" flag implies "--no-start", and can be used to prevent definition of the
created VM on the PVC cluster. This can be useful for the administrator to create a "template"
set of VM disks via the normal provisioner, but without ever starting the resulting VM. The
resulting disk(s) can then be used as source volumes in other disk templates.
The "--script-arg" option can be specified as many times as required to pass additional,
VM-specific arguments to the provisioner install() function, beyond those set by the profile.
2020-01-04 14:31:22 -05:00
"""
if not define_flag:
start_flag = False
retcode, retdata = pvc_provisioner.vm_create(config, name, profile, wait_flag, define_flag, start_flag, script_args)
if retcode and wait_flag:
task_id = retdata
click.echo("Task ID: {}".format(task_id))
click.echo()
# Wait for the task to start
click.echo("Waiting for task to start...", nl=False)
while True:
time.sleep(1)
task_status = pvc_provisioner.task_status(config, task_id, is_watching=True)
if task_status.get('state') != 'PENDING':
break
click.echo(".", nl=False)
click.echo(" done.")
click.echo()
# Start following the task state, updating progress as we go
total_task = task_status.get('total')
with click.progressbar(length=total_task, show_eta=False) as bar:
last_task = 0
maxlen = 0
while True:
time.sleep(1)
if task_status.get('state') != 'RUNNING':
break
if task_status.get('current') > last_task:
current_task = int(task_status.get('current'))
bar.update(current_task - last_task)
last_task = current_task
# The extensive spaces at the end cause this to overwrite longer previous messages
curlen = len(str(task_status.get('status')))
if curlen > maxlen:
maxlen = curlen
lendiff = maxlen - curlen
overwrite_whitespace = " " * lendiff
click.echo(" " + task_status.get('status') + overwrite_whitespace, nl=False)
task_status = pvc_provisioner.task_status(config, task_id, is_watching=True)
if task_status.get('state') == 'SUCCESS':
bar.update(total_task - last_task)
click.echo()
retdata = task_status.get('state') + ": " + task_status.get('status')
2020-01-04 14:31:22 -05:00
cleanup(retcode, retdata)
2020-01-04 14:31:22 -05:00
2020-01-04 14:31:22 -05:00
###############################################################################
# pvc provisioner status
###############################################################################
@click.command(name='status', short_help='Show status of provisioner job.')
@click.argument(
2020-01-12 14:01:47 -05:00
'job', required=False, default=None
2020-01-04 14:31:22 -05:00
)
@cluster_req
2020-01-04 14:31:22 -05:00
def provisioner_status(job):
"""
2020-01-12 14:01:47 -05:00
Show status of provisioner job JOB or a list of jobs.
2020-01-04 14:31:22 -05:00
"""
retcode, retdata = pvc_provisioner.task_status(config, job)
if job is None and retcode:
retdata = pvc_provisioner.format_list_task(retdata)
2020-01-04 14:31:22 -05:00
cleanup(retcode, retdata)
2020-01-02 11:18:46 -05:00
###############################################################################
# pvc maintenance
###############################################################################
@click.group(name='maintenance', short_help='Manage PVC cluster maintenance state.', context_settings=CONTEXT_SETTINGS)
def cli_maintenance():
"""
Manage the maintenance mode of the PVC cluster.
"""
pass
2020-01-02 11:18:46 -05:00
###############################################################################
# pvc maintenance on
###############################################################################
@click.command(name='on', short_help='Enable cluster maintenance mode.')
@cluster_req
def maintenance_on():
"""
Enable maintenance mode on the PVC cluster.
"""
retcode, retdata = pvc_cluster.maintenance_mode(config, 'true')
cleanup(retcode, retdata)
2020-01-02 11:18:46 -05:00
###############################################################################
# pvc maintenance off
###############################################################################
@click.command(name='off', short_help='Disable cluster maintenance mode.')
@cluster_req
def maintenance_off():
"""
Disable maintenance mode on the PVC cluster.
"""
retcode, retdata = pvc_cluster.maintenance_mode(config, 'false')
cleanup(retcode, retdata)
2020-01-02 11:18:46 -05:00
2019-08-25 21:18:33 -04:00
###############################################################################
2019-10-22 11:23:12 -04:00
# pvc status
2019-08-25 21:18:33 -04:00
###############################################################################
2019-10-22 11:23:12 -04:00
@click.command(name='status', short_help='Show current cluster status.')
@click.option(
'-f', '--format', 'oformat', default='plain', show_default=True,
2020-11-17 12:32:16 -05:00
type=click.Choice(['plain', 'short', 'json', 'json-pretty']),
2019-10-22 11:23:12 -04:00
help='Output format of cluster status information.'
)
@cluster_req
2019-10-22 11:23:12 -04:00
def status_cluster(oformat):
"""
Show basic information and health for the active PVC cluster.
2020-11-17 12:32:16 -05:00
Output formats:
2020-11-17 12:32:16 -05:00
plain: Full text, full colour output for human-readability.
2020-11-17 12:32:16 -05:00
short: Health-only, full colour output for human-readability.
2020-11-17 12:32:16 -05:00
json: Compact JSON representation for machine parsing.
2020-11-17 12:32:16 -05:00
json-pretty: Pretty-printed JSON representation for machine parsing or human-readability.
2019-10-22 11:23:12 -04:00
"""
2020-01-02 12:18:41 -05:00
2019-12-29 20:33:51 -05:00
retcode, retdata = pvc_cluster.get_info(config)
2019-10-22 11:23:12 -04:00
if retcode:
retdata = pvc_cluster.format_info(retdata, oformat)
2019-12-29 20:33:51 -05:00
cleanup(retcode, retdata)
###############################################################################
# pvc task
###############################################################################
@click.group(name='task', short_help='Perform PVC cluster tasks.', context_settings=CONTEXT_SETTINGS)
def cli_task():
"""
Perform administrative tasks against the PVC cluster.
"""
pass
###############################################################################
# pvc task backup
###############################################################################
@click.command(name='backup', short_help='Create JSON backup of cluster.')
@click.option(
'-f', '--file', 'filename',
default=None, type=click.File(mode='w'),
help='Write backup data to this file.'
)
@cluster_req
def task_backup(filename):
"""
Create a JSON-format backup of the cluster Zookeeper database.
"""
retcode, retdata = pvc_cluster.backup(config)
if retcode:
if filename is not None:
json.dump(json.loads(retdata), filename)
cleanup(retcode, 'Backup written to "{}".'.format(filename.name))
else:
cleanup(retcode, retdata)
else:
cleanup(retcode, retdata)
###############################################################################
# pvc task restore
###############################################################################
@click.command(name='restore', short_help='Restore JSON backup to cluster.')
@click.option(
'-f', '--file', 'filename',
required=True, default=None, type=click.File(),
help='Read backup data from this file.'
)
@click.option(
'-y', '--yes', 'confirm_flag',
is_flag=True, default=False,
help='Confirm the restore'
)
@cluster_req
def task_restore(filename, confirm_flag):
"""
Restore the JSON backup data from a file to the cluster.
"""
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:
exit(0)
cluster_data = json.loads(filename.read())
retcode, retmsg = pvc_cluster.restore(config, cluster_data)
cleanup(retcode, retmsg)
2019-10-22 11:23:12 -04:00
###############################################################################
2020-11-25 10:36:48 -05:00
# pvc task init
2019-10-22 11:23:12 -04:00
###############################################################################
@click.command(name='init', short_help='Initialize a new cluster.')
2021-05-30 23:59:17 -04:00
@click.option(
'-o', '--overwite', 'overwrite_flag',
is_flag=True, default=False,
help='Remove and overwrite any existing data'
)
2019-06-21 14:16:32 -04:00
@click.option(
'-y', '--yes', 'confirm_flag',
2019-06-21 15:09:15 -04:00
is_flag=True, default=False,
help='Confirm the initialization'
2019-06-21 14:16:32 -04:00
)
@cluster_req
2021-05-30 23:59:17 -04:00
def task_init(confirm_flag, overwrite_flag):
"""
2019-03-10 20:40:03 -04:00
Perform initialization of a new PVC cluster.
2021-05-30 23:59:17 -04:00
If the '-o'/'--overwrite' option is specified, all existing data in the cluster will be deleted
before new, empty data is written.
It is not advisable to do this against a running cluster - all node daemons should be stopped
first and the API daemon started manually before running this command.
"""
if not confirm_flag and not config['unsafe']:
try:
2020-11-07 13:19:38 -05:00
click.confirm('Remove all existing cluster data from coordinators and initialize a new cluster', prompt_suffix='? ', abort=True)
2020-11-06 18:55:10 -05:00
except Exception:
2019-06-21 15:38:32 -04:00
exit(0)
# Easter-egg
click.echo("Some music while we're Layin' Pipe? https://youtu.be/sw8S_Kv89IU")
2021-05-30 23:59:17 -04:00
retcode, retmsg = pvc_cluster.initialize(config, overwrite_flag)
2019-12-29 20:33:51 -05:00
cleanup(retcode, retmsg)
2018-06-05 01:39:59 -04:00
###############################################################################
# pvc
###############################################################################
2018-06-05 01:39:59 -04:00
@click.group(context_settings=CONTEXT_SETTINGS)
@click.option(
'-c', '--cluster', '_cluster', envvar='PVC_CLUSTER', default=None,
2021-03-17 13:58:23 -04:00
help='Cluster to connect to.'
)
2019-12-26 11:20:57 -05:00
@click.option(
'-v', '--debug', '_debug', envvar='PVC_DEBUG', is_flag=True, default=False,
help='Additional debug details.'
)
2020-06-25 11:09:55 -04:00
@click.option(
'-q', '--quiet', '_quiet', envvar='PVC_QUIET', is_flag=True, default=False,
help='Suppress cluster connection information.'
)
@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
2018-07-18 22:30:23 -04:00
Environment variables:
"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.
"""
2019-12-26 11:20:57 -05:00
global config
store_data = get_store(store_path)
config = get_config(store_data, _cluster)
2020-01-02 11:19:11 -05:00
if not config.get('badcfg', None):
config['debug'] = _debug
config['unsafe'] = _unsafe
2020-01-06 09:23:35 -05:00
2020-06-25 11:09:55 -04:00
if not _quiet:
if config['api_scheme'] == 'https' and not config['verify_ssl']:
ssl_unverified_msg = ' (unverified)'
else:
ssl_unverified_msg = ''
2020-06-25 11:09:55 -04:00
click.echo(
'Using cluster "{}" - Host: "{}" Scheme: "{}{}" Prefix: "{}"'.format(
2020-06-25 11:09:55 -04:00
config['cluster'],
config['api_host'],
config['api_scheme'],
ssl_unverified_msg,
2020-06-25 11:09:55 -04:00
config['api_prefix']
),
err=True
)
click.echo('', err=True)
config = dict()
2018-06-05 01:39:59 -04:00
#
# Click command tree
#
2020-01-02 11:18:46 -05:00
cli_cluster.add_command(cluster_add)
cli_cluster.add_command(cluster_remove)
cli_cluster.add_command(cluster_list)
cli_node.add_command(node_secondary)
cli_node.add_command(node_primary)
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)
vm_vcpu.add_command(vm_vcpu_get)
vm_vcpu.add_command(vm_vcpu_set)
vm_memory.add_command(vm_memory_get)
vm_memory.add_command(vm_memory_set)
vm_network.add_command(vm_network_get)
vm_network.add_command(vm_network_add)
vm_network.add_command(vm_network_remove)
vm_volume.add_command(vm_volume_get)
vm_volume.add_command(vm_volume_add)
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)
2019-03-12 21:09:54 -04:00
cli_vm.add_command(vm_dump)
cli_vm.add_command(vm_start)
cli_vm.add_command(vm_restart)
cli_vm.add_command(vm_shutdown)
cli_vm.add_command(vm_stop)
2019-10-23 23:37:42 -04:00
cli_vm.add_command(vm_disable)
cli_vm.add_command(vm_move)
cli_vm.add_command(vm_migrate)
cli_vm.add_command(vm_unmigrate)
2019-08-07 13:42:01 -04:00
cli_vm.add_command(vm_flush_locks)
cli_vm.add_command(vm_vcpu)
cli_vm.add_command(vm_memory)
cli_vm.add_command(vm_network)
cli_vm.add_command(vm_volume)
cli_vm.add_command(vm_info)
cli_vm.add_command(vm_log)
cli_vm.add_command(vm_list)
2018-09-21 23:43:30 -04:00
cli_network.add_command(net_add)
2018-09-23 15:26:20 -04:00
cli_network.add_command(net_modify)
2018-09-21 23:43:30 -04:00
cli_network.add_command(net_remove)
cli_network.add_command(net_info)
cli_network.add_command(net_list)
cli_network.add_command(net_dhcp)
cli_network.add_command(net_acl)
2021-06-21 17:12:53 -04:00
cli_network.add_command(net_sriov)
net_dhcp.add_command(net_dhcp_list)
2019-12-29 16:13:32 -05:00
net_dhcp.add_command(net_dhcp_add)
net_dhcp.add_command(net_dhcp_remove)
2018-09-21 23:43:30 -04:00
2018-10-17 00:23:27 -04:00
net_acl.add_command(net_acl_add)
net_acl.add_command(net_acl_remove)
net_acl.add_command(net_acl_list)
2021-06-21 17:12:53 -04:00
net_sriov.add_command(net_sriov_pf)
net_sriov.add_command(net_sriov_vf)
net_sriov_pf.add_command(net_sriov_pf_list)
net_sriov_vf.add_command(net_sriov_vf_list)
net_sriov_vf.add_command(net_sriov_vf_info)
net_sriov_vf.add_command(net_sriov_vf_set)
2020-08-24 14:57:52 -04:00
ceph_benchmark.add_command(ceph_benchmark_run)
2020-08-25 12:16:23 -04:00
ceph_benchmark.add_command(ceph_benchmark_info)
2020-08-24 14:57:52 -04:00
ceph_benchmark.add_command(ceph_benchmark_list)
2018-10-28 22:15:25 -04:00
ceph_osd.add_command(ceph_osd_add)
ceph_osd.add_command(ceph_osd_remove)
2018-11-01 22:00:59 -04:00
ceph_osd.add_command(ceph_osd_in)
ceph_osd.add_command(ceph_osd_out)
ceph_osd.add_command(ceph_osd_set)
ceph_osd.add_command(ceph_osd_unset)
2018-10-30 09:17:32 -04:00
ceph_osd.add_command(ceph_osd_list)
2018-10-27 18:11:58 -04:00
2018-10-31 23:38:17 -04:00
ceph_pool.add_command(ceph_pool_add)
ceph_pool.add_command(ceph_pool_remove)
ceph_pool.add_command(ceph_pool_list)
2018-10-27 18:11:58 -04:00
ceph_volume.add_command(ceph_volume_add)
ceph_volume.add_command(ceph_volume_upload)
ceph_volume.add_command(ceph_volume_resize)
ceph_volume.add_command(ceph_volume_rename)
2019-12-29 20:33:51 -05:00
ceph_volume.add_command(ceph_volume_clone)
ceph_volume.add_command(ceph_volume_remove)
ceph_volume.add_command(ceph_volume_list)
ceph_volume.add_command(ceph_volume_snapshot)
ceph_volume_snapshot.add_command(ceph_volume_snapshot_add)
ceph_volume_snapshot.add_command(ceph_volume_snapshot_rename)
ceph_volume_snapshot.add_command(ceph_volume_snapshot_remove)
ceph_volume_snapshot.add_command(ceph_volume_snapshot_list)
cli_storage.add_command(ceph_status)
cli_storage.add_command(ceph_util)
2020-08-24 14:57:52 -04:00
cli_storage.add_command(ceph_benchmark)
cli_storage.add_command(ceph_osd)
cli_storage.add_command(ceph_pool)
cli_storage.add_command(ceph_volume)
2020-01-04 11:58:30 -05:00
provisioner_template_system.add_command(provisioner_template_system_list)
provisioner_template_system.add_command(provisioner_template_system_add)
provisioner_template_system.add_command(provisioner_template_system_modify)
2020-01-04 11:58:30 -05:00
provisioner_template_system.add_command(provisioner_template_system_remove)
provisioner_template_network.add_command(provisioner_template_network_list)
provisioner_template_network.add_command(provisioner_template_network_add)
provisioner_template_network.add_command(provisioner_template_network_remove)
provisioner_template_network.add_command(provisioner_template_network_vni)
provisioner_template_network_vni.add_command(provisioner_template_network_vni_add)
provisioner_template_network_vni.add_command(provisioner_template_network_vni_remove)
provisioner_template_storage.add_command(provisioner_template_storage_list)
provisioner_template_storage.add_command(provisioner_template_storage_add)
provisioner_template_storage.add_command(provisioner_template_storage_remove)
provisioner_template_storage.add_command(provisioner_template_storage_disk)
provisioner_template_storage_disk.add_command(provisioner_template_storage_disk_add)
provisioner_template_storage_disk.add_command(provisioner_template_storage_disk_remove)
provisioner_template.add_command(provisioner_template_system)
provisioner_template.add_command(provisioner_template_network)
provisioner_template.add_command(provisioner_template_storage)
provisioner_template.add_command(provisioner_template_list)
2020-01-04 13:04:01 -05:00
provisioner_userdata.add_command(provisioner_userdata_list)
provisioner_userdata.add_command(provisioner_userdata_show)
2020-01-04 13:04:01 -05:00
provisioner_userdata.add_command(provisioner_userdata_add)
provisioner_userdata.add_command(provisioner_userdata_modify)
provisioner_userdata.add_command(provisioner_userdata_remove)
provisioner_script.add_command(provisioner_script_list)
provisioner_script.add_command(provisioner_script_show)
2020-01-04 13:04:01 -05:00
provisioner_script.add_command(provisioner_script_add)
provisioner_script.add_command(provisioner_script_modify)
provisioner_script.add_command(provisioner_script_remove)
provisioner_ova.add_command(provisioner_ova_list)
provisioner_ova.add_command(provisioner_ova_upload)
provisioner_ova.add_command(provisioner_ova_remove)
2020-01-04 14:06:36 -05:00
provisioner_profile.add_command(provisioner_profile_list)
provisioner_profile.add_command(provisioner_profile_add)
provisioner_profile.add_command(provisioner_profile_modify)
provisioner_profile.add_command(provisioner_profile_remove)
2020-01-02 11:18:46 -05:00
cli_provisioner.add_command(provisioner_template)
2020-01-04 13:04:01 -05:00
cli_provisioner.add_command(provisioner_userdata)
cli_provisioner.add_command(provisioner_script)
cli_provisioner.add_command(provisioner_ova)
2020-01-04 14:06:36 -05:00
cli_provisioner.add_command(provisioner_profile)
2020-01-04 14:31:22 -05:00
cli_provisioner.add_command(provisioner_create)
cli_provisioner.add_command(provisioner_status)
2020-01-02 11:18:46 -05:00
cli_maintenance.add_command(maintenance_on)
cli_maintenance.add_command(maintenance_off)
cli_task.add_command(task_backup)
cli_task.add_command(task_restore)
cli_task.add_command(task_init)
2020-01-02 11:18:46 -05:00
cli.add_command(cli_cluster)
cli.add_command(cli_node)
cli.add_command(cli_vm)
2018-09-21 23:43:30 -04:00
cli.add_command(cli_network)
cli.add_command(cli_storage)
2020-01-02 11:18:46 -05:00
cli.add_command(cli_provisioner)
cli.add_command(cli_maintenance)
cli.add_command(cli_task)
2019-10-22 11:23:12 -04:00
cli.add_command(status_cluster)
2018-06-05 01:39:59 -04:00
2018-06-05 01:39:59 -04:00
#
# Main entry point
#
def main():
return cli(obj={})
2018-06-05 01:39:59 -04:00
if __name__ == '__main__':
main()