def __init__(self):
        self._all_commands = None

        self._controller_commands = ControllerCommands()
        self._node_commands = NodeCommands()
        self._storage_pool_dfn_commands = StoragePoolDefinitionCommands()
        self._storage_pool_commands = StoragePoolCommands()
        self._resource_dfn_commands = ResourceDefinitionCommands()
        self._volume_dfn_commands = VolumeDefinitionCommands()
        self._resource_commands = ResourceCommands()
        self._snapshot_commands = SnapshotCommands()
        self._misc_commands = MiscCommands()
        self._zsh_generator = None
        self._parser = self.setup_parser()
        self._all_commands = self.parser_cmds(self._parser)
        self._linstorapi = None
    def __init__(self):
        self._state_service = StateService(self)
        self._all_commands = None

        self._controller_commands = ControllerCommands()
        self._node_commands = NodeCommands()
        self._storage_pool_commands = StoragePoolCommands()
        self._resource_dfn_commands = ResourceDefinitionCommands()
        self._resource_grp_commands = ResourceGroupCommands()
        self._volume_grp_commands = VolumeGroupCommands()
        self._volume_dfn_commands = VolumeDefinitionCommands()
        self._resource_commands = ResourceCommands(self._state_service)
        self._resource_conn_commands = ResourceConnectionCommands()
        self._volume_commands = VolumeCommands()
        self._snapshot_commands = SnapshotCommands()
        self._drbd_proxy_commands = DrbdProxyCommands()
        self._misc_commands = MiscCommands()
        self._physical_storage_commands = PhysicalStorageCommands()
        self._zsh_generator = None
        self._parser = self.setup_parser()
        self._all_commands = self.parser_cmds(self._parser)
        self._linstorapi = None  # type: linstor.Linstor
Example #3
0
    def __init__(self):
        self._state_service = StateService(self)
        self._all_commands = None

        self._controller_commands = ControllerCommands()
        self._node_commands = NodeCommands()
        self._storage_pool_commands = StoragePoolCommands()
        self._resource_dfn_commands = ResourceDefinitionCommands()
        self._resource_grp_commands = ResourceGroupCommands()
        self._volume_grp_commands = VolumeGroupCommands()
        self._volume_dfn_commands = VolumeDefinitionCommands()
        self._resource_commands = ResourceCommands(self._state_service)
        self._resource_conn_commands = ResourceConnectionCommands()
        self._volume_commands = VolumeCommands()
        self._snapshot_commands = SnapshotCommands()
        self._drbd_proxy_commands = DrbdProxyCommands()
        self._misc_commands = MiscCommands()
        self._physical_storage_commands = PhysicalStorageCommands()
        self._error_report_commands = ErrorReportCommands()
        self._exos_commands = ExosCommands()
        self._advise_commands = AdviceCommands()
        self._backup_commands = BackupCommands()
        self._remote_commands = RemoteCommands()
        self._file_commands = FileCommands()

        self._command_list = [
            self._controller_commands, self._node_commands,
            self._resource_dfn_commands, self._resource_grp_commands,
            self._volume_grp_commands, self._resource_commands,
            self._resource_conn_commands, self._volume_commands,
            self._snapshot_commands, self._drbd_proxy_commands,
            self._storage_pool_commands, self._volume_dfn_commands,
            self._physical_storage_commands, self._error_report_commands,
            self._exos_commands, self._advise_commands, self._backup_commands,
            self._remote_commands, self._file_commands, self._misc_commands
        ]

        self._zsh_generator = None
        self._parser = self.setup_parser()
        self._all_commands = self.parser_cmds(self._parser)
        self._linstorapi = None  # type: Optional[linstor.Linstor]
class LinStorCLI(object):
    """
    linstor command line client
    """

    readline_history_file = "~/.config/linstor/client.history"

    def __init__(self):
        self._state_service = StateService(self)
        self._all_commands = None

        self._controller_commands = ControllerCommands()
        self._node_commands = NodeCommands()
        self._storage_pool_commands = StoragePoolCommands()
        self._resource_dfn_commands = ResourceDefinitionCommands()
        self._resource_grp_commands = ResourceGroupCommands()
        self._volume_grp_commands = VolumeGroupCommands()
        self._volume_dfn_commands = VolumeDefinitionCommands()
        self._resource_commands = ResourceCommands(self._state_service)
        self._resource_conn_commands = ResourceConnectionCommands()
        self._volume_commands = VolumeCommands()
        self._snapshot_commands = SnapshotCommands()
        self._drbd_proxy_commands = DrbdProxyCommands()
        self._misc_commands = MiscCommands()
        self._physical_storage_commands = PhysicalStorageCommands()
        self._zsh_generator = None
        self._parser = self.setup_parser()
        self._all_commands = self.parser_cmds(self._parser)
        self._linstorapi = None  # type: linstor.Linstor

    def setup_parser(self):
        parser = argparse.ArgumentParser(prog="linstor")
        """
        ATTENTION! ATTENTION!
        If you add a new global option here, don't forget to update:
        utils.py:filter_new_args
        otherwise drbd options will fail!
        ATTENTION OVER! ATTENTION OVER!
        """
        parser.add_argument('--version',
                            '-v',
                            action='version',
                            version='%(prog)s ' + VERSION + '; ' + GITHASH)
        parser.add_argument(
            '--no-color',
            action="store_true",
            help=
            'Do not use colors in output. Useful for old terminals/scripting.')
        parser.add_argument(
            '--no-utf8',
            action="store_true",
            default=not sys.stdout.isatty(),
            help='Do not use utf-8 characters in output (i.e., tables).')
        parser.add_argument(
            '--warn-as-error',
            action="store_true",
            help='Treat WARN return code as error (i.e., return code > 0).')
        parser.add_argument(
            '--curl',
            action="store_true",
            help=
            "Do not execute the action, only output a curl equivalent command."
        )
        parser.add_argument(
            '--controllers',
            default='localhost:%d' % linstor.Linstor.REST_PORT,
            help=
            'Comma separated list of controllers (e.g.: "host1:port,host2:port"). '
            'If the environment variable %s is set, '
            'the ones set via this argument get appended.' %
            KEY_LS_CONTROLLERS)
        parser.add_argument('-m', '--machine-readable', action="store_true")
        parser.add_argument(
            '--output-version',
            choices=['v0', 'v1'],
            default=os.environ.get(ENV_OUTPUT_VERSION, "v0"),
            help="Machine readable output format, default 'v0'. "
            "Can also be set via environment variable '{env}'".format(
                env=ENV_OUTPUT_VERSION))
        parser.add_argument('--verbose', '-V', action='store_true')
        parser.add_argument(
            '-t',
            '--timeout',
            default=300,
            type=int,
            help="Connection/Command timeout value in seconds.")
        parser.add_argument(
            '--disable-config',
            action="store_true",
            help="Disable config loading and only use commandline arguments.")
        parser.add_argument('--user', '-u', help="Linstor username to use")
        parser.add_argument('--password', '-P', help="Linstor user password")
        parser.add_argument('--certfile', help="SSL certificate file")
        parser.add_argument('--keyfile', help="SSL key file")
        parser.add_argument('--cafile', help="SSL CA certificate file")
        parser.add_argument('--allow-insecure-auth',
                            action='store_true',
                            help="Allow password authentication with HTTP")

        subp = parser.add_subparsers(
            title='subcommands',
            description='valid subcommands',
            help='Use the list command to print a '
            'nicer looking overview of all valid commands')

        # interactive mode
        parser_ia = subp.add_parser(Commands.INTERACTIVE,
                                    description='Start interactive mode')
        parser_ia.set_defaults(func=self.cmd_interactive)

        # help
        p_help = subp.add_parser(Commands.HELP,
                                 description='Print help for a command')
        p_help.add_argument('command', nargs='*')
        p_help.set_defaults(func=self.cmd_help, always_allowed=True)

        # list
        p_list = subp.add_parser(Commands.LIST_COMMANDS,
                                 aliases=['commands', 'list'],
                                 description='List available commands')
        p_list.add_argument('-t',
                            '--tree',
                            action="store_true",
                            help="Print a tree view of all commands.")
        p_list.set_defaults(func=self.cmd_list, always_allowed=True)

        # exit
        p_exit = subp.add_parser(Commands.EXIT,
                                 aliases=['quit'],
                                 description='Only useful in interactive mode')
        p_exit.set_defaults(func=self.cmd_exit, always_allowed=True)

        # controller commands
        self._controller_commands.setup_commands(subp)

        # add all node commands
        self._node_commands.setup_commands(subp)

        # new-resource definition
        self._resource_dfn_commands.setup_commands(subp)

        self._resource_grp_commands.setup_commands(subp)
        self._volume_grp_commands.setup_commands(subp)

        # add all resource commands
        self._resource_commands.setup_commands(subp)

        self._resource_conn_commands.setup_commands(subp)

        self._volume_commands.setup_commands(subp)

        # add all snapshot commands
        self._snapshot_commands.setup_commands(subp)

        # add all DRBD proxy commands
        self._drbd_proxy_commands.setup_commands(subp)

        # add all storage pools commands
        self._storage_pool_commands.setup_commands(subp)

        # add all volume definition commands
        self._volume_dfn_commands.setup_commands(subp)

        self._physical_storage_commands.setup_commands(subp)

        # misc commands
        self._misc_commands.setup_commands(subp)

        # dm-migrate
        c_dmmigrate = subp.add_parser(
            Commands.DMMIGRATE,
            description='Generate a migration script from drbdmanage to linstor'
        )
        c_dmmigrate.add_argument(
            'ctrlvol',
            help='json dump generated by "drbdmanage export-ctrlvol"')
        c_dmmigrate.add_argument(
            'script', help='file name of the generated migration shell script')
        c_dmmigrate.set_defaults(func=MigrateCommands.cmd_dmmigrate)

        # zsh completer
        self._zsh_generator = ZshGenerator(subp)
        zsh_compl = subp.add_parser(
            Commands.GEN_ZSH_COMPLETER,
            description='Generate a zsh completion script')
        zsh_compl.set_defaults(func=self._zsh_generator.cmd_completer)

        argcomplete.autocomplete(parser)

        subp.metavar = "{%s}" % ", ".join(sorted(Commands.MainList))

        return parser

    @staticmethod
    def merge_config_arguments(pargs):
        global_entries = linstor.Config.get_section('global')
        for key, val in global_entries.items():
            pargs.insert(0, "--" + key)
            if val:
                pargs.insert(1, val)
        return pargs

    def parse(self, pargs):
        # read global options from config file
        if '--disable-config' not in pargs:
            pargs = LinStorCLI.merge_config_arguments(pargs)
        # very basic way to default into interactive if no options or commands are specified
        # only python 3.4+ argparse supports default subparsers
        if not pargs:
            pargs.append("interactive")
        return self._parser.parse_args(pargs)

    @classmethod
    def _report_linstor_error(cls, le):
        sys.stderr.write("Error: " + le.message + '\n')
        for err in le.all_errors():
            sys.stderr.write(' ' * 2 + err.message + '\n')

    def parse_and_execute(self, pargs, is_interactive=False):
        rc = ExitCode.OK
        try:
            args = self.parse(pargs)

            local_only_cmds = [
                self.cmd_list, MigrateCommands.cmd_dmmigrate,
                self._zsh_generator.cmd_completer, self.cmd_help
            ]

            # only connect if not already connected or a local only command was executed
            conn_errors = []
            contrl_list = linstor.MultiLinstor.controller_uri_list(
                os.environ.get(KEY_LS_CONTROLLERS, "") + ',' +
                args.controllers)
            if self._linstorapi is None and args.func not in local_only_cmds:
                username = None
                password = None
                if args.user:
                    username = args.user
                    if args.password:
                        password = args.password
                    else:
                        password = getpass.getpass("Enter Linstor password:"******"Connected to {h}".format(
                        h=self._linstorapi.controller_host()))
                current_state = self._state_service.get_state()
                allowed_states = vars(args).get('allowed_states',
                                                [DefaultState])
                always_allowed = vars(args).get('always_allowed', False)
                if always_allowed or current_state.__class__ in allowed_states:
                    rc = args.func(args)
                else:
                    sys.stderr.write(
                        "Error: Command not allowed in state '{state.name}'\n".
                        format(state=current_state))
                    rc = ExitCode.ILLEGAL_STATE
        except (ArgumentError, argparse.ArgumentTypeError,
                linstor.LinstorArgumentError) as ae:
            try:
                self.parse(
                    list(
                        itertools.takewhile(lambda x: not x.startswith('-'),
                                            pargs)) + ['-h'])
            except SystemExit:
                pass
            sys.stderr.write(ae.message + '\n')
            return ExitCode.ARGPARSE_ERROR
        except utils.LinstorClientError as lce:
            sys.stderr.write(lce.message + '\n')
            return lce.exit_code
        except linstor.LinstorNetworkError as le:
            self._report_linstor_error(le)
            rc = ExitCode.CONNECTION_ERROR
        except linstor.LinstorTimeoutError as le:
            self._report_linstor_error(le)
            rc = ExitCode.CONNECTION_TIMEOUT
        except linstor.LinstorError as le:
            self._report_linstor_error(le)
            rc = ExitCode.UNKNOWN_ERROR
        finally:
            if self._linstorapi and not is_interactive:
                self._linstorapi.disconnect()

        return rc

    @staticmethod
    def parser_cmds(parser):
        # AFAIK there is no other way to get the subcommands out of argparse.
        # This avoids at least to manually keep track of subcommands

        cmds = dict()
        subparsers_actions = [
            action for action in parser._actions
            if isinstance(action, argparse._SubParsersAction)
        ]
        for subparsers_action in subparsers_actions:
            for choice, subparser in subparsers_action.choices.items():
                parser_hash = subparser.__hash__
                if parser_hash not in cmds:
                    cmds[parser_hash] = list()
                cmds[parser_hash].append(choice)

        # sort subcommands and their aliases,
        # subcommand dictates sortorder, not its alias (assuming alias is
        # shorter than the subcommand itself)
        cmds_sorted = [
            sorted(cmd, key=len, reverse=True) for cmd in cmds.values()
        ]

        # "add" and "new" have the same length (as well as "delete" and
        # "remove), therefore prefer one of them to group commands for the
        # "list" command
        for cmds in cmds_sorted:
            idx = 0
            found = False
            for idx, cmd in enumerate(cmds):
                if cmd.startswith("create-") or cmd.startswith("delete-"):
                    found = True
                    break
            if found:
                cmds.insert(0, cmds.pop(idx))

        # sort subcommands themselves
        cmds_sorted.sort(key=lambda a: a[0])
        return cmds_sorted

    def parser_cmds_description(self, all_commands):
        toplevel = [top[0] for top in all_commands]

        subparsers_actions = [
            action for action in self._parser._actions
            if isinstance(action, argparse._SubParsersAction)
        ]
        description = {}
        for subparsers_action in subparsers_actions:
            for choice, subparser in subparsers_action.choices.items():
                if choice in toplevel:
                    description[choice] = subparser.description

        return description

    def check_parser_commands(self):

        parser_cmds = LinStorCLI.parser_cmds(self._parser)
        for cmd in parser_cmds:
            mcos = [x for x in cmd if x in Commands.MainList + Commands.Hidden]
            if len(mcos) != 1:
                raise AssertionError("no main command found for group: " +
                                     str(cmd))

        all_cmds = [y for x in parser_cmds for y in x]
        for cmd in Commands.MainList + Commands.Hidden:
            if cmd not in all_cmds:
                raise AssertionError("defined command not used in argparse: " +
                                     str(cmd))

        return True

    @staticmethod
    def get_commands(parser, with_aliases=True):
        cmds = []
        for cmd in LinStorCLI.parser_cmds(parser):
            cmds.append(cmd[0])
            if with_aliases:
                for al in cmd[1:]:
                    cmds.append(al)
        return cmds

    @staticmethod
    def get_command_aliases(all_commands, cmd):
        return [
            x for subx in all_commands if cmd in subx for x in subx
            if cmd not in x
        ]

    @staticmethod
    def gen_cmd_tree(subp):
        cmd_map = {}
        for cmd in subp._name_parser_map:
            argparse_cmd = subp._name_parser_map[cmd]
            new_subp = argparse_cmd._actions[-1]
            if isinstance(new_subp, argparse._SubParsersAction):
                if argparse_cmd.prog in cmd_map:
                    cmd_map[argparse_cmd.prog] =\
                        (cmd_map[argparse_cmd.prog][0] + [cmd], LinStorCLI.gen_cmd_tree(new_subp))
                else:
                    cmd_map[argparse_cmd.prog] = ([
                        cmd
                    ], LinStorCLI.gen_cmd_tree(new_subp))
            else:
                if argparse_cmd.prog in cmd_map:
                    cmd_map[argparse_cmd.prog] = (
                        cmd_map[argparse_cmd.prog][0] + [cmd], {})
                else:
                    cmd_map[argparse_cmd.prog] = ([cmd], {})

        return cmd_map

    @staticmethod
    def print_cmd_tree(entry, indent=0):
        for fullcmd in sorted(entry.keys()):
            cmd = fullcmd[fullcmd.rindex(' '):].strip()
            aliases, sub_cmds = entry[fullcmd]
            p_str = cmd
            if len(aliases) > 1:
                p_str += " ({al})".format(al=sorted(aliases, key=len)[0])
            print(" " * indent + "- " + p_str)
            LinStorCLI.print_cmd_tree(sub_cmds, indent + 2)

    def cmd_list(self, args):
        return self.print_cmds(args.tree)

    def print_cmds(self, tree=False):
        sys.stdout.write(
            'Use "help <command>" to get help for a specific command.\n\n')
        sys.stdout.write('Available commands:\n')
        # import pprint
        # pp = pprint.PrettyPrinter()
        # pp.pprint(self._all_commands)

        if tree:
            subp = self._parser._actions[-1]
            assert (isinstance(subp, argparse._SubParsersAction))
            cmd_map = LinStorCLI.gen_cmd_tree(subp)
            LinStorCLI.print_cmd_tree({
                k: v
                for k, v in cmd_map.items()
                if k[k.rindex(' '):].strip() in Commands.MainList
            })
        else:
            for cmd in sorted(Commands.MainList):
                sys.stdout.write("- " + cmd)
                aliases = LinStorCLI.get_command_aliases(
                    self._all_commands, cmd)
                if aliases:
                    sys.stdout.write(" (%s)" % (", ".join(aliases)))
                sys.stdout.write("\n")

        return 0

    def cmd_interactive(self, args):
        if self._state_service.has_state():
            sys.stderr.write(
                "The client is already running in interactive mode\n")
        else:
            self.print_cmds()
            sys.stdout.write("\n")
            self._state_service.enter_state(DefaultState(),
                                            verbose=args.verbose)

    def run_interactive(self, verbose):
        all_cmds = [i for sl in self._all_commands for i in sl]

        # helper function
        def unknown(cmd):
            sys.stdout.write("\n" + "Command \"%s\" not known!\n" % cmd)
            self.print_cmds()

        # helper function
        def parsecatch(cmds_):
            rc = ExitCode.OK

            # remove linstor if cmd started with it
            if cmds_ and cmds_[0] == 'linstor':
                cmds_ = cmds_[1:]

            try:
                cmds_clone = list(cmds_)
                rc = self.parse_and_execute(cmds_, is_interactive=True)
            except SystemExit as se:
                cmd = cmds_clone[0]
                if cmd in [Commands.EXIT, "quit"]:
                    sys.exit(ExitCode.OK)
                elif cmd == "help":
                    if len(cmds_clone) == 1:
                        self.print_cmds()
                        return
                    else:
                        cmd = " ".join(cmds_clone[1:])
                        if cmd not in all_cmds:
                            unknown(cmd)
                elif cmd in all_cmds:
                    if '-h' in cmds_clone or '--help' in cmds:
                        return
                    if se.code == ExitCode.ARGPARSE_ERROR:
                        sys.stderr.write(
                            "\nIncorrect syntax. Use 'help {cmd}' for more information:\n"
                            .format(cmd=cmd))
                        rc = ExitCode.ARGPARSE_ERROR
                else:
                    unknown(cmd)
                    rc = ExitCode.ARGPARSE_ERROR
            except KeyboardInterrupt:
                pass
            except BaseException:
                traceback.print_exc(file=sys.stderr)

            if rc == ExitCode.CONNECTION_ERROR:
                sys.exit(rc)

            return rc

        # try to load readline
        # if loaded, raw_input makes use of it
        if sys.version_info < (3, ):
            my_input = raw_input
        else:
            my_input = input

        abs_readline_hist_path = None
        try:
            import readline
            # seems after importing readline it is not possible to output to sys.stderr
            completer = argcomplete.CompletionFinder(self._parser)
            readline.set_completer_delims("")
            readline.set_completer(completer.rl_complete)
            readline.parse_and_bind("tab: complete")
            abs_readline_hist_path = os.path.expanduser(
                self.readline_history_file)
            if os.path.exists(abs_readline_hist_path):
                readline.read_history_file(abs_readline_hist_path)
        except ImportError:
            pass

        last_rc = ExitCode.OK
        while self._state_service.has_state():
            try:
                cmds = my_input('{state.prompt}{h} ==> '.format(
                    state=self._state_service.get_state(),
                    h='(' + self._linstorapi.controller_host() +
                    ')' if verbose else "")).strip()

                cmds = [cmd.strip() for cmd in cmds.split()]
                if not cmds:
                    self.print_cmds()
                else:
                    last_rc = parsecatch(cmds)

                if last_rc != ExitCode.OK:
                    while self._state_service.has_state(
                    ) and self._state_service.get_state().terminate_on_error:
                        self._state_service.pop_state()
            except EOFError:  # raised by ctrl-d
                self._state_service.pop_state()
            except KeyboardInterrupt:  # raised by ctrl-c
                self._state_service.clear_state()
            sys.stdout.write("\n")

        if abs_readline_hist_path:
            try:
                os.makedirs(os.path.dirname(abs_readline_hist_path))
            except OSError:
                pass
            readline.write_history_file(abs_readline_hist_path)

        return last_rc

    def cmd_help(self, args):
        return self.parse_and_execute(args.command + ["-h"])

    def cmd_exit(self, _):
        sys.exit(ExitCode.OK)

    def run(self):
        # TODO(rck): try/except
        rc = self.parse_and_execute(sys.argv[1:])
        sys.exit(rc)

    def user_confirm(self, question):
        """
        Ask yes/no questions. Requires the user to answer either "yes" or "no".
        If the input stream closes, it defaults to "no".
        returns: True for "yes", False for "no"
        """
        sys.stdout.write(question + "\n")
        sys.stdout.write("  yes/no: ")
        sys.stdout.flush()
        fn_rc = False
        while True:
            answer = sys.stdin.readline()
            if len(answer) != 0:
                if answer.endswith("\n"):
                    answer = answer[:len(answer) - 1]
                if answer.lower() == "yes":
                    fn_rc = True
                    break
                elif answer.lower() == "no":
                    break
                else:
                    sys.stdout.write("Please answer \"yes\" or \"no\": ")
                    sys.stdout.flush()
            else:
                # end of stream, no more input
                sys.stdout.write("\n")
                break
        return fn_rc
class LinStorCLI(object):
    """
    linstor command line client
    """

    interactive = False

    def __init__(self):
        self._all_commands = None

        self._controller_commands = ControllerCommands()
        self._node_commands = NodeCommands()
        self._storage_pool_dfn_commands = StoragePoolDefinitionCommands()
        self._storage_pool_commands = StoragePoolCommands()
        self._resource_dfn_commands = ResourceDefinitionCommands()
        self._volume_dfn_commands = VolumeDefinitionCommands()
        self._resource_commands = ResourceCommands()
        self._snapshot_commands = SnapshotCommands()
        self._misc_commands = MiscCommands()
        self._zsh_generator = None
        self._parser = self.setup_parser()
        self._all_commands = self.parser_cmds(self._parser)
        self._linstorapi = None

    def setup_parser(self):
        parser = argparse.ArgumentParser(prog="linstor")
        parser.add_argument('--version',
                            '-v',
                            action='version',
                            version='%(prog)s ' + VERSION + '; ' + GITHASH)
        parser.add_argument(
            '--no-color',
            action="store_true",
            help=
            'Do not use colors in output. Useful for old terminals/scripting.')
        parser.add_argument(
            '--no-utf8',
            action="store_true",
            default=not sys.stdout.isatty(),
            help='Do not use utf-8 characters in output (i.e., tables).')
        parser.add_argument(
            '--warn-as-error',
            action="store_true",
            help='Treat WARN return code as error (i.e., return code > 0).')
        parser.add_argument(
            '--controllers',
            default='localhost:%d' % sharedconsts.DFLT_CTRL_PORT_PLAIN,
            help=
            'Comma separated list of controllers (e.g.: "host1:port,host2:port"). '
            'If the environment variable %s is set, '
            'the ones set via this argument get appended.' %
            KEY_LS_CONTROLLERS)
        parser.add_argument('-m', '--machine-readable', action="store_true")
        parser.add_argument('-t',
                            '--timeout',
                            default=300,
                            type=int,
                            help="Connection timeout value.")
        parser.add_argument(
            '--disable-config',
            action="store_true",
            help="Disable config loading and only use commandline arguments.")

        subp = parser.add_subparsers(
            title='subcommands',
            description='valid subcommands',
            help='Use the list command to print a '
            'nicer looking overview of all valid commands')

        # interactive mode
        parser_ia = subp.add_parser(Commands.INTERACTIVE,
                                    description='Start interactive mode')
        parser_ia.set_defaults(func=self.cmd_interactive)

        # help
        p_help = subp.add_parser(Commands.HELP,
                                 description='Print help for a command')
        p_help.add_argument('command', nargs='*')
        p_help.set_defaults(func=self.cmd_help)

        # list
        p_list = subp.add_parser(Commands.LIST_COMMANDS,
                                 aliases=['commands', 'list'],
                                 description='List available commands')
        p_list.add_argument('-t',
                            '--tree',
                            action="store_true",
                            help="Print a tree view of all commands.")
        p_list.set_defaults(func=self.cmd_list)

        # exit
        p_exit = subp.add_parser(Commands.EXIT,
                                 aliases=['quit'],
                                 description='Only useful in interactive mode')
        p_exit.set_defaults(func=self.cmd_exit)

        # controller commands
        self._controller_commands.setup_commands(subp)

        # add all node commands
        self._node_commands.setup_commands(subp)

        # new-resource definition
        self._resource_dfn_commands.setup_commands(subp)

        # add all resource commands
        self._resource_commands.setup_commands(subp)

        # add all snapshot commands
        self._snapshot_commands.setup_commands(subp)

        # add all storage pool definition commands
        self._storage_pool_dfn_commands.setup_commands(subp)

        # add all storage pools commands
        self._storage_pool_commands.setup_commands(subp)

        # add all volume definition commands
        self._volume_dfn_commands.setup_commands(subp)

        # misc commands
        self._misc_commands.setup_commands(subp)

        # dm-migrate
        c_dmmigrate = subp.add_parser(
            Commands.DMMIGRATE,
            description='Generate a migration script from drbdmanage to linstor'
        )
        c_dmmigrate.add_argument(
            'ctrlvol',
            help='json dump generated by "drbdmanage export-ctrlvol"')
        c_dmmigrate.add_argument(
            'script', help='file name of the generated migration shell script')
        c_dmmigrate.set_defaults(func=MigrateCommands.cmd_dmmigrate)

        # zsh completer
        self._zsh_generator = ZshGenerator(subp)
        zsh_compl = subp.add_parser(
            Commands.GEN_ZSH_COMPLETER,
            description='Generate a zsh completion script')
        zsh_compl.set_defaults(func=self._zsh_generator.cmd_completer)

        argcomplete.autocomplete(parser)

        subp.metavar = "{%s}" % ", ".join(sorted(Commands.MainList))

        return parser

    @staticmethod
    def read_config(config_file):
        cp = configparser.SafeConfigParser()
        cp.read(config_file)
        config = {}
        for section in cp.sections():
            config[section] = cp.items(section)
        return config

    @staticmethod
    def merge_config_arguments(pargs):
        home_dir = os.path.expanduser("~")
        config_file_name = "linstor-client.conf"
        user_conf = os.path.join(home_dir, ".config", "linstor",
                                 config_file_name)
        sys_conf = os.path.join('/etc', 'linstor', config_file_name)

        entries = None
        if os.path.exists(user_conf):
            entries = LinStorCLI.read_config(user_conf)
        elif os.path.exists(sys_conf):
            entries = LinStorCLI.read_config(sys_conf)

        if entries:
            global_entries = entries.get('global', [])
            for key, val in global_entries:
                pargs.insert(0, "--" + key)
                if val:
                    pargs.insert(1, val)
        return pargs

    def parse(self, pargs):
        # read global options from config file
        if '--disable-config' not in pargs:
            pargs = LinStorCLI.merge_config_arguments(pargs)
        return self._parser.parse_args(pargs)

    @classmethod
    def _report_linstor_error(cls, le):
        sys.stderr.write("Error: " + le.message + '\n')
        for err in le.all_errors():
            sys.stderr.write(' ' * 2 + err.message + '\n')

    def parse_and_execute(self, pargs):
        rc = ExitCode.OK
        try:
            args = self.parse(pargs)

            local_only_cmds = [
                self.cmd_list, MigrateCommands.cmd_dmmigrate,
                self._zsh_generator.cmd_completer, self.cmd_help
            ]

            # only connect if not already connected or a local only command was executed
            if self._linstorapi is None and args.func not in local_only_cmds:
                self._linstorapi = linstor.Linstor(Commands.controller_list(
                    args.controllers)[0],
                                                   timeout=args.timeout)
                self._controller_commands._linstor = self._linstorapi
                self._node_commands._linstor = self._linstorapi
                self._storage_pool_dfn_commands._linstor = self._linstorapi
                self._storage_pool_commands._linstor = self._linstorapi
                self._resource_dfn_commands._linstor = self._linstorapi
                self._volume_dfn_commands._linstor = self._linstorapi
                self._resource_commands._linstor = self._linstorapi
                self._snapshot_commands._linstor = self._linstorapi
                self._misc_commands._linstor = self._linstorapi
                self._linstorapi.connect()
            rc = args.func(args)
        except ArgumentError as ae:
            sys.stderr.write(ae.message + '\n')
            try:
                self.parse(
                    list(
                        itertools.takewhile(lambda x: not x.startswith('-'),
                                            pargs)) + ['-h'])
            except SystemExit:
                pass
            return ExitCode.ARGPARSE_ERROR
        except utils.LinstorClientError as lce:
            sys.stderr.write(lce.message + '\n')
            return lce.exit_code
        except linstor.LinstorNetworkError as le:
            self._report_linstor_error(le)
            rc = ExitCode.CONNECTION_ERROR
        except linstor.LinstorTimeoutError as le:
            self._report_linstor_error(le)
            rc = ExitCode.CONNECTION_TIMEOUT
        except linstor.LinstorError as le:
            self._report_linstor_error(le)
            rc = ExitCode.UNKNOWN_ERROR

        return rc

    @staticmethod
    def parser_cmds(parser):
        # AFAIK there is no other way to get the subcommands out of argparse.
        # This avoids at least to manually keep track of subcommands

        cmds = dict()
        subparsers_actions = [
            action for action in parser._actions
            if isinstance(action, argparse._SubParsersAction)
        ]
        for subparsers_action in subparsers_actions:
            for choice, subparser in subparsers_action.choices.items():
                parser_hash = subparser.__hash__
                if parser_hash not in cmds:
                    cmds[parser_hash] = list()
                cmds[parser_hash].append(choice)

        # sort subcommands and their aliases,
        # subcommand dictates sortorder, not its alias (assuming alias is
        # shorter than the subcommand itself)
        cmds_sorted = [
            sorted(cmd, key=len, reverse=True) for cmd in cmds.values()
        ]

        # "add" and "new" have the same length (as well as "delete" and
        # "remove), therefore prefer one of them to group commands for the
        # "list" command
        for cmds in cmds_sorted:
            idx = 0
            found = False
            for idx, cmd in enumerate(cmds):
                if cmd.startswith("create-") or cmd.startswith("delete-"):
                    found = True
                    break
            if found:
                cmds.insert(0, cmds.pop(idx))

        # sort subcommands themselves
        cmds_sorted.sort(key=lambda a: a[0])
        return cmds_sorted

    def parser_cmds_description(self, all_commands):
        toplevel = [top[0] for top in all_commands]

        subparsers_actions = [
            action for action in self._parser._actions
            if isinstance(action, argparse._SubParsersAction)
        ]
        description = {}
        for subparsers_action in subparsers_actions:
            for choice, subparser in subparsers_action.choices.items():
                if choice in toplevel:
                    description[choice] = subparser.description

        return description

    def check_parser_commands(self):

        parser_cmds = LinStorCLI.parser_cmds(self._parser)
        for cmd in parser_cmds:
            mcos = [x for x in cmd if x in Commands.MainList + Commands.Hidden]
            if len(mcos) != 1:
                raise AssertionError("no main command found for group: " +
                                     str(cmd))

        all_cmds = [y for x in parser_cmds for y in x]
        for cmd in Commands.MainList + Commands.Hidden:
            if cmd not in all_cmds:
                raise AssertionError("defined command not used in argparse: " +
                                     str(cmd))

        return True

    @staticmethod
    def get_commands(parser, with_aliases=True):
        cmds = []
        for cmd in LinStorCLI.parser_cmds(parser):
            cmds.append(cmd[0])
            if with_aliases:
                for al in cmd[1:]:
                    cmds.append(al)
        return cmds

    @staticmethod
    def get_command_aliases(all_commands, cmd):
        return [
            x for subx in all_commands if cmd in subx for x in subx
            if cmd not in x
        ]

    @staticmethod
    def gen_cmd_tree(subp):
        cmd_map = {}
        for cmd in subp._name_parser_map:
            argparse_cmd = subp._name_parser_map[cmd]
            new_subp = argparse_cmd._actions[-1]
            if isinstance(new_subp, argparse._SubParsersAction):
                if argparse_cmd.prog in cmd_map:
                    cmd_map[argparse_cmd.prog] =\
                        (cmd_map[argparse_cmd.prog][0] + [cmd], LinStorCLI.gen_cmd_tree(new_subp))
                else:
                    cmd_map[argparse_cmd.prog] = ([
                        cmd
                    ], LinStorCLI.gen_cmd_tree(new_subp))
            else:
                if argparse_cmd.prog in cmd_map:
                    cmd_map[argparse_cmd.prog] = (
                        cmd_map[argparse_cmd.prog][0] + [cmd], {})
                else:
                    cmd_map[argparse_cmd.prog] = ([cmd], {})

        return cmd_map

    @staticmethod
    def print_cmd_tree(entry, indent=0):
        for fullcmd in sorted(entry.keys()):
            cmd = fullcmd[fullcmd.rindex(' '):].strip()
            aliases, sub_cmds = entry[fullcmd]
            p_str = cmd
            if len(aliases) > 1:
                p_str += " ({al})".format(cmd=cmd,
                                          al=sorted(aliases, key=len)[0])
            print(" " * indent + "- " + p_str)
            LinStorCLI.print_cmd_tree(sub_cmds, indent + 2)

    def cmd_list(self, args):
        sys.stdout.write(
            'Use "help <command>" to get help for a specific command.\n\n')
        sys.stdout.write('Available commands:\n')
        # import pprint
        # pp = pprint.PrettyPrinter()
        # pp.pprint(self._all_commands)

        if args.tree:
            subp = self._parser._actions[-1]
            assert (isinstance(subp, argparse._SubParsersAction))
            cmd_map = LinStorCLI.gen_cmd_tree(subp)
            LinStorCLI.print_cmd_tree({
                k: v
                for k, v in cmd_map.items()
                if k[k.rindex(' '):].strip() in Commands.MainList
            })
        else:
            for cmd in sorted(Commands.MainList):
                sys.stdout.write("- " + cmd)
                aliases = LinStorCLI.get_command_aliases(
                    self._all_commands, cmd)
                if aliases:
                    sys.stdout.write(" (%s)" % (", ".join(aliases)))
                sys.stdout.write("\n")

        return 0

    def cmd_interactive(self, args):
        all_cmds = [i for sl in self._all_commands for i in sl]

        # helper function
        def unknown(cmd):
            sys.stdout.write("\n" + "Command \"%s\" not known!\n" % cmd)
            self.cmd_list(args)

        # helper function
        def parsecatch(cmds_):
            rc = ExitCode.OK
            try:
                rc = self.parse_and_execute(cmds_)
            except SystemExit as se:
                cmd = cmds_[0]
                if cmd in [Commands.EXIT, "quit"]:
                    sys.exit(ExitCode.OK)
                elif cmd == "help":
                    if len(cmds_) == 1:
                        self.cmd_list(args)
                        return
                    else:
                        cmd = " ".join(cmds_[1:])
                        if cmd not in all_cmds:
                            unknown(cmd)
                elif cmd in all_cmds:
                    if '-h' in cmds_ or '--help' in cmds:
                        return
                    if se.code == ExitCode.ARGPARSE_ERROR:
                        sys.stdout.write(
                            "\nIncorrect syntax. Use 'help {cmd}' for more information:\n"
                            .format(cmd=cmd))
                else:
                    unknown(cmd)
            except KeyboardInterrupt:
                pass
            except BaseException:
                traceback.print_exc(file=sys.stdout)

            if rc == ExitCode.CONNECTION_ERROR:
                sys.exit(rc)

        # main part of interactive mode:
        if not LinStorCLI.interactive:
            LinStorCLI.interactive = True

            # try to load readline
            # if loaded, raw_input makes use of it
            if sys.version_info < (3, ):
                my_input = raw_input
            else:
                my_input = input

            try:
                import readline
                # seems after importing readline it is not possible to output to sys.stderr
                completer = argcomplete.CompletionFinder(self._parser)
                readline.set_completer_delims("")
                readline.set_completer(completer.rl_complete)
                readline.parse_and_bind("tab: complete")
            except ImportError:
                pass

            args.tree = False
            self.cmd_list(args)
            while True:
                try:
                    sys.stdout.write("\n")
                    cmds = my_input('LINSTOR ==> ').strip()

                    cmds = [cmd.strip() for cmd in cmds.split()]
                    if not cmds:
                        self.cmd_list(args)
                    else:
                        parsecatch(cmds)
                except (EOFError,
                        KeyboardInterrupt):  # raised by ctrl-d, ctrl-c
                    sys.stdout.write(
                        "\n")  # additional newline, makes shell prompt happy
                    break
            LinStorCLI.interactive = False
        else:
            sys.stderr.write(
                "The client is already running in interactive mode\n")

    def cmd_help(self, args):
        return self.parse_and_execute(args.command + ["-h"])

    def cmd_exit(self, _):
        sys.exit(ExitCode.OK)

    def run(self):
        # TODO(rck): try/except
        sys.exit(self.parse_and_execute(sys.argv[1:]))

    def user_confirm(self, question):
        """
        Ask yes/no questions. Requires the user to answer either "yes" or "no".
        If the input stream closes, it defaults to "no".
        returns: True for "yes", False for "no"
        """
        sys.stdout.write(question + "\n")
        sys.stdout.write("  yes/no: ")
        sys.stdout.flush()
        fn_rc = False
        while True:
            answer = sys.stdin.readline()
            if len(answer) != 0:
                if answer.endswith("\n"):
                    answer = answer[:len(answer) - 1]
                if answer.lower() == "yes":
                    fn_rc = True
                    break
                elif answer.lower() == "no":
                    break
                else:
                    sys.stdout.write("Please answer \"yes\" or \"no\": ")
                    sys.stdout.flush()
            else:
                # end of stream, no more input
                sys.stdout.write("\n")
                break
        return fn_rc