Add support for multiple clusters in CLI

This commit is contained in:
Joshua Boniface 2019-12-30 13:27:40 -05:00
parent 18c14cbf77
commit 18e6192178
1 changed files with 252 additions and 25 deletions

View File

@ -29,9 +29,12 @@ import difflib
import re import re
import colorama import colorama
import yaml import yaml
import json
import lxml.etree as etree import lxml.etree as etree
import requests import requests
from distutils.util import strtobool
import cli_lib.ansiprint as ansiprint import cli_lib.ansiprint as ansiprint
import cli_lib.cluster as pvc_cluster import cli_lib.cluster as pvc_cluster
import cli_lib.node as pvc_node import cli_lib.node as pvc_node
@ -42,11 +45,81 @@ import cli_lib.ceph as pvc_ceph
myhostname = socket.gethostname().split('.')[0] myhostname = socket.gethostname().split('.')[0]
zk_host = '' zk_host = ''
config = dict() default_store_data = {
config['debug'] = False 'cfgfile': '/etc/pvc/pvc-api.yaml' # pvc/api/listen_address, pvc/api/listen_port
config['api_scheme'] = 'http' }
config['api_host'] = 'localhost:7370'
config['api_prefix'] = '/api/v1' #
# 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'
return host, port, scheme
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):
host, port, scheme = read_from_yaml(cfgfile)
else:
print('Attempted to load API configuration from a nonexistent file; using defaults')
host = 'localhost'
port = '7370'
scheme = 'http'
else:
# This is a static configuration, get the raw details
host = cluster_details['host']
port = cluster_details['port']
scheme = cluster_details['scheme']
config = dict()
config['debug'] = False
config['cluster'] = cluster
config['api_host'] = '{}:{}'.format(host, port)
config['api_scheme'] = scheme
config['api_prefix'] = prefix
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)
with open(store_file, 'w') as fh:
fh.write(json.dumps(store_data, sort_keys=True, indent=4))
home_dir = os.environ.get('HOME', None)
if home_dir:
store_path = '{}/.config/pvc'.format(home_dir)
else:
print('No home dir found - not permanently saving any configurations as this user')
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) CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=120)
@ -60,6 +133,163 @@ def cleanup(retcode, retmsg):
click.echo(retmsg) click.echo(retmsg)
exit(1) 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(
'-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.argument(
'name'
)
def cluster_add(address, port, ssl, name):
"""
Add a new PVC cluster NAME, via its API connection details, to the configuration of the local CLI client. Replaces any existing cluster with this name.
"""
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] = {
'host': address,
'port': port,
'scheme': scheme
}
# Update the store
update_store(store_path, existing_config)
###############################################################################
# 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)
###############################################################################
# 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
address_length = 10
port_length = 5
scheme_length = 5
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):
address, port, scheme = read_from_yaml(cfgfile)
else:
address = cluster_details.get('host', None)
port = cluster_details.get('port', None)
scheme = cluster_details.get('scheme', None)
_name_length = len(cluster) + 1
if _name_length > name_length:
name_length = _name_length
_address_length = len(address) + 1
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
# Display the data nicely
click.echo("Available clusters:")
click.echo()
click.echo(
'{bold}{name: <{name_length}} {address: <{address_length}} {port: <{port_length}} {scheme: <{scheme_length}}{end_bold}'.format(
bold=ansiprint.bold(),
end_bold=ansiprint.end(),
name="Name",
name_length=name_length,
address="Address",
address_length=address_length,
port="Port",
port_length=port_length,
scheme="Scheme",
scheme_length=scheme_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
cfgfile = cluster_details.get('cfgfile')
if os.path.isfile(cfgfile):
address, port, scheme = read_from_yaml(cfgfile)
else:
address = cluster_details.get('host', None)
port = cluster_details.get('port', None)
scheme = cluster_details.get('scheme', None)
click.echo(
'{bold}{name: <{name_length}} {address: <{address_length}} {port: <{port_length}} {scheme: <{scheme_length}}{end_bold}'.format(
bold=ansiprint.bold(),
end_bold=ansiprint.end(),
name=cluster,
name_length=name_length,
address=address,
address_length=address_length,
port=port,
port_length=port_length,
scheme=scheme,
scheme_length=scheme_length
)
)
############################################################################### ###############################################################################
# pvc node # pvc node
############################################################################### ###############################################################################
@ -1669,43 +1899,34 @@ def init_cluster(yes):
############################################################################### ###############################################################################
@click.group(context_settings=CONTEXT_SETTINGS) @click.group(context_settings=CONTEXT_SETTINGS)
@click.option( @click.option(
'-z', '--zookeeper', '_zk_host', envvar='PVC_ZOOKEEPER', default=None, '-c', '--cluster', '_cluster', envvar='PVC_CLUSTER', default=None,
help='Zookeeper connection string.' help='Zookeeper connection string.'
) )
@click.option( @click.option(
'-v', '--debug', '_debug', envvar='PVC_DEBUG', is_flag=True, default=False, '-v', '--debug', '_debug', envvar='PVC_DEBUG', is_flag=True, default=False,
help='Additional debug details.' help='Additional debug details.'
) )
def cli(_zk_host, _debug): def cli(_cluster, _debug):
""" """
Parallel Virtual Cluster CLI management tool Parallel Virtual Cluster CLI management tool
Environment variables: Environment variables:
"PVC_ZOOKEEPER": Set the cluster Zookeeper address instead of using "--zookeeper". "PVC_CLUSTER": Set the cluster to access instead of using --cluster/-c
If no PVC_ZOOKEEPER/--zookeeper is specified, attempts to load coordinators list from /etc/pvc/pvcd.yaml. If no PVC_CLUSTER/--cluster is specified, attempts first to load the "local" cluster, checking
for an API configuration in "/etc/pvc/pvc-api.yaml". If this is also not found, connection defaults
to "http://localhost:7370" as a last restort.
""" """
# If no zk_host was passed, try to read from /etc/pvc/pvcd.yaml; otherwise fail
if _zk_host is None:
try:
cfgfile = '/etc/pvc/pvcd.yaml'
with open(cfgfile) as cfgf:
o_config = yaml.load(cfgf)
_zk_host = o_config['pvc']['cluster']['coordinators']
except:
_zk_host = None
if _zk_host is None:
print('ERROR: Must specify a PVC_ZOOKEEPER value or have a coordinator set configured in /etc/pvc/pvcd.yaml.')
exit(1)
global zk_host
zk_host = _zk_host
global config global config
store_data = get_store(store_path)
config = get_config(store_data, _cluster)
config['debug'] = _debug config['debug'] = _debug
click.echo('Using cluster "{}" - Host: "{}" Scheme: "{}" Prefix: "{}"'.format(config['cluster'], config['api_host'], config['api_scheme'], config['api_prefix']))
click.echo()
config = dict()
# #
# Click command tree # Click command tree
@ -1786,13 +2007,19 @@ cli_ceph.add_command(ceph_volume)
cli_storage.add_command(cli_ceph) cli_storage.add_command(cli_ceph)
cli_cluster.add_command(cluster_add)
cli_cluster.add_command(cluster_remove)
cli_cluster.add_command(cluster_list)
cli.add_command(cli_node) cli.add_command(cli_node)
cli.add_command(cli_vm) cli.add_command(cli_vm)
cli.add_command(cli_network) cli.add_command(cli_network)
cli.add_command(cli_storage) cli.add_command(cli_storage)
cli.add_command(cli_cluster)
cli.add_command(status_cluster) cli.add_command(status_cluster)
cli.add_command(init_cluster) cli.add_command(init_cluster)
# #
# Main entry point # Main entry point
# #