<commit_before># Copyright (C) 2012 Red Hat, Inc.
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
#
# Author: tasleson
import os
import sys
import getpass
import time

from argparse import ArgumentParser

import common
import client
import data
from version import VERSION
from data import Capabilities, PlugData


##@package lsm.cmdline


## Users are reporting errors with broken pipe when piping output
# to another program.  This appears to be related to this issue:
# http://bugs.python.org/issue11380
# Unable to reproduce, but hopefully this will address it.
# @param msg    The message to be written to stdout
def out(msg):
    try:
        sys.stdout.write(str(msg))
        sys.stdout.write("\n")
        sys.stdout.flush()
    except IOError:
        sys.exit(1)


## Wraps the invocation to the command line
# @param    client  Object to invoke calls on (optional)
def cmd_line_wrapper(c=None):
    """
    Common command line code, called.
    """
    try:
        cli = CmdLine()
        cli.process(c)
    except ArgError as ae:
        sys.stderr.write(str(ae))
        sys.stderr.flush()
        sys.exit(2)
    except common.LsmError as le:
        sys.stderr.write(str(le) + "\n")
        sys.stderr.flush()
        sys.exit(4)
    except KeyboardInterrupt:
        sys.exit(1)


## 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'):
    for i in l:
        if i.id == the_id:
            return i
    raise ArgError('%s with id %s not found!' % (friendly_name, the_id))


list_choices = ['VOLUMES', 'INITIATORS', 'POOLS', 'FS', 'SNAPSHOTS',
                'EXPORTS', "NFS_CLIENT_AUTH", 'ACCESS_GROUPS',
                'SYSTEMS', 'DISKS', 'PLUGINS']

initiator_id_types = ('WWPN', 'WWNN', 'ISCSI', 'HOSTNAME', 'SAS')
initiator_id_help = "initiator id type: " + ", ".join(initiator_id_types)

provision_types = ('DEFAULT', 'THIN', 'FULL')
provision_help = "provisioning type: " + ", ".join(provision_types)

access_types = ('RO', 'RW')
access_help = "access type: " + ", ".join(access_types)

replicate_types = ('SNAPSHOT', 'CLONE', 'COPY', 'MIRROR_ASYNC', 'MIRROR_SYNC')
replicate_help = "replication type: " + ", ".join(replicate_types)

size_help = 'size - Can use B, K, M, G, T, P postfix (IEC sizing)'

member_types = ('DISK', 'VOLUME', 'POOL')
member_help = "member type: " + ", ".join(member_types)

raid_types = ('JBOD', 'RAID1', 'RAID3', 'RAID4', 'RAID5', 'RAID6')
raid_help = "RAID type: " + ", ".join(raid_types)

cmds = (
    dict(
        name='list',
        help="List records of different types",
        args=[
            dict(name='--type',
                 help='List records of type: ' +
                 ", ".join(list_choices) +
                 '. SNAPSHOTS requires --fs <fs id>.\n' +
                 '      POOLS can have -o switch.',
                 choices=list_choices,
                 type=str.upper),
        ],
        optional=[
            dict(name=('-o', '--optional'),
                 help='Retrieve additional optional info if available',
                 default=False,
                 action='store_true'),
            dict(name=('--fs'),
                 help="File system id which is required when listing "
                      "SNAPSHOTS"),
        ],
    ),

    dict(
        name='delete-fs',
        help='Delete a filesystem',
        args=[
            dict(name="--fs_id", help='filesystem id'),
        ],
    ),

    dict(
        name='delete-access-group',
        help='Deletes an access group',
        args=[
            dict(name="--group_id", help='access group id'),
        ],
    ),

    dict(
        name='capabilities',
        help='Retrieves array capabilities',
        args=[
            dict(name="--system_id", help='system id'),
        ],
    ),

    dict(
        name='plugin-info',
        help='Retrieves plugin description and version',
    ),

    dict(
        name='create-volume',
        help='Creates a volume (logical unit)',
        args=[
            dict(name="--name", help='volume name'),
            dict(name="--size", help=size_help),
            dict(name="--pool", help='pool id'),
        ],
        optional=[
            dict(name="--provisioning", help=provision_help,
                 choices=provision_types),
        ],
    ),

    dict(
        name='create-fs',
        help='Creates a file system',
        args=[
            dict(name="--name", help='name of the file system'),
            dict(name="--size", help=size_help),
            dict(name="--pool", help='pool id'),
        ],
    ),

    dict(
        name='clone-fs',
        help='Creates a file system clone',
        args=[
            dict(name="--source_name", help='existing source file system id'),
            dict(name="--dest_name", help='new file system id'),
        ],
        optional=[
            dict(name="--type", help=provision_help,
                 choices=provision_types),
            dict(name="--backing-snapshot", help='backing snapshot id'),
        ],
    ),

    dict(
        name='create-access-group',
        help='Creates an access group',
        args=[
            dict(name="--name", help='new access group name'),
            dict(name="--id", help='initiator id'),
            dict(name="--type", help=initiator_id_help,
                 choices=initiator_id_types,
                 type=str.upper),
            dict(name="--system", help='system id'),
        ],
    ),

    dict(
        name='access-group-add',
        help='Adds an initiator to an access group',
        args=[
            dict(name="--gid", help='group id'),
            dict(name="--iid", help='initiator id'),
            dict(name="--type", help=initiator_id_help,
                 choices=initiator_id_types,
                 type=str.upper),
        ],
    ),

    dict(
        name='access-group-remove',
        help='Removes an initiator from an access group',
        args=[
            dict(name="--gid", help='group id'),
            dict(name="--iid", help='initiator id'),
        ],
    ),

    dict(
        name='access-group-volumes',
        help='Lists the volumes that the access group has'
             ' been granted access to',
        args=[
            dict(name="--gid", help='access group id'),
        ],
    ),

    dict(
        name='volume-access-group',
        help='Lists the access group(s) that have access'
             ' to volume',
        args=[
            dict(name="--vol_id", help='volume id'),
        ],
    ),

    dict(
        name='volumes-accessible-initiator',
        help='Lists the volumes that are accessible '
             'by the initiator',
        args=[
            dict(name="--iid", help='initiator id'),
        ],
    ),

    dict(
        name='initiators-granted-volume',
        help='Lists the initiators that have been '
             'granted access to specified volume',
        args=[
            dict(name="--vol_id", help='volume id'),
        ],
    ),

    dict(
        name='iscsi-chap',
        help='Configures ISCSI inbound/outbound CHAP authentication',
        args=[
            dict(name="--iid", help='initiator id'),
        ],
        optional=[
            dict(name="--in-user", help='inbound chap user name'),
            dict(name="--in-password", help='inbound chap password'),
            dict(name="--out-user", help='outbound chap user name'),
            dict(name="--out-password", help='outbound chap password'),
        ],
    ),

    dict(
        name='create-ss',
        help='Creates a snapshot',
        args=[
            dict(name="--name", help='snapshot name'),
            dict(name="--fs", help='file system id'),
        ],
        optional=[
            dict(name="--file",
                 help='default: all files. May use more than once.',
                 action='append', default=[]),
        ],
    ),

    dict(
        name='clone-file',
        help='Creates a clone of a file (thin provisioned)',
        args=[
            dict(name="--fs", help='file system'),
            dict(name="--src", help='source file to clone (relative path)'),
            dict(name="--dest", help='destination file (relative path)'),
        ],
        optional=[
            dict(name="--backing-snapshot", help='backing snapshot id'),
        ],
    ),

    dict(
        name='delete-volume',
        help='Deletes a volume given its id',
        args=[
            dict(name="--id", help='volume id'),
        ],
    ),

    dict(
        name='delete-ss',
        help='Creates a snapshot',
        args=[
            dict(name="--id", help='snapshot id'),
            dict(name="--fs", help='file system id'),
        ],
    ),

    dict(
        name='replicate-volume',
        help='Replicates a volume',
        args=[
            dict(name="--id", help='Volume id to replicate'),
            dict(name="--name", help='New replicated volume name'),
            dict(name="--type", help=replicate_help,
                 choices=replicate_types),
        ],
        optional=[
            dict(name="--pool", help='pool id'),
        ],
    ),

    dict(
        name='access-grant',
        help='Grants access to an initiator to a volume',
        args=[
            dict(name="--id", help='initiator id'),
            dict(name="--type", help=initiator_id_help,
                 choices=initiator_id_types,
                 type=str.upper),
            dict(name="--volume", help='volume id'),
            dict(name="--access", help=access_help,
                 choices=access_types),
        ],
    ),

    dict(
        name='access-grant-group',
        help='Grants access to an access group to a volume',
        args=[
            dict(name="--id", help='access group id'),
            dict(name="--volume", help='volume id'),
            dict(name="--access", help=access_help,
                 choices=access_types),
        ],
    ),

    dict(
        name='access-revoke',
        help='Removes access for an initiator to a volume',
        args=[
            dict(name="--id", help='initiator id'),
            dict(name="--volume", help='volume id'),
        ],
    ),

    dict(
        name='access-revoke-group',
        help='Removes access for an access group to a volume',
        args=[
            dict(name="--id", help='group id'),
            dict(name="--volume", help='volume id'),
        ],
    ),

    dict(
        name='resize-volume',
        help='Resizes a volume',
        args=[
            dict(name="--id", help='volume id'),
            dict(name="--size", help=size_help),
        ],
    ),

    dict(
        name='resize-fs',
        help='Resizes a filesystem',
        args=[
            dict(name="--id", help='filesystem id'),
            dict(name="--size", help=size_help),
        ],
    ),

    dict(
        name='nfs-export-remove',
        help='Removes an NFS export',
        args=[
            dict(name="--id", help='nfs export id'),
        ],
    ),

    dict(
        name='nfs-export-fs',
        help='Creates an NFS export',
        args=[
            dict(name="--id", help='nfs export id'),
        ],
        optional=[
            dict(name="--exportpath", help="e.g. '/foo/bar'"),
            dict(name="--anonuid", help='uid to map to anonymous'),
            dict(name="--anongid", help='gid to map to anonymous'),
            dict(name="--auth-type", help='NFS client authentication type'),
            dict(name="--root", help='no root squash', action='append',
                 default=[]),
            dict(name="--ro", help='read-only', action='append', default=[]),
            dict(name="--rw", help='read-write', action='append', default=[]),
        ],
    ),

    dict(
        name='restore-ss',
        help='Restores a FS or specified files to '
             'previous snapshot state,',
        args=[
            dict(name="--id", help='snapshot id'),
            dict(name="--fs", help='file system'),
        ],
        optional=[
            dict(name="--file", help='file name', action='append',
                 default=[]),
            dict(name="--fileas", help='restore file name', action='append',
                 default=[]),
            dict(name="--all", help='exclusive option'),
        ],
    ),

    dict(
        name='job-status',
        help='Retrieve information about a job',
        args=[
            dict(name="--id", help='job status id'),
        ],
    ),

    dict(
        name='replicate-volume-range',
        help='Replicates a portion of a volume',
        args=[
            dict(name="--src", help='source volume id'),
            dict(name="--dest", help='destination volume id'),
            dict(name="--type", help=replicate_help,
                 choices=replicate_types),
            dict(name="--src_start", help='source block start number',
                 action='append'),
            dict(name="--dest_start", help='destination block start number',
                 action='append'),
            dict(name="--count", help='number of blocks to replicate',
                 action='append'),
        ],
    ),

    dict(
        name='replicate-volume-range-block-size',
        help='Size of each replicated block on a system in bytes',
        args=[
            dict(name="--id", help='system id'),
        ],
    ),

    dict(
        name='volume-dependants',
        help='Returns True if volume has a dependant child',
        args=[
            dict(name="--id", help='volume id'),
        ],
    ),

    dict(
        name='volume-dependants-rm',
        help='Removes dependencies',
        args=[
            dict(name="--id", help='volume id'),
        ],
    ),

    dict(
        name='fs-dependants',
        help='Returns True if a child dependency exists',
        args=[
            dict(name="--id", help='filesystem id'),
        ],
        optional=[
            dict(name="--file", help='For file check', action="append",
                 default=[]),
        ],
    ),

    dict(
        name='fs-dependants-rm',
        help='Removes dependencies',
        args=[
            dict(name="--id", help='filesystem id'),
        ],
        optional=[
            dict(name="--file",
                 help='File or files to remove dependencies for',
                 action='append',
                 default=[]),
        ],
    ),

    dict(
        name='create-pool',
        help='Creates a pool',
        args=[
            dict(name="--pool_id", help='pool id'),
            dict(name="--system_id", help='system id'),
        ],
        optional=[
            dict(name="--member-id",
                 help='Pool member ID, could be ID of Disk/Pool/Volume.'
                 ' This option is repeatable',
                 action='append'),
            dict(name="--member-type", help=member_help,
                 choices=member_types),
            dict(name="--raid-type", help=raid_help,
                 choices=raid_types,
                 type=str.upper),
            dict(name="--size", help=size_help),
            dict(name="--provisioning",
                 help=provision_help,
                 choices=provision_types),
            dict(name="--member-count",
                 help='Pool member count, 1 or greater'),
        ],
    ),

    dict(
        name='delete-pool',
        help='Delete a pool',
        args=[
            dict(name="--id", help='pool id'),
        ],
    ),
)


## Class that encapsulates the command line arguments for lsmcli
# Note: This class is used by lsmcli and any python plug-ins.
class CmdLine:
    """
    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 = common.getch()
            if pressed.upper() == 'Y':
                return True
            else:
                out('Operation aborted!')
                return False
        else:
            return True

    def _display_data_script_way(self, all_key_2_values, key_2_str, key_seq):
        """
        Display like iscsiadm do. Better for scripting.
        """
        key_column_width = 1
        value_column_width = 1
        key_sequence = []
        for key_name in key_seq:
            # Use suggested key sequence to sort
            if key_name not in all_key_2_values[0].keys():
                continue
            else:
                key_sequence.extend([key_name])

        for key_2_value in all_key_2_values:
            for key_name in key_sequence:
                # find the max column width of key
                cur_key_width = len(key_2_str[key_name])
                if cur_key_width > key_column_width:
                    key_column_width = cur_key_width
                # find the max column width of value
                cur_value = key_2_value[key_name]
                cur_value_width = 0
                if isinstance(cur_value, list):
                    if len(cur_value) == 0:
                        continue
                    cur_value_width = len(str(cur_value[0]))
                else:
                    cur_value_width = len(str(cur_value))
                if cur_value_width > value_column_width:
                    value_column_width = cur_value_width

        spliter = ' | '
        if self.args.sep is not None:
            spliter = self.args.sep
        row_format = '%%-%ds%s%%-%ds' % (key_column_width,
                                         spliter,
                                         value_column_width)
        sub_row_format = '%s%s%%-%ds' % (' ' * key_column_width,
                                         spliter,
                                         value_column_width)
        obj_spliter = '%s%s%s' % ('-' * key_column_width,
                                  '-' * len(spliter),
                                  '-' * value_column_width)

        for key_2_value in all_key_2_values:
            out(obj_spliter)
            for key_name in key_sequence:
                key_str = key_2_str[key_name]
                value = key_2_value[key_name]
                if isinstance(value, list):
                    flag_first_data = True
                    for sub_value in value:
                        if flag_first_data:
                            out(row_format % (key_str, str(sub_value)))
                            flag_first_data = False
                        else:
                            out(sub_row_format % str(sub_value))
                else:
                    out(row_format % (key_str, str(value)))
        out(obj_spliter)

    ##
    # Tries to make the output better when it varies considerably from
    # plug-in to plug-in.
    # @param    rows    Data, first row is header all other data.
    def display_data(self, objects):
        if len(objects) == 0:
            out("No data retrieved")
            return

        if  hasattr(self.args, 'optional') and self.args.optional:
            self.args.script = True

        # Assuming all objects are from the same class.
        key_2_str = objects[0].str_of_key()
        key_seq = objects[0].key_display_sequence()
        all_key_2_values = []
        for obj in objects:
            obj_key_2_value = obj.value_of_key(
                key_name=None,
                human=self.args.human,
                enum_as_number=self.args.enum,
                list_convert=False)
            for key in obj_key_2_value.keys():
                if obj_key_2_value[key] is None:
                    del obj_key_2_value[key]
            all_key_2_values.extend([obj_key_2_value])

        if self.args.script:
            self._display_data_script_way(all_key_2_values,
                                          key_2_str,
                                          key_seq)
        else:
            two_d_list = CmdLine._convert_to_two_d_list(all_key_2_values,
                                                        key_2_str,
                                                        key_seq)
            self._display_two_d_list(two_d_list)

    @staticmethod
    def _convert_to_two_d_list(all_key_2_values, key_2_str, key_seq):
        two_d_list = []
        key_sequence = []
        for key_name in key_seq:
            # Use suggested key sequence to sort
            if key_name in all_key_2_values[0].keys():
                key_sequence.extend([key_name])
        column_width = len(key_sequence)

        # find out column width
        row_width = 0
        for key_2_value in all_key_2_values:
            cur_max_wd = 0
            for key_name in key_2_value.keys():
                if isinstance(key_2_value[key_name], list):
                    cur_row_width = len(key_2_value[key_name])
                    if cur_row_width > cur_max_wd:
                        cur_max_wd = cur_row_width
                else:
                    pass
            if cur_max_wd == 0:
                cur_max_wd = 1
            row_width += cur_max_wd
        # one line for header
        row_width += 1
        # init 2D list
        for raw in range(0, row_width):
            new = []
            for column in range(0, column_width):
                new.append('')
            two_d_list.append(new)
        # header
        for index in range(0, len(key_sequence)):
            key_name = key_sequence[index]
            two_d_list[0][index] = key_2_str[key_name]

        current_row_num = 0
        for key_2_value in all_key_2_values:
            current_row_num += 1
            save_row_num = current_row_num
            for index in range(0, len(key_sequence)):
                key_name = key_sequence[index]
                value = key_2_value[key_name]
                if isinstance(value, list):
                    for sub_index in range(0, len(value)):
                        tmp_row_num = save_row_num + sub_index
                        two_d_list[tmp_row_num][index] = str(value[sub_index])

                    if save_row_num + len(value) > current_row_num:
                        current_row_num = save_row_num + len(value) - 1
                else:
                    two_d_list[save_row_num][index] = str(value)
        return two_d_list

    def _display_two_d_list(self, two_d_list):
        row_formats = []
        spliter_row_formats = []
        row_start = 0
        if self.args.sep is not None:
            row_start = 1
            if self.args.header:
                row_start = 0
        for column_index in range(0, len(two_d_list[0])):
            max_width = CmdLine._find_max_width(
                two_d_list,
                column_index,
                row_start)
            row_formats.extend(['%%-%ds' % max_width])
            spliter_row_formats.extend(['-' * max_width])
        row_format = ''
        if self.args.sep is not None:
            row_format = self.args.sep.join(row_formats)
        else:
            row_format = " | ".join(row_formats)
        for row_index in range(row_start, len(two_d_list)):
            out(row_format % tuple(two_d_list[row_index]))
            if row_index == 0:
                spliter = '-+-'.join(spliter_row_formats)
                out(spliter)

    @staticmethod
    def _find_max_width(two_d_list, column_index, row_start=0):
        max_width = 1
        for row_index in range(row_start, len(two_d_list)):
            row_data = two_d_list[row_index]
            if len(row_data[column_index]) > max_width:
                max_width = len(row_data[column_index])
        return max_width

    def display_available_plugins(self):
        d = []
        sep = '<}{>'
        plugins = client.Client.get_available_plugins(sep)

        for p in plugins:
            desc, version = p.split(sep)
            d.append(PlugData(desc, version))

        self.display_data(d)

    ## 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)

        parent_parser.add_argument(
            '-v', '--version', action='version',
            version="%s %s" % (sys.argv[0], VERSION))

        parent_parser.add_argument(
            '-u', '--uri', action="store", type=str,
            dest="uri", help='uniform resource identifier (env LSMCLI_URI)')

        parent_parser.add_argument(
            '-P', '--prompt', action="store_true", dest="prompt",
            help='prompt for password (env LSMCLI_PASSWORD)')

        parent_parser.add_argument(
            '-H', '--human', action="store_true", dest="human",
            help='print sizes in human readable format\n'
                 '(e.g., MiB, GiB, TiB)')

        parent_parser.add_argument(
            '-t', '--terse', action="store", dest="sep",
            help='print output in terse form with "SEP" '
                 'as a record separator')

        parent_parser.add_argument(
            '-e', '--enum', action="store_true", dest="enum", default=False,
            help='display enumerated types as numbers instead of text')

        parent_parser.add_argument(
            '-f', '--force', action="store_true", dest="force", default=False,
            help='bypass confirmation prompt for data loss operations')

        parent_parser.add_argument(
            '-w', '--wait', action="store", type=int, dest="wait",
            default=30000, help="command timeout value in ms (default = 30s)")

        parent_parser.add_argument(
            '--header', action="store_true", dest="header",
            help='include the header with terse')

        parent_parser.add_argument(
            '-b', action="store_true", dest="async", default=False,
            help='run the command async. instead of waiting for completion. '
                 'Command will exit(7) and job id written to stdout.')

        parent_parser.add_argument(
            '-s', '--script', action="store_true", dest="script",
            default=False, help='Displaying data in script friendly way.')

        parser = ArgumentParser(
            description='The libStorageMgmt command line interface.'
                        ' Run %(prog)s <command> -h for more on each command.',
            epilog='Copyright 2012-2013 Red Hat, Inc.\n'
                   'Please report bugs to '
                   '<<EMAIL>>\n',
            parents=[parent_parser])

        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])
            group = sub_parser.add_argument_group("required arguments")
            for arg in cmd.get('args', []):
                name = arg['name']
                del arg['name']
                group.add_argument(name, required=True, **arg)
            for arg in cmd.get('optional', []):
                flags = arg['name']
                del arg['name']
                if not isinstance(flags, tuple):
                    flags = (flags,)
                sub_parser.add_argument(*flags, **arg)

            sub_parser.set_defaults(
                func=getattr(self, cmd['name'].replace("-", "_")))<commit_msg>Differentiate subcommand-specific optional args from global optional args<commit_after># Copyright (C) 2012 Red Hat, Inc.
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
#
# Author: tasleson
import os
import sys
import getpass
import time

from argparse import ArgumentParser

import common
import client
import data
from version import VERSION
from data import Capabilities, PlugData


##@package lsm.cmdline


## Users are reporting errors with broken pipe when piping output
# to another program.  This appears to be related to this issue:
# http://bugs.python.org/issue11380
# Unable to reproduce, but hopefully this will address it.
# @param msg    The message to be written to stdout
def out(msg):
    try:
        sys.stdout.write(str(msg))
        sys.stdout.write("\n")
        sys.stdout.flush()
    except IOError:
        sys.exit(1)


## Wraps the invocation to the command line
# @param    client  Object to invoke calls on (optional)
def cmd_line_wrapper(c=None):
    """
    Common command line code, called.
    """
    try:
        cli = CmdLine()
        cli.process(c)
    except ArgError as ae:
        sys.stderr.write(str(ae))
        sys.stderr.flush()
        sys.exit(2)
    except common.LsmError as le:
        sys.stderr.write(str(le) + "\n")
        sys.stderr.flush()
        sys.exit(4)
    except KeyboardInterrupt:
        sys.exit(1)


## 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'):
    for i in l:
        if i.id == the_id:
            return i
    raise ArgError('%s with id %s not found!' % (friendly_name, the_id))


list_choices = ['VOLUMES', 'INITIATORS', 'POOLS', 'FS', 'SNAPSHOTS',
                'EXPORTS', "NFS_CLIENT_AUTH", 'ACCESS_GROUPS',
                'SYSTEMS', 'DISKS', 'PLUGINS']

initiator_id_types = ('WWPN', 'WWNN', 'ISCSI', 'HOSTNAME', 'SAS')
initiator_id_help = "initiator id type: " + ", ".join(initiator_id_types)

provision_types = ('DEFAULT', 'THIN', 'FULL')
provision_help = "provisioning type: " + ", ".join(provision_types)

access_types = ('RO', 'RW')
access_help = "access type: " + ", ".join(access_types)

replicate_types = ('SNAPSHOT', 'CLONE', 'COPY', 'MIRROR_ASYNC', 'MIRROR_SYNC')
replicate_help = "replication type: " + ", ".join(replicate_types)

size_help = 'size - Can use B, K, M, G, T, P postfix (IEC sizing)'

member_types = ('DISK', 'VOLUME', 'POOL')
member_help = "member type: " + ", ".join(member_types)

raid_types = ('JBOD', 'RAID1', 'RAID3', 'RAID4', 'RAID5', 'RAID6')
raid_help = "RAID type: " + ", ".join(raid_types)

cmds = (
    dict(
        name='list',
        help="List records of different types",
        args=[
            dict(name='--type',
                 help='List records of type: ' +
                 ", ".join(list_choices) +
                 '. SNAPSHOTS requires --fs <fs id>.\n' +
                 '      POOLS can have -o switch.',
                 choices=list_choices,
                 type=str.upper),
        ],
        optional=[
            dict(name=('-o', '--optional'),
                 help='Retrieve additional optional info if available',
                 default=False,
                 action='store_true'),
            dict(name=('--fs'),
                 help="File system id which is required when listing "
                      "SNAPSHOTS"),
        ],
    ),

    dict(
        name='delete-fs',
        help='Delete a filesystem',
        args=[
            dict(name="--fs_id", help='filesystem id'),
        ],
    ),

    dict(
        name='delete-access-group',
        help='Deletes an access group',
        args=[
            dict(name="--group_id", help='access group id'),
        ],
    ),

    dict(
        name='capabilities',
        help='Retrieves array capabilities',
        args=[
            dict(name="--system_id", help='system id'),
        ],
    ),

    dict(
        name='plugin-info',
        help='Retrieves plugin description and version',
    ),

    dict(
        name='create-volume',
        help='Creates a volume (logical unit)',
        args=[
            dict(name="--name", help='volume name'),
            dict(name="--size", help=size_help),
            dict(name="--pool", help='pool id'),
        ],
        optional=[
            dict(name="--provisioning", help=provision_help,
                 choices=provision_types),
        ],
    ),

    dict(
        name='create-fs',
        help='Creates a file system',
        args=[
            dict(name="--name", help='name of the file system'),
            dict(name="--size", help=size_help),
            dict(name="--pool", help='pool id'),
        ],
    ),

    dict(
        name='clone-fs',
        help='Creates a file system clone',
        args=[
            dict(name="--source_name", help='existing source file system id'),
            dict(name="--dest_name", help='new file system id'),
        ],
        optional=[
            dict(name="--type", help=provision_help,
                 choices=provision_types),
            dict(name="--backing-snapshot", help='backing snapshot id'),
        ],
    ),

    dict(
        name='create-access-group',
        help='Creates an access group',
        args=[
            dict(name="--name", help='new access group name'),
            dict(name="--id", help='initiator id'),
            dict(name="--type", help=initiator_id_help,
                 choices=initiator_id_types,
                 type=str.upper),
            dict(name="--system", help='system id'),
        ],
    ),

    dict(
        name='access-group-add',
        help='Adds an initiator to an access group',
        args=[
            dict(name="--gid", help='group id'),
            dict(name="--iid", help='initiator id'),
            dict(name="--type", help=initiator_id_help,
                 choices=initiator_id_types,
                 type=str.upper),
        ],
    ),

    dict(
        name='access-group-remove',
        help='Removes an initiator from an access group',
        args=[
            dict(name="--gid", help='group id'),
            dict(name="--iid", help='initiator id'),
        ],
    ),

    dict(
        name='access-group-volumes',
        help='Lists the volumes that the access group has'
             ' been granted access to',
        args=[
            dict(name="--gid", help='access group id'),
        ],
    ),

    dict(
        name='volume-access-group',
        help='Lists the access group(s) that have access'
             ' to volume',
        args=[
            dict(name="--vol_id", help='volume id'),
        ],
    ),

    dict(
        name='volumes-accessible-initiator',
        help='Lists the volumes that are accessible '
             'by the initiator',
        args=[
            dict(name="--iid", help='initiator id'),
        ],
    ),

    dict(
        name='initiators-granted-volume',
        help='Lists the initiators that have been '
             'granted access to specified volume',
        args=[
            dict(name="--vol_id", help='volume id'),
        ],
    ),

    dict(
        name='iscsi-chap',
        help='Configures ISCSI inbound/outbound CHAP authentication',
        args=[
            dict(name="--iid", help='initiator id'),
        ],
        optional=[
            dict(name="--in-user", help='inbound chap user name'),
            dict(name="--in-password", help='inbound chap password'),
            dict(name="--out-user", help='outbound chap user name'),
            dict(name="--out-password", help='outbound chap password'),
        ],
    ),

    dict(
        name='create-ss',
        help='Creates a snapshot',
        args=[
            dict(name="--name", help='snapshot name'),
            dict(name="--fs", help='file system id'),
        ],
        optional=[
            dict(name="--file",
                 help='default: all files. May use more than once.',
                 action='append', default=[]),
        ],
    ),

    dict(
        name='clone-file',
        help='Creates a clone of a file (thin provisioned)',
        args=[
            dict(name="--fs", help='file system'),
            dict(name="--src", help='source file to clone (relative path)'),
            dict(name="--dest", help='destination file (relative path)'),
        ],
        optional=[
            dict(name="--backing-snapshot", help='backing snapshot id'),
        ],
    ),

    dict(
        name='delete-volume',
        help='Deletes a volume given its id',
        args=[
            dict(name="--id", help='volume id'),
        ],
    ),

    dict(
        name='delete-ss',
        help='Creates a snapshot',
        args=[
            dict(name="--id", help='snapshot id'),
            dict(name="--fs", help='file system id'),
        ],
    ),

    dict(
        name='replicate-volume',
        help='Replicates a volume',
        args=[
            dict(name="--id", help='Volume id to replicate'),
            dict(name="--name", help='New replicated volume name'),
            dict(name="--type", help=replicate_help,
                 choices=replicate_types),
        ],
        optional=[
            dict(name="--pool", help='pool id'),
        ],
    ),

    dict(
        name='access-grant',
        help='Grants access to an initiator to a volume',
        args=[
            dict(name="--id", help='initiator id'),
            dict(name="--type", help=initiator_id_help,
                 choices=initiator_id_types,
                 type=str.upper),
            dict(name="--volume", help='volume id'),
            dict(name="--access", help=access_help,
                 choices=access_types),
        ],
    ),

    dict(
        name='access-grant-group',
        help='Grants access to an access group to a volume',
        args=[
            dict(name="--id", help='access group id'),
            dict(name="--volume", help='volume id'),
            dict(name="--access", help=access_help,
                 choices=access_types),
        ],
    ),

    dict(
        name='access-revoke',
        help='Removes access for an initiator to a volume',
        args=[
            dict(name="--id", help='initiator id'),
            dict(name="--volume", help='volume id'),
        ],
    ),

    dict(
        name='access-revoke-group',
        help='Removes access for an access group to a volume',
        args=[
            dict(name="--id", help='group id'),
            dict(name="--volume", help='volume id'),
        ],
    ),

    dict(
        name='resize-volume',
        help='Resizes a volume',
        args=[
            dict(name="--id", help='volume id'),
            dict(name="--size", help=size_help),
        ],
    ),

    dict(
        name='resize-fs',
        help='Resizes a filesystem',
        args=[
            dict(name="--id", help='filesystem id'),
            dict(name="--size", help=size_help),
        ],
    ),

    dict(
        name='nfs-export-remove',
        help='Removes an NFS export',
        args=[
            dict(name="--id", help='nfs export id'),
        ],
    ),

    dict(
        name='nfs-export-fs',
        help='Creates an NFS export',
        args=[
            dict(name="--id", help='nfs export id'),
        ],
        optional=[
            dict(name="--exportpath", help="e.g. '/foo/bar'"),
            dict(name="--anonuid", help='uid to map to anonymous'),
            dict(name="--anongid", help='gid to map to anonymous'),
            dict(name="--auth-type", help='NFS client authentication type'),
            dict(name="--root", help='no root squash', action='append',
                 default=[]),
            dict(name="--ro", help='read-only', action='append', default=[]),
            dict(name="--rw", help='read-write', action='append', default=[]),
        ],
    ),

    dict(
        name='restore-ss',
        help='Restores a FS or specified files to '
             'previous snapshot state,',
        args=[
            dict(name="--id", help='snapshot id'),
            dict(name="--fs", help='file system'),
        ],
        optional=[
            dict(name="--file", help='file name', action='append',
                 default=[]),
            dict(name="--fileas", help='restore file name', action='append',
                 default=[]),
            dict(name="--all", help='exclusive option'),
        ],
    ),

    dict(
        name='job-status',
        help='Retrieve information about a job',
        args=[
            dict(name="--id", help='job status id'),
        ],
    ),

    dict(
        name='replicate-volume-range',
        help='Replicates a portion of a volume',
        args=[
            dict(name="--src", help='source volume id'),
            dict(name="--dest", help='destination volume id'),
            dict(name="--type", help=replicate_help,
                 choices=replicate_types),
            dict(name="--src_start", help='source block start number',
                 action='append'),
            dict(name="--dest_start", help='destination block start number',
                 action='append'),
            dict(name="--count", help='number of blocks to replicate',
                 action='append'),
        ],
    ),

    dict(
        name='replicate-volume-range-block-size',
        help='Size of each replicated block on a system in bytes',
        args=[
            dict(name="--id", help='system id'),
        ],
    ),

    dict(
        name='volume-dependants',
        help='Returns True if volume has a dependant child',
        args=[
            dict(name="--id", help='volume id'),
        ],
    ),

    dict(
        name='volume-dependants-rm',
        help='Removes dependencies',
        args=[
            dict(name="--id", help='volume id'),
        ],
    ),

    dict(
        name='fs-dependants',
        help='Returns True if a child dependency exists',
        args=[
            dict(name="--id", help='filesystem id'),
        ],
        optional=[
            dict(name="--file", help='For file check', action="append",
                 default=[]),
        ],
    ),

    dict(
        name='fs-dependants-rm',
        help='Removes dependencies',
        args=[
            dict(name="--id", help='filesystem id'),
        ],
        optional=[
            dict(name="--file",
                 help='File or files to remove dependencies for',
                 action='append',
                 default=[]),
        ],
    ),

    dict(
        name='create-pool',
        help='Creates a pool',
        args=[
            dict(name="--pool_id", help='pool id'),
            dict(name="--system_id", help='system id'),
        ],
        optional=[
            dict(name="--member-id",
                 help='Pool member ID, could be ID of Disk/Pool/Volume.'
                 ' This option is repeatable',
                 action='append'),
            dict(name="--member-type", help=member_help,
                 choices=member_types),
            dict(name="--raid-type", help=raid_help,
                 choices=raid_types,
                 type=str.upper),
            dict(name="--size", help=size_help),
            dict(name="--provisioning",
                 help=provision_help,
                 choices=provision_types),
            dict(name="--member-count",
                 help='Pool member count, 1 or greater'),
        ],
    ),

    dict(
        name='delete-pool',
        help='Delete a pool',
        args=[
            dict(name="--id", help='pool id'),
        ],
    ),
)


## Class that encapsulates the command line arguments for lsmcli
# Note: This class is used by lsmcli and any python plug-ins.
class CmdLine:
    """
    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 = common.getch()
            if pressed.upper() == 'Y':
                return True
            else:
                out('Operation aborted!')
                return False
        else:
            return True

    def _display_data_script_way(self, all_key_2_values, key_2_str, key_seq):
        """
        Display like iscsiadm do. Better for scripting.
        """
        key_column_width = 1
        value_column_width = 1
        key_sequence = []
        for key_name in key_seq:
            # Use suggested key sequence to sort
            if key_name not in all_key_2_values[0].keys():
                continue
            else:
                key_sequence.extend([key_name])

        for key_2_value in all_key_2_values:
            for key_name in key_sequence:
                # find the max column width of key
                cur_key_width = len(key_2_str[key_name])
                if cur_key_width > key_column_width:
                    key_column_width = cur_key_width
                # find the max column width of value
                cur_value = key_2_value[key_name]
                cur_value_width = 0
                if isinstance(cur_value, list):
                    if len(cur_value) == 0:
                        continue
                    cur_value_width = len(str(cur_value[0]))
                else:
                    cur_value_width = len(str(cur_value))
                if cur_value_width > value_column_width:
                    value_column_width = cur_value_width

        spliter = ' | '
        if self.args.sep is not None:
            spliter = self.args.sep
        row_format = '%%-%ds%s%%-%ds' % (key_column_width,
                                         spliter,
                                         value_column_width)
        sub_row_format = '%s%s%%-%ds' % (' ' * key_column_width,
                                         spliter,
                                         value_column_width)
        obj_spliter = '%s%s%s' % ('-' * key_column_width,
                                  '-' * len(spliter),
                                  '-' * value_column_width)

        for key_2_value in all_key_2_values:
            out(obj_spliter)
            for key_name in key_sequence:
                key_str = key_2_str[key_name]
                value = key_2_value[key_name]
                if isinstance(value, list):
                    flag_first_data = True
                    for sub_value in value:
                        if flag_first_data:
                            out(row_format % (key_str, str(sub_value)))
                            flag_first_data = False
                        else:
                            out(sub_row_format % str(sub_value))
                else:
                    out(row_format % (key_str, str(value)))
        out(obj_spliter)

    ##
    # Tries to make the output better when it varies considerably from
    # plug-in to plug-in.
    # @param    rows    Data, first row is header all other data.
    def display_data(self, objects):
        if len(objects) == 0:
            out("No data retrieved")
            return

        if  hasattr(self.args, 'optional') and self.args.optional:
            self.args.script = True

        # Assuming all objects are from the same class.
        key_2_str = objects[0].str_of_key()
        key_seq = objects[0].key_display_sequence()
        all_key_2_values = []
        for obj in objects:
            obj_key_2_value = obj.value_of_key(
                key_name=None,
                human=self.args.human,
                enum_as_number=self.args.enum,
                list_convert=False)
            for key in obj_key_2_value.keys():
                if obj_key_2_value[key] is None:
                    del obj_key_2_value[key]
            all_key_2_values.extend([obj_key_2_value])

        if self.args.script:
            self._display_data_script_way(all_key_2_values,
                                          key_2_str,
                                          key_seq)
        else:
            two_d_list = CmdLine._convert_to_two_d_list(all_key_2_values,
                                                        key_2_str,
                                                        key_seq)
            self._display_two_d_list(two_d_list)

    @staticmethod
    def _convert_to_two_d_list(all_key_2_values, key_2_str, key_seq):
        two_d_list = []
        key_sequence = []
        for key_name in key_seq:
            # Use suggested key sequence to sort
            if key_name in all_key_2_values[0].keys():
                key_sequence.extend([key_name])
        column_width = len(key_sequence)

        # find out column width
        row_width = 0
        for key_2_value in all_key_2_values:
            cur_max_wd = 0
            for key_name in key_2_value.keys():
                if isinstance(key_2_value[key_name], list):
                    cur_row_width = len(key_2_value[key_name])
                    if cur_row_width > cur_max_wd:
                        cur_max_wd = cur_row_width
                else:
                    pass
            if cur_max_wd == 0:
                cur_max_wd = 1
            row_width += cur_max_wd
        # one line for header
        row_width += 1
        # init 2D list
        for raw in range(0, row_width):
            new = []
            for column in range(0, column_width):
                new.append('')
            two_d_list.append(new)
        # header
        for index in range(0, len(key_sequence)):
            key_name = key_sequence[index]
            two_d_list[0][index] = key_2_str[key_name]

        current_row_num = 0
        for key_2_value in all_key_2_values:
            current_row_num += 1
            save_row_num = current_row_num
            for index in range(0, len(key_sequence)):
                key_name = key_sequence[index]
                value = key_2_value[key_name]
                if isinstance(value, list):
                    for sub_index in range(0, len(value)):
                        tmp_row_num = save_row_num + sub_index
                        two_d_list[tmp_row_num][index] = str(value[sub_index])

                    if save_row_num + len(value) > current_row_num:
                        current_row_num = save_row_num + len(value) - 1
                else:
                    two_d_list[save_row_num][index] = str(value)
        return two_d_list

    def _display_two_d_list(self, two_d_list):
        row_formats = []
        spliter_row_formats = []
        row_start = 0
        if self.args.sep is not None:
            row_start = 1
            if self.args.header:
                row_start = 0
        for column_index in range(0, len(two_d_list[0])):
            max_width = CmdLine._find_max_width(
                two_d_list,
                column_index,
                row_start)
            row_formats.extend(['%%-%ds' % max_width])
            spliter_row_formats.extend(['-' * max_width])
        row_format = ''
        if self.args.sep is not None:
            row_format = self.args.sep.join(row_formats)
        else:
            row_format = " | ".join(row_formats)
        for row_index in range(row_start, len(two_d_list)):
            out(row_format % tuple(two_d_list[row_index]))
            if row_index == 0:
                spliter = '-+-'.join(spliter_row_formats)
                out(spliter)

    @staticmethod
    def _find_max_width(two_d_list, column_index, row_start=0):
        max_width = 1
        for row_index in range(row_start, len(two_d_list)):
            row_data = two_d_list[row_index]
            if len(row_data[column_index]) > max_width:
                max_width = len(row_data[column_index])
        return max_width

    def display_available_plugins(self):
        d = []
        sep = '<}{>'
        plugins = client.Client.get_available_plugins(sep)

        for p in plugins:
            desc, version = p.split(sep)
            d.append(PlugData(desc, version))

        self.display_data(d)

    ## 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)

        parent_parser.add_argument(
            '-v', '--version', action='version',
            version="%s %s" % (sys.argv[0], VERSION))

        parent_parser.add_argument(
            '-u', '--uri', action="store", type=str,
            dest="uri", help='uniform resource identifier (env LSMCLI_URI)')

        parent_parser.add_argument(
            '-P', '--prompt', action="store_true", dest="prompt",
            help='prompt for password (env LSMCLI_PASSWORD)')

        parent_parser.add_argument(
            '-H', '--human', action="store_true", dest="human",
            help='print sizes in human readable format\n'
                 '(e.g., MiB, GiB, TiB)')

        parent_parser.add_argument(
            '-t', '--terse', action="store", dest="sep",
            help='print output in terse form with "SEP" '
                 'as a record separator')

        parent_parser.add_argument(
            '-e', '--enum', action="store_true", dest="enum", default=False,
            help='display enumerated types as numbers instead of text')

        parent_parser.add_argument(
            '-f', '--force', action="store_true", dest="force", default=False,
            help='bypass confirmation prompt for data loss operations')

        parent_parser.add_argument(
            '-w', '--wait', action="store", type=int, dest="wait",
            default=30000, help="command timeout value in ms (default = 30s)")

        parent_parser.add_argument(
            '--header', action="store_true", dest="header",
            help='include the header with terse')

        parent_parser.add_argument(
            '-b', action="store_true", dest="async", default=False,
            help='run the command async. instead of waiting for completion. '
                 'Command will exit(7) and job id written to stdout.')

        parent_parser.add_argument(
            '-s', '--script', action="store_true", dest="script",
            default=False, help='Displaying data in script friendly way.')

        parser = ArgumentParser(
            description='The libStorageMgmt command line interface.'
                        ' Run %(prog)s <command> -h for more on each command.',
            epilog='Copyright 2012-2013 Red Hat, Inc.\n'
                   'Please report bugs to '
                   '<<EMAIL>>\n',
            parents=[parent_parser])

        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])

            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("-", "_")))
{'fraction_non_alphanumeric': 0.12591036655704962, 'fraction_numerical': 0.004063290921299564, 'mean_word_length': 1.7158888992799028, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 40, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '872935', 'n_tokens_mistral': 16641, 'n_tokens_neox': 16119, 'n_words': 4042}