# # Copyright (c) 2016 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # """ Usage: storage-topology [options] options: -d, --diskview view disk data of all server nodes. -v, --vgview view VG data in all nodes. -a, --all view both disk and vg view. ( selected by default if any of the view options is NOT selected) -e, --extended include additional parameters like uuids in selected view(s) -h, --help display this usage Tool to show a consolidated view of system physical disks and logical volume groups data. """ import os import sys import argparse import datetime import logging import textwrap import keyring import subprocess import math from prettytable import PrettyTable from cgtsclient.common import utils from cgtsclient import client as cgts_client from cgtsclient import exc """---------------------------------------------------------------------------- Global definitions ----------------------------------------------------------------------------""" # logger logger = logging.getLogger(__name__) # show options show = {} def configure_debuggubg(debug): if debug: logging.basicConfig( format="%(levelname)s (%(module)s:%(lineno)d) %(message)s", level=logging.DEBUG) else: logging.basicConfig( format="%(levelname)s %(message)s", level=logging.WARNING) def parse_arguments(show): """ Parse command line arguments. """ parser = argparse.ArgumentParser( prog=os.path.basename(sys.argv[0]), formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent('''\ Tool to summarize server storage resouce usage. '''), epilog=textwrap.dedent('''\ Tables and Field descriptions: ------------------------------ SERVER Physical DISK view: Host - server host name Device Node - device node name Device Type - device node type ( extended view only) UUID - device node uuid ( extended view only) Size in GB - disk size in giga bytes PV Name - name of physical volume PV State - the physical volume state PV UUID - the physical volume uuid VG (name:state:uuid) name - VG name state - VG state uuid - VG uuid (extended view only) SERVER VOLUME GROUP view: Host - server host name Name - volume group name UUID - volume group uuid State - VG state Size - VG size in GB Current LVs - current Number of logical volumes (lv) Current PVs - current Number of physical volumes (pv) PV List (name:state:uuid) - Comma separated list of PVs name - physical volume name state - physical volume state uuid - physical volume uuid (extended view only) Parameters - list of VG parameters '''), ) # Global arguments parser.add_argument('-d', '--diskview', default=False, action='store_true', help="view all physical disks across all nodes" " including mapped physical volumes and" "logical volume groups") parser.add_argument('-v', '--vgview', default=False, action='store_true', help="view information pertaining to VGs in all nodes") parser.add_argument('-a', '--all', default=False, action='store_true', help="view both disk and vg views") parser.add_argument('--debug', default=bool(utils.env('SYSTEMCLIENT_DEBUG')), action='store_true', help=argparse.SUPPRESS) parser.add_argument('-e', '--extended', default=False, action="store_true", help="Print additional disk or vg information") # Parse arguments args = parser.parse_args() show['diskview'] = args.diskview show['vgview'] = args.vgview show['all'] = args.all show['extended'] = args.extended show['debug'] = args.debug # Configure logging to appropriate level configure_debuggubg(show['debug']) def get_system_creds(): """ Return keystone credentials by sourcing /etc/nova/openrc. """ d = {} proc = subprocess.Popen(['bash', '-c', 'source /etc/nova/openrc && env'], stdout=subprocess.PIPE) for line in proc.stdout: key, _, value = line.partition("=") if key == 'OS_USERNAME': d['os_username'] = value.strip() elif key == 'OS_PASSWORD': d['os_password'] = value.strip() elif key == 'OS_TENANT_NAME': d['os_tenant_name'] = value.strip() elif key == 'OS_AUTH_URL': d['os_auth_url'] = value.strip() elif key == 'OS_REGION_NAME': d['os_region_name'] = value.strip() proc.communicate() return d def convert_to_readable_size(size, orig_unit='B'): """ Converts size to human readable unit """ units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] # convert original size to bytes try: i = units.index(orig_unit) except: raise RuntimeError('Invalid size unit passed: %s' % (orig_unit)) size = size * pow(1024, i) unitIndex = int(math.floor(math.log(size, 1024))) # set size unit to PB max if size is greater than 1024 PB if unitIndex > 5: unitIndex = 5 sizer = math.pow(1024, unitIndex) newsize = round(size/sizer, 2) return "%s %s" % (newsize, units[unitIndex]) def print_disk_view(rows=None, extended=False): """ Print all summary Disk views using PrettyTable. """ disk_lables_extended = \ ['Host', 'Device Node', 'Device Type', 'UUID', 'Size', 'PV Name', 'PV State', 'PV UUID', 'VG (name:state:uuid)', ] disk_lables_brief = \ ['Host', 'Device Node', 'Size', 'PV Name', 'PV State', 'VG (name:state)'] if len(rows) > 0: print print("DISKs: (Physical disk view)") pt = PrettyTable(disk_lables_extended) if extended else \ PrettyTable(disk_lables_brief) pt.align = 'l' pt.align['Size'] = 'r' for r in rows: if len(r) == len(pt.field_names): pt.add_row(r) else: print("Disk row has incorrect number of values: %s" % r) print(pt) def print_vg_view(rows=None, extended=False): """ Print all summary VG views using PrettyTable. """ vg_labels_extended = \ ['Host', 'VG Name', 'UUID', 'VG State', 'VG Size', 'Current LVs', 'Current PVs', 'PV List (name:state:uuid)', 'VG Parameters'] vg_labels_brief = \ ['Host', 'VG Name', 'VG State', 'VG Size', 'Current LVs', 'Current PVs', 'PV List (name:state)', 'VG Parameters'] if len(rows) > 0: print print("VOLUME GROUPS: (VG view)") pt = PrettyTable(vg_labels_extended) if extended else \ PrettyTable(vg_labels_brief) pt.align = 'l' for C in ['VG Size', 'Current LVs', 'Current PVs']: pt.align[C] = 'r' for r in rows: if len(r) == len(pt.field_names): pt.add_row(r) else: print("VG row has incorrect number of values: %s" % r) print(pt) def get_info_and_display(cc, show=None): """ Get storage information from server nodes. Display the following information in table format. - disk data of all server nodes - VG data of all servers nodes """ # get list of server hosts and for each host retrieve # the disk, lvg. pv list objects. host_storage_attr = {} hosts = cc.ihost.list() hostnames = [] for h in hosts: hostname = getattr(h, 'hostname', '') hostnames.append(hostname) host_disks = cc.idisk.list(h.uuid) host_pvs = cc.ipv.list(h.uuid) host_lvgs = cc.ilvg.list(h.uuid) host_storage_attr[hostname] = {'host_disks': host_disks, 'host_pvs': host_pvs, 'host_lvgs': host_lvgs} disk_view = [] vg_view = [] pv_fields_ext = ['lvm_pv_name', 'pv_state', 'lvm_pv_uuid'] pv_fields = ['lvm_pv_name', 'pv_state'] disk_fields_ext = ['device_node', 'device_type', 'uuid', 'size_mib'] disk_fields = ['device_node', 'size_mib'] lvg_fields_ext = ['lvm_vg_name', 'uuid', 'vg_state', 'lvm_vg_size', 'lvm_cur_lv', 'lvm_cur_pv'] lvg_fields = ['lvm_vg_name', 'vg_state', 'lvm_vg_size', 'lvm_cur_lv', 'lvm_cur_pv'] # padding empty values in case not pv in disk. disk_pd_num_ext = 5 disk_pd_num = 3 pv_pd_num_ext = 4 pv_pd_num = 3 for k, v in host_storage_attr.items(): if show['diskview'] or show['all']: for disk_o in v['host_disks']: device_node = getattr(disk_o, 'device_node', '') drow = [k] if show['extended']: drow.extend([(getattr(disk_o, f, '')) for f in disk_fields_ext]) drow[4] = convert_to_readable_size( getattr(disk_o, 'size_mib', ''), 'MB') else: drow.extend([(getattr(disk_o, f, '')) for f in disk_fields]) drow[2] = convert_to_readable_size( getattr(disk_o, 'size_mib', ''), 'MB') if v['host_pvs']: first = True for pv_o in v['host_pvs']: pv_node = getattr(pv_o, 'idisk_device_node', '') if pv_node == device_node: if first: if show['extended']: drow.extend([(getattr(pv_o, f, '')) for f in pv_fields_ext]) else: drow.extend([(getattr(pv_o, f, '')) for f in pv_fields]) first = False else: disk_view.append(drow) # new row for next pv # padd for host, device_node, size if show['extended']: drow = [''] * disk_pd_num_ext drow.extend([(getattr(pv_o, f, '')) for f in pv_fields_ext]) else: drow = [''] * disk_pd_num drow.extend([(getattr(pv_o, f, '')) for f in pv_fields]) for vg_o in v['host_lvgs']: vg_str = "" if getattr(pv_o, 'lvm_vg_name', '') == \ getattr(vg_o, 'lvm_vg_name', ''): if show['extended']: vg_str += ":".join( [str(getattr(pv_o, 'lvm_vg_name', '')), str(getattr(vg_o, 'vg_state')), str(getattr(vg_o, 'uuid'))]) else: vg_str += ":".join( [str(getattr(pv_o, 'lvm_vg_name', '')), str(getattr(vg_o, 'vg_state'))]) drow.append(vg_str) else: if show['extended']: drow.extend([''] * pv_pd_num_ext) else: drow.extend([''] * pv_pd_num) disk_view.append(drow) # add to disk row disk view rows if show['vgview'] or show['all']: for vg_o in v['host_lvgs']: vgrow = [k] if show['extended']: vgrow.extend([(getattr(vg_o, f, '')) for f in lvg_fields_ext]) vgrow[4] = convert_to_readable_size( getattr(vg_o, 'lvm_vg_size', '')) else: vgrow.extend([(getattr(vg_o, f, '')) for f in lvg_fields]) vgrow[3] = convert_to_readable_size( getattr(vg_o, 'lvm_vg_size', '')) # list of current pvs for each VG count = 0 for pv_o in v['host_pvs']: pv_str = "" if getattr(vg_o, 'lvm_vg_name', '') == getattr( pv_o, 'lvm_vg_name', ''): count += 1 if count > 1: pv_str += ', ' if show['extended']: pv_str += ":".join( [str(getattr(pv_o, f, '')) for f in pv_fields_ext]) else: pv_str += ":".join( [str(getattr(pv_o, f, '')) for f in pv_fields]) vgrow.append(pv_str) vgrow.append(getattr(vg_o, 'capabilities', '')) vg_view.append(vgrow) print_disk_view(rows=disk_view, extended=show['extended']) print_vg_view(rows=vg_view, extended=show['extended']) def main(): try: # Process command line options and arguments, configure logging, # configure debug and show options parse_arguments(show) if not show['diskview'] and not show['vgview']: # both disk and vg views are printed show['all'] = True api_version = utils.env('SYSTEM_API_VERSION', default='1') # Print selected options, and timestamp prog = os.path.basename(sys.argv[0]) ts = datetime.datetime.now() if show['debug']: print("%s: %s options: view:%s System api version: %s" % (prog, ts.isoformat(), show, api_version)) cgts_client_creds = get_system_creds() if not cgts_client_creds['os_username']: raise exc.CommandError("You must provide a username via " "env[OS_USERNAME]") if not cgts_client_creds['os_password']: # priviledge check (only allow Keyring retrieval if we are root) if os.geteuid() == 0: cgts_client_creds['os_password'] = keyring.get_password( 'CGCS', cgts_client_creds['os_username']) else: raise exc.CommandError("You must provide a password via " "env[OS_PASSWORD]") if not cgts_client_creds['os_tenant_name']: raise exc.CommandError("You must provide a tenant_id via " "env[OS_TENANT_NAME]") if not cgts_client_creds['os_auth_url']: raise exc.CommandError("You must provide a auth url via " "env[OS_AUTH_URL]") if not cgts_client_creds['os_region_name']: raise exc.CommandError("You must provide a region_name via " "env[OS_REGION_NAME]") cc = cgts_client.get_client(api_version, **cgts_client_creds) # Get all info and display in table format get_info_and_display(cc, show) sys.exit(0) except KeyboardInterrupt as e: logger.warning('caught: %r, shutting down', e) sys.exit(0) except IOError as e: logger.warning('caught: %r, shutting down', e) sys.exit(0) except Exception as e: logger.error('exception: %r', e, exc_info=1) sys.exit(-4)