From b7eddc6ae9d80da07432a6729c238ff6f3c82980 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Wed, 25 Dec 2019 14:10:23 -0500 Subject: [PATCH] Add CLI lib --- client-cli/cli_lib/ansiprint.py | 82 ++ client-cli/cli_lib/ceph.py | 734 ++++++++++++ client-cli/cli_lib/cluster.py | 98 ++ client-cli/cli_lib/network.py | 379 ++++++ client-cli/cli_lib/node.py | 225 ++++ client-cli/cli_lib/vm.py | 360 ++++++ client-cli/cli_lib/zkhandler.py | 100 ++ client-cli/client_lib | 1 - client-cli/pvc.py | 16 +- client-cli/pvc.py.orig | 1946 +++++++++++++++++++++++++++++++ 10 files changed, 3932 insertions(+), 9 deletions(-) create mode 100644 client-cli/cli_lib/ansiprint.py create mode 100644 client-cli/cli_lib/ceph.py create mode 100644 client-cli/cli_lib/cluster.py create mode 100644 client-cli/cli_lib/network.py create mode 100644 client-cli/cli_lib/node.py create mode 100644 client-cli/cli_lib/vm.py create mode 100644 client-cli/cli_lib/zkhandler.py delete mode 120000 client-cli/client_lib create mode 100755 client-cli/pvc.py.orig diff --git a/client-cli/cli_lib/ansiprint.py b/client-cli/cli_lib/ansiprint.py new file mode 100644 index 00000000..6d37b8b8 --- /dev/null +++ b/client-cli/cli_lib/ansiprint.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +# ansiprint.py - Printing function for formatted messages +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018-2019 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import datetime + +# ANSII colours for output +def red(): + return '\033[91m' +def blue(): + return '\033[94m' +def cyan(): + return '\033[96m' +def green(): + return '\033[92m' +def yellow(): + return '\033[93m' +def purple(): + return '\033[95m' +def bold(): + return '\033[1m' +def end(): + return '\033[0m' + +# Print function +def echo(message, prefix, state): + # Get the date + date = '{} - '.format(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S.%f')) + endc = end() + + # Continuation + if state == 'c': + date = '' + colour = '' + prompt = ' ' + # OK + elif state == 'o': + colour = green() + prompt = '>>> ' + # Error + elif state == 'e': + colour = red() + prompt = '>>> ' + # Warning + elif state == 'w': + colour = yellow() + prompt = '>>> ' + # Tick + elif state == 't': + colour = purple() + prompt = '>>> ' + # Information + elif state == 'i': + colour = blue() + prompt = '>>> ' + else: + colour = bold() + prompt = '>>> ' + + # Append space to prefix + if prefix != '': + prefix = prefix + ' ' + + print(colour + prompt + endc + date + prefix + message) diff --git a/client-cli/cli_lib/ceph.py b/client-cli/cli_lib/ceph.py new file mode 100644 index 00000000..50aef7bc --- /dev/null +++ b/client-cli/cli_lib/ceph.py @@ -0,0 +1,734 @@ +#!/usr/bin/env python3 + +# ceph.py - PVC CLI client function library, Ceph cluster fuctions +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018-2019 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import re +import click +import json +import time +import math + +import cli_lib.ansiprint as ansiprint + +# +# Supplemental functions +# + +# Format byte sizes to/from human-readable units +byte_unit_matrix = { + 'B': 1, + 'K': 1024, + 'M': 1024*1024, + 'G': 1024*1024*1024, + 'T': 1024*1024*1024*1024, + 'P': 1024*1024*1024*1024*1024 +} +def format_bytes_tohuman(databytes): + datahuman = '' + for unit in sorted(byte_unit_matrix, key=byte_unit_matrix.get, reverse=True): + new_bytes = int(math.ceil(databytes / byte_unit_matrix[unit])) + # Round up if 5 or more digits + if new_bytes > 9999: + # We can jump down another level + continue + else: + # We're at the end, display with this size + datahuman = '{}{}'.format(new_bytes, unit) + + return datahuman + +def format_bytes_fromhuman(datahuman): + # Trim off human-readable character + dataunit = datahuman[-1] + datasize = int(datahuman[:-1]) + databytes = datasize * byte_unit_matrix[dataunit] + return '{}B'.format(databytes) + +# Format ops sizes to/from human-readable units +ops_unit_matrix = { + '': 1, + 'K': 1000, + 'M': 1000*1000, + 'G': 1000*1000*1000, + 'T': 1000*1000*1000*1000, + 'P': 1000*1000*1000*1000*1000 +} +def format_ops_tohuman(dataops): + datahuman = '' + for unit in sorted(ops_unit_matrix, key=ops_unit_matrix.get, reverse=True): + new_ops = int(math.ceil(dataops / ops_unit_matrix[unit])) + # Round up if 5 or more digits + if new_ops > 9999: + # We can jump down another level + continue + else: + # We're at the end, display with this size + datahuman = '{}{}'.format(new_ops, unit) + + return datahuman + +def format_ops_fromhuman(datahuman): + # Trim off human-readable character + dataunit = datahuman[-1] + datasize = int(datahuman[:-1]) + dataops = datasize * ops_unit_matrix[dataunit] + return '{}'.format(dataops) + +def format_pct_tohuman(datapct): + datahuman = "{0:.1f}".format(float(datapct * 100.0)) + return datahuman + +# +# Status functions +# +def format_raw_output(status_data): + click.echo('{bold}Ceph cluster {stype} (primary node {end}{blue}{primary}{end}{bold}){end}\n'.format(bold=ansiprint.bold(), end=ansiprint.end(), blue=ansiprint.blue(), stype=status_data['type'], primary=status_data['primary_node'])) + click.echo(status_data['ceph_data']) + click.echo('') + +# +# OSD functions +# +def getOutputColoursOSD(osd_information): + # Set the UP status + if osd_information['stats']['up'] == 1: + osd_up_flag = 'Yes' + osd_up_colour = ansiprint.green() + else: + osd_up_flag = 'No' + osd_up_colour = ansiprint.red() + + # Set the IN status + if osd_information['stats']['in'] == 1: + osd_in_flag = 'Yes' + osd_in_colour = ansiprint.green() + else: + osd_in_flag = 'No' + osd_in_colour = ansiprint.red() + + return osd_up_flag, osd_up_colour, osd_in_flag, osd_in_colour + +def format_list_osd(osd_list): + osd_list_output = [] + + osd_id_length = 3 + osd_up_length = 4 + osd_in_length = 4 + osd_size_length = 5 + osd_weight_length = 3 + osd_reweight_length = 5 + osd_pgs_length = 4 + osd_node_length = 5 + osd_used_length = 5 + osd_free_length = 6 + osd_util_length = 6 + osd_var_length = 5 + osd_wrops_length = 4 + osd_wrdata_length = 5 + osd_rdops_length = 4 + osd_rddata_length = 5 + + for osd_information in osd_list: + try: + # If this happens, the node hasn't checked in fully yet, so just ignore it + if osd_information['stats']['node'] == '|': + continue + except KeyError: + continue + + # Deal with the size to human readable + osd_information['stats']['size'] = osd_information['stats']['kb'] * 1024 + for datatype in 'size', 'wr_data', 'rd_data': + databytes = osd_information['stats'][datatype] + databytes_formatted = format_bytes_tohuman(int(databytes)) + osd_information['stats'][datatype] = databytes_formatted + for datatype in 'wr_ops', 'rd_ops': + dataops = osd_information['stats'][datatype] + dataops_formatted = format_ops_tohuman(int(dataops)) + osd_information['stats'][datatype] = dataops_formatted + + # Set the OSD ID length + _osd_id_length = len(osd_information['id']) + 1 + if _osd_id_length > osd_id_length: + osd_id_length = _osd_id_length + + _osd_node_length = len(osd_information['stats']['node']) + 1 + if _osd_node_length > osd_node_length: + osd_node_length = _osd_node_length + + # Set the size and length + _osd_size_length = len(str(osd_information['stats']['size'])) + 1 + if _osd_size_length > osd_size_length: + osd_size_length = _osd_size_length + + # Set the weight and length + _osd_weight_length = len(str(osd_information['stats']['weight'])) + 1 + if _osd_weight_length > osd_weight_length: + osd_weight_length = _osd_weight_length + + # Set the reweight and length + _osd_reweight_length = len(str(osd_information['stats']['reweight'])) + 1 + if _osd_reweight_length > osd_reweight_length: + osd_reweight_length = _osd_reweight_length + + # Set the pgs and length + _osd_pgs_length = len(str(osd_information['stats']['pgs'])) + 1 + if _osd_pgs_length > osd_pgs_length: + osd_pgs_length = _osd_pgs_length + + # Set the used/available/utlization%/variance and lengths + _osd_used_length = len(osd_information['stats']['used']) + 1 + if _osd_used_length > osd_used_length: + osd_used_length = _osd_used_length + + _osd_free_length = len(osd_information['stats']['avail']) + 1 + if _osd_free_length > osd_free_length: + osd_free_length = _osd_free_length + + osd_util = round(osd_information['stats']['utilization'], 2) + _osd_util_length = len(str(osd_util)) + 1 + if _osd_util_length > osd_util_length: + osd_util_length = _osd_util_length + + osd_var = round(osd_information['stats']['var'], 2) + _osd_var_length = len(str(osd_var)) + 1 + if _osd_var_length > osd_var_length: + osd_var_length = _osd_var_length + + # Set the read/write IOPS/data and length + _osd_wrops_length = len(osd_information['stats']['wr_ops']) + 1 + if _osd_wrops_length > osd_wrops_length: + osd_wrops_length = _osd_wrops_length + + _osd_wrdata_length = len(osd_information['stats']['wr_data']) + 1 + if _osd_wrdata_length > osd_wrdata_length: + osd_wrdata_length = _osd_wrdata_length + + _osd_rdops_length = len(osd_information['stats']['rd_ops']) + 1 + if _osd_rdops_length > osd_rdops_length: + osd_rdops_length = _osd_rdops_length + + _osd_rddata_length = len(osd_information['stats']['rd_data']) + 1 + if _osd_rddata_length > osd_rddata_length: + osd_rddata_length = _osd_rddata_length + + # Format the output header + osd_list_output.append('{bold}\ +{osd_id: <{osd_id_length}} \ +{osd_node: <{osd_node_length}} \ +{osd_up: <{osd_up_length}} \ +{osd_in: <{osd_in_length}} \ +{osd_size: <{osd_size_length}} \ +{osd_pgs: <{osd_pgs_length}} \ +{osd_weight: <{osd_weight_length}} \ +{osd_reweight: <{osd_reweight_length}} \ +Sp: {osd_used: <{osd_used_length}} \ +{osd_free: <{osd_free_length}} \ +{osd_util: <{osd_util_length}} \ +{osd_var: <{osd_var_length}} \ +Rd: {osd_rdops: <{osd_rdops_length}} \ +{osd_rddata: <{osd_rddata_length}} \ +Wr: {osd_wrops: <{osd_wrops_length}} \ +{osd_wrdata: <{osd_wrdata_length}} \ +{end_bold}'.format( + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + osd_id_length=osd_id_length, + osd_node_length=osd_node_length, + osd_up_length=osd_up_length, + osd_in_length=osd_in_length, + osd_size_length=osd_size_length, + osd_pgs_length=osd_pgs_length, + osd_weight_length=osd_weight_length, + osd_reweight_length=osd_reweight_length, + osd_used_length=osd_used_length, + osd_free_length=osd_free_length, + osd_util_length=osd_util_length, + osd_var_length=osd_var_length, + osd_wrops_length=osd_wrops_length, + osd_wrdata_length=osd_wrdata_length, + osd_rdops_length=osd_rdops_length, + osd_rddata_length=osd_rddata_length, + osd_id='ID', + osd_node='Node', + osd_up='Up', + osd_in='In', + osd_size='Size', + osd_pgs='PGs', + osd_weight='Wt', + osd_reweight='ReWt', + osd_used='Used', + osd_free='Free', + osd_util='Util%', + osd_var='Var', + osd_wrops='OPS', + osd_wrdata='Data', + osd_rdops='OPS', + osd_rddata='Data' + ) + ) + + for osd_information in osd_list: + try: + # If this happens, the node hasn't checked in fully yet, so just ignore it + if osd_information['stats']['node'] == '|': + continue + except KeyError: + continue + + osd_up_flag, osd_up_colour, osd_in_flag, osd_in_colour = getOutputColoursOSD(osd_information) + osd_util = round(osd_information['stats']['utilization'], 2) + osd_var = round(osd_information['stats']['var'], 2) + + # Format the output header + osd_list_output.append('{bold}\ +{osd_id: <{osd_id_length}} \ +{osd_node: <{osd_node_length}} \ +{osd_up_colour}{osd_up: <{osd_up_length}}{end_colour} \ +{osd_in_colour}{osd_in: <{osd_in_length}}{end_colour} \ +{osd_size: <{osd_size_length}} \ +{osd_pgs: <{osd_pgs_length}} \ +{osd_weight: <{osd_weight_length}} \ +{osd_reweight: <{osd_reweight_length}} \ + {osd_used: <{osd_used_length}} \ +{osd_free: <{osd_free_length}} \ +{osd_util: <{osd_util_length}} \ +{osd_var: <{osd_var_length}} \ + {osd_rdops: <{osd_rdops_length}} \ +{osd_rddata: <{osd_rddata_length}} \ + {osd_wrops: <{osd_wrops_length}} \ +{osd_wrdata: <{osd_wrdata_length}} \ +{end_bold}'.format( + bold='', + end_bold='', + end_colour=ansiprint.end(), + osd_id_length=osd_id_length, + osd_node_length=osd_node_length, + osd_up_length=osd_up_length, + osd_in_length=osd_in_length, + osd_size_length=osd_size_length, + osd_pgs_length=osd_pgs_length, + osd_weight_length=osd_weight_length, + osd_reweight_length=osd_reweight_length, + osd_used_length=osd_used_length, + osd_free_length=osd_free_length, + osd_util_length=osd_util_length, + osd_var_length=osd_var_length, + osd_wrops_length=osd_wrops_length, + osd_wrdata_length=osd_wrdata_length, + osd_rdops_length=osd_rdops_length, + osd_rddata_length=osd_rddata_length, + osd_id=osd_information['id'], + osd_node=osd_information['stats']['node'], + osd_up_colour=osd_up_colour, + osd_up=osd_up_flag, + osd_in_colour=osd_in_colour, + osd_in=osd_in_flag, + osd_size=osd_information['stats']['size'], + osd_pgs=osd_information['stats']['pgs'], + osd_weight=osd_information['stats']['weight'], + osd_reweight=osd_information['stats']['reweight'], + osd_used=osd_information['stats']['used'], + osd_free=osd_information['stats']['avail'], + osd_util=osd_util, + osd_var=osd_var, + osd_wrops=osd_information['stats']['wr_ops'], + osd_wrdata=osd_information['stats']['wr_data'], + osd_rdops=osd_information['stats']['rd_ops'], + osd_rddata=osd_information['stats']['rd_data'] + ) + ) + + click.echo('\n'.join(sorted(osd_list_output))) + + +# +# Pool functions +# +def format_list_pool(pool_list): + pool_list_output = [] + + pool_name_length = 5 + pool_id_length = 3 + pool_used_length = 5 + pool_usedpct_length = 5 + pool_free_length = 5 + pool_num_objects_length = 6 + pool_num_clones_length = 7 + pool_num_copies_length = 7 + pool_num_degraded_length = 9 + pool_read_ops_length = 4 + pool_read_data_length = 5 + pool_write_ops_length = 4 + pool_write_data_length = 5 + + for pool_information in pool_list: + # Deal with the size to human readable + for datatype in ['free_bytes', 'used_bytes', 'write_bytes', 'read_bytes']: + databytes = pool_information['stats'][datatype] + databytes_formatted = format_bytes_tohuman(int(databytes)) + pool_information['stats'][datatype] = databytes_formatted + for datatype in ['write_ops', 'read_ops']: + dataops = pool_information['stats'][datatype] + dataops_formatted = format_ops_tohuman(int(dataops)) + pool_information['stats'][datatype] = dataops_formatted + for datatype in ['used_percent']: + datapct = pool_information['stats'][datatype] + datapct_formatted = format_pct_tohuman(float(datapct)) + pool_information['stats'][datatype] = datapct_formatted + + # Set the Pool name length + _pool_name_length = len(pool_information['name']) + 1 + if _pool_name_length > pool_name_length: + pool_name_length = _pool_name_length + + # Set the id and length + _pool_id_length = len(str(pool_information['stats']['id'])) + 1 + if _pool_id_length > pool_id_length: + pool_id_length = _pool_id_length + + # Set the used and length + _pool_used_length = len(str(pool_information['stats']['used_bytes'])) + 1 + if _pool_used_length > pool_used_length: + pool_used_length = _pool_used_length + + # Set the usedpct and length + _pool_usedpct_length = len(str(pool_information['stats']['used_percent'])) + 1 + if _pool_usedpct_length > pool_usedpct_length: + pool_usedpct_length = _pool_usedpct_length + + # Set the free and length + _pool_free_length = len(str(pool_information['stats']['free_bytes'])) + 1 + if _pool_free_length > pool_free_length: + pool_free_length = _pool_free_length + + # Set the num_objects and length + _pool_num_objects_length = len(str(pool_information['stats']['num_objects'])) + 1 + if _pool_num_objects_length > pool_num_objects_length: + pool_num_objects_length = _pool_num_objects_length + + # Set the num_clones and length + _pool_num_clones_length = len(str(pool_information['stats']['num_object_clones'])) + 1 + if _pool_num_clones_length > pool_num_clones_length: + pool_num_clones_length = _pool_num_clones_length + + # Set the num_copies and length + _pool_num_copies_length = len(str(pool_information['stats']['num_object_copies'])) + 1 + if _pool_num_copies_length > pool_num_copies_length: + pool_num_copies_length = _pool_num_copies_length + + # Set the num_degraded and length + _pool_num_degraded_length = len(str(pool_information['stats']['num_objects_degraded'])) + 1 + if _pool_num_degraded_length > pool_num_degraded_length: + pool_num_degraded_length = _pool_num_degraded_length + + # Set the read/write IOPS/data and length + _pool_write_ops_length = len(str(pool_information['stats']['write_ops'])) + 1 + if _pool_write_ops_length > pool_write_ops_length: + pool_write_ops_length = _pool_write_ops_length + + _pool_write_data_length = len(pool_information['stats']['write_bytes']) + 1 + if _pool_write_data_length > pool_write_data_length: + pool_write_data_length = _pool_write_data_length + + _pool_read_ops_length = len(str(pool_information['stats']['read_ops'])) + 1 + if _pool_read_ops_length > pool_read_ops_length: + pool_read_ops_length = _pool_read_ops_length + + _pool_read_data_length = len(pool_information['stats']['read_bytes']) + 1 + if _pool_read_data_length > pool_read_data_length: + pool_read_data_length = _pool_read_data_length + + # Format the output header + pool_list_output.append('{bold}\ +{pool_id: <{pool_id_length}} \ +{pool_name: <{pool_name_length}} \ +{pool_used: <{pool_used_length}} \ +{pool_usedpct: <{pool_usedpct_length}} \ +{pool_free: <{pool_free_length}} \ +Obj: {pool_objects: <{pool_objects_length}} \ +{pool_clones: <{pool_clones_length}} \ +{pool_copies: <{pool_copies_length}} \ +{pool_degraded: <{pool_degraded_length}} \ +Rd: {pool_read_ops: <{pool_read_ops_length}} \ +{pool_read_data: <{pool_read_data_length}} \ +Wr: {pool_write_ops: <{pool_write_ops_length}} \ +{pool_write_data: <{pool_write_data_length}} \ +{end_bold}'.format( + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + pool_id_length=pool_id_length, + pool_name_length=pool_name_length, + pool_used_length=pool_used_length, + pool_usedpct_length=pool_usedpct_length, + pool_free_length=pool_free_length, + pool_objects_length=pool_num_objects_length, + pool_clones_length=pool_num_clones_length, + pool_copies_length=pool_num_copies_length, + pool_degraded_length=pool_num_degraded_length, + pool_write_ops_length=pool_write_ops_length, + pool_write_data_length=pool_write_data_length, + pool_read_ops_length=pool_read_ops_length, + pool_read_data_length=pool_read_data_length, + pool_id='ID', + pool_name='Name', + pool_used='Used', + pool_usedpct='%', + pool_free='Free', + pool_objects='Count', + pool_clones='Clones', + pool_copies='Copies', + pool_degraded='Degraded', + pool_write_ops='OPS', + pool_write_data='Data', + pool_read_ops='OPS', + pool_read_data='Data' + ) + ) + + for pool_information in pool_list: + # Format the output header + pool_list_output.append('{bold}\ +{pool_id: <{pool_id_length}} \ +{pool_name: <{pool_name_length}} \ +{pool_used: <{pool_used_length}} \ +{pool_usedpct: <{pool_usedpct_length}} \ +{pool_free: <{pool_free_length}} \ + {pool_objects: <{pool_objects_length}} \ +{pool_clones: <{pool_clones_length}} \ +{pool_copies: <{pool_copies_length}} \ +{pool_degraded: <{pool_degraded_length}} \ + {pool_read_ops: <{pool_read_ops_length}} \ +{pool_read_data: <{pool_read_data_length}} \ + {pool_write_ops: <{pool_write_ops_length}} \ +{pool_write_data: <{pool_write_data_length}} \ +{end_bold}'.format( + bold='', + end_bold='', + pool_id_length=pool_id_length, + pool_name_length=pool_name_length, + pool_used_length=pool_used_length, + pool_usedpct_length=pool_usedpct_length, + pool_free_length=pool_free_length, + pool_objects_length=pool_num_objects_length, + pool_clones_length=pool_num_clones_length, + pool_copies_length=pool_num_copies_length, + pool_degraded_length=pool_num_degraded_length, + pool_write_ops_length=pool_write_ops_length, + pool_write_data_length=pool_write_data_length, + pool_read_ops_length=pool_read_ops_length, + pool_read_data_length=pool_read_data_length, + pool_id=pool_information['stats']['id'], + pool_name=pool_information['name'], + pool_used=pool_information['stats']['used_bytes'], + pool_usedpct=pool_information['stats']['used_percent'], + pool_free=pool_information['stats']['free_bytes'], + pool_objects=pool_information['stats']['num_objects'], + pool_clones=pool_information['stats']['num_object_clones'], + pool_copies=pool_information['stats']['num_object_copies'], + pool_degraded=pool_information['stats']['num_objects_degraded'], + pool_write_ops=pool_information['stats']['write_ops'], + pool_write_data=pool_information['stats']['write_bytes'], + pool_read_ops=pool_information['stats']['read_ops'], + pool_read_data=pool_information['stats']['read_bytes'] + ) + ) + + click.echo('\n'.join(sorted(pool_list_output))) + + +# +# Volume functions +# +def format_list_volume(volume_list): + volume_list_output = [] + + volume_name_length = 5 + volume_pool_length = 5 + volume_size_length = 5 + volume_objects_length = 8 + volume_order_length = 6 + volume_format_length = 7 + volume_features_length = 10 + + for volume_information in volume_list: + # Set the Volume name length + _volume_name_length = len(volume_information['name']) + 1 + if _volume_name_length > volume_name_length: + volume_name_length = _volume_name_length + + # Set the Volume pool length + _volume_pool_length = len(volume_information['pool']) + 1 + if _volume_pool_length > volume_pool_length: + volume_pool_length = _volume_pool_length + + # Set the size and length + _volume_size_length = len(str(volume_information['stats']['size'])) + 1 + if _volume_size_length > volume_size_length: + volume_size_length = _volume_size_length + + # Set the num_objects and length + _volume_objects_length = len(str(volume_information['stats']['objects'])) + 1 + if _volume_objects_length > volume_objects_length: + volume_objects_length = _volume_objects_length + + # Set the order and length + _volume_order_length = len(str(volume_information['stats']['order'])) + 1 + if _volume_order_length > volume_order_length: + volume_order_length = _volume_order_length + + # Set the format and length + _volume_format_length = len(str(volume_information['stats']['format'])) + 1 + if _volume_format_length > volume_format_length: + volume_format_length = _volume_format_length + + # Set the features and length + _volume_features_length = len(str(','.join(volume_information['stats']['features']))) + 1 + if _volume_features_length > volume_features_length: + volume_features_length = _volume_features_length + + # Format the output header + volume_list_output.append('{bold}\ +{volume_name: <{volume_name_length}} \ +{volume_pool: <{volume_pool_length}} \ +{volume_size: <{volume_size_length}} \ +{volume_objects: <{volume_objects_length}} \ +{volume_order: <{volume_order_length}} \ +{volume_format: <{volume_format_length}} \ +{volume_features: <{volume_features_length}} \ +{end_bold}'.format( + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + volume_name_length=volume_name_length, + volume_pool_length=volume_pool_length, + volume_size_length=volume_size_length, + volume_objects_length=volume_objects_length, + volume_order_length=volume_order_length, + volume_format_length=volume_format_length, + volume_features_length=volume_features_length, + volume_name='Name', + volume_pool='Pool', + volume_size='Size', + volume_objects='Objects', + volume_order='Order', + volume_format='Format', + volume_features='Features', + ) + ) + + for volume_information in volume_list: + volume_list_output.append('{bold}\ +{volume_name: <{volume_name_length}} \ +{volume_pool: <{volume_pool_length}} \ +{volume_size: <{volume_size_length}} \ +{volume_objects: <{volume_objects_length}} \ +{volume_order: <{volume_order_length}} \ +{volume_format: <{volume_format_length}} \ +{volume_features: <{volume_features_length}} \ +{end_bold}'.format( + bold='', + end_bold='', + volume_name_length=volume_name_length, + volume_pool_length=volume_pool_length, + volume_size_length=volume_size_length, + volume_objects_length=volume_objects_length, + volume_order_length=volume_order_length, + volume_format_length=volume_format_length, + volume_features_length=volume_features_length, + volume_name=volume_information['name'], + volume_pool=volume_information['pool'], + volume_size=volume_information['stats']['size'], + volume_objects=volume_information['stats']['objects'], + volume_order=volume_information['stats']['order'], + volume_format=volume_information['stats']['format'], + volume_features=','.join(volume_information['stats']['features']), + ) + ) + + click.echo('\n'.join(sorted(volume_list_output))) + + +# +# Snapshot functions +# +def format_list_snapshot(snapshot_list): + snapshot_list_output = [] + + snapshot_name_length = 5 + snapshot_volume_length = 7 + snapshot_pool_length = 5 + + for snapshot in snapshot_list: + volume, snapshot_name = snapshot.split('@') + snapshot_pool, snapshot_volume = volume.split('/') + + # Set the Snapshot name length + _snapshot_name_length = len(snapshot_name) + 1 + if _snapshot_name_length > snapshot_name_length: + snapshot_name_length = _snapshot_name_length + + # Set the Snapshot volume length + _snapshot_volume_length = len(snapshot_volume) + 1 + if _snapshot_volume_length > snapshot_volume_length: + snapshot_volume_length = _snapshot_volume_length + + # Set the Snapshot pool length + _snapshot_pool_length = len(snapshot_pool) + 1 + if _snapshot_pool_length > snapshot_pool_length: + snapshot_pool_length = _snapshot_pool_length + + # Format the output header + snapshot_list_output.append('{bold}\ +{snapshot_name: <{snapshot_name_length}} \ +{snapshot_volume: <{snapshot_volume_length}} \ +{snapshot_pool: <{snapshot_pool_length}} \ +{end_bold}'.format( + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + snapshot_name_length=snapshot_name_length, + snapshot_volume_length=snapshot_volume_length, + snapshot_pool_length=snapshot_pool_length, + snapshot_name='Name', + snapshot_volume='Volume', + snapshot_pool='Pool', + ) + ) + + for snapshot in snapshot_list: + volume, snapshot_name = snapshot.split('@') + snapshot_pool, snapshot_volume = volume.split('/') + snapshot_list_output.append('{bold}\ +{snapshot_name: <{snapshot_name_length}} \ +{snapshot_volume: <{snapshot_volume_length}} \ +{snapshot_pool: <{snapshot_pool_length}} \ +{end_bold}'.format( + bold='', + end_bold='', + snapshot_name_length=snapshot_name_length, + snapshot_volume_length=snapshot_volume_length, + snapshot_pool_length=snapshot_pool_length, + snapshot_name=snapshot_name, + snapshot_volume=snapshot_volume, + snapshot_pool=snapshot_pool, + ) + ) + + click.echo('\n'.join(sorted(snapshot_list_output))) diff --git a/client-cli/cli_lib/cluster.py b/client-cli/cli_lib/cluster.py new file mode 100644 index 00000000..8487def7 --- /dev/null +++ b/client-cli/cli_lib/cluster.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 + +# cluster.py - PVC CLI client function library, cluster management +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018-2019 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import click +import json + +import cli_lib.ansiprint as ansiprint + +def format_info(cluster_information, oformat): + if oformat == 'json': + print(json.dumps(cluster_information)) + return + + if oformat == 'json-pretty': + print(json.dumps(cluster_information, indent=4)) + return + + # Plain formatting, i.e. human-readable + if cluster_information['health'] == 'Optimal': + health_colour = ansiprint.green() + else: + health_colour = ansiprint.yellow() + + ainformation = [] + ainformation.append('{}PVC cluster status:{}'.format(ansiprint.bold(), ansiprint.end())) + ainformation.append('') + ainformation.append('{}Cluster health:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), health_colour, cluster_information['health'], ansiprint.end())) + ainformation.append('{}Primary node:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['primary_node'])) + ainformation.append('{}Cluster upstream IP:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['upstream_ip'])) + ainformation.append('') + ainformation.append('{}Total nodes:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['nodes']['total'])) + ainformation.append('{}Total VMs:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['vms']['total'])) + ainformation.append('{}Total networks:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['networks'])) + ainformation.append('{}Total OSDs:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['osds']['total'])) + ainformation.append('{}Total pools:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['pools'])) + ainformation.append('{}Total volumes:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['volumes'])) + ainformation.append('{}Total snapshots:{} {}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['snapshots'])) + + nodes_string = '{}Nodes:{} {}/{} {}ready,run{}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['nodes']['run,ready'], cluster_information['nodes']['total'], ansiprint.green(), ansiprint.end()) + for state, count in cluster_information['nodes'].items(): + if state == 'total' or state == 'run,ready': + continue + + nodes_string += ' {}/{} {}{}{}'.format(count, cluster_information['nodes']['total'], ansiprint.yellow(), state, ansiprint.end()) + + ainformation.append('') + ainformation.append(nodes_string) + + vms_string = '{}VMs:{} {}/{} {}start{}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['vms']['start'], cluster_information['vms']['total'], ansiprint.green(), ansiprint.end()) + for state, count in cluster_information['vms'].items(): + if state == 'total' or state == 'start': + continue + + if state == 'disable': + colour = ansiprint.blue() + else: + colour = ansiprint.yellow() + + vms_string += ' {}/{} {}{}{}'.format(count, cluster_information['vms']['total'], colour, state, ansiprint.end()) + + ainformation.append('') + ainformation.append(vms_string) + + if cluster_information['osds']['total'] > 0: + osds_string = '{}Ceph OSDs:{} {}/{} {}up,in{}'.format(ansiprint.purple(), ansiprint.end(), cluster_information['osds']['up,in'], cluster_information['osds']['total'], ansiprint.green(), ansiprint.end()) + for state, count in cluster_information['osds'].items(): + if state == 'total' or state == 'up,in': + continue + + osds_string += ' {}/{} {}{}{}'.format(count, cluster_information['osds']['total'], ansiprint.yellow(), state, ansiprint.end()) + + ainformation.append('') + ainformation.append(osds_string) + + information = '\n'.join(ainformation) + click.echo(information) + + click.echo('') + diff --git a/client-cli/cli_lib/network.py b/client-cli/cli_lib/network.py new file mode 100644 index 00000000..5694fd98 --- /dev/null +++ b/client-cli/cli_lib/network.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 + +# network.py - PVC CLI client function library, Network fuctions +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018-2019 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import difflib +import colorama +import click + +import cli_lib.ansiprint as ansiprint + +# +# Cluster search functions +# +def isValidMAC(macaddr): + allowed = re.compile(r""" + ( + ^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$ + ) + """, + re.VERBOSE|re.IGNORECASE) + + if allowed.match(macaddr): + return True + else: + return False + +def isValidIP(ipaddr): + ip4_blocks = str(ipaddr).split(".") + if len(ip4_blocks) == 4: + for block in ip4_blocks: + # Check if number is digit, if not checked before calling this function + if not block.isdigit(): + return False + tmp = int(block) + if 0 > tmp > 255: + return False + return True + return False + +# +# Direct functions +# +def getOutputColours(network_information): + if network_information['ip6']['network'] != "None": + v6_flag_colour = ansiprint.green() + else: + v6_flag_colour = ansiprint.blue() + if network_information['ip4']['network'] != "None": + v4_flag_colour = ansiprint.green() + else: + v4_flag_colour = ansiprint.blue() + + if network_information['ip6']['dhcp_flag'] == "True": + dhcp6_flag_colour = ansiprint.green() + else: + dhcp6_flag_colour = ansiprint.blue() + if network_information['ip4']['dhcp_flag'] == "True": + dhcp4_flag_colour = ansiprint.green() + else: + dhcp4_flag_colour = ansiprint.blue() + + return v6_flag_colour, v4_flag_colour, dhcp6_flag_colour, dhcp4_flag_colour + +def format_info(network_information, long_output): + if not network_information: + click.echo("No network found") + return + + v6_flag_colour, v4_flag_colour, dhcp6_flag_colour, dhcp4_flag_colour = getOutputColours(network_information) + + # Format a nice output: do this line-by-line then concat the elements at the end + ainformation = [] + ainformation.append('{}Virtual network information:{}'.format(ansiprint.bold(), ansiprint.end())) + ainformation.append('') + # Basic information + ainformation.append('{}VNI:{} {}'.format(ansiprint.purple(), ansiprint.end(), network_information['vni'])) + ainformation.append('{}Type:{} {}'.format(ansiprint.purple(), ansiprint.end(), network_information['type'])) + ainformation.append('{}Description:{} {}'.format(ansiprint.purple(), ansiprint.end(), network_information['description'])) + if network_information['type'] == 'managed': + ainformation.append('{}Domain:{} {}'.format(ansiprint.purple(), ansiprint.end(), network_information['domain'])) + ainformation.append('{}DNS Servers:{} {}'.format(ansiprint.purple(), ansiprint.end(), ', '.join(network_information['name_servers']))) + if network_information['ip6']['network'] != "None": + ainformation.append('') + ainformation.append('{}IPv6 network:{} {}'.format(ansiprint.purple(), ansiprint.end(), network_information['ip6']['network'])) + ainformation.append('{}IPv6 gateway:{} {}'.format(ansiprint.purple(), ansiprint.end(), network_information['ip6']['gateway'])) + ainformation.append('{}DHCPv6 enabled:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), dhcp6_flag_colour, network_information['ip6']['dhcp_flag'], ansiprint.end())) + if network_information['ip4']['network'] != "None": + ainformation.append('') + ainformation.append('{}IPv4 network:{} {}'.format(ansiprint.purple(), ansiprint.end(), network_information['ip4']['network'])) + ainformation.append('{}IPv4 gateway:{} {}'.format(ansiprint.purple(), ansiprint.end(), network_information['ip4']['gateway'])) + ainformation.append('{}DHCPv4 enabled:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), dhcp4_flag_colour, network_information['ip4']['dhcp_flag'], ansiprint.end())) + if network_information['ip4']['dhcp_flag'] == "True": + ainformation.append('{}DHCPv4 range:{} {} - {}'.format(ansiprint.purple(), ansiprint.end(), network_information['ip4']['dhcp_start'], network_information['ip4']['dhcp_end'])) + + if long_output: + dhcp4_reservations_list = getNetworkDHCPReservations(zk_conn, vni) + if dhcp4_reservations_list: + ainformation.append('') + ainformation.append('{}Client DHCPv4 reservations:{}'.format(ansiprint.bold(), ansiprint.end())) + ainformation.append('') + # Only show static reservations in the detailed information + dhcp4_reservations_string = formatDHCPLeaseList(zk_conn, vni, dhcp4_reservations_list, reservations=True) + for line in dhcp4_reservations_string.split('\n'): + ainformation.append(line) + + firewall_rules = zkhandler.listchildren(zk_conn, '/networks/{}/firewall_rules'.format(vni)) + if firewall_rules: + ainformation.append('') + ainformation.append('{}Network firewall rules:{}'.format(ansiprint.bold(), ansiprint.end())) + ainformation.append('') + formatted_firewall_rules = get_list_firewall_rules(zk_conn, vni) + + # Join it all together + click.echo('\n'.join(ainformation)) + +def format_list(network_list): + if not network_list: + click.echo("No network found") + return + + network_list_output = [] + + # Determine optimal column widths + net_vni_length = 5 + net_description_length = 12 + net_nettype_length = 8 + net_domain_length = 6 + net_v6_flag_length = 6 + net_dhcp6_flag_length = 7 + net_v4_flag_length = 6 + net_dhcp4_flag_length = 7 + for network_information in network_list: + # vni column + _net_vni_length = len(str(network_information['vni'])) + 1 + if _net_vni_length > net_vni_length: + net_vni_length = _net_vni_length + # description column + _net_description_length = len(network_information['description']) + 1 + if _net_description_length > net_description_length: + net_description_length = _net_description_length + # domain column + _net_domain_length = len(network_information['domain']) + 1 + if _net_domain_length > net_domain_length: + net_domain_length = _net_domain_length + + # Format the string (header) + network_list_output.append('{bold}\ +{net_vni: <{net_vni_length}} \ +{net_description: <{net_description_length}} \ +{net_nettype: <{net_nettype_length}} \ +{net_domain: <{net_domain_length}} \ +{net_v6_flag: <{net_v6_flag_length}} \ +{net_dhcp6_flag: <{net_dhcp6_flag_length}} \ +{net_v4_flag: <{net_v4_flag_length}} \ +{net_dhcp4_flag: <{net_dhcp4_flag_length}} \ +{end_bold}'.format( + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + net_vni_length=net_vni_length, + net_description_length=net_description_length, + net_nettype_length=net_nettype_length, + net_domain_length=net_domain_length, + net_v6_flag_length=net_v6_flag_length, + net_dhcp6_flag_length=net_dhcp6_flag_length, + net_v4_flag_length=net_v4_flag_length, + net_dhcp4_flag_length=net_dhcp4_flag_length, + net_vni='VNI', + net_description='Description', + net_nettype='Type', + net_domain='Domain', + net_v6_flag='IPv6', + net_dhcp6_flag='DHCPv6', + net_v4_flag='IPv4', + net_dhcp4_flag='DHCPv4', + ) + ) + + for network_information in network_list: + v6_flag_colour, v4_flag_colour, dhcp6_flag_colour, dhcp4_flag_colour = getOutputColours(network_information) + if network_information['ip4']['network'] != "None": + v4_flag = 'True' + else: + v4_flag = 'False' + + if network_information['ip6']['network'] != "None": + v6_flag = 'True' + else: + v6_flag = 'False' + + if network_information['ip4']['dhcp_flag'] == "True": + dhcp4_range = '{} - {}'.format(network_information['ip4']['dhcp_start'], network_information['ip4']['dhcp_end']) + else: + dhcp4_range = 'N/A' + + network_list_output.append( + '{bold}\ +{net_vni: <{net_vni_length}} \ +{net_description: <{net_description_length}} \ +{net_nettype: <{net_nettype_length}} \ +{net_domain: <{net_domain_length}} \ +{v6_flag_colour}{net_v6_flag: <{net_v6_flag_length}}{colour_off} \ +{dhcp6_flag_colour}{net_dhcp6_flag: <{net_dhcp6_flag_length}}{colour_off} \ +{v4_flag_colour}{net_v4_flag: <{net_v4_flag_length}}{colour_off} \ +{dhcp4_flag_colour}{net_dhcp4_flag: <{net_dhcp4_flag_length}}{colour_off} \ +{end_bold}'.format( + bold='', + end_bold='', + net_vni_length=net_vni_length, + net_description_length=net_description_length, + net_nettype_length=net_nettype_length, + net_domain_length=net_domain_length, + net_v6_flag_length=net_v6_flag_length, + net_dhcp6_flag_length=net_dhcp6_flag_length, + net_v4_flag_length=net_v4_flag_length, + net_dhcp4_flag_length=net_dhcp4_flag_length, + net_vni=network_information['vni'], + net_description=network_information['description'], + net_nettype=network_information['type'], + net_domain=network_information['domain'], + net_v6_flag=v6_flag, + v6_flag_colour=v6_flag_colour, + net_dhcp6_flag=network_information['ip6']['dhcp_flag'], + dhcp6_flag_colour=dhcp6_flag_colour, + net_v4_flag=v4_flag, + v4_flag_colour=v4_flag_colour, + net_dhcp4_flag=network_information['ip4']['dhcp_flag'], + dhcp4_flag_colour=dhcp4_flag_colour, + colour_off=ansiprint.end() + ) + ) + + click.echo('\n'.join(sorted(network_list_output))) + +def format_list_dhcp(dhcp_lease_list): + dhcp_lease_list_output = [] + + # Determine optimal column widths + lease_hostname_length = 9 + lease_ip4_address_length = 11 + lease_mac_address_length = 13 + lease_timestamp_length = 13 + for dhcp_lease_information in dhcp_lease_list: + # hostname column + _lease_hostname_length = len(dhcp_lease_information['hostname']) + 1 + if _lease_hostname_length > lease_hostname_length: + lease_hostname_length = _lease_hostname_length + # ip4_address column + _lease_ip4_address_length = len(dhcp_lease_information['ip4_address']) + 1 + if _lease_ip4_address_length > lease_ip4_address_length: + lease_ip4_address_length = _lease_ip4_address_length + # mac_address column + _lease_mac_address_length = len(dhcp_lease_information['mac_address']) + 1 + if _lease_mac_address_length > lease_mac_address_length: + lease_mac_address_length = _lease_mac_address_length + + # Format the string (header) + dhcp_lease_list_output.append('{bold}\ +{lease_hostname: <{lease_hostname_length}} \ +{lease_ip4_address: <{lease_ip4_address_length}} \ +{lease_mac_address: <{lease_mac_address_length}} \ +{lease_timestamp: <{lease_timestamp_length}} \ +{end_bold}'.format( + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + lease_hostname_length=lease_hostname_length, + lease_ip4_address_length=lease_ip4_address_length, + lease_mac_address_length=lease_mac_address_length, + lease_timestamp_length=lease_timestamp_length, + lease_hostname='Hostname', + lease_ip4_address='IP Address', + lease_mac_address='MAC Address', + lease_timestamp='Timestamp' + ) + ) + + for dhcp_lease_information in dhcp_lease_list: + dhcp_lease_list_output.append('{bold}\ +{lease_hostname: <{lease_hostname_length}} \ +{lease_ip4_address: <{lease_ip4_address_length}} \ +{lease_mac_address: <{lease_mac_address_length}} \ +{lease_timestamp: <{lease_timestamp_length}} \ +{end_bold}'.format( + bold='', + end_bold='', + lease_hostname_length=lease_hostname_length, + lease_ip4_address_length=lease_ip4_address_length, + lease_mac_address_length=lease_mac_address_length, + lease_timestamp_length=12, + lease_hostname=dhcp_lease_information['hostname'], + lease_ip4_address=dhcp_lease_information['ip4_address'], + lease_mac_address=dhcp_lease_information['mac_address'], + lease_timestamp=dhcp_lease_information['timestamp'] + ) + ) + + click.echo('\n'.join(sorted(dhcp_lease_list_output))) + +def format_list_acl(acl_list): + acl_list_output = [] + + # Determine optimal column widths + acl_direction_length = 10 + acl_order_length = 6 + acl_description_length = 12 + acl_rule_length = 5 + for acl_information in acl_list: + # order column + _acl_order_length = len(str(acl_information['order'])) + 1 + if _acl_order_length > acl_order_length: + acl_order_length = _acl_order_length + # description column + _acl_description_length = len(acl_information['description']) + 1 + if _acl_description_length > acl_description_length: + acl_description_length = _acl_description_length + # rule column + _acl_rule_length = len(acl_information['rule']) + 1 + if _acl_rule_length > acl_rule_length: + acl_rule_length = _acl_rule_length + + # Format the string (header) + acl_list_output.append('{bold}\ +{acl_direction: <{acl_direction_length}} \ +{acl_order: <{acl_order_length}} \ +{acl_description: <{acl_description_length}} \ +{acl_rule: <{acl_rule_length}} \ +{end_bold}'.format( + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + acl_direction_length=acl_direction_length, + acl_order_length=acl_order_length, + acl_description_length=acl_description_length, + acl_rule_length=acl_rule_length, + acl_direction='Direction', + acl_order='Order', + acl_description='Description', + acl_rule='Rule', + ) + ) + + for acl_information in acl_list: + acl_list_output.append('{bold}\ +{acl_direction: <{acl_direction_length}} \ +{acl_order: <{acl_order_length}} \ +{acl_description: <{acl_description_length}} \ +{acl_rule: <{acl_rule_length}} \ +{end_bold}'.format( + bold='', + end_bold='', + acl_direction_length=acl_direction_length, + acl_order_length=acl_order_length, + acl_description_length=acl_description_length, + acl_rule_length=acl_rule_length, + acl_direction=acl_information['direction'], + acl_order=acl_information['order'], + acl_description=acl_information['description'], + acl_rule=acl_information['rule'], + ) + ) + + click.echo('\n'.join(sorted(acl_list_output))) + diff --git a/client-cli/cli_lib/node.py b/client-cli/cli_lib/node.py new file mode 100644 index 00000000..bcc3d50d --- /dev/null +++ b/client-cli/cli_lib/node.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 + +# node.py - PVC CLI client function library, node management +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018-2019 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import difflib +import colorama +import click + +import cli_lib.ansiprint as ansiprint + +def getOutputColours(node_information): + if node_information['daemon_state'] == 'run': + daemon_state_colour = ansiprint.green() + elif node_information['daemon_state'] == 'stop': + daemon_state_colour = ansiprint.red() + elif node_information['daemon_state'] == 'shutdown': + daemon_state_colour = ansiprint.yellow() + elif node_information['daemon_state'] == 'init': + daemon_state_colour = ansiprint.yellow() + elif node_information['daemon_state'] == 'dead': + daemon_state_colour = ansiprint.red() + ansiprint.bold() + else: + daemon_state_colour = ansiprint.blue() + + if node_information['coordinator_state'] == 'primary': + coordinator_state_colour = ansiprint.green() + elif node_information['coordinator_state'] == 'secondary': + coordinator_state_colour = ansiprint.blue() + else: + coordinator_state_colour = ansiprint.cyan() + + if node_information['domain_state'] == 'ready': + domain_state_colour = ansiprint.green() + else: + domain_state_colour = ansiprint.blue() + + return daemon_state_colour, coordinator_state_colour, domain_state_colour + +def format_info(node_information, long_output): + daemon_state_colour, coordinator_state_colour, domain_state_colour = getOutputColours(node_information) + + # Format a nice output; do this line-by-line then concat the elements at the end + ainformation = [] + # Basic information + ainformation.append('{}Name:{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['name'])) + ainformation.append('{}Daemon State:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), daemon_state_colour, node_information['daemon_state'], ansiprint.end())) + ainformation.append('{}Coordinator State:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), coordinator_state_colour, node_information['coordinator_state'], ansiprint.end())) + ainformation.append('{}Domain State:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), domain_state_colour, node_information['domain_state'], ansiprint.end())) + ainformation.append('{}Active VM Count:{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['domains_count'])) + if long_output: + ainformation.append('') + ainformation.append('{}Architecture:{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['arch'])) + ainformation.append('{}Operating System:{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['os'])) + ainformation.append('{}Kernel Version:{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['kernel'])) + ainformation.append('') + ainformation.append('{}Host CPUs:{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['vcpu']['total'])) + ainformation.append('{}vCPUs:{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['vcpu']['allocated'])) + ainformation.append('{}Load:{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['load'])) + ainformation.append('{}Total RAM (MiB):{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['memory']['total'])) + ainformation.append('{}Used RAM (MiB):{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['memory']['used'])) + ainformation.append('{}Free RAM (MiB):{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['memory']['free'])) + ainformation.append('{}Allocated RAM (MiB):{} {}'.format(ansiprint.purple(), ansiprint.end(), node_information['memory']['allocated'])) + + # Join it all together + information = '\n'.join(ainformation) + click.echo(information) + + click.echo('') + +def format_list(node_list): + node_list_output = [] + + # Determine optimal column widths + node_name_length = 5 + daemon_state_length = 7 + coordinator_state_length = 12 + domain_state_length = 8 + domains_count_length = 4 + cpu_count_length = 6 + load_length = 5 + mem_total_length = 6 + mem_used_length = 5 + mem_free_length = 5 + mem_alloc_length = 4 + for node_information in node_list: + # node_name column + _node_name_length = len(node_information['name']) + 1 + if _node_name_length > node_name_length: + node_name_length = _node_name_length + # daemon_state column + _daemon_state_length = len(node_information['daemon_state']) + 1 + if _daemon_state_length > daemon_state_length: + daemon_state_length = _daemon_state_length + # coordinator_state column + _coordinator_state_length = len(node_information['coordinator_state']) + 1 + if _coordinator_state_length > coordinator_state_length: + coordinator_state_length = _coordinator_state_length + # domain_state column + _domain_state_length = len(node_information['domain_state']) + 1 + if _domain_state_length > domain_state_length: + domain_state_length = _domain_state_length + # domains_count column + _domains_count_length = len(str(node_information['domains_count'])) + 1 + if _domains_count_length > domains_count_length: + domains_count_length = _domains_count_length + # cpu_count column + _cpu_count_length = len(str(node_information['cpu_count'])) + 1 + if _cpu_count_length > cpu_count_length: + cpu_count_length = _cpu_count_length + # load column + _load_length = len(str(node_information['load'])) + 1 + if _load_length > load_length: + load_length = _load_length + # mem_total column + _mem_total_length = len(str(node_information['memory']['total'])) + 1 + if _mem_total_length > mem_total_length: + mem_total_length = _mem_total_length + # mem_used column + _mem_used_length = len(str(node_information['memory']['used'])) + 1 + if _mem_used_length > mem_used_length: + mem_used_length = _mem_used_length + # mem_free column + _mem_free_length = len(str(node_information['memory']['free'])) + 1 + if _mem_free_length > mem_free_length: + mem_free_length = _mem_free_length + # mem_alloc column + _mem_alloc_length = len(str(node_information['memory']['allocated'])) + 1 + if _mem_alloc_length > mem_alloc_length: + mem_alloc_length = _mem_alloc_length + + # Format the string (header) + node_list_output.append( + '{bold}{node_name: <{node_name_length}} \ +St: {daemon_state_colour}{node_daemon_state: <{daemon_state_length}}{end_colour} {coordinator_state_colour}{node_coordinator_state: <{coordinator_state_length}}{end_colour} {domain_state_colour}{node_domain_state: <{domain_state_length}}{end_colour} \ +Res: {node_domains_count: <{domains_count_length}} {node_cpu_count: <{cpu_count_length}} {node_load: <{load_length}} \ +Mem (M): {node_mem_total: <{mem_total_length}} {node_mem_used: <{mem_used_length}} {node_mem_free: <{mem_free_length}} {node_mem_allocated: <{mem_alloc_length}}{end_bold}'.format( + node_name_length=node_name_length, + daemon_state_length=daemon_state_length, + coordinator_state_length=coordinator_state_length, + domain_state_length=domain_state_length, + domains_count_length=domains_count_length, + cpu_count_length=cpu_count_length, + load_length=load_length, + mem_total_length=mem_total_length, + mem_used_length=mem_used_length, + mem_free_length=mem_free_length, + mem_alloc_length=mem_alloc_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + daemon_state_colour='', + coordinator_state_colour='', + domain_state_colour='', + end_colour='', + node_name='Name', + node_daemon_state='Daemon', + node_coordinator_state='Coordinator', + node_domain_state='Domain', + node_domains_count='VMs', + node_cpu_count='vCPUs', + node_load='Load', + node_mem_total='Total', + node_mem_used='Used', + node_mem_free='Free', + node_mem_allocated='VMs' + ) + ) + + # Format the string (elements) + for node_information in node_list: + daemon_state_colour, coordinator_state_colour, domain_state_colour = getOutputColours(node_information) + node_list_output.append( + '{bold}{node_name: <{node_name_length}} \ + {daemon_state_colour}{node_daemon_state: <{daemon_state_length}}{end_colour} {coordinator_state_colour}{node_coordinator_state: <{coordinator_state_length}}{end_colour} {domain_state_colour}{node_domain_state: <{domain_state_length}}{end_colour} \ + {node_domains_count: <{domains_count_length}} {node_cpu_count: <{cpu_count_length}} {node_load: <{load_length}} \ + {node_mem_total: <{mem_total_length}} {node_mem_used: <{mem_used_length}} {node_mem_free: <{mem_free_length}} {node_mem_allocated: <{mem_alloc_length}}{end_bold}'.format( + node_name_length=node_name_length, + daemon_state_length=daemon_state_length, + coordinator_state_length=coordinator_state_length, + domain_state_length=domain_state_length, + domains_count_length=domains_count_length, + cpu_count_length=cpu_count_length, + load_length=load_length, + mem_total_length=mem_total_length, + mem_used_length=mem_used_length, + mem_free_length=mem_free_length, + mem_alloc_length=mem_alloc_length, + bold='', + end_bold='', + daemon_state_colour=daemon_state_colour, + coordinator_state_colour=coordinator_state_colour, + domain_state_colour=domain_state_colour, + end_colour=ansiprint.end(), + node_name=node_information['name'], + node_daemon_state=node_information['daemon_state'], + node_coordinator_state=node_information['coordinator_state'], + node_domain_state=node_information['domain_state'], + node_domains_count=node_information['domains_count'], + node_cpu_count=node_information['vcpu']['allocated'], + node_load=node_information['load'], + node_mem_total=node_information['memory']['total'], + node_mem_used=node_information['memory']['used'], + node_mem_free=node_information['memory']['free'], + node_mem_allocated=node_information['memory']['allocated'] + ) + ) + + click.echo('\n'.join(sorted(node_list_output))) diff --git a/client-cli/cli_lib/vm.py b/client-cli/cli_lib/vm.py new file mode 100644 index 00000000..7e0d85b5 --- /dev/null +++ b/client-cli/cli_lib/vm.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 + +# vm.py - PVC CLI client function library, VM fuctions +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018-2019 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import difflib +import colorama +import click + +from collections import deque + +import client_lib.ansiprint as ansiprint +import client_lib.zkhandler as zkhandler +import client_lib.common as common + +import client_lib.ceph as ceph + +def get_console_log(zk_conn, domain, lines=1000): + # Validate that VM exists in cluster + dom_uuid = getDomainUUID(zk_conn, domain) + if not dom_uuid: + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Get the data from ZK + console_log = zkhandler.readdata(zk_conn, '/domains/{}/consolelog'.format(dom_uuid)) + + # Shrink the log buffer to length lines + shrunk_log = console_log.split('\n')[-lines:] + loglines = '\n'.join(shrunk_log) + + # Show it in the pager (less) + try: + pager = subprocess.Popen(['less', '-R'], stdin=subprocess.PIPE) + pager.communicate(input=loglines.encode('utf8')) + except FileNotFoundError: + return False, 'ERROR: The "less" pager is required to view console logs.' + + return True, '' + +def follow_console_log(zk_conn, domain, lines=10): + # Validate that VM exists in cluster + dom_uuid = getDomainUUID(zk_conn, domain) + if not dom_uuid: + return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) + + # Get the initial data from ZK + console_log = zkhandler.readdata(zk_conn, '/domains/{}/consolelog'.format(dom_uuid)) + + # Shrink the log buffer to length lines + shrunk_log = console_log.split('\n')[-lines:] + loglines = '\n'.join(shrunk_log) + + # Print the initial data and begin following + print(loglines, end='') + + try: + while True: + # Grab the next line set + new_console_log = zkhandler.readdata(zk_conn, '/domains/{}/consolelog'.format(dom_uuid)) + # Split the new and old log strings into constitutent lines + old_console_loglines = console_log.split('\n') + new_console_loglines = new_console_log.split('\n') + # Set the console log to the new log value for the next iteration + console_log = new_console_log + # Remove the lines from the old log until we hit the first line of the new log; this + # ensures that the old log is a string that we can remove from the new log entirely + for index, line in enumerate(old_console_loglines, start=0): + if line == new_console_loglines[0]: + del old_console_loglines[0:index] + break + # Rejoin the log lines into strings + old_console_log = '\n'.join(old_console_loglines) + new_console_log = '\n'.join(new_console_loglines) + # Remove the old lines from the new log + diff_console_log = new_console_log.replace(old_console_log, "") + # If there's a difference, print it out + if diff_console_log: + print(diff_console_log, end='') + # Wait a second + time.sleep(1) + except kazoo.exceptions.NoNodeError: + return False, 'ERROR: VM has gone away.' + except: + return False, 'ERROR: Lost connection to Zookeeper node.' + + return True, '' + +def format_info(zk_conn, domain_information, long_output): + # Format a nice output; do this line-by-line then concat the elements at the end + ainformation = [] + ainformation.append('{}Virtual machine information:{}'.format(ansiprint.bold(), ansiprint.end())) + ainformation.append('') + # Basic information + ainformation.append('{}UUID:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['uuid'])) + ainformation.append('{}Name:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['name'])) + ainformation.append('{}Description:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['description'])) + ainformation.append('{}Profile:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['profile'])) + ainformation.append('{}Memory (M):{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['memory'])) + ainformation.append('{}vCPUs:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['vcpu'])) + ainformation.append('{}Topology (S/C/T):{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['vcpu_topology'])) + + if long_output == True: + # Virtualization information + ainformation.append('') + ainformation.append('{}Emulator:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['emulator'])) + ainformation.append('{}Type:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['type'])) + ainformation.append('{}Arch:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['arch'])) + ainformation.append('{}Machine:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['machine'])) + ainformation.append('{}Features:{} {}'.format(ansiprint.purple(), ansiprint.end(), ' '.join(domain_information['features']))) + + # PVC cluster information + ainformation.append('') + dstate_colour = { + 'start': ansiprint.green(), + 'restart': ansiprint.yellow(), + 'shutdown': ansiprint.yellow(), + 'stop': ansiprint.red(), + 'disable': ansiprint.blue(), + 'fail': ansiprint.red(), + 'migrate': ansiprint.blue(), + 'unmigrate': ansiprint.blue() + } + ainformation.append('{}State:{} {}{}{}'.format(ansiprint.purple(), ansiprint.end(), dstate_colour[domain_information['state']], domain_information['state'], ansiprint.end())) + ainformation.append('{}Current Node:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['node'])) + if not domain_information['last_node']: + domain_information['last_node'] = "N/A" + ainformation.append('{}Previous Node:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['last_node'])) + + # Get a failure reason if applicable + if domain_information['failed_reason']: + ainformation.append('') + ainformation.append('{}Failure reason:{} {}'.format(ansiprint.purple(), ansiprint.end(), domain_information['failed_reason'])) + + if not domain_information['node_selector']: + formatted_node_selector = "False" + else: + formatted_node_selector = domain_information['node_selector'] + + if not domain_information['node_limit']: + formatted_node_limit = "False" + else: + formatted_node_limit = ', '.join(domain_information['node_limit']) + + if not domain_information['node_autostart']: + formatted_node_autostart = "False" + else: + formatted_node_autostart = domain_information['node_autostart'] + + ainformation.append('{}Migration selector:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_selector)) + ainformation.append('{}Node limit:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_limit)) + ainformation.append('{}Autostart:{} {}'.format(ansiprint.purple(), ansiprint.end(), formatted_node_autostart)) + + # Network list + net_list = [] + for net in domain_information['networks']: + # Split out just the numerical (VNI) part of the brXXXX name + net_vnis = re.findall(r'\d+', net['source']) + if net_vnis: + net_vni = net_vnis[0] + else: + net_vni = re.sub('br', '', net['source']) + net_exists = zkhandler.exists(zk_conn, '/networks/{}'.format(net_vni)) + if not net_exists and net_vni != 'cluster': + net_list.append(ansiprint.red() + net_vni + ansiprint.end() + ' [invalid]') + else: + net_list.append(net_vni) + ainformation.append('') + ainformation.append('{}Networks:{} {}'.format(ansiprint.purple(), ansiprint.end(), ', '.join(net_list))) + + if long_output == True: + # Disk list + ainformation.append('') + name_length = 0 + for disk in domain_information['disks']: + _name_length = len(disk['name']) + 1 + if _name_length > name_length: + name_length = _name_length + ainformation.append('{0}Disks:{1} {2}ID Type {3: <{width}} Dev Bus{4}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), 'Name', ansiprint.end(), width=name_length)) + for disk in domain_information['disks']: + ainformation.append(' {0: <3} {1: <5} {2: <{width}} {3: <4} {4: <5}'.format(domain_information['disks'].index(disk), disk['type'], disk['name'], disk['dev'], disk['bus'], width=name_length)) + ainformation.append('') + ainformation.append('{}Interfaces:{} {}ID Type Source Model MAC{}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), ansiprint.end())) + for net in domain_information['networks']: + ainformation.append(' {0: <3} {1: <8} {2: <10} {3: <8} {4}'.format(domain_information['networks'].index(net), net['type'], net['source'], net['model'], net['mac'])) + # Controller list + ainformation.append('') + ainformation.append('{}Controllers:{} {}ID Type Model{}'.format(ansiprint.purple(), ansiprint.end(), ansiprint.bold(), ansiprint.end())) + for controller in domain_information['controllers']: + ainformation.append(' {0: <3} {1: <14} {2: <8}'.format(domain_information['controllers'].index(controller), controller['type'], controller['model'])) + + # Join it all together + information = '\n'.join(ainformation) + click.echo(information) + + click.echo('') + +def format_list(zk_conn, vm_list, raw): + # Function to strip the "br" off of nets and return a nicer list + def getNiceNetID(domain_information): + # Network list + net_list = [] + for net in domain_information['networks']: + # Split out just the numerical (VNI) part of the brXXXX name + net_vnis = re.findall(r'\d+', net['source']) + if net_vnis: + net_vni = net_vnis[0] + else: + net_vni = re.sub('br', '', net['source']) + net_list.append(net_vni) + return net_list + + # Handle raw mode since it just lists the names + if raw: + for vm in sorted(item['name'] for item in vm_list): + click.echo(vm) + return True, '' + + vm_list_output = [] + + # Determine optimal column widths + # Dynamic columns: node_name, node, migrated + vm_name_length = 5 + vm_uuid_length = 37 + vm_state_length = 6 + vm_nets_length = 9 + vm_ram_length = 8 + vm_vcpu_length = 6 + vm_node_length = 8 + vm_migrated_length = 10 + for domain_information in vm_list: + net_list = getNiceNetID(domain_information) + # vm_name column + _vm_name_length = len(domain_information['name']) + 1 + if _vm_name_length > vm_name_length: + vm_name_length = _vm_name_length + # vm_state column + _vm_state_length = len(domain_information['state']) + 1 + if _vm_state_length > vm_state_length: + vm_state_length = _vm_state_length + # vm_nets column + _vm_nets_length = len(','.join(net_list)) + 1 + if _vm_nets_length > vm_nets_length: + vm_nets_length = _vm_nets_length + # vm_node column + _vm_node_length = len(domain_information['node']) + 1 + if _vm_node_length > vm_node_length: + vm_node_length = _vm_node_length + # vm_migrated column + _vm_migrated_length = len(domain_information['migrated']) + 1 + if _vm_migrated_length > vm_migrated_length: + vm_migrated_length = _vm_migrated_length + + # Format the string (header) + vm_list_output.append( + '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <{vm_uuid_length}} \ +{vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \ +{vm_networks: <{vm_nets_length}} \ +{vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \ +{vm_node: <{vm_node_length}} \ +{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( + vm_name_length=vm_name_length, + vm_uuid_length=vm_uuid_length, + vm_state_length=vm_state_length, + vm_nets_length=vm_nets_length, + vm_ram_length=vm_ram_length, + vm_vcpu_length=vm_vcpu_length, + vm_node_length=vm_node_length, + vm_migrated_length=vm_migrated_length, + bold=ansiprint.bold(), + end_bold=ansiprint.end(), + vm_state_colour='', + end_colour='', + vm_name='Name', + vm_uuid='UUID', + vm_state='State', + vm_networks='Networks', + vm_memory='RAM (M)', + vm_vcpu='vCPUs', + vm_node='Node', + vm_migrated='Migrated' + ) + ) + + # Format the string (elements) + for domain_information in vm_list: + if domain_information['state'] == 'start': + vm_state_colour = ansiprint.green() + elif domain_information['state'] == 'restart': + vm_state_colour = ansiprint.yellow() + elif domain_information['state'] == 'shutdown': + vm_state_colour = ansiprint.yellow() + elif domain_information['state'] == 'stop': + vm_state_colour = ansiprint.red() + elif domain_information['state'] == 'fail': + vm_state_colour = ansiprint.red() + else: + vm_state_colour = ansiprint.blue() + + # Handle colouring for an invalid network config + raw_net_list = getNiceNetID(domain_information) + net_list = [] + vm_net_colour = '' + for net_vni in raw_net_list: + net_exists = zkhandler.exists(zk_conn, '/networks/{}'.format(net_vni)) + if not net_exists and net_vni != 'cluster': + vm_net_colour = ansiprint.red() + net_list.append(net_vni) + + vm_list_output.append( + '{bold}{vm_name: <{vm_name_length}} {vm_uuid: <{vm_uuid_length}} \ +{vm_state_colour}{vm_state: <{vm_state_length}}{end_colour} \ +{vm_net_colour}{vm_networks: <{vm_nets_length}}{end_colour} \ +{vm_memory: <{vm_ram_length}} {vm_vcpu: <{vm_vcpu_length}} \ +{vm_node: <{vm_node_length}} \ +{vm_migrated: <{vm_migrated_length}}{end_bold}'.format( + vm_name_length=vm_name_length, + vm_uuid_length=vm_uuid_length, + vm_state_length=vm_state_length, + vm_nets_length=vm_nets_length, + vm_ram_length=vm_ram_length, + vm_vcpu_length=vm_vcpu_length, + vm_node_length=vm_node_length, + vm_migrated_length=vm_migrated_length, + bold='', + end_bold='', + vm_state_colour=vm_state_colour, + end_colour=ansiprint.end(), + vm_name=domain_information['name'], + vm_uuid=domain_information['uuid'], + vm_state=domain_information['state'], + vm_net_colour=vm_net_colour, + vm_networks=','.join(net_list), + vm_memory=domain_information['memory'], + vm_vcpu=domain_information['vcpu'], + vm_node=domain_information['node'], + vm_migrated=domain_information['migrated'] + ) + ) + + click.echo('\n'.join(sorted(vm_list_output))) + + return True, '' + diff --git a/client-cli/cli_lib/zkhandler.py b/client-cli/cli_lib/zkhandler.py new file mode 100644 index 00000000..317d00ad --- /dev/null +++ b/client-cli/cli_lib/zkhandler.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +# zkhandler.py - Secure versioned ZooKeeper updates +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018-2019 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import kazoo.client +import uuid + +import client_lib.ansiprint as ansiprint + +# Exists function +def exists(zk_conn, key): + stat = zk_conn.exists(key) + if stat: + return True + else: + return False + +# Child list function +def listchildren(zk_conn, key): + children = zk_conn.get_children(key) + return children + +# Delete key function +def deletekey(zk_conn, key, recursive=True): + zk_conn.delete(key, recursive=recursive) + +# Data read function +def readdata(zk_conn, key): + data_raw = zk_conn.get(key) + data = data_raw[0].decode('utf8') + meta = data_raw[1] + return data + +# Data write function +def writedata(zk_conn, kv): + # Start up a transaction + zk_transaction = zk_conn.transaction() + + # Proceed one KV pair at a time + for key in sorted(kv): + data = kv[key] + + # Check if this key already exists or not + if not zk_conn.exists(key): + # We're creating a new key + zk_transaction.create(key, str(data).encode('utf8')) + else: + # We're updating a key with version validation + orig_data = zk_conn.get(key) + version = orig_data[1].version + + # Set what we expect the new version to be + new_version = version + 1 + + # Update the data + zk_transaction.set_data(key, str(data).encode('utf8')) + + # Set up the check + try: + zk_transaction.check(key, new_version) + except TypeError: + print('Zookeeper key "{}" does not match expected version'.format(key)) + return False + + # Commit the transaction + try: + zk_transaction.commit() + return True + except Exception: + return False + +# Write lock function +def writelock(zk_conn, key): + lock_id = str(uuid.uuid1()) + lock = zk_conn.WriteLock('{}'.format(key), lock_id) + return lock + +# Read lock function +def readlock(zk_conn, key): + lock_id = str(uuid.uuid1()) + lock = zk_conn.ReadLock('{}'.format(key), lock_id) + return lock diff --git a/client-cli/client_lib b/client-cli/client_lib deleted file mode 120000 index 37daac79..00000000 --- a/client-cli/client_lib +++ /dev/null @@ -1 +0,0 @@ -../client-common \ No newline at end of file diff --git a/client-cli/pvc.py b/client-cli/pvc.py index 3741d214..5e059f15 100755 --- a/client-cli/pvc.py +++ b/client-cli/pvc.py @@ -29,15 +29,15 @@ import difflib import re import colorama import yaml +import requests -import client_lib.ansiprint as ansiprint -import client_lib.common as pvc_common -import client_lib.cluster as pvc_cluster -import client_lib.node as pvc_node -import client_lib.vm as pvc_vm -import client_lib.network as pvc_network -import client_lib.ceph as pvc_ceph -#import client_lib.provisioner as pvc_provisioner +import cli_lib.ansiprint as ansiprint +import cli_lib.common as pvc_common +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 myhostname = socket.gethostname().split('.')[0] zk_host = '' diff --git a/client-cli/pvc.py.orig b/client-cli/pvc.py.orig new file mode 100755 index 00000000..3741d214 --- /dev/null +++ b/client-cli/pvc.py.orig @@ -0,0 +1,1946 @@ +#!/usr/bin/env python3 + +# pvc.py - PVC client command-line interface +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018-2019 Joshua M. Boniface +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################### + +import socket +import click +import tempfile +import os +import subprocess +import difflib +import re +import colorama +import yaml + +import client_lib.ansiprint as ansiprint +import client_lib.common as pvc_common +import client_lib.cluster as pvc_cluster +import client_lib.node as pvc_node +import client_lib.vm as pvc_vm +import client_lib.network as pvc_network +import client_lib.ceph as pvc_ceph +#import client_lib.provisioner as pvc_provisioner + +myhostname = socket.gethostname().split('.')[0] +zk_host = '' + +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=120) + +def cleanup(retcode, retmsg, zk_conn=None): + if zk_conn: + pvc_common.stopZKConnection(zk_conn) + if retcode == True: + if retmsg != '': + click.echo(retmsg) + exit(0) + else: + if retmsg != '': + click.echo(retmsg) + exit(1) + +############################################################################### +# pvc node +############################################################################### +@click.group(name='node', short_help='Manage a PVC node.', context_settings=CONTEXT_SETTINGS) +def cli_node(): + """ + Manage the state of a node in the PVC cluster. + """ + pass + +############################################################################### +# pvc node secondary +############################################################################### +@click.command(name='secondary', short_help='Set a node in secondary node status.') +@click.argument( + 'node' +) +def node_secondary(node): + """ + Take NODE out of primary router mode. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_node.secondary_node(zk_conn, node) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc node primary +############################################################################### +@click.command(name='primary', short_help='Set a node in primary status.') +@click.argument( + 'node' +) +def node_primary(node): + """ + Put NODE into primary router mode. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_node.primary_node(zk_conn, node) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc node flush +############################################################################### +@click.command(name='flush', short_help='Take a node out of service.') +@click.option( + '-w', '--wait', 'wait', is_flag=True, default=False, + help='Wait for migrations to complete before returning.' +) +@click.argument( + 'node', default=myhostname +) +def node_flush(node, wait): + """ + Take NODE out of active service and migrate away all VMs. If unspecified, defaults to this host. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_node.flush_node(zk_conn, node, wait) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc node ready/unflush +############################################################################### +@click.command(name='ready', short_help='Restore node to service.') +@click.argument( + 'node', default=myhostname +) +@click.option( + '-w', '--wait', 'wait', is_flag=True, default=False, + help='Wait for migrations to complete before returning.' +) +def node_ready(node, wait): + """ + Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_node.ready_node(zk_conn, node, wait) + cleanup(retcode, retmsg, zk_conn) + +@click.command(name='unflush', short_help='Restore node to service.') +@click.argument( + 'node', default=myhostname +) +@click.option( + '-w', '--wait', 'wait', is_flag=True, default=False, + help='Wait for migrations to complete before returning.' +) +def node_unflush(node, wait): + """ + Restore NODE to active service and migrate back all VMs. If unspecified, defaults to this host. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_node.ready_node(zk_conn, node, wait) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc node info +############################################################################### +@click.command(name='info', short_help='Show details of a node object.') +@click.argument( + 'node', default=myhostname +) +@click.option( + '-l', '--long', 'long_output', is_flag=True, default=False, + help='Display more detailed information.' +) +def node_info(node, long_output): + """ + Show information about node NODE. If unspecified, defaults to this host. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_node.get_info(zk_conn, node) + if retcode: + pvc_node.format_info(retdata, long_output) + if long_output: + click.echo('{}Virtual machines on node:{}'.format(ansiprint.bold(), ansiprint.end())) + click.echo('') + pvc_vm.get_list(zk_conn, node, None, None, None) + click.echo('') + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc node list +############################################################################### +@click.command(name='list', short_help='List all node objects.') +@click.argument( + 'limit', default=None, required=False +) +def node_list(limit): + """ + List all nodes in the cluster; optionally only match names matching regex LIMIT. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_node.get_list(zk_conn, limit) + if retcode: + pvc_node.format_list(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc vm +############################################################################### +@click.group(name='vm', short_help='Manage a PVC virtual machine.', context_settings=CONTEXT_SETTINGS) +def cli_vm(): + """ + Manage the state of a virtual machine in the PVC cluster. + """ + pass + +############################################################################### +# pvc vm define +############################################################################### +@click.command(name='define', short_help='Define a new virtual machine from a Libvirt XML file.') +@click.option( + '-t', '--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']), + help='Method to determine optimal target node during autoselect; saved with VM.' +) +@click.option( + '-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.' +) +@click.argument( + 'config', type=click.File() +) +def vm_define(config, target_node, node_limit, node_selector, node_autostart): + """ + Define a new virtual machine from Libvirt XML configuration file CONFIG. + """ + + if node_limit: + node_limit = node_limit.split(',') + + # Open the XML file + config_data = config.read() + config.close() + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.define_vm(zk_conn, config_data, target_node, node_limit, node_selector, node_autostart) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# 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, + 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']), + help='Method to determine optimal target node during autoselect.' +) +@click.option( + '-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.argument( + 'domain' +) +def vm_meta(domain, node_limit, node_selector, node_autostart): + """ + 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: + cleanup(False, 'At least one metadata option must be specified to update.') + + if node_limit: + node_limit = node_limit.split(',') + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.modify_vm_metadata(zk_conn, domain, node_limit, node_selector, node_autostart) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm modify +############################################################################### +@click.command(name='modify', short_help='Modify an existing VM configuration.') +@click.option( + '-e', '--editor', 'editor', is_flag=True, + help='Use local editor to modify existing config.' +) +@click.option( + '-r', '--restart', 'restart', is_flag=True, + help='Immediately restart VM to apply new config.' +) +@click.argument( + 'domain' +) +@click.argument( + 'config', type=click.File(), default=None, required=False +) +def vm_modify(domain, config, editor, restart): + """ + Modify existing virtual machine DOMAIN, either in-editor or with replacement CONFIG. DOMAIN may be a UUID or name. + """ + + if editor == False and config == None: + cleanup(False, 'Either an XML config file or the "--editor" option must be specified.') + + zk_conn = pvc_common.startZKConnection(zk_host) + + dom_uuid = pvc_vm.getDomainUUID(zk_conn, domain) + if dom_uuid == None: + cleanup(False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain)) + dom_name = pvc_vm.getDomainName(zk_conn, dom_uuid) + + if editor == True: + # Grab the current config + current_vm_config = zk_conn.get('/domains/{}/xml'.format(dom_uuid))[0].decode('ascii') + + # Write it to a tempfile + fd, path = tempfile.mkstemp() + fw = os.fdopen(fd, 'w') + fw.write(current_vm_config) + fw.close() + + # Edit it + editor = os.getenv('EDITOR', 'vi') + subprocess.call('%s %s' % (editor, path), shell=True) + + # Open the tempfile to read + with open(path, 'r') as fr: + new_vm_config = fr.read() + fr.close() + + # Delete the tempfile + os.unlink(path) + + # Show a diff and confirm + diff = list(difflib.unified_diff(current_vm_config.split('\n'), new_vm_config.split('\n'), fromfile='current', tofile='modified', fromfiledate='', tofiledate='', n=3, lineterm='')) + if len(diff) < 1: + click.echo('Aborting with no modifications.') + exit(0) + + click.echo('Pending modifications:') + click.echo('') + for line in diff: + if re.match('^\+', line) != None: + click.echo(colorama.Fore.GREEN + line + colorama.Fore.RESET) + elif re.match('^\-', line) != None: + click.echo(colorama.Fore.RED + line + colorama.Fore.RESET) + elif re.match('^\^', line) != None: + click.echo(colorama.Fore.BLUE + line + colorama.Fore.RESET) + else: + click.echo(line) + click.echo('') + + click.confirm('Write modifications to Zookeeper?', abort=True) + + if restart: + click.echo('Writing modified config of VM "{}" and restarting.'.format(dom_name)) + else: + click.echo('Writing modified config of VM "{}".'.format(dom_name)) + + # We're operating in replace mode + else: + # Open the XML file + new_vm_config = config.read() + config.close() + + if restart: + click.echo('Replacing config of VM "{}" with file "{}" and restarting.'.format(dom_name, config.name)) + else: + click.echo('Replacing config of VM "{}" with file "{}".'.format(dom_name, config.name)) + + retcode, retmsg = pvc_vm.modify_vm(zk_conn, domain, restart, new_vm_config) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm undefine +############################################################################### +@click.command(name='undefine', short_help='Undefine a virtual machine.') +@click.argument( + 'domain' +) +def vm_undefine(domain): + """ + Stop virtual machine DOMAIN and remove it from the cluster database, preserving disks. DOMAIN may be a UUID or name. + """ + + # Ensure at least one search method is set + if domain == None: + click.echo("ERROR: You must specify either a name or UUID value.") + exit(1) + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.undefine_vm(zk_conn, domain, is_cli=True) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm remove +############################################################################### +@click.command(name='remove', short_help='Remove a virtual machine.') +@click.argument( + 'domain' +) +def vm_remove(domain): + """ + Stop virtual machine DOMAIN and remove it, along with all disks, from the cluster. DOMAIN may be a UUID or name. + """ + + # Ensure at least one search method is set + if domain == None: + click.echo("ERROR: You must specify either a name or UUID value.") + exit(1) + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.remove_vm(zk_conn, domain, is_cli=True) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm dump +############################################################################### +@click.command(name='dump', short_help='Dump a virtual machine XML to stdout.') +@click.argument( + 'domain' +) +def vm_dump(domain): + """ + Dump the Libvirt XML definition of virtual machine DOMAIN to stdout. DOMAIN may be a UUID or name. + """ + + # Ensure at least one search method is set + if domain == None: + click.echo("ERROR: You must specify either a name or UUID value.") + exit(1) + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.dump_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm start +############################################################################### +@click.command(name='start', short_help='Start up a defined virtual machine.') +@click.argument( + 'domain' +) +def vm_start(domain): + """ + Start virtual machine DOMAIN on its configured node. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.start_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm restart +############################################################################### +@click.command(name='restart', short_help='Restart a running virtual machine.') +@click.argument( + 'domain' +) +def vm_restart(domain): + """ + Restart running virtual machine DOMAIN. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.restart_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm shutdown +############################################################################### +@click.command(name='shutdown', short_help='Gracefully shut down a running virtual machine.') +@click.argument( + 'domain' +) +def vm_shutdown(domain): + """ + Gracefully shut down virtual machine DOMAIN. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.shutdown_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm stop +############################################################################### +@click.command(name='stop', short_help='Forcibly halt a running virtual machine.') +@click.argument( + 'domain' +) +def vm_stop(domain): + """ + Forcibly halt (destroy) running virtual machine DOMAIN. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.stop_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm disable +############################################################################### +@click.command(name='disable', short_help='Mark a virtual machine as disabled.') +@click.argument( + 'domain' +) +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. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.disable_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm move +############################################################################### +@click.command(name='move', short_help='Permanently move a virtual machine to another node.') +@click.argument( + 'domain' +) +@click.option( + '-t', '--target', 'target_node', default=None, + help='Target node to migrate to; autodetect if unspecified.' +) +def vm_move(domain, target_node): + """ + Permanently move virtual machine DOMAIN, via live migration if running and possible, to another node. DOMAIN may be a UUID or name. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.move_vm(zk_conn, domain, target_node) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm migrate +############################################################################### +@click.command(name='migrate', short_help='Temporarily migrate a virtual machine to another node.') +@click.argument( + 'domain' +) +@click.option( + '-t', '--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.' +) +def vm_migrate(domain, target_node, force_migrate): + """ + 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. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.migrate_vm(zk_conn, domain, target_node, force_migrate, is_cli=True) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm unmigrate +############################################################################### +@click.command(name='unmigrate', short_help='Restore a migrated virtual machine to its original node.') +@click.argument( + 'domain' +) +def vm_unmigrate(domain): + """ + Restore previously migrated virtual machine DOMAIN, via live migration if possible, to its original node. DOMAIN may be a UUID or name. If DOMAIN is not running, it will be started on the target node. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.unmigrate_vm(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm flush-locks +############################################################################### +@click.command(name='flush-locks', short_help='Flush stale RBD locks for a virtual machine.') +@click.argument( + 'domain' +) +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. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_vm.flush_locks(zk_conn, domain) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm info +############################################################################### +@click.command(name='info', short_help='Show details of a VM object.') +@click.argument( + 'domain' +) +@click.option( + '-l', '--long', 'long_output', is_flag=True, default=False, + help='Display more detailed information.' +) +def vm_info(domain, long_output): + """ + Show information about virtual machine DOMAIN. DOMAIN may be a UUID or name. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_vm.get_info(zk_conn, domain) + if retcode: + pvc_vm.format_info(zk_conn, retdata, long_output) + retdata = '' + cleanup(retcode, retdata, zk_conn) + + +############################################################################### +# pvc vm log +############################################################################### +@click.command(name='log', short_help='Show console logs of a VM object.') +@click.argument( + 'domain' +) +@click.option( + '-l', '--lines', 'lines', default=1000, show_default=True, + help='Display this many log lines from the end of the log buffer.' +) +@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.' +) +def vm_log(domain, lines, follow): + """ + Show console logs of virtual machine DOMAIN on its current node in the 'less' 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. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + if follow: + # Handle the "new" default of the follow + if lines == 1000: + lines = 10 + retcode, retmsg = pvc_vm.follow_console_log(zk_conn, domain, lines) + else: + retcode, retmsg = pvc_vm.get_console_log(zk_conn, domain, lines) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc vm list +############################################################################### +@click.command(name='list', short_help='List all VM objects.') +@click.argument( + 'limit', default=None, required=False +) +@click.option( + '-t', '--target', 'target_node', default=None, + 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.' +) +@click.option( + '-r', '--raw', 'raw', is_flag=True, default=False, + help='Display the raw list of VM names only.' +) +def vm_list(target_node, target_state, limit, raw): + """ + List all virtual machines in the cluster; optionally only match names matching regex LIMIT. + + NOTE: Red-coloured network lists indicate one or more configured networks are missing/invalid. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_vm.get_list(zk_conn, target_node, target_state, limit) + if retcode: + pvc_vm.format_list(zk_conn, retdata, raw) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# 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 + +############################################################################### +# pvc network add +############################################################################### +@click.command(name='add', short_help='Add a new virtual network to the cluster.') +@click.option( + '-d', '--description', 'description', + required=True, + help='Description of the network; must be unique and not contain whitespace.' +) +@click.option( + '-p', '--type', 'nettype', + required=True, + 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, + help='Domain name of the network.' +) +@click.option( + '--dns-server', 'name_servers', + multiple=True, + help='DNS nameserver for network; multiple entries may be specified.' +) +@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".' +) +@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"]' +) +@click.option( + '--dhcp/--no-dhcp', 'dhcp_flag', + is_flag=True, + default=False, + help='Enable/disable IPv4 DHCP for clients on subnet.' +) +@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.' +) +@click.argument( + 'vni' +) +def net_add(vni, description, nettype, domain, ip_network, ip_gateway, ip6_network, ip6_gateway, dhcp_flag, dhcp_start, dhcp_end, name_servers): + """ + Add a new virtual network with VXLAN identifier VNI to the cluster. + + Examples: + + pvc network add 101 --type bridged + + > Creates vLAN 101 and a simple bridge on the VNI dev interface. + + pvc network add 1001 --type managed --domain test.local --ipnet 10.1.1.0/24 --gateway 10.1.1.1 + + > 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. + """ + + if nettype == 'managed' and not ip_network and not ip6_network: + click.echo('Error: At least one of "-i" / "--ipnet" or "-i6" / "--ipnet6" must be specified.') + exit(1) + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_network.add_network(zk_conn, vni, description, nettype, domain, name_servers, ip_network, ip_gateway, ip6_network, ip6_gateway, dhcp_flag, dhcp_start, dhcp_end) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc network modify +############################################################################### +@click.command(name='modify', short_help='Modify an existing virtual network.') +@click.option( + '-d', '--description', 'description', + default=None, + help='Description of the network; must be unique and not contain whitespace.' +) +@click.option( + '-n', '--domain', 'domain', + default=None, + help='Domain name of the network.' +) +@click.option( + '--dns-server', 'name_servers', + multiple=True, + help='DNS nameserver for network; multiple entries may be specified (will overwrite all previous entries).' +) +@click.option( + '-i', '--ipnet', 'ip4_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.' +) +@click.option( + '-g', '--gateway', 'ip4_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.' +) +@click.option( + '--dhcp/--no-dhcp', 'dhcp_flag', + is_flag=True, + default=None, + help='Enable/disable DHCP for clients on subnet.' +) +@click.option( + '--dhcp-start', 'dhcp_start', + default=None, + help='DHCP range start address.' +) +@click.option( + '--dhcp-end', 'dhcp_end', + default=None, + help='DHCP range end address.' +) +@click.argument( + 'vni' +) +def net_modify(vni, description, domain, name_servers, ip6_network, ip6_gateway, ip4_network, ip4_gateway, dhcp_flag, dhcp_start, dhcp_end): + """ + 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 + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_network.modify_network(zk_conn, vni, description=description, domain=domain, name_servers=name_servers, ip6_network=ip6_network, ip6_gateway=ip6_gateway, ip4_network=ip4_network, ip4_gateway=ip4_gateway, dhcp_flag=dhcp_flag, dhcp_start=dhcp_start, dhcp_end=dhcp_end) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc network remove +############################################################################### +@click.command(name='remove', short_help='Remove a virtual network from the cluster.') +@click.argument( + 'net' +) +def net_remove(net): + """ + Remove an existing virtual network NET from the cluster; NET can be either a VNI or description. + + 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. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_network.remove_network(zk_conn, net) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# 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.' +) +def net_info(vni, long_output): + """ + Show information about virtual network VNI. + """ + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_network.get_info(zk_conn, vni) + if retcode: + pvc_network.format_info(retdata, long_output) + retdata = '' + cleanup(retcode, retdata, zk_conn) + + +############################################################################### +# pvc network list +############################################################################### +@click.command(name='list', short_help='List all VM objects.') +@click.argument( + 'limit', default=None, required=False +) +def net_list(limit): + """ + List all virtual networks in the cluster; optionally only match VNIs or Descriptions matching regex LIMIT. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_network.get_list(zk_conn, limit) + if retcode: + pvc_network.format_list(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# 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 + +############################################################################### +# pvc network dhcp list +############################################################################### +@click.command(name='list', short_help='List active DHCP leases.') +@click.argument( + 'net' +) +@click.argument( + 'limit', default=None, required=False +) +def net_dhcp_list(net, limit): + """ + List all DHCP leases in virtual network NET; optionally only match elements matching regex LIMIT; NET can be either a VNI or description. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_network.get_list_dhcp(zk_conn, net, limit, only_static=False) + if retcode: + pvc_network.format_list_dhcp(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc network dhcp static +############################################################################### +@click.group(name='static', short_help='Manage DHCP static reservations in a PVC virtual network.', context_settings=CONTEXT_SETTINGS) +def net_dhcp_static(): + """ + Manage host DHCP static reservations of a VXLAN network in the PVC cluster. + """ + pass + +############################################################################### +# pvc network dhcp static add +############################################################################### +@click.command(name='add', short_help='Add a DHCP static reservation.') +@click.argument( + 'net' +) +@click.argument( + 'ipaddr' +) +@click.argument( + 'hostname' +) +@click.argument( + 'macaddr' +) +def net_dhcp_static_add(net, ipaddr, macaddr, hostname): + """ + Add a new DHCP static reservation of IP address IPADDR with hostname HOSTNAME for MAC address MACADDR to virtual network NET; NET can be either a VNI or description. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_network.add_dhcp_reservation(zk_conn, net, ipaddr, macaddr, hostname) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc network dhcp static remove +############################################################################### +@click.command(name='remove', short_help='Remove a DHCP static reservation.') +@click.argument( + 'net' +) +@click.argument( + 'reservation' +) +def net_dhcp_static_remove(net, reservation): + """ + Remove a DHCP static reservation RESERVATION from virtual network NET; RESERVATION can be either a MAC address, an IP address, or a hostname; NET can be either a VNI or description. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_network.remove_dhcp_reservation(zk_conn, net, reservation) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc network dhcp static list +############################################################################### +@click.command(name='list', short_help='List DHCP static reservations.') +@click.argument( + 'net' +) +@click.argument( + 'limit', default=None, required=False +) +def net_dhcp_static_list(net, limit): + """ + List all DHCP static reservations in virtual network NET; optionally only match elements matching regex LIMIT; NET can be either a VNI or description. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_network.get_list_dhcp(zk_conn, net, limit, only_static=True) + if retcode: + pvc_network.format_list_dhcp(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# 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 + +############################################################################### +# pvc network acl add +############################################################################### +@click.command(name='add', short_help='Add firewall ACL.') +@click.option( + '--in/--out', 'direction', + is_flag=True, + required=True, + default=None, + 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' +) +def net_acl_add(net, direction, description, rule, order): + """ + 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 can be either a VNI or description. + + 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. + + Example: + + pvc network acl add 1001 --in --rule "tcp dport 22 ct state new accept" --description "ssh-in" --order 3 + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_network.add_acl(zk_conn, net, direction, description, rule, order) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc network acl remove +############################################################################### +@click.command(name='remove', short_help='Remove firewall ACL.') +@click.option( + '--in/--out', 'direction', + is_flag=True, + required=True, + default=None, + help='Inbound or outbound rule set.' +) +@click.argument( + 'net' +) +@click.argument( + 'rule', +) +def net_acl_remove(net, rule, direction): + """ + Remove an NFT firewall rule RULE from network NET; RULE can be either a sequence order identifier or description; NET can be either a VNI or description." + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_network.remove_acl(zk_conn, net, rule, direction) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc network acl list +############################################################################### +@click.command(name='list', short_help='List firewall ACLs.') +@click.option( + '--in/--out', 'direction', + is_flag=True, + required=False, + default=None, + help='Inbound or outbound rule set only.' +) +@click.argument( + 'net' +) +@click.argument( + 'limit', default=None, required=False +) +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. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_network.get_list_acl(zk_conn, net, limit, direction) + if retcode: + pvc_network.format_list_acl(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# 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 ceph +############################################################################### +@click.group(name='ceph', short_help='Manage the PVC Ceph storage cluster.', context_settings=CONTEXT_SETTINGS) +def cli_ceph(): + """ + Manage the Ceph storage of the PVC cluster. + + NOTE: The PVC Ceph interface is limited to the most common tasks. Any other administrative tasks must be performed on a node directly. + """ + pass + +############################################################################### +# pvc storage ceph status +############################################################################### +@click.command(name='status', short_help='Show storage cluster status.') +def ceph_status(): + """ + Show detailed status of the storage cluster. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_ceph.get_status(zk_conn) + if retcode: + pvc_ceph.format_raw_output(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc storage ceph df +############################################################################### +@click.command(name='df', short_help='Show storage cluster utilization.') +def ceph_radosdf(): + """ + Show utilization of the storage cluster. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_ceph.get_radosdf(zk_conn) + if retcode: + pvc_ceph.format_raw_output(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc storage ceph osd +############################################################################### +@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 + +############################################################################### +# pvc storage ceph osd add +############################################################################### +@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( + '--yes', 'yes', + is_flag=True, default=False, + help='Pre-confirm the disk destruction.' +) +def ceph_osd_add(node, device, weight, yes): + """ + Add a new Ceph OSD on node NODE with block device DEVICE to the cluster. + """ + + if not yes: + click.echo('DANGER: This will completely destroy all data on {} disk {}.'.format(node, device)) + choice = input('Are you sure you want to do this? (y/N) ') + if choice != 'y' and choice != 'Y': + exit(0) + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.add_osd(zk_conn, node, device, weight) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph osd remove +############################################################################### +@click.command(name='remove', short_help='Remove OSD.') +@click.argument( + 'osdid' +) +@click.option( + '--yes', 'yes', + is_flag=True, default=False, + help='Pre-confirm the removal.' +) +def ceph_osd_remove(osdid, yes): + """ + Remove a Ceph OSD with ID OSDID from the cluster. + """ + + if not yes: + click.echo('DANGER: This will completely remove OSD {} from cluster. OSDs will rebalance.'.format(osdid)) + choice = input('Are you sure you want to do this? (y/N) ') + if choice != 'y' and choice != 'Y': + exit(0) + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.remove_osd(zk_conn, osdid) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph osd in +############################################################################### +@click.command(name='in', short_help='Online OSD.') +@click.argument( + 'osdid' +) +def ceph_osd_in(osdid): + """ + Set a Ceph OSD with ID OSDID online in the cluster. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.in_osd(zk_conn, osdid) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph osd out +############################################################################### +@click.command(name='out', short_help='Offline OSD.') +@click.argument( + 'osdid' +) +def ceph_osd_out(osdid): + """ + Set a Ceph OSD with ID OSDID offline in the cluster. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.out_osd(zk_conn, osdid) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph osd set +############################################################################### +@click.command(name='set', short_help='Set property.') +@click.argument( + 'osd_property' +) +def ceph_osd_set(osd_property): + """ + Set a Ceph OSD property OSD_PROPERTY on the cluster. + + Valid properties are: + + full|pause|noup|nodown|noout|noin|nobackfill|norebalance|norecover|noscrub|nodeep-scrub|notieragent|sortbitwise|recovery_deletes|require_jewel_osds|require_kraken_osds + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.set_osd(zk_conn, osd_property) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph osd unset +############################################################################### +@click.command(name='unset', short_help='Unset property.') +@click.argument( + 'osd_property' +) +def ceph_osd_unset(osd_property): + """ + Unset a Ceph OSD property OSD_PROPERTY on the cluster. + + Valid properties are: + + full|pause|noup|nodown|noout|noin|nobackfill|norebalance|norecover|noscrub|nodeep-scrub|notieragent|sortbitwise|recovery_deletes|require_jewel_osds|require_kraken_osds + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.unset_osd(zk_conn, osd_property) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph osd list +############################################################################### +@click.command(name='list', short_help='List cluster OSDs.') +@click.argument( + 'limit', default=None, required=False +) +def ceph_osd_list(limit): + """ + List all Ceph OSDs in the cluster; optionally only match elements matching ID regex LIMIT. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_ceph.get_list_osd(zk_conn, limit) + if retcode: + pvc_ceph.format_list_osd(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc storage ceph pool +############################################################################### +@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 + +############################################################################### +# pvc storage ceph pool add +############################################################################### +@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. + """ +) +def ceph_pool_add(name, pgs, replcfg): + """ + Add a new Ceph RBD pool with name NAME and PGS placement groups. + + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.add_pool(zk_conn, name, pgs, replcfg) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph pool remove +############################################################################### +@click.command(name='remove', short_help='Remove RBD pool.') +@click.argument( + 'name' +) +@click.option( + '--yes', 'yes', + is_flag=True, default=False, + help='Pre-confirm the removal.' +) +def ceph_pool_remove(name, yes): + """ + Remove a Ceph RBD pool with name NAME and all volumes on it. + """ + + if not yes: + click.echo('DANGER: This will completely remove pool {} and all data contained in it.'.format(name)) + choice = input('Are you sure you want to do this? (y/N) ') + if choice != 'y' and choice != 'Y': + pool_name_check = input('Please enter the pool name to confirm: ') + if pool_name_check != name: + exit(0) + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.remove_pool(zk_conn, name) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph pool list +############################################################################### +@click.command(name='list', short_help='List cluster RBD pools.') +@click.argument( + 'limit', default=None, required=False +) +def ceph_pool_list(limit): + """ + List all Ceph RBD pools in the cluster; optionally only match elements matching name regex LIMIT. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_ceph.get_list_pool(zk_conn, limit) + if retcode: + pvc_ceph.format_list_pool(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc storage ceph volume +############################################################################### +@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 + +############################################################################### +# pvc storage ceph volume add +############################################################################### +@click.command(name='add', short_help='Add new RBD volume.') +@click.argument( + 'pool' +) +@click.argument( + 'name' +) +@click.argument( + 'size' +) +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. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.add_volume(zk_conn, pool, name, size) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph volume remove +############################################################################### +@click.command(name='remove', short_help='Remove RBD volume.') +@click.argument( + 'pool' +) +@click.argument( + 'name' +) +@click.option( + '--yes', 'yes', + is_flag=True, default=False, + help='Pre-confirm the removal.' +) +def ceph_volume_remove(pool, name, yes): + """ + Remove a Ceph RBD volume with name NAME from pool POOL. + """ + + if not yes: + click.echo('DANGER: This will completely remove volume {} from pool {} and all data contained in it.'.format(name, pool)) + choice = input('Are you sure you want to do this? (y/N) ') + if choice != 'y' and choice != 'Y': + exit(0) + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.remove_volume(zk_conn, pool, name) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph volume resize +############################################################################### +@click.command(name='resize', short_help='Resize RBD volume.') +@click.argument( + 'pool' +) +@click.argument( + 'name' +) +@click.argument( + 'size' +) +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.]. + """ + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.resize_volume(zk_conn, pool, name, size) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph volume rename +############################################################################### +@click.command(name='rename', short_help='Rename RBD volume.') +@click.argument( + 'pool' +) +@click.argument( + 'name' +) +@click.argument( + 'new_name' +) +def ceph_volume_rename(pool, name, new_name): + """ + Rename an existing Ceph RBD volume with name NAME in pool POOL to name NEW_NAME. + """ + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.rename_volume(zk_conn, pool, name, new_name) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph volume clone +############################################################################### +@click.command(name='rename', short_help='Clone RBD volume.') +@click.argument( + 'pool' +) +@click.argument( + 'name' +) +@click.argument( + 'new_name' +) +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. + """ + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.clone_volume(zk_conn, pool, name, new_name) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph volume list +############################################################################### +@click.command(name='list', short_help='List cluster RBD volumes.') +@click.argument( + 'limit', default=None, required=False +) +@click.option( + '-p', '--pool', 'pool', + default=None, show_default=True, + help='Show volumes from this pool only.' +) +def ceph_volume_list(limit, pool): + """ + List all Ceph RBD volumes in the cluster; optionally only match elements matching name regex LIMIT. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_ceph.get_list_volume(zk_conn, pool, limit) + if retcode: + pvc_ceph.format_list_volume(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc storage ceph volume snapshot +############################################################################### +@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 + +############################################################################### +# pvc storage ceph volume snapshot add +############################################################################### +@click.command(name='add', short_help='Add new RBD volume snapshot.') +@click.argument( + 'pool' +) +@click.argument( + 'volume' +) +@click.argument( + 'name' +) +def ceph_volume_snapshot_add(pool, volume, name): + """ + Add a snapshot with name NAME of Ceph RBD volume VOLUME in pool POOL. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.add_snapshot(zk_conn, pool, volume, name) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph 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' +) +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. + """ + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.rename_snapshot(zk_conn, pool, volume, name, new_name) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph volume snapshot remove +############################################################################### +@click.command(name='remove', short_help='Remove RBD volume snapshot.') +@click.argument( + 'pool' +) +@click.argument( + 'volume' +) +@click.argument( + 'name' +) +@click.option( + '--yes', 'yes', + is_flag=True, default=False, + help='Pre-confirm the removal.' +) +def ceph_volume_snapshot_remove(pool, volume, name, yes): + """ + Remove a Ceph RBD volume snapshot with name NAME from volume VOLUME in pool POOL. + """ + + if not yes: + click.echo('DANGER: This will completely remove snapshot {} from volume {}/{} and all data contained in it.'.format(name, pool, volume)) + choice = input('Are you sure you want to do this? (y/N) ') + if choice != 'y' and choice != 'Y': + exit(0) + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retmsg = pvc_ceph.remove_snapshot(zk_conn, pool, volume, name) + cleanup(retcode, retmsg, zk_conn) + +############################################################################### +# pvc storage ceph volume snapshot list +############################################################################### +@click.command(name='list', short_help='List cluster RBD volume shapshots.') +@click.argument( + 'limit', default=None, required=False +) +@click.option( + '-p', '--pool', 'pool', + default=None, show_default=True, + help='Show snapshots from this pool only.' +) +@click.option( + '-p', '--volume', 'volume', + default=None, show_default=True, + help='Show snapshots from this volume only.' +) +def ceph_volume_snapshot_list(pool, volume, limit): + """ + List all Ceph RBD volume snapshots; optionally only match elements matching name regex LIMIT. + """ + + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_ceph.get_list_snapshot(zk_conn, pool, volume, limit) + if retcode: + pvc_ceph.format_list_snapshot(retdata) + retdata = '' + cleanup(retcode, retdata, zk_conn) + + +############################################################################### +# pvc status +############################################################################### +@click.command(name='status', short_help='Show current cluster status.') +@click.option( + '-f', '--format', 'oformat', default='plain', show_default=True, + type=click.Choice(['plain', 'json', 'json-pretty']), + help='Output format of cluster status information.' +) +def status_cluster(oformat): + """ + Show basic information and health for the active PVC cluster. + """ + zk_conn = pvc_common.startZKConnection(zk_host) + retcode, retdata = pvc_cluster.get_info(zk_conn) + if retcode: + pvc_cluster.format_info(retdata, oformat) + retdata = '' + cleanup(retcode, retdata, zk_conn) + +############################################################################### +# pvc init +############################################################################### +@click.command(name='init', short_help='Initialize a new cluster.') +@click.option( + '--yes', 'yes', + is_flag=True, default=False, + help='Pre-confirm the initialization.' +) +def init_cluster(yes): + """ + Perform initialization of a new PVC cluster. + """ + + if not yes: + click.echo('DANGER: This will remove any existing cluster on these coordinators and create a new cluster. Any existing resources on the old cluster will be left abandoned.') + choice = input('Are you sure you want to do this? (y/N) ') + if choice != 'y' and choice != 'Y': + exit(0) + + click.echo('Initializing a new cluster with Zookeeper address "{}".'.format(zk_host)) + + # Easter-egg + click.echo("Some music while we're Layin' Pipe? https://youtu.be/sw8S_Kv89IU") + + # Open a Zookeeper connection + zk_conn = pvc_common.startZKConnection(zk_host) + + # Destroy the existing data + try: + zk_conn.delete('/networks', recursive=True) + zk_conn.delete('/domains', recursive=True) + zk_conn.delete('/nodes', recursive=True) + zk_conn.delete('/primary_node', recursive=True) + zk_conn.delete('/ceph', recursive=True) + except: + pass + + # Create the root keys + transaction = zk_conn.transaction() + transaction.create('/nodes', ''.encode('ascii')) + transaction.create('/primary_node', 'none'.encode('ascii')) + transaction.create('/domains', ''.encode('ascii')) + transaction.create('/networks', ''.encode('ascii')) + transaction.create('/ceph', ''.encode('ascii')) + transaction.create('/ceph/osds', ''.encode('ascii')) + transaction.create('/ceph/pools', ''.encode('ascii')) + transaction.create('/ceph/volumes', ''.encode('ascii')) + transaction.create('/ceph/snapshots', ''.encode('ascii')) + transaction.create('/cmd', ''.encode('ascii')) + transaction.create('/cmd/domains', ''.encode('ascii')) + transaction.create('/cmd/ceph', ''.encode('ascii')) + transaction.commit() + + # Close the Zookeeper connection + pvc_common.stopZKConnection(zk_conn) + + click.echo('Successfully initialized new cluster. Any running PVC daemons will need to be restarted.') + + +############################################################################### +# pvc +############################################################################### +@click.group(context_settings=CONTEXT_SETTINGS) +@click.option( + '-z', '--zookeeper', '_zk_host', envvar='PVC_ZOOKEEPER', default=None, + help='Zookeeper connection string.' +) +def cli(_zk_host): + """ + Parallel Virtual Cluster CLI management tool + + Environment variables: + + "PVC_ZOOKEEPER": Set the cluster Zookeeper address instead of using "--zookeeper". + + If no PVC_ZOOKEEPER/--zookeeper is specified, attempts to load coordinators list from /etc/pvc/pvcd.yaml. + """ + + # 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 + + +# +# Click command tree +# +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) + +cli_vm.add_command(vm_define) +cli_vm.add_command(vm_meta) +cli_vm.add_command(vm_modify) +cli_vm.add_command(vm_undefine) +cli_vm.add_command(vm_remove) +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) +cli_vm.add_command(vm_disable) +cli_vm.add_command(vm_move) +cli_vm.add_command(vm_migrate) +cli_vm.add_command(vm_unmigrate) +cli_vm.add_command(vm_flush_locks) +cli_vm.add_command(vm_info) +cli_vm.add_command(vm_log) +cli_vm.add_command(vm_list) + +cli_network.add_command(net_add) +cli_network.add_command(net_modify) +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) + +net_dhcp.add_command(net_dhcp_list) +net_dhcp.add_command(net_dhcp_static) + +net_dhcp_static.add_command(net_dhcp_static_add) +net_dhcp_static.add_command(net_dhcp_static_remove) +net_dhcp_static.add_command(net_dhcp_static_list) + +net_acl.add_command(net_acl_add) +net_acl.add_command(net_acl_remove) +net_acl.add_command(net_acl_list) + +ceph_osd.add_command(ceph_osd_add) +ceph_osd.add_command(ceph_osd_remove) +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) +ceph_osd.add_command(ceph_osd_list) + +ceph_pool.add_command(ceph_pool_add) +ceph_pool.add_command(ceph_pool_remove) +ceph_pool.add_command(ceph_pool_list) + +ceph_volume.add_command(ceph_volume_add) +ceph_volume.add_command(ceph_volume_resize) +ceph_volume.add_command(ceph_volume_rename) +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_ceph.add_command(ceph_status) +cli_ceph.add_command(ceph_radosdf) +cli_ceph.add_command(ceph_osd) +cli_ceph.add_command(ceph_pool) +cli_ceph.add_command(ceph_volume) + +cli_storage.add_command(cli_ceph) + +cli.add_command(cli_node) +cli.add_command(cli_vm) +cli.add_command(cli_network) +cli.add_command(cli_storage) +cli.add_command(status_cluster) +cli.add_command(init_cluster) + +# +# Main entry point +# +def main(): + return cli(obj={}) + +if __name__ == '__main__': + main() +