# Copyright (C) 2012-2016 Red Hat, Inc.
# (C) Copyright 2017 Hewlett Packard Enterprise Development LP
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; If not, see <http://www.gnu.org/licenses/>.
#
# Author: tasleson
# Gris Ge <fge@redhat.com>
import os
import sys
import getpass
import re
import time
import tty
import termios
from argparse import ArgumentParser, ArgumentTypeError
from argparse import RawTextHelpFormatter
import six
from lsm import (Client, Pool, VERSION, LsmError, Disk,
Volume, JobStatus, ErrorNumber, BlockRange,
uri_parse, Proxy, size_human_2_size_bytes,
AccessGroup, FileSystem, NfsExport, TargetPort, LocalDisk,
Battery)
from lsm.lsmcli.data_display import (
DisplayData, PlugData, out,
vol_provision_str_to_type, vol_rep_type_str_to_type, VolumeRAIDInfo,
PoolRAIDInfo, VcrCap, LocalDiskInfo, VolumeRAMCacheInfo)
_CONNECTION_FREE_COMMANDS = ['local-disk-list',
'local-disk-ident-led-on',
'local-disk-ident-led-off',
'local-disk-fault-led-on',
'local-disk-fault-led-off']
if six.PY3:
long = int
try:
from collections import OrderedDict
except ImportError:
# python 2.6 or earlier, use backport
# noinspection PyUnresolvedReferences
from ordereddict import OrderedDict
# Wraps the invocation to the command line
# @param c Object to invoke calls on (optional)
def cmd_line_wrapper(c=None):
"""
Common command line code, called.
"""
err_exit = 0
cli = None
try:
cli = CmdLine()
cli.process(c)
except ArgError as ae:
sys.stderr.write(str(ae))
sys.stderr.flush()
err_exit = 2
except LsmError as le:
sys.stderr.write(str(le) + "\n")
sys.stderr.flush()
if le.code == ErrorNumber.PERMISSION_DENIED:
err_exit = 13 # common error code for EACCES
else:
err_exit = 4
except KeyboardInterrupt:
err_exit = 1
except SystemExit as se:
# argparse raises a SystemExit
err_exit = se.code
except:
import traceback
traceback.print_exc(file=sys.stdout)
# We get *any* other exception don't return a successful error code
err_exit = 2
finally:
# Regardless of what happens, we will try to close the connection if
# possible to allow the plugin to clean up gracefully.
if cli:
try:
# This will exit if are successful
cli.shutdown(err_exit)
except Exception:
pass
sys.exit(err_exit)
# Get a character from stdin without needing a return key pressed.
# Returns the character pressed
def getch():
fd = sys.stdin.fileno()
prev = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, prev)
return ch
def parse_convert_init(init_id):
"""
If init_id is a WWPN, convert it into LSM standard version:
(?:[0-9a-f]{2}:){7}[0-9a-f]{2}
Return (converted_init_id, lsm_init_type)
"""
valid, converted_init_type, converted_init_id = \
AccessGroup.initiator_id_verify(init_id)
if valid:
return converted_init_id, converted_init_type
raise ArgError("--init \"%s\" is not a valid WWPN or iSCSI IQN" % init_id)
def _check_init(init_id):
"""
Call back from validating an initiator
:param init_id: Initiator to validate
:return: Value of initiator or raises an exception
"""
valid, _, converted_init_id = \
AccessGroup.initiator_id_verify(init_id)
if valid:
return converted_init_id
raise ArgumentTypeError("\"%s\" is invalid WWPN or iSCSI IQN" % init_id)
def _check_positive_integer(num):
"""
Call back for validating a positive integer
:param num: Number string to check
:return: Numeric value, else exception
"""
try:
rc = long(num, 10)
if rc < 0:
raise ArgumentTypeError(
"invalid: require positive integer value '%d'" % rc)
return rc
except ValueError:
raise ArgumentTypeError(
"invalid: not a positive integer value '%s'" % num)
_CHILD_OPTION_DST_PREFIX = 'child_'
def _upper(s):
return s.upper()
def _valid_ip4_address(address):
"""
Check if a string represents a valid ip4 address
:param address: String representing address
:return: True if valid address, else false
"""
if not address:
return False
parts = address.split('.')
if len(parts) != 4:
return False
if '/' in address:
return False
for i in parts:
if not 0 < len(i) <= 3:
return False
if len(i) > 1 and i[0] == '0':
return False
try:
if int(i, 10) > 255:
return False
except ValueError:
return False
return True
def _valid_ip6_address(address):
"""
Check if a string represents a valid ipv6 address
:param address: String representing address
:return: True if valid address, else false
"""
allowed = 'ABCDEFabcdef0123456789:'
has_zeros = False
if not address:
return False
if '/' in address:
return False
if len(address.split("::")) > 2:
return False
parts = address.split(':')
if len(parts) < 3 or len(parts) > 9:
return False
# Check for ipv4 suffix, validate and remove while adding padding for
# addl. checks.
if '.' in parts[-1]:
if not _valid_ip4_address(parts.pop()):
print("Not valid ipv suffix")
return False
parts.extend(['0', '0'])
if '::' in address:
parts = [p for p in parts if p != '']
# Add one segment of zero to catch full address with extra ':'
parts.append('0')
has_zeros = True
if (has_zeros and len(parts) <= 8) or len(parts) == 8:
return all(len(x) <= 4 for x in parts) and \
all(x in allowed for x in "".join(parts))
return False
def _is_valid_network_name(ip_hn):
"""
Checks to see if the supplied string is a valid ip4/6 or hostname
:param ip_hn: String representing address user inputted
:return: True if valid IP address or hostname
"""
allowed = re.compile("(?!-)[A-Z0-9-]{1,63}(?<!-)$", re.IGNORECASE)
digits_only = re.compile("^[0-9.]+$")
# Check ipv4, ipv6, then for valid hostname
if _valid_ip4_address(ip_hn):
return True
if _valid_ip6_address(ip_hn):
return True
if len(ip_hn) > 255:
return False
# A hostname cannot exist with only digits per spec. as that is confusing
# for distinguishing IP from hostname
if digits_only.match(ip_hn):
return False
if ip_hn[-1] == ".":
ip_hn = ip_hn[:-1] # Yes, absolute hostnames have a trailing dot!
return all(allowed.match(x) for x in ip_hn.split("."))
def _add_common_options(arg_parser, is_child=False):
"""
As https://bugs.python.org/issue23058 indicate, argument parser should
not have subparser sharing the same argument and destination.
For subparser, we add common options as 'child_xxx' destination.
For default value, False is the only allowed default value in root.
"""
prefix = ''
if is_child:
prefix = _CHILD_OPTION_DST_PREFIX
arg_parser.add_argument(
'-v', '--version', action='version',
version="%s %s" % (sys.argv[0], VERSION))
arg_parser.add_argument(
'-u', '--uri', action="store", type=str, metavar='<URI>',
dest="%suri" % prefix,
help='Uniform resource identifier (env LSMCLI_URI)')
arg_parser.add_argument(
'-P', '--prompt', action="store_true", dest="%sprompt" % prefix,
help='Prompt for password (env LSMCLI_PASSWORD)')
arg_parser.add_argument(
'-H', '--human', action="store_true", dest="%shuman" % prefix,
help='Print sizes in human readable format\n'
'(e.g., MiB, GiB, TiB)')
arg_parser.add_argument(
'-t', '--terse', action="store", dest="%ssep" % prefix,
metavar='<SEP>',
help='Print output in terse form with "SEP" '
'as a record separator')
arg_parser.add_argument(
'-e', '--enum', action="store_true", dest="%senum" % prefix,
default=False,
help='Display enumerated types as numbers instead of text')
arg_parser.add_argument(
'-f', '--force', action="store_true", dest="%sforce" % prefix,
default=False,
help='Bypass confirmation prompt for data loss operations')
arg_parser.add_argument(
'-w', '--wait', action="store", dest="%swait" % prefix,
help="Command timeout value in ms (default = 30s)",
type=_check_positive_integer)
arg_parser.add_argument(
'--header', action="store_true", dest="%sheader" % prefix,
help='Include the header with terse')
arg_parser.add_argument(
'-b', action="store_true", dest="%s_async" % prefix, default=False,
help='Run the command async. Instead of waiting for completion.\n '
'Command will exit(7) and job id written to stdout.')
arg_parser.add_argument(
'-s', '--script', action="store_true", dest="%sscript" % prefix,
default=False,
help='Displaying data in script friendly way with '
'additional information(if exists)')
if is_child:
default_dict = dict()
default_dict['%swait' % prefix] = 30000
arg_parser.set_defaults(**default_dict)
def _add_sd_paths(lsm_obj):
lsm_obj.sd_paths = []
try:
if len(lsm_obj.vpd83) > 0:
lsm_obj.sd_paths = LocalDisk.vpd83_search(lsm_obj.vpd83)
except LsmError as lsm_err:
if lsm_err.code != ErrorNumber.NO_SUPPORT:
raise
return lsm_obj
# This class represents a command line argument error
class ArgError(Exception):
def __init__(self, message, *args, **kwargs):
"""
Class represents an error.
"""
Exception.__init__(self, *args, **kwargs)
self.msg = message
def __str__(self):
return "%s: error: %s\n" % (os.path.basename(sys.argv[0]), self.msg)
# Finds an item based on the id. Each list item requires a member "id"
# @param l list to search
# @param the_id the id to match
# @param friendly_name - name to put in the exception saying what we
# couldn't find
def _get_item(l, the_id, friendly_name='item', raise_error=True):
for item in l:
if item.id == the_id:
return item
if raise_error:
raise ArgError('%s with ID %s not found!' % (friendly_name, the_id))
else:
return None
def _check_network_host(addr):
"""
Custom value checker for hostname/IP address
:param addr:
:return:
"""
valid = _is_valid_network_name(addr)
if valid:
return addr
raise ArgumentTypeError("%s is invalid IP or hostname" % addr)
list_choices = ['VOLUMES', 'POOLS', 'FS', 'SNAPSHOTS',
'EXPORTS', "NFS_CLIENT_AUTH", 'ACCESS_GROUPS',
'SYSTEMS', 'DISKS', 'PLUGINS', 'TARGET_PORTS', 'BATTERIES']
provision_types = ('DEFAULT', 'THIN', 'FULL')
provision_help = "provisioning type: " + ", ".join(provision_types)
replicate_types = ('CLONE', 'COPY', 'MIRROR_ASYNC', 'MIRROR_SYNC')
replicate_help = "replication type: " + ", ".join(replicate_types)
policy_types = ['ENABLE', 'DISABLE']
policy_help = 'Policy: ' + ', '.join(policy_types)
policy_opt = dict(name="--policy", metavar='<POLICY>',
help=policy_help, choices=policy_types,
type=_upper)
write_cache_policy_types = ['WB', 'AUTO', 'WT']
write_cache_policy_help = 'Write cache polices: ' + \
', '.join(write_cache_policy_types) + \
' which stand for "write back", "auto", ' + \
'"write through"'
write_cache_policy_opt = dict(name="--policy", metavar='<POLICY>',
help=write_cache_policy_help,
choices=write_cache_policy_types,
type=_upper)
size_help = 'Can use B, KiB, MiB, GiB, TiB, PiB postfix (IEC sizing)'
sys_id_opt = dict(name='--sys', metavar='<SYS_ID>', help='System ID')
sys_id_filter_opt = sys_id_opt.copy()
sys_id_filter_opt['help'] = \
'Search by System ID. Only supported for: \n' \
'(VOLUMES, POOLS, FS, DISKS, ACCESS_GROUPS,\n' \
'TARGET_PORTS, BATTERIES)'
pool_id_opt = dict(name='--pool', metavar='<POOL_ID>', help='Pool ID')
pool_id_filter_opt = pool_id_opt.copy()
pool_id_filter_opt['help'] = \
'Search by Pool ID. Only supported for:\n' \
'(VOLUMES, POOLS, FS)'
vol_id_opt = dict(name='--vol', metavar='<VOL_ID>', help='Volume ID')
vol_id_filter_opt = vol_id_opt.copy()
vol_id_filter_opt['help'] = \
'Search by Volume ID. Only supported for:\n' \
'(VOLUMES, ACCESS_GROUPS)'
fs_id_opt = dict(name='--fs', metavar='<FS_ID>', help='File System ID')
fs_id_filter_opt = fs_id_opt.copy()
fs_id_filter_opt['help'] = \
'Search by FS ID. Only supported for:\n' \
'(FS, SNAPSHOTS, EXPORTS)'
ag_id_opt = dict(name='--ag', metavar='<AG_ID>', help='Access Group ID')
ag_id_filter_opt = ag_id_opt.copy()
ag_id_filter_opt['help'] = \
'Search by Access Group ID. Only supported for:\n' \
'(ACCESS_GROUPS, VOLUMES)'
init_id_opt = dict(name='--init', metavar='<INIT_ID>', help='Initiator ID',
type=_check_init)
snap_id_opt = dict(name='--snap', metavar='<SNAP_ID>', help='Snapshot ID')
export_id_opt = dict(name='--export', metavar='<EXPORT_ID>', help='Export ID')
nfs_export_id_filter_opt = dict(
name='--nfs-export', metavar='<NFS_EXPORT_ID>',
help=
'Search by NFS Export ID. Only supported for:\n'
'(EXPORTS)')
disk_id_filter_opt = dict(name='--disk', metavar='<DISK_ID>',
help='Search by Disk ID. Only supported for:\n'
'(DISKS)')
size_opt = dict(name='--size', metavar='<SIZE>', help=size_help)
tgt_id_filter_opt = dict(name="--tgt", metavar='<TGT_ID>',
help="Search by target port ID. Only supported for:\n"
"(TARGET_PORTS)")
local_disk_path_opt = dict(name='--path', help="Local disk path",
metavar='<DISK_PATH>')
cmds = (
dict(
name='list',
help="List records of different types",
args=[
dict(name='--type',
help="List records of type:\n " +
"\n ".join(list_choices) +
"\n\nWhen listing SNAPSHOTS, it requires --fs <FS_ID>.",
metavar='<TYPE>',
choices=list_choices,
type=_upper),
],
optional=[
dict(sys_id_filter_opt),
dict(pool_id_filter_opt),
dict(vol_id_filter_opt),
dict(disk_id_filter_opt),
dict(ag_id_filter_opt),
dict(fs_id_filter_opt),
dict(nfs_export_id_filter_opt),
dict(tgt_id_filter_opt),
],
),
dict(
name='job-status',
help='Retrieve information about a job',
args=[
dict(name="--job", metavar="<JOB_ID>", help='job status id'),
],
),
dict(
name='capabilities',
help='Retrieves array capabilities',
args=[
dict(sys_id_opt),
],
),
dict(
name='plugin-info',
help='Retrieves plugin description and version',
),
dict(
name='volume-create',
help='Creates a volume (logical unit)',
args=[
dict(name="--name", help='volume name', metavar='<NAME>'),
dict(size_opt),
dict(pool_id_opt),
],
optional=[
dict(name="--provisioning", help=provision_help,
default='DEFAULT',
choices=provision_types,
type=_upper),
],
),
dict(
name='volume-raid-create',
help='Creates a RAIDed volume on hardware RAID',
args=[
dict(name="--name", help='volume name', metavar='<NAME>'),
dict(name="--disk", metavar='<DISK>',
help='Free disks for new RAIDed volume.\n'
'This is repeatable argument.',
action='append'),
dict(name="--raid-type",
help="RAID type for the new RAID group. "
"Should be one of these:\n %s" %
"\n ".
join(VolumeRAIDInfo.VOL_CREATE_RAID_TYPES_STR),
choices=VolumeRAIDInfo.VOL_CREATE_RAID_TYPES_STR,
type=_upper),
],
optional=[
dict(name="--strip-size",
help="Strip size. " + size_help),
],
),
dict(
name='volume-raid-create-cap',
help='Query capability of creating a RAIDed volume on hardware RAID',
args=[
dict(sys_id_opt),
],
),
dict(
name='volume-delete',
help='Deletes a volume given its id',
args=[
dict(vol_id_opt),
],
),
dict(
name='volume-resize',
help='Re-sizes a volume',
args=[
dict(vol_id_opt),
dict(name='--size', metavar='<NEW_SIZE>',
help="New size. %s" % size_help),
],
),
dict(
name='volume-replicate',
help='Creates a new volume and replicates provided volume to it.',
args=[
dict(vol_id_opt),
dict(name="--name", metavar='<NEW_VOL_NAME>',
help='The name for New replicated volume'),
dict(name="--rep-type", metavar='<REPL_TYPE>',
help=replicate_help, choices=replicate_types),
],
optional=[
dict(name="--pool",
help='Pool ID to contain the new volume.\nBy default, '
'new volume will be created in the same pool.'),
],
),
dict(
name='volume-replicate-range',
help='Replicates a portion of a volume to existing volume',
args=[
dict(name="--src-vol", metavar='<SRC_VOL_ID>',
help='Source volume id'),
dict(name="--dst-vol", metavar='<DST_VOL_ID>',
help='Destination volume id'),
dict(name="--rep-type", metavar='<REP_TYPE>',
help="Replication type: CLONE, COPY",
choices=["CLONE", "COPY"]),
dict(name="--src-start", metavar='<SRC_START_BLK>',
help='Source volume start block number.\n'
'This is repeatable argument.',
action='append', type=_check_positive_integer),
dict(name="--dst-start", metavar='<DST_START_BLK>',
help='Destination volume start block number.\n'
'This is repeatable argument.',
action='append', type=_check_positive_integer),
dict(name="--count", metavar='<BLK_COUNT>',
help='Number of blocks to replicate.\n'
'This is repeatable argument.',
action='append', type=_check_positive_integer),
],
),
dict(
name='volume-replicate-range-block-size',
help='Size of each replicated block on a system in bytes',
args=[
dict(sys_id_opt),
],
),
dict(
name='volume-dependants',
help='Returns True if volume has a dependant child, like replication',
args=[
dict(vol_id_opt),
],
),
dict(
name='volume-dependants-rm',
help='Removes volume dependencies',
args=[
dict(vol_id_opt),
],
),
dict(
name='volume-access-group',
help='Lists the access group(s) that have access to volume',
args=[
dict(vol_id_opt),
],
),
dict(
name='volume-mask',
help='Grants access to an access group to a volume, '
'like LUN Masking',
args=[
dict(vol_id_opt),
dict(ag_id_opt),
],
),
dict(
name='volume-unmask',
help='Revoke the access of specified access group to a volume',
args=[
dict(ag_id_opt),
dict(vol_id_opt),
],
),
dict(
name='volume-enable',
help='Enable block access of a volume',
args=[
dict(vol_id_opt),
],
),
dict(
name='volume-disable',
help='Disable block access of a volume',
args=[
dict(vol_id_opt),
],
),
dict(
name='volume-raid-info',
help='Query volume RAID information',
args=[
dict(vol_id_opt),
],
),
dict(
name='volume-ident-led-on',
help='Enable the IDENT LED for a volume',
args=[
dict(name="--vol", metavar='<VOL_ID>',
help='Targeted volume.\n'),
],
),
dict(
name='volume-ident-led-off',
help='Disable the IDENT LED for a volume',
args=[
dict(name="--vol", metavar='<VOL_ID>',
help='Targeted volume.\n'),
],
),
dict(
name='system-read-cache-pct-update',
help='Change the read cache percentage of a system',
args=[
dict(name="--sys", metavar='<SYS_ID>',
help='Targeted system.\n'),
dict(name="--read-pct",
help="Read cache percentage.\n",
type=_check_positive_integer),
],
),
dict(
name='pool-member-info',
help='Query Pool membership information',
args=[
dict(pool_id_opt),
],
),
dict(
name='access-group-create',
help='Create an access group',
args=[
dict(name='--name', metavar='<AG_NAME>',
help="Human readable name for access group"),
# TODO: _client.py access_group_create should support multiple
# initiators when creating.
dict(init_id_opt),
dict(sys_id_opt),
],
),
dict(
name='access-group-add',
help='Add an initiator into existing access group',
args=[
dict(ag_id_opt),
dict(init_id_opt),
],
),
dict(
name='access-group-remove',
help='Remove an initiator from existing access group',
args=[
dict(ag_id_opt),
dict(init_id_opt),
],
),
dict(
name='access-group-delete',
help='Deletes an access group',
args=[
dict(ag_id_opt),
],
),
dict(
name='access-group-volumes',
help='Lists the volumes that the access group has'
' been granted access to',
args=[
dict(ag_id_opt),
],
),
dict(
name='iscsi-chap',
help='Configures iSCSI inbound/outbound CHAP authentication',
args=[
dict(init_id_opt),
],
optional=[
dict(name="--in-user", metavar='<IN_USER>',
help='Inbound chap user name'),
dict(name="--in-pass", metavar='<IN_PASS>',
help='Inbound chap password'),
dict(name="--out-user", metavar='<OUT_USER>',
help='Outbound chap user name'),
dict(name="--out-pass", metavar='<OUT_PASS>',
help='Outbound chap password'),
],
),
dict(
name='fs-create',
help='Creates a file system',
args=[
dict(name="--name", metavar='<FS_NAME>',
help='name of the file system'),
dict(size_opt),
dict(pool_id_opt),
],
),
dict(
name='fs-delete',
help='Delete a filesystem',
args=[
dict(fs_id_opt)
],
),
dict(
name='fs-resize',
help='Re-sizes a filesystem',
args=[
dict(fs_id_opt),
dict(name="--size", metavar="<NEW_SIZE>",
help="New size. %s" % size_help),
],
),
dict(
name='fs-export',
help='Export a filesystem via NFS.',
args=[
dict(fs_id_opt),
],
optional=[
dict(name="--exportpath", metavar='<EXPORT_PATH>',
help="NFS server export path. e.g. '/foo/bar'."),
dict(name="--anonuid", metavar='<ANON_UID>',
help='UID(User ID) to map to anonymous user',
default=NfsExport.ANON_UID_GID_NA,
type=_check_positive_integer),
dict(name="--anongid", metavar='<ANON_GID>',
help='GID(Group ID) to map to anonymous user',
default=NfsExport.ANON_UID_GID_NA,
type=_check_positive_integer),
dict(name="--auth-type", metavar='<AUTH_TYPE>',
help='NFS client authentication type'),
dict(name="--root-host", metavar='<ROOT_HOST>',
help="The host/IP has root access.\n"
"This is repeatable argument.",
action='append',
default=[], type=_check_network_host),
dict(name="--ro-host", metavar='<RO_HOST>',
help="The host/IP has readonly access.\n"
"This is repeatable argument.\n"
"At least one '--ro-host' or '--rw-host' is required.",
action='append', default=[], type=_check_network_host),
dict(name="--rw-host", metavar='<RW_HOST>',
help="The host/IP has readwrite access.\n"
"This is repeatable argument.\n"
"At least one '--ro-host' or '--rw-host' is required.",
action='append', default=[], type=_check_network_host),
],
),
dict(
name='fs-unexport',
help='Remove an NFS export',
args=[
dict(export_id_opt),
],
),
dict(
name='fs-clone',
help='Creates a file system clone',
args=[
dict(name="--src-fs", metavar='<SRC_FS_ID>',
help='The ID of existing source file system.'),
dict(name="--dst-name", metavar='<DST_FS_NAME>',
help='The name for newly created destination file system.'),
],
optional=[
dict(name="--backing-snapshot", metavar='<BE_SS_ID>',
help='backing snapshot id'),
],
),
dict(
name='fs-snap-create',
help='Creates a snapshot',
args=[
dict(name="--name", metavar="<SNAP_NAME>",
help='The human friendly name of new snapshot'),
dict(fs_id_opt),
],
),
dict(
name='fs-snap-delete',
help='Deletes a snapshot',
args=[
dict(snap_id_opt),
dict(fs_id_opt), # TODO: why we need filesystem ID?
],
),
dict(
name='fs-snap-restore',
help='Restores a FS or specified files to '
'previous snapshot state',
args=[
dict(snap_id_opt),
dict(fs_id_opt),
],
optional=[
dict(name="--file", metavar="<FILE_PATH>",
help="Only restore provided file\n"
"Without this argument, all files will be restored\n"
"This is a repeatable argument.",
action='append', default=[]),
dict(name="--fileas", metavar="<NEW_FILE_PATH>",
help="store restore file name to another name.\n"
"This is a repeatable argument.",
action='append',
default=[]),
],
),
dict(
name='fs-dependants',
help='Returns True if filesystem has a child '
'dependency(clone/snapshot) exists',
args=[
dict(fs_id_opt),
],
optional=[
dict(name="--file", metavar="<FILE_PATH>",
action="append", default=[],
help="For file check\nThis is a repeatable argument."),
],
),
dict(
name='fs-dependants-rm',
help='Removes file system dependencies',
args=[
dict(fs_id_opt),
],
optional=[
dict(name="--file", action='append', default=[],
help='File or files to remove dependencies for.\n'
"This is a repeatable argument.",),
],
),
dict(
name='file-clone',
help='Creates a clone of a file (thin provisioned)',
args=[
dict(fs_id_opt),
dict(name="--src", metavar="<SRC_FILE_PATH>",
help='source file to clone (relative path)\n'
"This is a repeatable argument.",),
dict(name="--dst", metavar="<DST_FILE_PATH>",
help='Destination file (relative path)'
", this is a repeatable argument."),
],
optional=[
dict(name="--backing-snapshot", help='backing snapshot id'),
],
),
dict(
name='local-disk-list',
help='Query local disk information',
args=[
],
optional=[
],
),
dict(
name='volume-cache-info',
help='Query volume RAM cache information',
args=[
dict(vol_id_opt),
],
),
dict(
name='volume-phy-disk-cache-update',
help='Update volume physical disk cache setting',
args=[
dict(vol_id_opt),
dict(policy_opt),
],
),
dict(
name='volume-read-cache-policy-update',
help='Update volume read cache policy',
args=[
dict(vol_id_opt),
dict(policy_opt),
],
),
dict(
name='volume-write-cache-policy-update',
help='Update volume write cache policy',
args=[
dict(vol_id_opt),
dict(write_cache_policy_opt),
],
),
dict(
name='local-disk-ident-led-on',
help='Turn on the identification LED for a local disk',
args=[
dict(local_disk_path_opt),
],
),
dict(
name='local-disk-ident-led-off',
help='Turn off the identification LED for a local disk',
args=[
dict(local_disk_path_opt),
],
),
dict(
name='local-disk-fault-led-on',
help='Turn on the fault LED for a local disk',
args=[
dict(local_disk_path_opt),
],
),
dict(
name='local-disk-fault-led-off',
help='Turn off the fault LED for a local disk',
args=[
dict(local_disk_path_opt),
],
),
)
aliases = dict(
ls='list --type systems',
lp='list --type pools',
lv='list --type volumes',
ld='list --type disks',
la='list --type access_groups',
lf='list --type fs',
lt='list --type target_ports',
c='capabilities',
p='plugin-info',
vc='volume-create',
vrc='volume-raid-create',
vrcc='volume-raid-create-cap',
vd='volume-delete',
vr='volume-resize',
vm='volume-mask',
vu='volume-unmask',
ve='volume-enable',
vi='volume-disable',
ac='access-group-create',
aa='access-group-add',
ar='access-group-remove',
ad='access-group-delete',
vri='volume-raid-info',
vilon='volume-ident-led-on',
viloff='volume-ident-led-off',
srcpu='system-read-cache-pct-update',
pmi='pool-member-info',
ldl='local-disk-list',
lb='list --type batteries',
vci='volume-cache-info',
vpdcu='volume-phy-disk-cache-update',
vrcpu='volume-read-cache-policy-update',
vwcpu='volume-write-cache-policy-update',
ldilon='local-disk-ident-led-on',
ldiloff='local-disk-ident-led-off',
ldflon='local-disk-fault-led-on',
ldfloff='local-disk-fault-led-off',
)
# Class that encapsulates the command line arguments for lsmcli
# Note: This class is used by lsmcli and any python plug-ins.
class CmdLine(object):
"""
Command line interface class.
"""
##
# Warn of imminent data loss
# @param deleting Indicate data will be lost vs. may be lost
# (re-size)
# @return True if operation confirmed, else False
def confirm_prompt(self, deleting):
"""
Give the user a chance to bail.
"""
if not self.args.force:
msg = "will" if deleting else "may"
out("Warning: You are about to do an operation that %s cause data "
"to be lost!\nPress [Y|y] to continue, any other key to abort"
% msg)
pressed = getch()
if pressed.upper() == 'Y':
return True
else:
out('Operation aborted!')
return False
else:
return True
##
# Tries to make the output better when it varies considerably from
# plug-in to plug-in.
# @param objects Data, first row is header all other data.
def display_data(self, objects):
display_all = False
if len(objects) == 0:
return
display_way = DisplayData.DISPLAY_WAY_DEFAULT
flag_with_header = True
if self.args.sep:
flag_with_header = False
if self.args.header:
flag_with_header = True
if self.args.script:
display_way = DisplayData.DISPLAY_WAY_SCRIPT
DisplayData.display_data(
objects, display_way=display_way, flag_human=self.args.human,
flag_enum=self.args.enum,
splitter=self.args.sep, flag_with_header=flag_with_header,
flag_dsp_all_data=display_all)
def display_available_plugins(self):
d = []
sep = '<}{>'
plugins = Client.available_plugins(sep)
for p in plugins:
desc, version = p.split(sep)
d.append(PlugData(desc, version))
self.display_data(d)
@staticmethod
def handle_alias():
"""
Walk the command line argument list and build up a new command line
with the appropriate substitutions which is then passed to argparse, so
that we can avoid adding more sub parsers and do all argument parsing
before the need to talk to the library
:return copy of command line args with alias expansion:
"""
rc = []
for i in sys.argv[1:]:
if i in aliases:
rc.extend(aliases[i].split(" "))
else:
rc.append(i)
return rc
@staticmethod
def alias_help_text():
rc = "command aliases:\n"
for k, v in sorted(aliases.items()):
rc += " {0:<18} Alias of '{1}'\n".format(k, v)
return rc
# All the command line arguments and options are created in this method
def cli(self):
"""
Command line interface parameters
"""
parent_parser = ArgumentParser(add_help=False)
_add_common_options(parent_parser, is_child=True)
parser = ArgumentParser(
description='The libStorageMgmt command line interface.'
' Run %(prog)s <command> -h for more on each command.',
epilog=CmdLine.alias_help_text() +
'\n\nCopyright 2012-2018 Red Hat, Inc.\n'
'Please report bugs to '
'<libstoragemgmt-devel@lists.fedorahosted.org>\n',
formatter_class=RawTextHelpFormatter)
_add_common_options(parser, is_child=False)
subparsers = parser.add_subparsers(metavar="command")
# Walk the command list and add all of them to the parser
for cmd in cmds:
sub_parser = subparsers.add_parser(
cmd['name'], help=cmd['help'], parents=[parent_parser],
formatter_class=RawTextHelpFormatter)
group = sub_parser.add_argument_group("cmd required arguments")
for arg in cmd.get('args', []):
name = arg['name']
del arg['name']
group.add_argument(name, required=True, **arg)
group = sub_parser.add_argument_group("cmd optional arguments")
for arg in cmd.get('optional', []):
flags = arg['name']
del arg['name']
if not isinstance(flags, tuple):
flags = (flags,)
group.add_argument(*flags, **arg)
sub_parser.set_defaults(
func=getattr(self, cmd['name'].replace("-", "_")))
if len(sys.argv) == 1:
parser.print_usage()
exit(1)
self.parser = parser
known_args = parser.parse_args(args=CmdLine.handle_alias())
# Copy child value to root.
for k, v in vars(known_args).items():
if k.startswith(_CHILD_OPTION_DST_PREFIX):
root_k = k[len(_CHILD_OPTION_DST_PREFIX):]
if getattr(known_args, root_k) is None or \
getattr(known_args, root_k) is False:
setattr(known_args, root_k, v)
return known_args
# Display the types of nfs client authentication that are supported.
# @return None
def display_nfs_client_authentication(self):
"""
Dump the supported nfs client authentication types
"""
if self.args.sep:
out(self.args.sep.join(self.c.export_auth()))
else:
out(", ".join(self.c.export_auth()))
# Determine what the search key and search value are for listing
# @param args Argparse argument object
# @return (key, value) tuple
@staticmethod
def _get_search_key_value(args):
search_key = None
search_value = None
search_args = ((args.sys, 'system_id'),
(args.pool, 'pool_id'),
(args.vol, 'volume_id'),
(args.disk, 'disk_id'),
(args.ag, 'access_group_id'),
(args.fs, 'fs_id'),
(args.nfs_export, 'nfs_export_id'),
(args.tgt, 'tgt_port_id'))
for sa in search_args:
if sa[0]:
if search_key:
raise ArgError(
"Search key specified more than once (%s, %s)" %
(search_key, sa[1]))
else:
(search_value, search_key) = sa
return search_key, search_value
# Method that calls the appropriate method based on what the list type is
# @param args Argparse argument object
def list(self, args):
(search_key, search_value) = CmdLine._get_search_key_value(args)
if args.type == 'VOLUMES':
lsm_vols = []
if search_key == 'volume_id':
search_key = 'id'
if search_key == 'access_group_id':
lsm_ag = _get_item(self.c.access_groups(), args.ag,
"Access Group", raise_error=False)
if lsm_ag:
lsm_vols = self.c.volumes_accessible_by_access_group(
lsm_ag)
elif search_key and search_key not in Volume.SUPPORTED_SEARCH_KEYS:
raise ArgError("Search key '%s' is not supported by "
"volume listing." % search_key)
else:
lsm_vols = self.c.volumes(search_key, search_value)
self.display_data(list(_add_sd_paths(v) for v in lsm_vols))
elif args.type == 'POOLS':
if search_key == 'pool_id':
search_key = 'id'
if search_key and search_key not in Pool.SUPPORTED_SEARCH_KEYS:
raise ArgError("Search key '%s' is not supported by "
"pool listing." % search_key)
self.display_data(
self.c.pools(search_key, search_value))
elif args.type == 'FS':
if search_key == 'fs_id':
search_key = 'id'
if search_key and \
search_key not in FileSystem.SUPPORTED_SEARCH_KEYS:
raise ArgError("Search key '%s' is not supported by "
"volume listing." % search_key)
self.display_data(self.c.fs(search_key, search_value))
elif args.type == 'SNAPSHOTS':
if args.fs is None:
raise ArgError("--fs <file system id> required")
if search_key and search_key != "fs_id":
raise ArgError("Search key '%s' is not supported by "
"snapshot listing." % search_key)
fs = _get_item(self.c.fs(), args.fs, 'File System')
self.display_data(self.c.fs_snapshots(fs))
elif args.type == 'EXPORTS':
if search_key == 'nfs_export_id':
search_key = 'id'
if search_key and \
search_key not in NfsExport.SUPPORTED_SEARCH_KEYS:
raise ArgError("Search key '%s' is not supported by "
"NFS Export listing" % search_key)
self.display_data(self.c.exports(search_key, search_value))
elif args.type == 'NFS_CLIENT_AUTH':
if search_key:
raise ArgError("NFS client authentication type listing with "
"search is not supported")
self.display_nfs_client_authentication()
elif args.type == 'ACCESS_GROUPS':
if search_key == 'access_group_id':
search_key = 'id'
if search_key == 'volume_id':
lsm_vol = _get_item(self.c.volumes(), args.vol,
"Volume", raise_error=False)
if lsm_vol:
return self.display_data(
self.c.access_groups_granted_to_volume(lsm_vol))
else:
return self.display_data([])
elif (search_key and
search_key not in AccessGroup.SUPPORTED_SEARCH_KEYS):
raise ArgError("Search key '%s' is not supported by "
"Access Group listing" % search_key)
self.display_data(
self.c.access_groups(search_key, search_value))
elif args.type == 'SYSTEMS':
if search_key:
raise ArgError("System listing with search is not supported")
self.display_data(self.c.systems())
elif args.type == 'DISKS':
if search_key == 'disk_id':
search_key = 'id'
if search_key and search_key not in Disk.SUPPORTED_SEARCH_KEYS:
raise ArgError("Search key '%s' is not supported by "
"disk listing" % search_key)
self.display_data(
list(_add_sd_paths(d)
for d in self.c.disks(search_key, search_value)))
elif args.type == 'TARGET_PORTS':
if search_key == 'tgt_port_id':
search_key = 'id'
if search_key and \
search_key not in TargetPort.SUPPORTED_SEARCH_KEYS:
raise ArgError("Search key '%s' is not supported by "
"target port listing" % search_key)
self.display_data(
self.c.target_ports(search_key, search_value))
elif args.type == 'PLUGINS':
if search_key:
raise ArgError("Plugins listing with search is not supported")
self.display_available_plugins()
elif args.type == 'BATTERIES':
if search_key and \
search_key not in Battery.SUPPORTED_SEARCH_KEYS:
raise ArgError("Search key '%s' is not supported by "
"battery listing" % search_key)
self.display_data(
self.c.batteries(search_key, search_value))
else:
raise ArgError("unsupported listing type=%s" % args.type)
# Creates an access group.
def access_group_create(self, args):
system = _get_item(self.c.systems(), args.sys, "System")
(init_id, init_type) = parse_convert_init(args.init)
access_group = self.c.access_group_create(args.name, init_id,
init_type, system)
self.display_data([access_group])
def _add_rm_access_grp_init(self, args, op):
lsm_ag = _get_item(self.c.access_groups(), args.ag, "Access Group")
(init_id, init_type) = parse_convert_init(args.init)
if op:
return self.c.access_group_initiator_add(lsm_ag, init_id,
init_type)
else:
return self.c.access_group_initiator_delete(lsm_ag, init_id,
init_type)
# Adds an initiator from an access group
def access_group_add(self, args):
self.display_data([self._add_rm_access_grp_init(args, True)])
# Removes an initiator from an access group
def access_group_remove(self, args):
self.display_data([self._add_rm_access_grp_init(args, False)])
def access_group_volumes(self, args):
agl = self.c.access_groups()
group = _get_item(agl, args.ag, "Access Group")
vols = self.c.volumes_accessible_by_access_group(group)
self.display_data(list(_add_sd_paths(v) for v in vols))
def iscsi_chap(self, args):
(init_id, init_type) = parse_convert_init(args.init)
if init_type != AccessGroup.INIT_TYPE_ISCSI_IQN:
raise ArgError("--init \"%s\" is not a valid iSCSI IQN" % args.init)
if self.args.in_user and not self.args.in_pass:
raise ArgError("--in-user requires --in-pass")
if self.args.in_pass and not self.args.in_user:
raise ArgError("--in-pass requires --in-user")
if self.args.out_user and not self.args.out_pass:
raise ArgError("--out-user requires --out-pass")
if self.args.out_pass and not self.args.out_user:
raise ArgError("--out-pass requires --out-user")
# Enforce consistency across all
if self.args.out_user and self.args.out_pass and not \
(self.args.in_user and self.args.in_pass):
raise ArgError("out-user and out-password only supported if "
"inbound is supplied")
self.c.iscsi_chap_auth(init_id, args.in_user,
self.args.in_pass,
self.args.out_user,
self.args.out_pass)
def volume_access_group(self, args):
vol = _get_item(self.c.volumes(), args.vol, "Volume")
groups = self.c.access_groups_granted_to_volume(vol)
self.display_data(groups)
# Used to delete access group
def access_group_delete(self, args):
agl = self.c.access_groups()
group = _get_item(agl, args.ag, "Access Group")
return self.c.access_group_delete(group)
# Used to delete a file system
def fs_delete(self, args):
fs = _get_item(self.c.fs(), args.fs, "File System")
if self.confirm_prompt(True):
self._wait_for_it("fs-delete", self.c.fs_delete(fs), None)
# Used to create a file system
def fs_create(self, args):
p = _get_item(self.c.pools(), args.pool, "Pool")
fs = self._wait_for_it("fs-create",
*self.c.fs_create(p, args.name,
self._size(args.size)))
self.display_data([fs])
# Used to resize a file system
def fs_resize(self, args):
fs = _get_item(self.c.fs(), args.fs, "File System")
size = self._size(args.size)
if size == fs.total_space:
raise LsmError(
ErrorNumber.NO_STATE_CHANGE, "Specified size same as current")
if self.confirm_prompt(False):
fs = self._wait_for_it("fs-resize",
*self.c.fs_resize(fs, size))
self.display_data([fs])
# Used to clone a file system
def fs_clone(self, args):
src_fs = _get_item(
self.c.fs(), args.src_fs, "Source File System")
ss = None
if args.backing_snapshot:
# go get the snapshot
ss = _get_item(self.c.fs_snapshots(src_fs),
args.backing_snapshot, "Snapshot")
fs = self._wait_for_it(
"fs_clone", *self.c.fs_clone(src_fs, args.dst_name, ss))
self.display_data([fs])
# Used to clone a file(s)
def file_clone(self, args):
fs = _get_item(self.c.fs(), args.fs, "File System")
if self.args.backing_snapshot:
# go get the snapshot
ss = _get_item(self.c.fs_snapshots(fs),
args.backing_snapshot, "Snapshot")
else:
ss = None
self._wait_for_it(
"fs_file_clone", self.c.fs_file_clone(fs, args.src, args.dst, ss),
None)
# Converts a size parameter into the appropriate number of bytes
# @param s Size to convert to bytes handles B, K, M, G, T, P postfix
# @return Size in bytes
@staticmethod
def _size(s):
size_bytes = size_human_2_size_bytes(s)
if size_bytes <= 0:
raise ArgError("Incorrect size argument format: '%s'" % s)
return size_bytes
def _cp(self, cap, val):
if self.args.sep is not None:
s = self.args.sep
else:
s = ':'
if val:
v = "SUPPORTED"
else:
v = "UNSUPPORTED"
out("%s%s%s" % (cap, s, v))
def capabilities(self, args):
s = _get_item(self.c.systems(), args.sys, "System")
cap = self.c.capabilities(s)
sup_caps = sorted(cap.get_supported().values())
all_caps = sorted(cap.get_supported(True).values())
sep = DisplayData.DEFAULT_SPLITTER
if self.args.sep is not None:
sep = self.args.sep
cap_data = OrderedDict()
# Show support capabilities first
for v in sup_caps:
cap_data[v] = 'SUPPORTED'
for v in all_caps:
if v not in sup_caps:
cap_data[v] = 'UNSUPPORTED'
DisplayData.display_data_script_way([cap_data], sep)
def plugin_info(self, args):
desc, version = self.c.plugin_info()
if args.sep:
out("%s%s%s" % (desc, args.sep, version))
else:
out("Description: %s Version: %s" % (desc, version))
# Creates a volume
def volume_create(self, args):
# Get pool
p = _get_item(self.c.pools(), args.pool, "Pool")
vol = self._wait_for_it(
"volume-create",
*self.c.volume_create(
p,
args.name,
self._size(args.size),
vol_provision_str_to_type(args.provisioning)))
self.display_data([_add_sd_paths(vol)])
# Creates a snapshot
def fs_snap_create(self, args):
# Get fs
fs = _get_item(self.c.fs(), args.fs, "File System")
ss = self._wait_for_it("snapshot-create",
*self.c.fs_snapshot_create(
fs,
args.name))
self.display_data([ss])
# Restores a snap shot
def fs_snap_restore(self, args):
# Get snapshot
fs = _get_item(self.c.fs(), args.fs, "File System")
ss = _get_item(self.c.fs_snapshots(fs), args.snap, "Snapshot")
files = self.args.file
if len(files) == 0:
files = None
flag_all_files = True
if self.args.file:
flag_all_files = False
if self.args.fileas:
if len(self.args.file) != len(self.args.fileas):
raise ArgError(
"number of --file not equal to --fileas")
if self.confirm_prompt(True):
self._wait_for_it(
'fs-snap-restore',
self.c.fs_snapshot_restore(
fs, ss, files, self.args.fileas, flag_all_files),
None)
# Deletes a volume
def volume_delete(self, args):
v = _get_item(self.c.volumes(), args.vol, "Volume")
if self.confirm_prompt(True):
self._wait_for_it("volume-delete", self.c.volume_delete(v),
None)
# Deletes a snap shot
def fs_snap_delete(self, args):
fs = _get_item(self.c.fs(), args.fs, "File System")
ss = _get_item(self.c.fs_snapshots(fs), args.snap, "Snapshot")
if self.confirm_prompt(True):
self._wait_for_it("fs_snap_delete",
self.c.fs_snapshot_delete(fs, ss), None)
# Waits for an operation to complete by polling for the status of the
# operations.
# @param msg Message to display if this job fails
# @param job The job id to wait on
# @param item The item that could be available now if there is no job
def _wait_for_it(self, msg, job, item):
if not job:
return item
else:
# If a user doesn't want to wait, return the job id to stdout
# and exit with job in progress
if self.args._async:
out(job)
self.shutdown(ErrorNumber.JOB_STARTED)
while True:
(s, percent, item) = self.c.job_status(job)
if s == JobStatus.INPROGRESS:
# Add an option to spit out progress?
# print "%s - Percent %s complete" % (job, percent)
time.sleep(0.25)
elif s == JobStatus.COMPLETE:
self.c.job_free(job)
return item
else:
# Something better to do here?
raise ArgError(msg + " job error code= " + str(s))
# Retrieves the status of the specified job
def job_status(self, args):
(s, percent, item) = self.c.job_status(args.job)
if s == JobStatus.COMPLETE:
if item:
self.display_data([_add_sd_paths(item)])
self.c.job_free(args.job)
else:
out(str(percent))
self.shutdown(ErrorNumber.JOB_STARTED)
# Replicates a volume
def volume_replicate(self, args):
p = None
if args.pool:
p = _get_item(self.c.pools(), args.pool, "Pool")
v = _get_item(self.c.volumes(), args.vol, "Volume")
rep_type = vol_rep_type_str_to_type(args.rep_type)
if rep_type == Volume.REPLICATE_UNKNOWN:
raise ArgError("invalid replication type= %s" % rep_type)
vol = self._wait_for_it(
"replicate volume",
*self.c.volume_replicate(p, rep_type, v, args.name))
self.display_data([_add_sd_paths(vol)])
# Check to see if block ranges are overlapping
@staticmethod
def _check_overlap(ranges):
def _overlap(r, member):
for i in range(1, len(r)):
ps = getattr(r[i - 1], member) # Previous start
pc = r[i - 1].block_count # Previous count
cs = getattr(r[i], member) # Current start
cc = r[i].block_count # Current count
if ps + pc > cs:
raise ArgError("Overlapping %s replication "
"range %d..%d overlaps with %d..%d" %
(member, ps, ps + pc - 1, cs, cs + cc - 1))
# Sort the src ranges
ranges.sort(key=lambda x: x.src_block)
_overlap(ranges, "src_block")
ranges.sort(key=lambda x: x.dest_block)
_overlap(ranges, "dest_block")
# Replicates a range of a volume
def volume_replicate_range(self, args):
src = _get_item(self.c.volumes(), args.src_vol, "Source Volume")
dst = _get_item(self.c.volumes(), args.dst_vol,
"Destination Volume")
rep_type = vol_rep_type_str_to_type(args.rep_type)
if rep_type == Volume.REPLICATE_UNKNOWN:
raise ArgError("invalid replication type= %s" % rep_type)
src_starts = args.src_start
dst_starts = args.dst_start
counts = args.count
if not len(src_starts) \
or not (len(src_starts) == len(dst_starts) == len(counts)):
raise ArgError("Differing numbers of src_start, dest_start, "
"and count parameters")
ranges = []
for b in range(len(src_starts)):
# Validate some assumptions for source & count
count = long(counts[b])
src_start = long(src_starts[b])
dst_start = long(dst_starts[b])
if count < 0:
raise ArgError("--count: value < 0")
if src_start < 0:
raise ArgError("--src-start: value < 0")
if dst_start < 0:
raise ArgError("--dst_start: value < 0")
if src_start + count > src.num_of_blocks:
raise ArgError("--src-start + --count > source size")
if dst_start + count > dst.num_of_blocks:
raise ArgError("--dst-start + --count > destination size")
ranges.append(BlockRange(src_start, dst_start, count))
CmdLine._check_overlap(ranges)
if self.confirm_prompt(False):
self.c.volume_replicate_range(rep_type, src, dst, ranges)
#
# Returns the block size in bytes for each block represented in
# volume_replicate_range
def volume_replicate_range_block_size(self, args):
s = _get_item(self.c.systems(), args.sys, "System")
out(self.c.volume_replicate_range_block_size(s))
def volume_mask(self, args):
vol = _get_item(self.c.volumes(), args.vol, 'Volume')
ag = _get_item(self.c.access_groups(), args.ag, 'Access Group')
self.c.volume_mask(ag, vol)
def volume_unmask(self, args):
ag = _get_item(self.c.access_groups(), args.ag, "Access Group")
vol = _get_item(self.c.volumes(), args.vol, "Volume")
return self.c.volume_unmask(ag, vol)
# Re-sizes a volume
def volume_resize(self, args):
v = _get_item(self.c.volumes(), args.vol, "Volume")
size = self._size(args.size)
if size == v.block_size * v.num_of_blocks:
raise LsmError(
ErrorNumber.NO_STATE_CHANGE, "Specified size same as current")
if self.confirm_prompt(False):
vol = self._wait_for_it("resize",
*self.c.volume_resize(v, size))
self.display_data([_add_sd_paths(vol)])
# Enable a volume
def volume_enable(self, args):
v = _get_item(self.c.volumes(), args.vol, "Volume")
self.c.volume_enable(v)
# Disable a volume
def volume_disable(self, args):
v = _get_item(self.c.volumes(), args.vol, "Volume")
self.c.volume_disable(v)
# Removes a nfs export
def fs_unexport(self, args):
export = _get_item(self.c.exports(), args.export, "NFS Export")
self.c.export_remove(export)
# Exports a file system as a NFS export
def fs_export(self, args):
fs = _get_item(self.c.fs(), args.fs, "File System")
# Check to see if we have some type of access specified
if len(args.rw_host) == 0 \
and len(args.ro_host) == 0:
raise ArgError(" please specify --ro-host or --rw-host")
export = self.c.export_fs(
fs.id,
args.exportpath,
args.root_host,
args.rw_host,
args.ro_host,
args.anonuid,
args.anongid,
args.auth_type,
None)
self.display_data([export])
# Displays volume dependants.
def volume_dependants(self, args):
v = _get_item(self.c.volumes(), args.vol, "Volume")
rc = self.c.volume_child_dependency(v)
out(rc)
# Removes volume dependants.
def volume_dependants_rm(self, args):
v = _get_item(self.c.volumes(), args.vol, "Volume")
self._wait_for_it("volume-dependant-rm",
self.c.volume_child_dependency_rm(v), None)
def volume_raid_info(self, args):
lsm_vol = _get_item(self.c.volumes(), args.vol, "Volume")
self.display_data(
[
VolumeRAIDInfo(
lsm_vol.id, *self.c.volume_raid_info(lsm_vol))])
def pool_member_info(self, args):
lsm_pool = _get_item(self.c.pools(), args.pool, "Pool")
self.display_data(
[
PoolRAIDInfo(
lsm_pool.id, *self.c.pool_member_info(lsm_pool))])
def volume_raid_create(self, args):
raid_type = VolumeRAIDInfo.raid_type_str_to_lsm(args.raid_type)
all_lsm_disks = self.c.disks()
lsm_disks = [d for d in all_lsm_disks if d.id in args.disk]
if len(lsm_disks) != len(args.disk):
raise LsmError(
ErrorNumber.NOT_FOUND_DISK,
"Disk ID %s not found" %
', '.join(set(args.disk) - set(d.id for d in all_lsm_disks)))
busy_disks = [d.id for d in lsm_disks
if not d.status & Disk.STATUS_FREE]
if len(busy_disks) >= 1:
raise LsmError(
ErrorNumber.DISK_NOT_FREE,
"Disk %s is not free" % ", ".join(busy_disks))
if args.strip_size:
strip_size = size_human_2_size_bytes(args.strip_size)
else:
strip_size = Volume.VCR_STRIP_SIZE_DEFAULT
self.display_data([
_add_sd_paths(
self.c.volume_raid_create(
args.name, raid_type, lsm_disks, strip_size))])
def volume_raid_create_cap(self, args):
lsm_sys = _get_item(self.c.systems(), args.sys, "System")
self.display_data([
VcrCap(lsm_sys.id, *self.c.volume_raid_create_cap_get(lsm_sys))])
def volume_ident_led_on(self, args):
lsm_volume = _get_item(self.c.volumes(), args.vol, "Volume")
self.c.volume_ident_led_on(lsm_volume)
def volume_ident_led_off(self, args):
lsm_volume = _get_item(self.c.volumes(), args.vol, "Volume")
self.c.volume_ident_led_off(lsm_volume)
def system_read_cache_pct_update(self, args):
lsm_system = _get_item(self.c.systems(), args.sys, "System")
try:
read_pct = int(args.read_pct)
except ValueError as ve:
raise LsmError(ErrorNumber.INVALID_ARGUMENT, str(ve))
self.c.system_read_cache_pct_update(lsm_system, read_pct)
lsm_system = _get_item(self.c.systems(), args.sys, "System")
self.display_data([lsm_system])
# Displays file system dependants
def fs_dependants(self, args):
fs = _get_item(self.c.fs(), args.fs, "File System")
rc = self.c.fs_child_dependency(fs, args.file)
out(rc)
# Removes file system dependants
def fs_dependants_rm(self, args):
fs = _get_item(self.c.fs(), args.fs, "File System")
self._wait_for_it("fs-dependants-rm",
self.c.fs_child_dependency_rm(fs,
args.file),
None)
def _read_configfile(self):
"""
Set uri from config file. Will be overridden by cmdline option or
env var if present.
"""
allowed_config_options = ("uri",)
config_path = os.path.expanduser("~") + "/.lsmcli"
if not os.path.exists(config_path):
return
with open(config_path) as f:
for line in f:
if line.lstrip().startswith("#"):
continue
try:
name, val = [x.strip() for x in line.split("=", 1)]
if name in allowed_config_options:
setattr(self, name, val)
except ValueError:
pass
def is_connection_free_cmd(self):
"""
Return True if current command is one of _CONNECTION_FREE_COMMANDS.
"""
if self.args.func.__name__.replace("_", "-") in \
_CONNECTION_FREE_COMMANDS:
return True
return False
# Class constructor.
def __init__(self):
self.uri = None
self.c = None
self.parser = None
self.unknown_args = None
self.args = self.cli()
self.cleanup = None
self.tmo = int(self.args.wait)
if not self.tmo or self.tmo < 0:
raise ArgError("[-w|--wait] requires a non-zero positive integer")
if self.is_connection_free_cmd():
return
self._read_configfile()
if os.getenv('LSMCLI_URI') is not None:
self.uri = os.getenv('LSMCLI_URI')
self.password = os.getenv('LSMCLI_PASSWORD')
if self.args.uri is not None:
self.uri = self.args.uri
if self.uri is None:
# We need a valid plug-in to instantiate even if all we are trying
# to do is list the plug-ins at the moment to keep that code
# the same in all cases, even though it isn't technically
# required for the client library (static method)
# TODO: Make this not necessary.
if 'type' in self.args and self.args.type == "PLUGINS":
self.uri = "sim://"
self.password = None
else:
raise ArgError("--uri missing or export LSMCLI_URI")
# Lastly get the password if requested.
if self.args.prompt:
self.password = getpass.getpass()
if self.password is not None:
# Check for username
u = uri_parse(self.uri)
if u['username'] is None:
raise ArgError("password specified with no user name in uri")
# Does appropriate clean-up
# @param ec The exit code
def shutdown(self, ec=None):
if self.cleanup:
self.cleanup()
if ec:
sys.exit(ec)
# Process the specified command
# @param cli The object instance to invoke methods on.
def process(self, cli=None):
"""
Process the parsed command.
"""
if self.is_connection_free_cmd():
self.args.func(self.args)
else:
if cli:
# Directly invoking code though a wrapper to catch unsupported
# operations.
self.c = Proxy(cli())
self.c.plugin_register(self.uri, self.password, self.tmo)
self.cleanup = self.c.plugin_unregister
else:
# Going across the ipc pipe
self.c = Proxy(Client(self.uri, self.password, self.tmo))
if os.getenv('LSM_DEBUG_PLUGIN'):
input("Attach debugger to plug-in, "
"press <return> when ready...")
self.cleanup = self.c.close
self.args.func(self.args)
self.shutdown()
def local_disk_list(self, args):
local_disks = []
func_dict = {
"vpd83": LocalDisk.vpd83_get,
"rpm": LocalDisk.rpm_get,
"link_type": LocalDisk.link_type_get,
"serial_num": LocalDisk.serial_num_get,
"led_status": LocalDisk.led_status_get,
"link_speed": LocalDisk.link_speed_get,
"health_status": LocalDisk.health_status_get,
}
for disk_path in LocalDisk.list():
info_dict = {
"vpd83": "",
"rpm": Disk.RPM_NO_SUPPORT,
"link_type": Disk.LINK_TYPE_NO_SUPPORT,
"serial_num": "",
"led_status": Disk.LED_STATUS_UNKNOWN,
"link_speed": Disk.LINK_SPEED_UNKNOWN,
"health_status": Disk.HEALTH_STATUS_UNKNOWN,
}
for key in info_dict.keys():
try:
info_dict[key] = func_dict[key](disk_path)
except LsmError as lsm_err:
if lsm_err.code != ErrorNumber.NO_SUPPORT:
sys.stderr.write("WARN: %s('%s'): %d %s\n" %
(func_dict[key].__name__, disk_path,
lsm_err.code, lsm_err.msg))
local_disks.append(
LocalDiskInfo(disk_path,
info_dict["vpd83"],
info_dict["rpm"],
info_dict["link_type"],
info_dict["serial_num"],
info_dict["led_status"],
info_dict["link_speed"],
info_dict["health_status"]))
self.display_data(local_disks)
def volume_cache_info(self, args):
lsm_vol = _get_item(self.c.volumes(), args.vol, "Volume")
self.display_data(
[
VolumeRAMCacheInfo(
lsm_vol.id, *self.c.volume_cache_info(lsm_vol))])
def volume_phy_disk_cache_update(self, args):
lsm_vol = _get_item(self.c.volumes(), args.vol, "Volume")
if args.policy == "ENABLE":
policy = Volume.READ_CACHE_POLICY_ENABLED
else:
policy = Volume.READ_CACHE_POLICY_DISABLED
self.c.volume_physical_disk_cache_update(lsm_vol, policy)
self.display_data(
[
VolumeRAMCacheInfo(
lsm_vol.id, *self.c.volume_cache_info(lsm_vol))])
def volume_read_cache_policy_update(self, args):
lsm_vol = _get_item(self.c.volumes(), args.vol, "Volume")
if args.policy == "ENABLE":
policy = Volume.PHYSICAL_DISK_CACHE_ENABLED
else:
policy = Volume.PHYSICAL_DISK_CACHE_DISABLED
self.c.volume_read_cache_policy_update(lsm_vol, policy)
self.display_data(
[
VolumeRAMCacheInfo(
lsm_vol.id, *self.c.volume_cache_info(lsm_vol))])
def volume_write_cache_policy_update(self, args):
lsm_vol = _get_item(self.c.volumes(), args.vol, "Volume")
if args.policy == 'WB':
policy = Volume.WRITE_CACHE_POLICY_WRITE_BACK
elif args.policy == 'AUTO':
policy = Volume.WRITE_CACHE_POLICY_AUTO
else:
policy = Volume.WRITE_CACHE_POLICY_WRITE_THROUGH
self.c.volume_write_cache_policy_update(lsm_vol, policy)
self.display_data(
[
VolumeRAMCacheInfo(
lsm_vol.id, *self.c.volume_cache_info(lsm_vol))])
def local_disk_ident_led_on(self, args):
LocalDisk.ident_led_on(args.path)
def local_disk_ident_led_off(self, args):
LocalDisk.ident_led_off(args.path)
def local_disk_fault_led_on(self, args):
LocalDisk.fault_led_on(args.path)
def local_disk_fault_led_off(self, args):
LocalDisk.fault_led_off(args.path)