#!/usr/bin/env python3
#
# Copyright 2013-2025 Quobyte Inc. All rights reserved.
#
# pylint: disable=consider-using-f-string
# pylint: disable=too-many-branches
# pylint: disable=missing-function-docstring
# pylint: disable=missing-class-docstring
# pylint: disable=missing-module-docstring
# pylint: disable=too-many-lines
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments
# pylint: disable=no-else-return
# pylint: disable=no-else-raise

import argparse
import base64
import csv
import datetime
import errno
import getpass
import hashlib
import http.client as httplib
import json
import os
import re
import socket
import ssl
import subprocess
import signal
import sys
import tempfile
import time
import traceback

from sys import exit as sysexit
from urllib.parse import urlparse
from uuid import UUID


VALID_DEVICE_TYPES = (
    'DATA',
    'METADATA',
    'REGISTRY',
    'D',
    'R',
    'M',
)
VALID_DEVICE_MODES = (
    'ONLINE',
    'OFFLINE',
    'DRAIN',
    'REGENERATE',
    'DECOMMISSIONED',
)
VALID_DEVICE_LED_MODES = (
    'OFF',
    'FAIL',
    'LOCATE',
)
SUPPORTED_FS_TYPES = (
    'ext4',
    'xfs',
)
VALID_MOUNT_STATES = (
    'MOUNTED',
    'UNMOUNTED',
)
VALID_FILESYSTEM_CHECK_STATES = (
    'ENABLED',
    'DISABLED',
)
VALID_TRIM_METHODS = (
    'NONE',
    'DISCARD_MOUNT_OPTION',
    'FSTRIM_TASK'
)
SERVICE_TYPE_MAP = {
    'METADATA_SERVICE': 'M - Metadata',
    'STORAGE_SERVICE': 'D - Data',
    'DIRECTORY_SERVICE': 'R - Registry',
    'CLIENT': 'C - Native Client',
    'API_PROXY': 'A - API Service',
    'NFS_PROXY': 'N - NFS Gateway',
    'WEBCONSOLE': 'W - Webconsole',
    'S3_PROXY': 'S - S3 Gateway',
}

VALID_HEALTH_STATUS = (
    'HEALTHY',
    'DEFECTIVE',
)

QUOBYTE_DIRECTORY = ".quobyte"

TENANT_LOOKUP_CACHE = {}

TIMESTAMP_KEYS_RE = (
    'last_successful_scrub_ms',
    'last_device_available_ms',
    'last_fstrim_ms',
    'last_cleanup_ms',
    'next_maintenance_window_start_ms',
    'up_to_date_until_ms',
    'client_start_time_ms',
    '.*timestamp.*',
)
BYTE_KEYS_RE = (
    'LOGICAL_DISK_SPACE',
    '.*PHYSICAL_DISK_SPACE',
    '.*_bytes',
)

RELEASE_VERSION_LONG = "4.5 (2027c5892c)"

# Initialized from command line arguments
COMMAND_NAME = None
FORCE = None
VERBOSE = None
BYTES_UNIT = None

def piped_exit(err):
    try:
        sys.stdout.flush()
        sys.stdout.close()
        sys.stderr.flush()
        sys.stderr.close()
    finally:
        sysexit(err)


sys.exit = piped_exit


class InvalidInputException(Exception):

    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


class ServerErrorException(Exception):
    ENTITY_NOT_FOUND = -24
    INVALID_ARGUMENTS = -3

    def __init__(self, message, code):
        self.message = message
        self.code = code

    def __str__(self):
        return "'%(message)s' Code: %(code)d" % {'message': self.message, 'code': self.code}


class CommunicationException(Exception):

    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


class AccessDeniedException(Exception):

    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


class MalformedJsonException(Exception):

    def __init__(self, value_error):
        self.value_error = value_error

    def __str__(self):
        return str(self.value_error)


def _output_json_and_exit(args, data):
    if args.list_output == "json":
        print(json.dumps(data))
        sys.exit(0)


class SessionCookieHandler:
    COOKIE_FILE = ".qmgmt_session_cookie"

    def __init__(self):
        self._cookie = None
        home = self._get_home_directory("Required for storing the session cookie.")
        self._quobyte_path = os.path.join(home, QUOBYTE_DIRECTORY)
        self._cookie_path = os.path.join(self._quobyte_path, self.COOKIE_FILE)

    def _get_home_directory(self, error=""):
        home = os.path.expanduser("~")
        if home == "~":
            raise InvalidInputException(
                'Cannot determine user home directory. ' + error)
        return home

    def get_session_cookie(self):
        if os.path.exists(self._cookie_path):
            with open(self._cookie_path, 'r', encoding="utf-8") as f:
                return f.read()
        else:
            return None

    def set_session_cookie(self, cookie):
        self._ensure_quobyte_directory_exists()
        with open(self._cookie_path, 'w', encoding="utf-8") as f:
            f.write(cookie)

    def delete_session_cookie(self):
        if os.path.exists(self._cookie_path):
            os.unlink(self._cookie_path)

    def _ensure_quobyte_directory_exists(self):
        try:
            os.mkdir(self._quobyte_path)
        except OSError as e:
            if e.errno != errno.EEXIST:
                raise


class InteractiveCredentialsProvider:

    def __init__(self, default_credentials, do_not_prompt_for_password):
        self._credentials = None
        self._default_credentials = default_credentials
        self._do_not_prompt_for_password = do_not_prompt_for_password

    def set_credentials(self, credentials):
        self._credentials = credentials

    def get_credentials(self):
        return self._credentials

    def get_default_credentials(self):
        return self._default_credentials

    def clear_credentials(self):
        self.set_credentials(None)

    def fetch_from_environment(self):
        access_key = os.environ.get('QUOBYTE_ACCESS_KEY')
        if access_key:
            try:
                username, password = access_key.split(':', 1)
                self._credentials = BasicAuthCredentials(username, password)
                return
            except ValueError:
                print("Invalid access key in QUOBYTE_ACCESS_KEY, ignoring")

        username = os.environ.get('QUOBYTE_USER')
        password = os.environ.get('QUOBYTE_PASSWORD')
        if username and password:
            self._credentials = BasicAuthCredentials(username, password)

    def prompt_for_credentials(self):
        if self._do_not_prompt_for_password:
            raise AccessDeniedException("Interactive password prompt disabled")

        if self._credentials:
            # Reuse existing credentials (if the session cookie timed out)
            return

        shell_user = os.getenv("USER")
        sys.stderr.write("Username (" + shell_user + "): ")
        sys.stderr.flush()
        username = sys.stdin.readline().rstrip()
        if not username:
            username = shell_user
        password = getpass.getpass()
        if not password:
            raise AccessDeniedException("Empty password entered.")

        JsonRpc.login_method_string = "provided credentials"
        self._credentials = BasicAuthCredentials(username, password)


class BasicAuthCredentials:

    def __init__(self, username, password):
        self._username = username
        self._password = password

    def get_username(self):
        return self._username

    def get_authorization_header(self):
        auth_bytes = base64.standard_b64encode(
                b'%s:%s' % (self._username.encode("UTF-8"), self._password.encode("UTF-8")))
        auth = str(auth_bytes.decode("UTF-8"))
        return 'BASIC %s' % auth


def get_password_from_prompt(prompt="Please enter password: ",
                             retype_prompt="Please retype password: "):
    while True:
        pass1 = getpass.getpass(prompt=prompt)
        pass2 = getpass.getpass(prompt=retype_prompt)
        if pass1 == pass2:
            return pass1
        else:
            print_warning(0, "Provided passwords do not match. Please retry.")


def print_warning(level, *msg_lines):
    assert isinstance(level, int)
    if level == 0:
        level_msg = "WARNING: "
    elif level == 1:
        level_msg = "FATAL: "
    else:
        level_msg = ""
    sys.stderr.write(level_msg)
    sys.stderr.write((os.linesep + " " * len(level_msg)).join([str(l) for l in msg_lines])
                     + os.linesep)
    if level == 1 and COMMAND_NAME:
        sys.stderr.write("See '{0} -h' for help and examples.{1}".format(COMMAND_NAME, os.linesep))
    sys.stderr.flush()


def get_pbkdf2_sha512_hash(password, salt, iterations, size):
    return hashlib.pbkdf2_hmac('sha512', password, salt, iterations, size)


def timedelta(string):
    if string.lower().endswith('h'):
        return int(string[:-1]) * 3600
    elif string.lower().endswith('d'):
        return int(string[:-1]) * 24 * 3600
    else:
        return int(string)

class JsonRpc:
    """ Implements JSON RPC over HTTPS"""

    login_method_string = "qmgmt"

    def __init__(self, url, session_cookie_handler, credentials_provider, fail_fast,
                 ca, ignore_server_certificate):
        self._ca_file = ca
        self._connection = None
        self._id = time.time()
        self._fail_fast = fail_fast
        self._session_cookie_handler = session_cookie_handler
        self._credentials_provider = credentials_provider
        self._ignore_server_certificate = ignore_server_certificate
        self._tried_default_credentials = False
        self._tried_env_credentials = False
        self._setup_connection(url)
        self._session_expires_secs = None

    def _setup_connection(self, url):
        parsed_url = urlparse(url)
        self._url = parsed_url.geturl()
        self._netloc = parsed_url.netloc
        if parsed_url.scheme == 'https':
            # Will load the system CAs via SLContext.load_default_certs()
            ssl_context=ssl.create_default_context(
                ssl.Purpose.SERVER_AUTH, cafile=self._ca_file)
            if self._ignore_server_certificate:
                ssl_context.verify_mode = ssl.CERT_NONE
            self._connection = httplib.HTTPSConnection(
                self._netloc, context=ssl_context)
        else:
            self._connection = httplib.HTTPConnection(self._netloc)

    def set_credentials(self, credentials):
        self._credentials_provider.set_credentials(credentials)

    def set_session_expires(self, session_expires):
        self._session_expires_secs = session_expires

    def logout(self):
        cookie = self._session_cookie_handler.get_session_cookie()
        if not cookie:
            print_warning(0, "Skipping logout because there is no session cookie on disk.")
            return

        self.call(None, None, "/logout", result_is_json=False)
        self._session_cookie_handler.delete_session_cookie()

    def call(self, method_name, user_parameters, path="/", result_is_json=True):
        if self._fail_fast:
            parameters = {'retry': 'INTERACTIVE'}
        else:
            parameters = {'retry': 'INFINITELY'}
        if user_parameters:
            parameters.update(user_parameters)
        call_body = {'jsonrpc': '2.0',
                     'method': method_name,
                     'params': parameters,
                     'id': str(self._id)}
        outdated_cookie_was_deleted = False
        while True:
            try:
                self._id += 1
                if self._id > 1:
                    self._connection.close()

                headers = {}
                # Try authorization in this order:
                # a) cookie b) provided or cached credentials c) from environment
                # d) default credentials e) prompted credentials
                credentials = self._credentials_provider.get_credentials()
                cookie = self._session_cookie_handler.get_session_cookie()
                prompt = False
                if not credentials and not cookie:
                    if outdated_cookie_was_deleted:
                        print_warning(2, "Existing session cookie is no longer valid.")
                        outdated_cookie_was_deleted = False
                    if not self._tried_env_credentials:
                        self._credentials_provider.fetch_from_environment()
                        self._tried_env_credentials = True
                        if not self._credentials_provider.get_credentials():
                            continue
                        credentials = self._credentials_provider.get_credentials()
                        JsonRpc.login_method_string = "environment variables"
                    elif not self._tried_default_credentials:
                        # Try the default credentials once, after that prompt
                        # for username/password.
                        credentials = self._credentials_provider.get_default_credentials()
                        self._tried_default_credentials = True
                        JsonRpc.login_method_string = "default credentials"
                    else:
                        while not self._credentials_provider.get_credentials():
                            self._credentials_provider.prompt_for_credentials()
                        credentials = self._credentials_provider.get_credentials()
                        prompt = True

                session_expires = self._session_expires_secs

                if VERBOSE:
                    print("> POST")
                    print("> " + self._url + path)
                    print("> " + str(call_body))
                if session_expires:
                    headers["Session-Expires"] = session_expires
                if cookie:
                    headers["Cookie"] = cookie
                    if VERBOSE:
                        print("> " + str(headers) + os.linesep + ">")
                    self._connection.request(
                        "POST", self._url + path, json.dumps(call_body), headers)
                    response = self._connection.getresponse()
                    JsonRpc.login_method_string = "cookie"
                elif credentials:
                    headers["Authorization"] = credentials.get_authorization_header()
                    if VERBOSE:
                        print("> " + str(headers) + os.linesep + ">")
                    self._connection.request(
                        "POST", self._url + path, json.dumps(call_body), headers)
                    response = self._connection.getresponse()
                    if response.status == 200 and credentials == \
                            self._credentials_provider.get_default_credentials():
                        print_warning(
                            0, "Using default credentials, please configure access control.")
                else:
                    raise ValueError(
                        "Illegal state. Neither username/password nor cookie were set.")
                if response.status == 401:
                    if 'Authorization' in headers:
                        if credentials != self._credentials_provider.get_default_credentials():
                            print_warning(0,
                                "Provided username (%s) and password (not shown) were incorrect."
                                % self._credentials_provider.get_credentials().get_username())
                            self._credentials_provider.clear_credentials()
                    elif 'Cookie' in headers:
                        self._session_cookie_handler.delete_session_cookie()
                        outdated_cookie_was_deleted = True
                        # do not clear credentials in self._credentials_provider to allow reuse
                    else:
                        raise ValueError(
                            'Invalid state. Neither Authorization nor Cookie header is set.')
                    # Always retry. In non-interactive mode,
                    # prompt_for_credentials() will throw eventually.
                    continue

                if response.status in [301, 302, 307, 308]:
                    self._setup_connection(response.getheader("Location"))
                    continue

                if response.status != 200:
                    raise CommunicationException(
                        "HTTP request to API failed: " + str(response.status)
                        + " " + response.reason
                        + ". Please check the API service log for more details.")

                if prompt:
                    # keep in sync with ApiService api.userSessionIdleTimeout
                    print("Your session will expire in about 10 minutes, use "
                          "'qmgmt user login --timeout=<time in h or d>' "
                          "to create a longer session.")
                set_cookie_headers = response.getheader('set-cookie')
                if set_cookie_headers:
                    # API always returns the header, but usually there is no new session cookie
                    # Multiple set-cookie headers are stored comma separated by
                    # httplib
                    for set_cookie in set_cookie_headers.split(','):
                        if set_cookie.startswith('JSESSIONID='):
                            try:
                                self._session_cookie_handler.set_session_cookie(set_cookie)
                            except OSError as e:
                                print_warning(0, "Could not store session cookie:", e)

                if result_is_json:
                    content = response.read()
                    try:
                        result = json.loads(content.decode("utf-8"))
                    except ValueError:
                        content = content.replace(b'\0', b'\\u0000')  # issue 3863
                        try:
                            result = json.loads(content.decode("utf-8"))
                        except ValueError as ev:
                            print("Invalid JSON in response (wrong URL?):",
                                  content.decode("utf-8")[:50].replace('\n', ''), "...")
                            raise MalformedJsonException(ev) from ev

                    if 'error' in result and result["error"]:
                        if 'message' in result['error'] and 'code' in result['error']:
                            raise ServerErrorException(
                                result["error"]["message"], result["error"]["code"])
                        else:
                            raise ServerErrorException(
                                str(result["error"]), -1)
                    return result["result"]
                else:
                    return None
            except ssl.SSLError as e:
                # Generic catch because OpenSSL does not return meaningful errors.
                # Example for failed verification: [Errno 1] _ssl.c:520:
                # error:0407006A:rsa
                # routines:RSA_padding_check_PKCS1_type_1:block type is not 01
                print_warning(0, "Could not verify server certificate of API service using"
                                " CA certificate. Disable check with --no-check-certificate or"
                                " provide custom CA with --ca.")
                raise CommunicationException(
                        "Client SSL subsystem returned error: " + str(e)) from e
            except httplib.BadStatusLine as e:
                raise CommunicationException("If SSL is enabled for the API service, the URL must"
                                             " start with 'https://' for the URL. Failed to parse"
                                             " status code from server response.") from e
            except (httplib.HTTPException, socket.error) as e:
                if self._fail_fast:
                    raise CommunicationException(str(e)) from e
                else:
                    print_warning(0, "Encountered error, retrying:", e)
                    time.sleep(1)


class PrettyTable:
    """ Generalized output generator.

        Attributes:
            data(list): data to print
            columns(list): which columns to print, first used as key to sort
            header(list): names of the columns
            style(str): table, csv, json or keys
            sort_by_key: key to sort the table rows. Default is None
            reverse(boolean): False will sort ascending, True will sort descending. Default is False
    """

    def __init__(self,
                 args,
                 data,
                 columns,
                 header=None,
                 style="table",
                 sorting_depth=1,
                 sort_by_key=None,
                 reverse=False):
        self._args = args
        self._header = header
        if args.list_columns:
            self._columns = args.list_columns.split(",")
            self._header = args.list_columns.split(",")
            # Match api and pretty column names
            if header:
                assert len(header) == len(columns)
                for i, head in enumerate(header):
                    for j, col in enumerate(self._columns):
                        if col == head:
                            self._columns[j] = columns[i]
        else:
            self._columns = columns
            if args.add_columns:
                self._columns.extend(args.add_columns.split(","))
                if self._header:
                    self._header.extend(args.add_columns.split(","))
        if args.list_output:
            self._style = args.list_output
        else:
            self._style = style
        assert self._style in ("json", "table", "csv", "comp", "keys")
        self._data = data
        self._format_string = None
        self._row_separator = os.linesep
        self._depth = sorting_depth
        self._sort_by_key = sort_by_key
        self._reverse = reverse
        self._original_data = None
        if self._style in ["json", "keys"]:
            self._original_data = self._data
            self._data = []

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("--list-columns",
                            action="store",
                            metavar="STR",
                            dest="list_columns",
                            help="Limit listing to specified comma-separated"
                                " values. For possible not listed column"
                                " names check json output or quobyte API.")
        parser.add_argument("--add-columns",
                             action="store",
                             dest="add_columns",
                             metavar="STR",
                             help="Add comma-separated columns to the output"
                                  " list. For possible not listed column"
                                  " names check json output or quobyte API.")

    @staticmethod
    def _get_nested(dictionary, args):
        if not dictionary:
            return dictionary
        if isinstance(args, str):
            return dictionary.get(args)
        if isinstance(args[0], int):
            if len(dictionary) < args[0] + 1:
                return None
            else:
                value = dictionary[args[0]]
        else:
            value = dictionary.get(args[0], None)
        if len(args) == 1:
            return value
        else:
            return PrettyTable._get_nested(value, args[1:])

    @staticmethod
    def _set_nested(dictionary, args, value):
        if isinstance(args, str):
            dictionary[args] = value
            return
        if len(args) == 1:
            dictionary[args[0]] = value
        else:
            try:
                dictionary[args[0]]
            except:
                dictionary[args[0]] = {}
            PrettyTable._set_nested(dictionary[args[0]], args[1:], value)

    def _process_data(self):
        for row in self._data:
            for keys in self._columns:
                col = self._get_nested(row, keys)
                joined_keys = ''.join([str(k) for k in keys])
                if isinstance(col, list) and col:
                    try:
                        self._set_nested(row, keys, ';'.join(str(x) for x in col))
                    except TypeError:
                        # Ignore non-strings
                        self._set_nested(row, keys, str(col))
                elif isinstance(col, int) and "enabled" == joined_keys:
                    self._set_nested(row, keys, "True" if col == 1 else "False")
                elif isinstance(col, int) and \
                        any([re.match(p, joined_keys)
                             for p in BYTE_KEYS_RE]):
                    self._set_nested(row, keys, human_readable_bytes(col))
                elif isinstance(col, int) and \
                        any([re.match(p, joined_keys)
                             for p in TIMESTAMP_KEYS_RE]):
                    if not col:
                        continue
                    if joined_keys.endswith("_s") or joined_keys.endswith("_seconds"):
                        unit_delimiter = 1.
                    else:
                        unit_delimiter = 1000.
                    self._set_nested(row, keys,
                        datetime.datetime.fromtimestamp(
                            float(col) / unit_delimiter).strftime('%Y-%m-%d %H:%M:%S'))
                elif isinstance(keys, str) and keys == 'service_type':
                    self._set_nested(row, keys, service_type_to_human(col))

    @staticmethod
    def encode_with_fallback(val):
        try:
            return str(val)
        except UnicodeEncodeError:
            return val.encode('utf-8')

    def _get_format_string(self):
        if self._style == "table":
            self._format_string = ""
            for i, keys in enumerate(self._columns):
                encoded = [self.encode_with_fallback(self._get_nested(row, keys))
                        for row in self._data]
                if self._header:
                    encoded.append(self._header[i])
                else:
                    encoded.append(self._columns[i])
                len_max = len(max(encoded, key=len)) + 1
                len_max = min(len_max, 70)
                self._format_string += '{%i:<%s} ' % (i, len_max)
        elif self._style == "csv":
            self._format_string = \
                ','.join(["{%i}" % i for i, key in enumerate(self._columns)])
        elif self._style == "comp":
            self._format_string = \
                '/'.join(["{%i}" % i for i, key in enumerate(self._columns)])

    def rename_columns(self, header):
        if not self._args.list_columns:
            if self._args.add_columns:
                header.extend(self._args.add_columns.split(","))
            assert len(header) == len(self._columns)
            self._header = header

    def modify_cell(self, row_number, column_name, func):
        if row_number < len(self._data):
            row = self._data[row_number]
            self._set_nested(row,
                             column_name,
                             func(self._get_nested(row, column_name)))

    def modify_column(self, column_name, func):
        for row in self._data:
            self._set_nested(row,
                             column_name,
                             func(self._get_nested(row, column_name)))

    def append_column(self, key, name=None):
        self._columns.append(key)
        if self._header:
            self._header.append(name or key)

    def insert_column(self, position, key, name=None):
        self._columns.insert(position, key)
        if self._header:
            self._header.insert(position, name or key)

    def filter_column_value(self, column_name, value):
        _new_data = []
        for row in self._data:
            if self._get_nested(row, column_name) == value:
                _new_data.append(row)
        self._data = _new_data

    def filter_values(self, *values):
        _new_data = []
        values = [v for v in values if v]
        for row in self._data:
            for col in self._columns:
                if self._get_nested(row, col) in values:
                    _new_data.append(row)
        self._data = _new_data

    def _get_sorting_keys(self, v):
        for column in self._columns[:self._depth]:
            yield self._get_nested(v, column)

    def sort(self):
        if self._sort_by_key is None or not self._sort_by_key:
            self._data.sort(
                key=lambda v: [self._make_sort_tuple(x) for x in self._get_sorting_keys(v)],
                reverse=self._reverse)
        else:
            self._data.sort(
                key=lambda v: self._make_sort_tuple(v.get(self._sort_by_key)),
                reverse=self._reverse)

    # Shift the None value to the end and handles the dict type
    @staticmethod
    def _make_sort_tuple(x):
        if x is None:
            return (True, None)
        elif isinstance(x, dict):
            return (True, None) if not x else (False, str(x))
        else:
            return (False, x)

    @staticmethod
    def _to_row_string(x):
        if x is None:
            return "-"
        if isinstance(x, dict):
            return "-" if not x else str(x)
        else:
            return str(x)

    def out(self):
        if self._style == "json":
            return json.dumps(self._original_data)
        elif self._style == "keys":
            return ",".join(sorted(self._original_data, key=len)[-1].keys())
        self.sort()
        self._process_data()
        self._get_format_string()
        if self._style != "comp":
            result = self._format_string.format(
                *(self._header or self._columns))
        else:
            result = ''

        for row in self._data:
            result += self._row_separator + self._format_string.format(
                *[self._to_row_string(self._get_nested(row, keys)) for keys in self._columns])

        return result.strip()


class TasksTable(PrettyTable):
    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def _get_sorting_keys(self, v):
        for part in self._get_nested(v, self._columns[0]).split("."):
            try:
                yield int(part)
            except ValueError:
                print_warning(0, "Unexpected task id format.")
                yield part


class DataDumpHelper:

    @staticmethod
    def export_to_file(data_dump, file_name):
        with open(file_name, "w", encoding='utf8') as f:
            f.write(data_dump)

    @staticmethod
    def import_from_file(file_name):
        with open(file_name, "r", encoding='utf8') as f:
            return f.read()

    @staticmethod
    def import_from_stdin():
        return "\n".join(str(x) for x in sys.stdin.readlines())

    # Returns empty string if nothing changed. Raises IOError if editor returns an error.
    @staticmethod
    def edit(data_dump):
        file_name = "qmgmt_proto_dump_edit_%s" % hashlib.md5(
            str(time.localtime()).encode(encoding="utf-8", errors="strict")).hexdigest()
        temp_file = os.path.join(tempfile.gettempdir(), file_name)
        if os.path.exists(temp_file):
            os.unlink(temp_file)
        DataDumpHelper.export_to_file(data_dump, temp_file)

        editor = None
        if not editor:
            editor = os.environ.get('VISUAL', None)
        if not editor:
            editor = os.environ.get('EDITOR', 'vi')
        cmd = editor + ' ' + temp_file
        ret = subprocess.call(cmd, shell=True)
        if ret != 0:
            if os.path.exists(temp_file):
                os.unlink(temp_file)
            raise IOError("Editor returned with error: %s" % ret)

        edited_data_dump = DataDumpHelper.import_from_file(temp_file)
        result = edited_data_dump if (edited_data_dump != data_dump) else ""
        if os.path.exists(temp_file):
            os.unlink(temp_file)
        return result

    @staticmethod
    def remove_comments(data_dump):
        return re.sub(r'(?m)^ *#.*\n?', '', str(data_dump))

    @staticmethod
    def edit_json(json_dump):
        before = json_dump
        while True:
            after = DataDumpHelper.remove_comments(DataDumpHelper.edit(before))
            if not after or after == json_dump:
                return ""
            try:
                json.loads(after)
                return after
            except ValueError as e:
                before = str("# Failed to parse input as JSON\n# " + str(e) + "\n"
                             "# Comments will be removed automatically\n\n" + after)

class NameResolver:
    """ Resolves tenant and volume names and handles."""

    VOLUME_HANDLE_FORMAT = "[<tenant name or uuid>/]<volume name> or volume uuid"

    def __init__(self, json_rpc):
        self._json_rpc = json_rpc
        self._tenant_name_to_uuid = {}
        self._tenant_uuid_to_name = {}

    def lookup_tenant_uuid(self, tenant_id: str) -> str:
        """ Tenant id (typically a UUID) to name or None """
        if tenant_id in self._tenant_uuid_to_name:
            return self._tenant_uuid_to_name[tenant_id]

        result = self._json_rpc.call("getTenant", {'tenant_id': [tenant_id]})
        if result['tenant']:
            assert result['tenant'][0]['tenant_id'] == tenant_id
            tenant_name = result['tenant'][0]['name']
            self._tenant_uuid_to_name[tenant_id] = tenant_name
            return tenant_name
        return None

    def resolve_tenant_name(self, tenant_name: str) -> str:
        """ Tenant name to tenant UUID or None """
        if tenant_name in self._tenant_name_to_uuid:
            return self._tenant_name_to_uuid[tenant_name]
        try:
            result = self._json_rpc.call(
                "resolveTenantName", {'tenant_name': tenant_name})
            tenant_uuid = result['tenant_id']
            self._tenant_name_to_uuid[tenant_name] = tenant_uuid
            return tenant_uuid
        except ServerErrorException as e:
            if e.code == e.ENTITY_NOT_FOUND:
                return None
            else:
                raise

    def resolve_tenant(self, tenant_name_or_uuid: str):
        """ Resolve a tenant UUID or name to (tenant uuid, name) or throw """
        if is_valid_uuid(tenant_name_or_uuid):
            tenant_uuid = tenant_name_or_uuid
            tenant_name = self.lookup_tenant_uuid(tenant_uuid)
            if tenant_name:
                return tenant_uuid, tenant_name
        else:
            tenant_name = tenant_name_or_uuid
            tenant_uuid = self.resolve_tenant_name(tenant_name)
            if tenant_uuid:
                return tenant_uuid, tenant_name
        raise InvalidInputException("Unknown tenant (name or UUID): " + tenant_name_or_uuid)

    def lookup_volume_uuid(self, volume_uuid: str) -> str:
        """ Volume uuid to volume name or None """
        if not is_valid_uuid(volume_uuid):
            raise InvalidInputException("Not a volume UUID: " + volume_uuid)
        result = self._json_rpc.call(
            "getVolumeList", {'volume_uuid': [volume_uuid]})
        vol_list = result['volume']
        if vol_list:
            return vol_list[0]['name']
        return None

    def resolve_new_volume(self, new_volume: str):
        """ Resolve [<tenant name or uuid>/]<new volume name>
            to (tenant_id, volume name) """
        if '/' in new_volume:
            tenant, volume = new_volume.split('/', 1)
            try:
                tenant_uuid, _ = self.resolve_tenant(tenant)
                return tenant_uuid, volume
            except InvalidInputException:
                # Hierarchical volume name without tenant
                pass
        # In case no tenant is given, let Registry figure out which tenant is meant
        return '', new_volume

    def resolve_volume(self, tenant_slash_volume: str):
        """ Resolve a [<tenant name or uuid>/]<volume name or uuid>
            to (volume_uuid, volume_name, tenant_uuid) or throw """
        tenant_uuid = None
        if '/' in tenant_slash_volume:
            # Tenant/volume or only hierarchical volume
            tenant, volume = tenant_slash_volume.split("/", 1)  # volume name may be hierarchical
            try:
                tenant_uuid, _ = self.resolve_tenant(tenant)
            except InvalidInputException:
                volume = tenant_slash_volume
        else:
            # Only volume given
            volume = tenant_slash_volume

        if is_valid_uuid(volume):
             # we assume it's a volume uuid
            result = self._json_rpc.call("getVolumeList", {'volume_uuid': [volume]})['volume']
            if not result:
                raise InvalidInputException("Volume UUID %s not found" % volume)
            return result[0]['volume_uuid'], result[0]['name'], result[0]['tenant_domain']

        request = {}
        if tenant_uuid:
            request = {'tenant_domain': tenant_uuid}
        result = self._json_rpc.call("getVolumeList", request)['volume']

        candidates = []
        for volume_info in result:
            if volume_info['name'] == volume:
                candidates.append(volume_info)
        if not candidates:
            raise InvalidInputException(
                "No such volume '%s' in tenant %s" % (volume, tenant_uuid))
        if len(candidates) > 1:
            raise InvalidInputException(
                "Volume name '%s' exists in multiple tenants, please specify tenant" % volume)

        return (
            candidates[0]['volume_uuid'], candidates[0]['name'], candidates[0]['tenant_domain'])


class Command:
    COMMAND_DESCRIPTION = ""

    def __init__(self, json_rpc):
        self._json_rpc = json_rpc
        self._resolver = NameResolver(json_rpc)

    def run(self, args):
        assert False

    @staticmethod
    def completion_cmd():
        pass

    @staticmethod
    def add_specific_arguments(parser):
        pass


class UserLogin(Command):
    COMMAND_DESCRIPTION = "create a new authenticated session"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('username', nargs='?',
            help="read from std-in if not given")
        parser.add_argument('password', nargs='?',
            help="read from std-in if not given")
        parser.add_argument('--timeout', type=timedelta, dest="timeout",
            default="7d", help="accepts time delta in either (h)ours or "
                + "(d)ays, eg. 12h or 28d. 7 days is used as default "
                + "if not given")

    def run(self, args):
        if not args.username and not args.password:
            tmp_provider = InteractiveCredentialsProvider(None, False)
            tmp_provider.prompt_for_credentials()
            credentials = tmp_provider.get_credentials()
        elif args.username and not args.password:
            credentials = BasicAuthCredentials(args.username, getpass.getpass())
            JsonRpc.login_method_string = "positional argument and prompted password"
        else:
            credentials = BasicAuthCredentials(args.username, args.password)
            JsonRpc.login_method_string = "positional arguments"

        cookiehandler = SessionCookieHandler()
        if cookiehandler.get_session_cookie():
            cookiehandler.delete_session_cookie()

        self._json_rpc.set_session_expires(args.timeout)

        self._json_rpc.set_credentials(credentials)
        username = credentials.get_username()
        if "/" in username:
            parts = username.split("/")
            if len(parts) != 2:
                raise InvalidInputException("Invalid username: " + username)
            username = parts[1]
        response = self._json_rpc.call("getUsers", {"user_id" : [username]})
        userrole = "This user is unprivileged / has no management roles."
        if ('user_configuration' in response
                and response['user_configuration']
                and response['user_configuration'][0]['role']):
            userrole = ("This user has management roles "
                + str(response['user_configuration'][0]['role']) + ".")

        print("Login for '%s' was successful. %s"
              % (credentials.get_username(), userrole))
        expiry_date = datetime.datetime.now() + datetime.timedelta(
            seconds=args.timeout)
        expiry_date_string = expiry_date.strftime("%c")
        print("Your session will expire on %s or by executing 'qmgmt user logout'."
                % expiry_date_string)
        print("Use 'qmgmt user login --timeout=<time in h or d>' to create a "
              "longer session." )


class UserLogout(Command):
    COMMAND_DESCRIPTION = "destroy the current authenticated session"

    def run(self, args):
        self._json_rpc.logout()
        print("Logout was successful, deleted the authenticated session.")


class TenantLookup(Command):
    COMMAND_DESCRIPTION = "look up name of a particular tenant uuid"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_uuid', help="Tenant UUID")

    def run(self, args):
        if not args.tenant_uuid or not is_valid_uuid(args.tenant_uuid):
            print_warning(1, "Tenant UUID expected")
            return -2
        tenant_name = self._resolver.lookup_tenant_uuid(args.tenant_uuid)
        if not tenant_name:
            raise InvalidInputException("No tenant with UUID " + args.tenant_uuid)
        print(tenant_name)
        return 0

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} tenant list --list-columns=tenant_id"


class TenantResolve(Command):
    COMMAND_DESCRIPTION = "resolve tenant name to tenant id"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_name', help="Tenant name")

    def resolve_string(self, tenant):
        result = self._json_rpc.call("getTenant", {'tenant_id': [tenant]})
        if result['tenant']:
            if result['tenant'][0]['tenant_id'] == tenant:
                return tenant
        try:
            result = self._json_rpc.call(
                "resolveTenantName", {'tenant_name': tenant})
            return result['tenant_id']
        except ServerErrorException as e:
            if e.code in (e.ENTITY_NOT_FOUND, e.INVALID_ARGUMENTS):
                raise InvalidInputException("No such tenant: " + tenant) from e
            else:
                raise

    def run(self, args):
        uuid = self._resolver.resolve_tenant_name(args.tenant_name)
        if not uuid:
            raise InvalidInputException("No such tenant: " + args.tenant_name)
        print(uuid)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} tenant list --list-columns=name"


class TenantCreate(Command):
    COMMAND_DESCRIPTION = "create a new tenant"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument(
            '--tenant-id', help="Do not create UUID, but usethis id")
        parser.add_argument('name', help="tenant name")
        parser.add_argument(
            'restrict_to_network',
            default = "", nargs='?',
            help="list of one or more IP networks belonging to the domain. "
                 "Notation: <address>/<netmask length>")
        parser.add_argument('labels',
            default = "", nargs='?',
            help="comma-separated list of labels with the format: <name>:<value>")

    def run(self, args):
        restrict_to_network = strToList(args.restrict_to_network)
        labels = parse_labels(args.labels)
        tenant = {
            'name': args.name,
            'restrict_to_network': restrict_to_network}
        if args.tenant_id:
            tenant['tenant_uuid'] = args.tenant_id
        call = {'tenant': tenant, 'on_create_label': labels}
        create_result = self._json_rpc.call("setTenant", call)
        print("Success. Created new tenant with tenant id " + create_result['tenant_id'])


class TenantCreateWithId(TenantCreate):
    COMMAND_DESCRIPTION = "create a new tenant with a specific tenant uuid. ONLY SPECIAL USE."

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('name', help="tenant name")
        parser.add_argument('tenant_uuid', help="tenant UUID")
        parser.add_argument('restrict_to_network',
            default = "", nargs='?',
            help="list of one or more IP networks belonging to the domain. "
                 "Notation: <address>/<netmask length>")
        parser.add_argument('labels',
            default = "", nargs='?',
            help="comma-separated list of labels with the format: <name>:<value>")


class TenantUpdateName(Command):
    COMMAND_DESCRIPTION = "update a tenant's name"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_uuid_or_name', help="tenant name or UUID")
        parser.add_argument('new_name', help="tenant name")

    def run(self, args):
        uuid, name = self._resolver.resolve_tenant(args.tenant_uuid_or_name)
        result = self._json_rpc.call("getTenant", {'tenant_id': [uuid]})
        tenant = result['tenant'][0]

        tenant['name'] = args.new_name

        self._json_rpc.call("setTenant", {'tenant': tenant})
        print("Success. Renamed tenant %s from %s to %s" % (uuid, name, args.new_name))

class TenantUpdateRestrictToNetwork(Command):
    COMMAND_DESCRIPTION = "update a tenant's network restrictions"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_uuid_or_name', help="tenant name or UUID")
        parser.add_argument('restrict_to_network', help="list of comma-separated subnets")

    def run(self, args):
        uuid, _ = self._resolver.resolve_tenant(args.tenant_uuid_or_name)
        result = self._json_rpc.call("getTenant", {'tenant_id': [uuid]})
        tenant = result['tenant'][0]

        tenant['restrict_to_network'] = strToList(args.restrict_to_network)

        self._json_rpc.call("setTenant", {'tenant': tenant})
        print("Success. Updated tenant " + uuid)

class TenantUpdateAddVolumeAccess(Command):
    COMMAND_DESCRIPTION = "update a tenant's volume access"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_uuid_or_name', help="tenant name or UUID")
        parser.add_argument('volume_uuid_or_name', help="volume name or UUID")
        parser.add_argument('read_only', type=bool, default=False)
        parser.add_argument('restrict_to_network', help="network")

    def run(self, args):
        uuid, _ = self._resolver.resolve_tenant(args.tenant_uuid_or_name)
        result = self._json_rpc.call("getTenant", {'tenant_id': [uuid]})
        tenant = result['tenant'][0]

        volume_access = {}
        volume_uuid, _, _ = self._resolver.resolve_volume(
            args.tenant_uuid_or_name + '/' + args.volume_uuid_or_name)
        volume_access['volume_uuid'] = volume_uuid
        volume_access['read_only'] = args.read_only

        if args.restrict_to_network:
            volume_access_network = args.restrict_to_network
            volume_access['restrict_to_network'] = volume_access_network
            volume_access_list = tenant.get('volume_access')
            # remove duplicate networks
            for item in volume_access_list[:]:
                if item.get('volume_uuid') != volume_uuid:
                    continue
                elif item.get('restrict_to_network') == volume_access_network:
                    volume_access_list.remove(item)

            tenant['volume_access'].append(volume_access)

        self._json_rpc.call("setTenant", {'tenant': tenant})
        print("Success. Updated tenant " + uuid)

class TenantUpdateRemoveVolumeAccess(Command):
    COMMAND_DESCRIPTION = "update a tenant's volume access"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_uuid_or_name', help="tenant name or UUID")
        parser.add_argument('volume_uuid_or_name', help="volume name or UUID")
        parser.add_argument('remove_network', help="Comma separated list of network", default=None)

    def run(self, args):
        uuid, _ = self._resolver.resolve_tenant(args.tenant_uuid_or_name)
        result = self._json_rpc.call("getTenant", {'tenant_id': [uuid]})
        tenant = result['tenant'][0]

        try:
            volume_uuid, _, _ = self._resolver.resolve_volume(
                args.tenant_uuid_or_name + '/' + args.volume_uuid_or_name)
        except InvalidInputException:
            # Allow removal of network restrictions for deleted volumes if given a UUID.
            volume_uuid = args.volume_uuid_or_name
            if not is_valid_uuid(volume_uuid):
                raise
        volume_access_list = tenant.get('volume_access')
        for item in volume_access_list[:]:
            if item.get('volume_uuid') != volume_uuid:
                continue
            if args.remove_network and item.get('restrict_to_network') != args.remove_network:
                continue
            volume_access_list.remove(item)

        self._json_rpc.call("setTenant", {'tenant': tenant})
        print("Success. Updated tenant " + uuid)

class TenantDelete(Command):
    COMMAND_DESCRIPTION = "delete a tenant"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_uuid_or_name', help="tenant name or UUID")

    def run(self, args):
        tenant_uuid, tenant_name = self._resolver.resolve_tenant(args.tenant_uuid_or_name)
        _confirmation_prompt("Really delete tenant %s (%s)" % (tenant_uuid, tenant_name))
        self._json_rpc.call("deleteTenant", {'tenant_id': tenant_uuid})
        print("Success, deleted tenant " + args.tenant_uuid_or_name)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} tenant delete"

class TenantList(Command):
    COMMAND_DESCRIPTION = "list tenants"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        result = self._json_rpc.call("getTenant", {})
        ten_list = result['tenant']
        if ten_list:
            printer = PrettyTable(
                args,
                ten_list,
                ['name', 'tenant_id', 'restrict_to_network'],
                ["Name", "UUID", "restrict_to_network"])
            print(printer.out())
        else:
            print_warning(0, "Tenant list is empty.")

    @staticmethod
    def completion_cmd():
        return {"volume_access":
                "qmgmt${opts} tenant list --list-columns=name"}

class TenantShow(Command):
    COMMAND_DESCRIPTION = "show tenant details"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_uuid_or_name', help="tenant name or UUID")
        PrettyTable.add_specific_arguments(parser)

    def print_volume_access(self, args, uuid):
        call = {'tenant_id': [uuid]}
        result = self._json_rpc.call("getTenant", call)
        ten_list = result['tenant']
        if ten_list:
            vol_list = ten_list[0]['volume_access']
            if vol_list:
                printer = PrettyTable(
                    args,
                    vol_list,
                    ['volume_uuid', 'read_only', 'restrict_to_network'],
                    ["Volume UUID", "read only", "restrict to network"])
                print(printer.out())
            else:
                print_warning(0, "Volume access list for tenant: %s is empty." % uuid)

    def run(self, args):
        uuid, _ = self._resolver.resolve_tenant(args.tenant_uuid_or_name)
        self.print_volume_access(args, uuid)

    @staticmethod
    def completion_cmd():
        return {"volume_access":
                "qmgmt${opts} tenant list --list-columns=name"}

class VolumeCreate(Command):
    COMMAND_DESCRIPTION = "create a new volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_slash_name', help="[<tenant name or uuid>]/<new volume name>")
        parser.add_argument('user', help="user name of owner of root directory")
        parser.add_argument('group', help="group name of owner of root directory")
        parser.add_argument('access_mode', help="mode of root directory",
                             default="0700", nargs='?')
        parser.add_argument('labels',
                            help="comma-separated list of labels, <name>:<value>",
                            nargs='?', default="")
        parser.add_argument('encryption_profile', help="", nargs='?')

    def run(self, args):
        labels = parse_labels(args.labels)

        tenant_uuid, volume_name = self._resolver.resolve_new_volume(
            args.tenant_slash_name)

        call = {'name': volume_name,
                'root_user_id': args.user,
                'root_group_id': args.group,
                'access_mode': int(args.access_mode),
                'tenant_id': tenant_uuid,
                'label': labels}

        create_result = self._json_rpc.call("createVolume", call)
        print("Success. Created new volume with volume uuid",
              create_result['volume_uuid'])

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} tenant list --list-columns=name"


class VolumeMirror(Command):
    COMMAND_DESCRIPTION = "create mirrored volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_slash_name', help="[<tenant name or uuid>]/<new volume name>")
        parser.add_argument('location_slash_volume',
             help="[<external storage location uuid>/]<source volume uuid>, " +
                  "if empty, mirror volume from local cluster ")

    def create_mirror(self, args):
        local_tenant, local_volume_name = self._resolver.resolve_new_volume(
            args.tenant_slash_name)

        remote_volume = args.location_slash_volume
        if '/' in remote_volume:
            external_storage_location_uuid, remote_volume_uuid = remote_volume.split("/", 1)
        else:
            external_storage_location_uuid = ""
            remote_volume_uuid = remote_volume

        if not is_valid_uuid(remote_volume_uuid):
            raise InvalidInputException("Source volume is not a valid uuid: " + remote_volume_uuid)

        call = {'local_volume_name': local_volume_name,
                'local_tenant_id': local_tenant,
                'remote_volume_uuid': remote_volume_uuid,
                'external_storage_location_uuid': external_storage_location_uuid}
        return self._json_rpc.call("createMirroredVolume", call)

    def run(self, args):
        new_volume_uuid = self.create_mirror(args)['volume_uuid']
        print("Success. Created new mirrored volume with uuid", new_volume_uuid)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class VolumeDisconnect(Command):
    COMMAND_DESCRIPTION = "disconnect mirrored volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)

    def run(self, args):
        volume_uuid, volume_name, _ = self._resolver.resolve_volume(args.volume_handle)

        _confirmation_prompt(
            "Really disconnect mirrored volume %s (%s)" % (volume_name, volume_uuid))
        self._json_rpc.call("disconnectMirroredVolume", {'volume_uuid': volume_uuid})
        print("Success, disconnected volume " + args.volume_handle)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class VolumeErase(Command):
    COMMAND_DESCRIPTION = "erase a volume with all its files from data devices"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        parser.add_argument('--force',
                            help="erase volume immediately", action="store_true")
        parser.add_argument('--cancel', help="cancel scheduled or running volume erasure",
                            action="store_true")

    def run(self, args):
        volume_uuid, volume_name, tenant_uuid = self._resolver.resolve_volume(
            args.volume_handle)
        tenant_name = self._resolver.lookup_tenant_uuid(tenant_uuid)

        if args.cancel:
            self._json_rpc.call("cancelVolumeErasure", {'volume_uuid': volume_uuid})
            print("Success, cancelled erasure of volume %s (%s)" % (volume_name, volume_uuid))
        else:
            _confirmation_prompt(
                "Really erase volume %s (%s) in tenant %s (%s)?"
                % (volume_name, volume_uuid, tenant_name, tenant_uuid))
            self._json_rpc.call("eraseVolume", {'volume_uuid': volume_uuid, 'force': args.force})
            print("Success, requested erasure of volume " + args.volume_handle + ".")

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class VolumeDelete(Command):
    COMMAND_DESCRIPTION = "delete volumem metadata (requires cleanup task to delete data)"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)

    def run(self, args):
        volume_uuid, volume_name, tenant_uuid = self._resolver.resolve_volume(
            args.volume_handle)
        tenant_name = self._resolver.lookup_tenant_uuid(tenant_uuid)

        _confirmation_prompt(
            "Really delete volume %s (%s) in tenant %s (%s)?"
            % (volume_name, volume_uuid, tenant_name, tenant_uuid))

        self._json_rpc.call("deleteVolume", {'volume_uuid': volume_uuid})
        print("Success, deleted volume metadata of volume '" + volume_name + ". "
            + "The next device clean-up task will delete file data.")

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class SnapshotCreate(Command):
    COMMAND_DESCRIPTION = "create a new snapshot"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        parser.add_argument('snapshot_name', help="Name of snapshot")
        parser.add_argument('--comment', help="comment", default="")
        parser.add_argument('--pin',
                            help="exclude from automatic deletion by policy",
                            action="store_true")

    def run(self, args):
        volume_uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)

        self._json_rpc.call("createSnapshot", {'volume_uuid': volume_uuid,
                                               'name': args.snapshot_name,
                                               'comment': get_comment(args),
                                               'pinned': args.pin})
        print("Success, created snapshot " + args.snapshot_name + " on volume " + volume_uuid)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class SnapshotUpdate(Command):
    COMMAND_DESCRIPTION = "update a snapshot"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        parser.add_argument('snapshot_name', help="Name of snapshot")
        parser.add_argument('--comment', help="comment")
        parser.add_argument('--pin',
                            help="exclude from automatic deletion by policy",
                            action="store_true")

    def run(self, args):
        volume_uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)

        request = {'volume_uuid': volume_uuid, 'name': args.snapshot_name}
        has_update = False
        if args.comment:
            has_update = True
            request["comment"] = args.comment
        if args.pin:
            has_update = True
            request["pinned"] = args.pin
        if has_update:
            self._json_rpc.call("updateSnapshot", request)
            print("Success, updated snapshot", args.snapshot_name, "of volume", volume_uuid)
        else:
            print("No update requested for snapshot", args.snapshot_name)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} snapshot update --list-columns=tenant_domain,name"


class SnapshotErase(Command):
    COMMAND_DESCRIPTION = "erase a snapshot with all unreferenced files from data devices"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        parser.add_argument('snapshot_name', help="Name of snapshot")

    def run(self, args):
        volume_uuid, volume_name, _ = self._resolver.resolve_volume(args.volume_handle)
        _confirmation_prompt(
            "Really erase snapshot %s from volume %s (%s)?"
            % (args.snapshot_name, volume_name, volume_uuid))
        self._json_rpc.call(
            "eraseSnapshot", {'volume_uuid': volume_uuid, 'name': args.snapshot_name})
        print("Success, snapshot", args.snapshot_name, "from volume", volume_uuid, "will be erased")

    @staticmethod
    def completion_cmd():
        return ("qmgmt${opts} volume list --list-columns=tenant_domain,name",
                "qmgmt${opts} snapshot list ${prev} --list-columns=name")


class SnapshotDelete(Command):
    COMMAND_DESCRIPTION = "delete a snapshot without erasing any files from data devices"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        parser.add_argument('snapshot_name', help="Name of snapshot")

    def run(self, args):
        volume_uuid, volume_name, _ = self._resolver.resolve_volume(args.volume_handle)
        _confirmation_prompt(
            "Really delete snapshot %s from volume %s (%s)?"
            % (args.snapshot_name, volume_name, volume_uuid))
        self._json_rpc.call(
            "deleteSnapshot", {'volume_uuid': volume_uuid, 'name': args.snapshot_name})
        print("Success, snapshot", args.snapshot_name,
            "from volume", volume_uuid, "will be deleted")

    @staticmethod
    def completion_cmd():
        return ("qmgmt${opts} volume list --list-columns=tenant_domain,name",
                "qmgmt${opts} snapshot list ${prev} --list-columns=name")


class SnapshotList(Command):
    COMMAND_DESCRIPTION = "list snapshots"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        snapshots = self._json_rpc.call("listSnapshots", {'volume_uuid': uuid})['snapshot']
        printer = PrettyTable(
            args,
            snapshots,
            ['version', 'name', 'timestamp', 'comment', 'pinned'],
            ["Version", "Name", "Created", "Comment", "Pinned"])
        print(printer.out())

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class LicenseKeyImport(Command):
    COMMAND_DESCRIPTION = "import license key"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('license', help="license file or string. if not give, reads stdin")

    def do_import(self, args):
        key_data = args.license
        if os.path.isfile(key_data):
            try:
                with open(key_data, 'r', encoding="utf-8") as f:
                    key_data = f.readline().strip()
            except Exception:
                pass
        params = {'key': key_data}
        call_result = self._json_rpc.call("setLicenseKey", params)

        if call_result["verification_result"] == "OK":
            status_code = 0
            print("Success. License imported.")
        else:
            status_code = -1
            print("Failure. Verification result: ", call_result["verification_result"])
        return status_code

    def run(self, args):
        if not args.license:
            keydata = sys.stdin.readline().strip()
            args.append(keydata)
        return self.do_import(args)


class ConfigurationBase(Command):

    def __init__(self):
        super(Command, self).__init__()
        assert False, "overwrite me"
        self._configuration_type = None
        self._configuration_field = None
        self._configuration_cmd = None
        self._name_field = None
        self._delete_disabled = None

    def get_config_list(self):
        request = {}
        request['configuration_type'] = self._configuration_type
        result = self._json_rpc.call("getConfiguration", request)
        return result[self._configuration_field]

    def run_config(self, args, sub_command, config_name):
        request = {}
        request['configuration_type'] = self._configuration_type

        if sub_command == "export":
            request['configuration_type'] = self._configuration_type
            request['configuration_name'] = config_name
            result = self._json_rpc.call("exportConfiguration", request)
            dump = str(result['proto_dump'])
            if args.file:
                DataDumpHelper.export_to_file(dump, args.file)
            else:
                print(dump)
        elif sub_command == "import":
            request['configuration_type'] = self._configuration_type
            request['configuration_name'] = config_name
            if args.file:
                try:
                    dump = DataDumpHelper.import_from_file(args.file)
                except FileNotFoundError:
                    print_warning(1, "User provided configuration file '", args.file, "' "
                                  "does not exist")
                    return -2
            else:
                dump = DataDumpHelper.import_from_stdin()
            request['proto_dump'] = dump
            self._json_rpc.call("importConfiguration", request)
            print("Success. Updated configuration " + config_name)
        elif sub_command == "delete":
            if self._delete_disabled:
                print_warning(1, "Not supported")
                return -2
            request['configuration_type'] = self._configuration_type
            request['id'] = config_name
            result = self._json_rpc.call("deleteConfiguration", request)
            print("Success. Deleted configuration " + config_name)
        elif sub_command == "list":
            if not self._name_field:
                print_warning(1, "Not supported, use export")
                return -2
            if self._configuration_cmd in ['quota']:
                quotas = self._json_rpc.call(
                    "getQuota", {'include_default_quotas': True})['quotas']
                # Hide SYSTEM consumer type from customer
                for i in range(len(quotas) - 1, -1, -1):
                    if quotas[i]['consumer'][0].get('type') == "SYSTEM":
                        del quotas[i]
                printer = PrettyTable(
                    args,
                    quotas,
                    [('consumer', 0, 'tenant_id'),
                     ('limits', 0, 'limit_type'),
                     'id',
                     ('consumer', 0, 'type'),
                     ('consumer', 0,  'identifier'),
                     ('limits', 0, 'type'),
                     ('limits', 0, 'value'),
                     ('current_usage', 0, 'value')],
                    ["Tenant", "Quota-Type", "Quota-ID", "Consumer-Type",
                     "Consumer-ID", "Resource", "Limit", "Usage"], sorting_depth=2)
                for row_number in range(len(quotas)):
                    unit_type = quotas[row_number]['limits'][0].get('type')
                    consumer_type = quotas[row_number]['consumer'][0].get('type')
                    consumer_id = quotas[row_number]['consumer'][0].get('identifier')
                    tenant_id = quotas[row_number]['consumer'][0].get('tenant_id')
                    current_usage = quotas[row_number].get('current_usage')

                    # Modify cell: to human readible bytes
                    if (unit_type is not None) and \
                       (unit_type != 'FILE_COUNT') and \
                       (unit_type != 'VOLUME_COUNT'):

                        # quota limit
                        printer.modify_cell(
                            row_number,
                            ('limits', 0, 'value'),
                            human_readable_bytes)

                        # quota usage
                        if (current_usage is not None) and \
                            len(current_usage) > 0:
                            printer.modify_cell(
                                row_number,
                                ('current_usage', 0, 'value'),
                                human_readable_bytes)

                    # Modify cell: Consumer-ID Lookup
                    if (consumer_id is not None) and \
                       (consumer_type is not None):
                        if consumer_type == "TENANT":
                            printer.modify_cell(
                                row_number,
                                ('consumer', 0, 'identifier'),
                                self._resolver.lookup_tenant_uuid
                            )
                        elif consumer_type == "VOLUME":
                            printer.modify_cell(
                                row_number,
                                ('consumer', 0, 'identifier'),
                                self._resolver.lookup_volume_uuid
                            )

                    # Modify cell: Tenant Lookup
                    if tenant_id is not None:
                        printer.modify_cell(
                            row_number,
                            ('consumer', 0, 'tenant_id'),
                            self._resolver.lookup_tenant_uuid
                        )
                if hasattr(args, 'filter') and args.filter:
                    printer.filter_values(
                        args.filter,
                        self._resolver.lookup_tenant_uuid(args.filter),
                        self._resolver.lookup_volume_uuid(args.filter))
                print(printer.out())
            else:
                for config in self.get_config_list():
                    print(config[self._name_field])

        elif sub_command == "edit":
            request['configuration_type'] = self._configuration_type
            request['configuration_name'] = config_name
            export_result = self._json_rpc.call("exportConfiguration", request)
            before = str(export_result['proto_dump'])
            try:
                after = DataDumpHelper.edit(before)
                if after != "":
                    request['proto_dump'] = after
                    self._json_rpc.call("importConfiguration", request)
                    print("Success. Updated configuration " + config_name)
                else:
                    print("Success. Configuration %s was not modified." % config_name)
            except IOError as e:
                print("Failed. Configuration %s was not updated: %s" % (config_name, str(e)))
        else:
            print_warning(1, "Unknown subcommand")
            return -2

    def completion_cmd(self):
        r = {
            "export": "qmgmt${opts} " + self._configuration_cmd +
                      " list --list-columns=" + (self._name_field or ""),
            "import": "echo __files__",
        }
        if self._name_field and not self._delete_disabled:
            r["delete"] = "qmgmt${opts} " + self._configuration_cmd + \
                          " list --list-columns=" + self._name_field
        if self._name_field:
            r["list"] = None
            r["edit"] = "qmgmt${opts} " + self._configuration_cmd + \
                        " list --list-columns=" + (self._name_field or "")
        else:
            r["edit"] = None
        if self._configuration_cmd == 'user config':
            r["add"] = None
            r["set-password"] = None
        elif self._configuration_cmd == 'quota':
            r["create"] = None

        return r


class SystemConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        super(ConfigurationBase, self).__init__(json_rpc)
        self._configuration_type = 'SYSTEM_CONFIGURATION'
        self._configuration_field = 'system_configuration'
        self._configuration_cmd = 'systemconfig'
        self._name_field = None
        self._delete_disabled = True

class SystemConfigurationExport(SystemConfiguration):
    COMMAND_DESCRIPTION = "export general system configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        return self.run_config(args, "export", "ALL")

class SystemConfigurationImport(SystemConfiguration):
    COMMAND_DESCRIPTION = "import general system configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        return self.run_config(args, "import", "ALL")

class SystemConfigurationDelete(SystemConfiguration):
    COMMAND_DESCRIPTION = "delete general system configuration"

    def run(self, args):
        return self.run_config(args, "delete", "ALL")

class SystemConfigurationEdit(SystemConfiguration):
    COMMAND_DESCRIPTION = "edit general system configuration"

    def run(self, args):
        return self.run_config(args, "edit", "ALL")


class FailureDomainsConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        super(ConfigurationBase, self).__init__(json_rpc)
        self._configuration_type = 'FAILURE_DOMAINS'
        self._configuration_field = 'failure_domain_configuration'
        self._configuration_cmd = 'failuredomains'
        self._name_field = None
        self._delete_disabled = False

class FailureDomainsConfigurationExport(FailureDomainsConfiguration):
    COMMAND_DESCRIPTION = "export failure domains configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        return self.run_config(args, "export", "ALL")

class FailureDomainsConfigurationImport(FailureDomainsConfiguration):
    COMMAND_DESCRIPTION = "import failure domains configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        return self.run_config(args, "import", "ALL")

class FailureDomainsConfigurationDelete(FailureDomainsConfiguration):
    COMMAND_DESCRIPTION = "delete failure domains configuration"

    def run(self, args):
        return self.run_config(args, "delete", "ALL")

class FailureDomainsConfigurationEdit(FailureDomainsConfiguration):
    COMMAND_DESCRIPTION = "edit failure domains configuration"

    def run(self, args):
        return self.run_config(args, "edit", "ALL")


class QuotaConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        super(ConfigurationBase, self).__init__(json_rpc)
        self._configuration_type = 'QUOTA_POOL'
        self._configuration_field = 'quota_pool_configuration'
        self._configuration_cmd = 'quota'
        self._name_field = 'id'
        self._delete_disabled = False

class QuotaConfigurationList(QuotaConfiguration):
    COMMAND_DESCRIPTION = "manage quota configuration"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)
        parser.add_argument('filter', help="filter", nargs='?')

    def run(self, args):
        return self.run_config(args, "list", None)

class QuotaConfigurationExport(QuotaConfiguration):
    COMMAND_DESCRIPTION = "export quota configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        return self.run_config(args, "export", args.id)

class QuotaConfigurationImport(QuotaConfiguration):
    COMMAND_DESCRIPTION = "import quota configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        return self.run_config(args, "import", args.id)

class QuotaConfigurationDelete(QuotaConfiguration):
    COMMAND_DESCRIPTION = "delete quota configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "delete", args.id)

class QuotaConfigurationEdit(QuotaConfiguration):
    COMMAND_DESCRIPTION = "edit quota configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "edit", args.id)

class QuotaConfigurationCreate(Command):
    COMMAND_DESCRIPTION = "create a quota"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument(
            'type', help="quota type", type=str.upper,
            choices = ['DEVICE', 'GROUP', 'SYSTEM', 'TENANT', 'USER', 'VOLUME'])
        parser.add_argument(
            'identifier', help="identifier: uuid, name, ...")
        parser.add_argument(
            'resource_type', help="resource type", type=str.upper,
            choices = [
                "DIRECTORY_COUNT", "FILE_COUNT",
                "PHYSICAL_DISK_SPACE", "LOGICAL_DISK_SPACE",
                "HDD_PHYSICAL_DISK_SPACE", "HDD_LOGICAL_DISK_SPACE",
                "SSD_PHYSICAL_DISK_SPACE", "SSD_LOGICAL_DISK_SPACE"])
        parser.add_argument(
            'resource_value', help="resource limit", type=int)
        parser.add_argument(
            'tenant_id', help="needs to be specified for USER GROUP VOLUME consumer type",
            nargs='?')

    def run(self, args):
        consumer_type = args.type
        identifier = args.identifier
        resource_type = args.resource_type
        resource_value = args.resource_value

        if consumer_type == 'TENANT':
            identifier, _ = self._resolver.resolve_tenant(identifier)
        elif consumer_type == 'VOLUME':
            identifier, _, _ = self._resolver.resolve_volume(identifier)

        quota = {}
        quota['consumer'] = [{'identifier': str(identifier), 'type': consumer_type}]

        if args.type in ['USER', 'GROUP', 'VOLUME']:
            tenant_uuid, _ = self._resolver.resolve_tenant(args.tenant_id)
            quota['consumer'][0].update({'tenant_id': tenant_uuid})

        quota['limits'] = [{'type': resource_type, 'value': int(resource_value)}]
        self._json_rpc.call("setQuota", {'quotas': [quota]})
        quotas = self._json_rpc.call("getQuota", {'only_entity': quota['consumer'],
                                                  'only_resource_type': [resource_type]})
        print("Success. Created new quota, with id:", quotas['quotas'][0]['id'])


class RulesConfiguration(ConfigurationBase):
    COMMAND_DESCRIPTION = "manage automation rule configurations"

    def __init__(self, json_rpc):
        super(ConfigurationBase, self).__init__(json_rpc)
        self._configuration_type = 'RULE_CONFIGURATION'
        self._configuration_field = 'rule_configuration'
        self._configuration_cmd = 'rules'
        self._name_field = 'rule_identifier'
        # rules cannot be deleted because the set of rules is immutable
        self._delete_disabled = True

class RulesConfigurationList(RulesConfiguration):
    COMMAND_DESCRIPTION = "manage alert rules configuration"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)
        parser.add_argument('filter', help="filter", nargs='?')

    def run(self, args):
        return self.run_config(args, "list", None)

class RulesConfigurationExport(RulesConfiguration):
    COMMAND_DESCRIPTION = "export alert rules configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        return self.run_config(args, "export", args.id)

class RulesConfigurationImport(RulesConfiguration):
    COMMAND_DESCRIPTION = "import alert rules configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        return self.run_config(args, "import", args.id)

class RulesConfigurationDelete(RulesConfiguration):
    COMMAND_DESCRIPTION = "delete alert rules configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "delete", args.id)

class RulesConfigurationEdit(RulesConfiguration):
    COMMAND_DESCRIPTION = "edit alert rules configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "edit", args.id)


class ExternalStorageLocationsConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        super(ConfigurationBase, self).__init__(json_rpc)
        self._configuration_type = 'EXTERNAL_STORAGE_LOCATION'
        self._configuration_field = 'external_storage_location'
        self._configuration_cmd = 'external_locations'
        self._name_field = 'id'
        self._delete_disabled = False


class ExternalStorageLocationsConfigurationList(ExternalStorageLocationsConfiguration):
    COMMAND_DESCRIPTION = "list external storage location (S3, glcoud, azure) configuration"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        request = {}
        request['configuration_type'] = self._configuration_type
        result = self._json_rpc.call("getConfiguration", request)
        printer = PrettyTable(
            args,
            result['external_storage_location'],
            ['id', 'name', 'location_type'],
            ["Id", "Name", "Type"]
        )
        print(printer.out())


class ExternalStorageLocationsConfigurationCreate(ExternalStorageLocationsConfiguration):
    COMMAND_DESCRIPTION = "create external storage location (S3, glcoud, azure) configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('name', help="location name")
        parser.add_argument('location_type', type=str.upper,
                            choices=['EXTERNAL_STORAGE_S3',
                                     'EXTERNAL_STORAGE_AZURE',
                                     'EXTERNAL_STORAGE_QUOBYTE'])

    def run(self, args):
        name = args.name
        location_type = args.location_type
        template = {"name": name, "location_type": location_type, "comment": ""}
        if location_type == "EXTERNAL_STORAGE_S3":
            template["s3"] = {"bucket_url": "",
                                "access_key_id": "",
                                "access_key_secret": ""}
        elif location_type == "EXTERNAL_STORAGE_AZURE":
            template["azure"] = {"container_url": "",
                                    "access_key": ""}
        elif location_type == "EXTERNAL_STORAGE_QUOBYTE":
            template["quobyte"] = {
                "registry": [
                    "add here list of registries (SRV-record or address)"
                ]}
        before_edit = json.dumps(template, sort_keys=False, indent=2, separators=(',', ': ',))
        try:
            after_edit = DataDumpHelper.edit_json(before_edit)
            if not after_edit:
                print("Aborted. Configuration was not modified.")
            else:
                create_request = {
                    "configuration_type": self._configuration_type,
                    "external_storage_location": json.loads(after_edit)
                }
                self._json_rpc.call("createConfiguration", create_request)
                print("Success. Created new external storage location.")
        except IOError as e:
            print("Failed. Configuration %s was not created: %s" % (str(e)))


class ExternalStorageLocationsConfigurationEdit(ExternalStorageLocationsConfiguration):
    COMMAND_DESCRIPTION = "manage external storage location (S3, glcoud, azure) configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('name', help="location name")

    def run(self, args):
        request = {}
        config_name = args.name
        request['configuration_type'] = self._configuration_type
        request['configuration_name'] = config_name
        result = self._json_rpc.call("getConfiguration", request)
        if "external_storage_location" not in result or not result["external_storage_location"]:
            raise Exception("No external storage location found for " + config_name)
        config = result["external_storage_location"][0]
        before_edit = json.dumps(
            config, sort_keys=False, indent=2, separators=(',', ': ',))
        try:
            after_edit = DataDumpHelper.edit_json(before_edit)
            if not after_edit:
                print("Success. Configuration %s was not modified." % config_name)
            else:
                update_request = {
                    "configuration_type": self._configuration_type,
                    "external_storage_location": json.loads(after_edit)
                }
                self._json_rpc.call("updateConfiguration", update_request)
                print("Success. Updated configuration " + config_name)
        except IOError as e:
            print("Failed. Configuration %s was not updated: %s" % (config_name, str(e)))


class ExternalStorageLocationsConfigurationDelete(ExternalStorageLocationsConfiguration):
    COMMAND_DESCRIPTION = "manage external storage location (S3, glcoud, azure) configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('name', help="location name")

    def run(self, args):
        return self.run_config(args, "delete", args.name)


class ExternalStorageLocationsConfigurationImport(ExternalStorageLocationsConfiguration):
    COMMAND_DESCRIPTION = "manage external storage location (S3, glcoud, azure) configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        return self.run_config(args, "import", '')


class ExternalStorageLocationsConfigurationExport(ExternalStorageLocationsConfiguration):
    COMMAND_DESCRIPTION = "manage external storage location (S3, glcoud, azure) configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="location id")
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        return self.run_config(args, "export", args.id)


class NfsExportsConfiguration(ConfigurationBase):
    COMMAND_DESCRIPTION = "manage NFS exports configuration"

    def __init__(self, json_rpc):
        super(ConfigurationBase, self).__init__(json_rpc)
        self._configuration_type = 'EXPORTS'
        self._configuration_field = 'exports'
        self._configuration_cmd = 'nfs-exports'
        self._name_field = 'id'
        self._delete_disabled = False

class NfsExportsConfigurationList(NfsExportsConfiguration):
    COMMAND_DESCRIPTION = "list NFS exports configurations"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        return self.run_config(args, "list", None)

class NfsExportsConfigurationExport(NfsExportsConfiguration):
    COMMAND_DESCRIPTION = "export NFS exports configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        return self.run_config(args, "export", args.id)

class NfsExportsConfigurationImport(NfsExportsConfiguration):
    COMMAND_DESCRIPTION = "import NFS exports configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        return self.run_config(args, "import", args.id)

class NfsExportsConfigurationDelete(NfsExportsConfiguration):
    COMMAND_DESCRIPTION = "delete NFS exports configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "delete", args.id)

class NfsExportsConfigurationEdit(NfsExportsConfiguration):
    COMMAND_DESCRIPTION = "edit NFS exports configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "edit", args.id)


class NfsVirtualIpsConfiguration(ConfigurationBase):
    COMMAND_DESCRIPTION = "manage NFS virtual ips configuration"

    def __init__(self, json_rpc):
        super(ConfigurationBase, self).__init__(json_rpc)
        self._configuration_type = 'VIRTUAL_IP'
        self._configuration_field = 'virtual_ips'
        self._configuration_cmd = 'nfs-virtualips'
        self._name_field = 'id'
        self._delete_disabled = False

class NfsVirtualIpsConfigurationList(NfsVirtualIpsConfiguration):
    COMMAND_DESCRIPTION = "list NFS virtual IP configurations"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        return self.run_config(args, "list", None)

class NfsVirtualIpsConfigurationExport(NfsVirtualIpsConfiguration):
    COMMAND_DESCRIPTION = "export NFS virtual IP configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        return self.run_config(args, "export", args.id)

class NfsVirtualIpsConfigurationImport(NfsVirtualIpsConfiguration):
    COMMAND_DESCRIPTION = "import NFS virtual IP configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        return self.run_config(args, "import", args.id)

class NfsVirtualIpsConfigurationDelete(NfsVirtualIpsConfiguration):
    COMMAND_DESCRIPTION = "delete NFS virtual IP configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "delete", args.id)

class NfsVirtualIpsConfigurationEdit(NfsVirtualIpsConfiguration):
    COMMAND_DESCRIPTION = "edit NFS virtual IP configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "edit", args.id)


class UserConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        super(ConfigurationBase, self).__init__(json_rpc)
        self._configuration_type = 'USER'
        self._configuration_field = 'user_configuration'
        self._configuration_cmd = 'user config'
        self._name_field = 'id'
        self._delete_disabled = False

class UserConfigurationAdd(UserConfiguration):
    COMMAND_DESCRIPTION = "add user"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('user_id', help="user name")
        parser.add_argument('email')
        parser.add_argument('role')
        parser.add_argument('password')
        parser.add_argument('--member-of-tenant', help='Comma-separated list')
        parser.add_argument('--admin-of-tenant', help='Comma-separated list')
        parser.add_argument('--primary-group')
        parser.add_argument('--member-of-group', help='Comma-separated list')

    def populate_array_with_tenant_ids(self, array, comma_separated_tenant_list):
        if comma_separated_tenant_list:
            tenants = comma_separated_tenant_list.split(",")
            for tenant in tenants:
                tenant = tenant.strip()
                if tenant:
                    tenant_uuid, _ = self._resolver.resolve_tenant(tenant)
                    if tenant_uuid:
                        array.append(tenant_uuid)
        if len(array) > 1:
            # remove possible duplicates
            array = list(dict.fromkeys(array))

    def run(self, args):
        user_id = args.user_id
        email = args.email
        password = args.password
        role = args.role
        g_member = args.member_of_group
        t_member = args.member_of_tenant
        t_admin = args.admin_of_tenant
        primary_group = args.primary_group
        # To support legacy versions, check if the old input format should be used and set the
        # variables accordingly
        if not email and not password and not role and not g_member and not t_member \
                and not t_admin and not primary_group:
            # password = getpass.getpass()
            # install_quobyte.sh configure --password option.
            # Not visible to user, so not required to add in qmgmt help.
            if len(args) > 4 and args[-2] == "password":
                password = args[-1]
                args = args[:-2]
            if len(args) > 3:
                role = args[3]
            if len(args) > 4:
                t_member =args[4]
        member_of_tenants = []
        admin_of_tenants = []
        member_of_groups = []
        if g_member:
            groups = g_member.split(",")
            for group in groups:
                group = group.strip()
                if group:
                    member_of_groups.append(group)
        self.populate_array_with_tenant_ids(member_of_tenants, t_member)
        self.populate_array_with_tenant_ids(admin_of_tenants, t_admin)
        # set request
        request = {}
        if not password:
            password = get_password_from_prompt()
        request['user_name'] = user_id
        request['password'] = password
        if email:
            request['email'] = email
        if role:
            request['role'] = role
        if member_of_tenants:
            request['member_of_tenant_id'] = member_of_tenants
        if admin_of_tenants:
            request['admin_of_tenant_id'] = admin_of_tenants
        if primary_group:
            request['primary_group'] = primary_group
            # primary group should be also listed under the groups
            if primary_group in member_of_groups:
                member_of_groups.remove(primary_group)
            member_of_groups.insert(0, primary_group)
        if member_of_groups:
            request['member_of_group'] = member_of_groups
        else:
            request['member_of_group'] = [user_id]
        self._json_rpc.call("createUser", request)
        print("Success. Added user '%s'" % user_id)


class UserConfigurationSetPassword(UserConfiguration):
    COMMAND_DESCRIPTION = "change user password"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('user_id', nargs='?')
        parser.add_argument('--password')

    def run(self, args):
        if not args.user_id:
            shell_user = os.getenv("USER")
            sys.stderr.write("Username (" + shell_user + "): ")
            sys.stderr.flush()
            username = sys.stdin.readline().rstrip()
            if not username:
                username = shell_user
        else:
            username = args.user_id
        # get password
        new_password = args.password
        if not new_password:
            new_password = get_password_from_prompt()
        update_password_request = {"user_name": username, "password": new_password}
        self._json_rpc.call("updateUser", update_password_request)
        print("Success. Password for user '%s' has been updated." % username)


class UserConfigurationList(UserConfiguration):
    COMMAND_DESCRIPTION = "List users"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        request = {}
        request['configuration_type'] = self._configuration_type
        result = self._json_rpc.call("getConfiguration", request)
        printer = PrettyTable(
            args,
            result['user_configuration'],
            ['id', 'email', 'role'],
            ["Id", "Email", "Role"]
        )
        print(printer.out())


class UserConfigurationEdit(UserConfiguration):
    COMMAND_DESCRIPTION = "Edit users"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('user_id')

    def run(self, args):
        user_id = args.user_id
        response = self._json_rpc.call("getUsers", {"user_id": [user_id]})
        if "user_configuration" in response:
            help_message = (
                "# Update the parameters for user \"" + user_id + "\" (in JSON format).\n"
                "#\n"
                "# email:              email address of user\n"
                "# role:               user role. Following roles are currently supported:\n"
                "#                     SUPER_USER, SUPER_USER_READONLY, FILESYSTEM_ADMIN,\n"
                "#                     FILESYSTEM_ADMIN_READONLY, HARDWARE_OPERATOR\n"
                "#                     If no role is defined, user will have no privileges.\n"
                "#                     To define the role of tenant member or administrator\n"
                "#                     use member_of_tenant_id and admin_of_tenant_id fields\n"
                "#                     respectively\n"
                "# admin_of_tenant_id  list of tenant IDs, which can be administrated by user\n"
                "# member_of_tenant_id list of tenant IDs, of which the user is a member\n"
                "# primary_group       name of the user's primary group; required for the\n"
                "#                     creation of access keys for file system and S3\n"
                "# member_of_group     list of groups, of which the user is a member\n"
                "#\n"
            )
            if not response["user_configuration"]:
                print("Failed: cannot edit non-existing user " + user_id)
                return
            user_db_record = response["user_configuration"][0]
            initial_user_json = {
                "email": user_db_record.get("email", ""),
                "role": user_db_record.get("role")[0] if user_db_record.get("role", []) else "",
                "admin_of_tenant_id": user_db_record.get("admin_of_tenant_id", []),
                "member_of_tenant_id": user_db_record.get("member_of_tenant_id", []),
                "primary_group": user_db_record.get("primary_group", ""),
                "member_of_group": user_db_record.get("group", []),
            }
            initial_user_edit_text = help_message + json.dumps(
                initial_user_json, sort_keys=False, indent=2, separators=(',', ': '))
            try:
                edited_user_text = DataDumpHelper.edit_json(initial_user_edit_text)
                if edited_user_text != "":
                    update_user_request = json.loads(
                        edited_user_text.replace(help_message, "").replace("\'", "\""))
                    update_user_request["user_name"] = user_id
                    # check the user role config
                    if not update_user_request.get("role"):
                        update_user_request["delete_roles"] = True
                        update_user_request.pop("role", None)
                    self._json_rpc.call("updateUser", update_user_request)
                    print("Success. Updated user " + user_id)
                else:
                    print("Success. User %s was not modified." % user_id)
            except IOError as e:
                print("Failed. User %s was not updated: %s" % (user_id, str(e)))


class UserConfigurationExport(UserConfiguration):
    COMMAND_DESCRIPTION = "export user configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        return self.run_config(args, "export", args.id)

class UserConfigurationImport(UserConfiguration):
    COMMAND_DESCRIPTION = "import user configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        return self.run_config(args, "import", args.id)

class UserConfigurationDelete(UserConfiguration):
    COMMAND_DESCRIPTION = "delete user configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('id', help="id")

    def run(self, args):
        return self.run_config(args, "delete", args.id)


class PolicyRuleList(Command):
    COMMAND_DESCRIPTION = "list policy rules"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        call = {}
        result = self._json_rpc.call("getPolicyRules", call)
        policy_rules = result['policy_rule']
        printer = PrettyTable(
            args,
            policy_rules,
            ["enabled", "name", "creator", "description"],
            ["Enabled?", "Name", "Creator", "Description"]
        )
        print(printer.out())


class PolicyRuleEdit(Command):
    COMMAND_DESCRIPTION = "edit all policy rules in text editor"

    def run(self, args):
        call = {}
        result = self._json_rpc.call("exportPolicyRules", call)
        dump = result['proto_dump']
        try:
            edited_dump = DataDumpHelper.edit(dump)
            if edited_dump != "":
                call['creator'] = str(getpass.getuser())
                call['proto_dump'] = edited_dump
                self._json_rpc.call("importPolicyRules", call)
                print("Success. Updated policy rules.")
            else:
                print("Success. Policy rules not modified.")
        except IOError as e:
            print_warning(1, "Failed. Policy rules were not updated: %s" % str(e))


class PolicyRuleExport(Command):
    COMMAND_DESCRIPTION = "export policy rules to stdout or a file"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('file', help="output file", nargs='?')

    def run(self, args):
        call = {}
        result = self._json_rpc.call("exportPolicyRules", call)
        dump = result['proto_dump']
        if args.file:
            DataDumpHelper.export_to_file(dump, args.file)
        else:
            print(dump)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class PolicyRuleImport(Command):
    COMMAND_DESCRIPTION = "import policy rules from stdin or a file"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('file', help="input file", nargs='?')

    def run(self, args):
        call = {}
        if args.file:
            dump = DataDumpHelper.import_from_file(args.file)
        else:
            dump = DataDumpHelper.import_from_stdin()
        call['creator'] = str(getpass.getuser())
        call['proto_dump'] = dump
        self._json_rpc.call("importPolicyRules", call)
        print("Success. Imported policy rules.")

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class PolicyRuleFilter(Command):
    COMMAND_DESCRIPTION = "filter effective policy rules and/or policies for a subject"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument(
            'format',
            help="rules - lists effective policy rules with their effective policies, "
                 "policies - lists flat effective policies without containing policy rules.",
            choices = ['rules', 'policies'])
        parser.add_argument(
            'policy_subject',
            help="policy subject, followed by filters:"
             "tenant <tenant name or uuid>, "
             "volume [<tenant name or uuid>]/<volume name or uuid>, "
             "file [<tenant name or uuid>]/<volume name or uuid> </path/to/file>, "
             "client [<tenant name or uuid>]/<volume name or uuid> </path/to/file>"
                 " <client_ip_address> <NATIVE|S3|NFS>",
            choices = ['global', 'tenant', 'volume', 'file', 'client'])
        parser.add_argument('filter1',
                            help="[<tenant name or uuid>]/<volume name or uuid>",
                            nargs='?')
        parser.add_argument('filter2', nargs='?')
        parser.add_argument('filter3', nargs='?')
        parser.add_argument('filter4', nargs='?')

    def run(self, args):
        call = {}

        if args.format == "rules":
            call['policies_only'] = False
        elif args.format == "policies":
            call['policies_only'] = True

        subject = args.policy_subject
        policy_subject = {}
        if subject == "global":
            policy_subject['global'] = True
        elif subject == "tenant":
            if not args.filter1:
                print_warning(1, "Missing tenant UUID.")
                return -2
            policy_subject['tenant'] = {
                'uuid': self._resolver.resolve_tenant(args.filter1)[0]}
        elif subject == "volume":
            if not args.filter1:
                print_warning(1, "Missing volume UUID.")
                return -2
            policy_subject['volume'] = {
                'uuid': self._resolver.resolve_volume(args.filter1)[0]}
        elif subject == "file":
            if not args.filter1 or not args.filter2:
                print_warning(1, "Missing volume UUID and/or file path.")
                return -2
            policy_subject['volume'] = {
                'uuid': self._resolver.resolve_volume(args.filter1)[0]}
            policy_subject['file'] = {'path': args.filter2}
        elif subject == "client":
            if len(args) < 6:
                print_warning(1, "Missing parameter(s).")
                return -2
            client_subject = {
                "client_ip_address": args.filter3,
                "client_type": args.filter4
            }
            policy_subject['volume'] = {
                'uuid': self._resolver.resolve_volume(args.filter1)[0]}
            policy_subject['file'] = {'path': args.filter2}
            policy_subject['client'] = client_subject

        call['policy_subject'] = policy_subject

        result = self._json_rpc.call("dumpEffectivePolicyRules", call)
        proto_dump = result['proto_dump']
        print(proto_dump)

    @staticmethod
    def completion_cmd():
        return dict(
            (k, ("printf 'global\\ntenant\\nvolume\\nfile\\n'",
                 "if [ \"$prev\" = \"tenant\" ]; then qmgmt${opts}"
                 " tenant list --list-columns=name;"
                 " elif [ \"$prev\" = \"volume\" ] || [ \"$prev\" = \"file\" ];"
                 " then qmgmt${opts} volume list --list-columns=tenant_domain,name; fi"))
            for k in ["rules", "policies"]
            )


class PolicyRuleCreate(Command):
    COMMAND_DESCRIPTION = "create a policy rule from a template in text editor"

    def run(self, args):
        dump = """policy_rule: {
  name: ""
  # description: ""
  enabled: true

  scope: {
    # Add scope(s).
  }

  # Keep either policy_preset or policies.
  policy_preset: {
    id: ""
  }
  policies: {
    # Add policies.
  }
}
"""
        try:
            edited_dump = DataDumpHelper.edit(dump)
            if edited_dump != "":
                call = {
                    "creator": str(getpass.getuser()),
                    "proto_dump": edited_dump
                }
                self._json_rpc.call("importPolicyRules", call)
                print("Success. Created policy rule.")
            else:
                print_warning(0, "Created no policy rule.")
        except IOError as e:
            print_warning(1, "Failed. Policy rules could not be created: %s" % str(e))


class PolicyRuleUpdate(Command):
    COMMAND_DESCRIPTION = "update a policy rule in text editor"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('uuid_or_name', help="policy rule or name")

    def run(self, args):
        try:
            uuid = str(UUID(args.uuid_or_name, version=4))
        except:
            uuid = PolicyRuleResolve(self._json_rpc).resolve_name(args.uuid_or_name)
        call = {"policy_rule_uuid": uuid}
        dump = self._json_rpc.call("exportPolicyRules", call)["proto_dump"]
        try:
            edited_dump = DataDumpHelper.edit(dump)
            if edited_dump != "":
                call = {
                    "creator": str(getpass.getuser()),
                    "proto_dump": edited_dump
                }
                self._json_rpc.call("importPolicyRules", call)
                print("Success. Updated policy rule.")
            else:
                print_warning(0, "Did not update policy rule.")
        except IOError as e:
            print_warning(1, "Failed. Policy rules could not be updated: %s" % str(e))

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} policy-rule list --list-columns=name"


class PolicyRulePriority(Command):
    COMMAND_DESCRIPTION = "increase or decrease a policy rule's priority"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('change', choices = ['increase', 'decrease'])
        parser.add_argument('uuid_or_name', help="policy rule or name")

    def run(self, args):
        change_enum = args.change.upper()
        uuid_or_name = str(args.uuid_or_name)
        try:
            uuid = str(UUID(uuid_or_name, version=4))
        except:
            uuid = PolicyRuleResolve(self._json_rpc).resolve_name(uuid_or_name)
        try:
            call = {"policy_rule_uuid": uuid, "priority_change":  change_enum}
            self._json_rpc.call("changePolicyRulePriority", call)
            print("Success. Changed policy rule priority.")
        except IOError as e:
            print_warning(1, "Failed. Policy rule priority could not be changed: %s" % str(e))

    @staticmethod
    def completion_cmd():
        return ("printf 'increase\\ndecrease'",
                "qmgmt${opts} policy-rule list --list-columns=name")


class PolicyRuleDelete(Command):
    COMMAND_DESCRIPTION = "delete a policy rule"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('uuid_or_name', help="policy rule or name")

    def run(self, args):
        try:
            uuid = str(UUID(args.uuid_or_name, version=4))
        except:
            uuid = PolicyRuleResolve(self._json_rpc).resolve_name(args.uuid_or_name)
        _confirmation_prompt("Really delete policy rule %s?" % (args.uuid_or_name))
        try:
            call = {"policy_rule_uuid": [uuid]}
            self._json_rpc.call("deletePolicyRules", call)
            print("Success. Deleted policy rule.")
        except IOError as e:
            print_warning(1, "Failed. Policy rules could not be created: %s" % str(e))

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} policy-rule list --list-columns=name"


class PolicyRuleResolve(Command):
    COMMAND_DESCRIPTION = "resolve policy rule name to uuid"

    def resolve_name(self, name):
        call = {"policy_rule_name": name}
        response = self._json_rpc.call("resolvePolicyRuleName", call)
        return response["policy_rule_uuid"]


class PolicyPresetList(Command):
    COMMAND_DESCRIPTION = "list policy presets"

    def run(self, args):
        result = self._json_rpc.call("dumpPolicyPresets", {})
        proto_dump = result['proto_dump']
        print(proto_dump)


class VolumeUpdateName(Command):
    COMMAND_DESCRIPTION = "update volume name"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help="[<tenant name or uuid>]/<volume name or uuid>")
        parser.add_argument('new_name', help="new name")

    def run(self, args):
        uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        request = {}
        request['volume_uuid'] = uuid
        request['name'] = args.new_name
        self._json_rpc.call("updateVolume", request)
        print("Success. Updated volume " + request['volume_uuid'])


class VolumeUpdateAddReplicaDevice(Command):
    COMMAND_DESCRIPTION = "add volume metadata replica"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help="[<tenant name or uuid>]/<volume name or uuid>")
        parser.add_argument('device_id', help="Quobyte device id", type=int)

    def run(self, args):
        uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        request = {}
        request['volume_uuid'] = uuid
        request['add_replica_device_id'] = args.device_id
        self._json_rpc.call("updateVolume", request)
        print("Success. Updated volume " + request['volume_uuid'])


class VolumeUpdateRemoveReplicaDevice(Command):
    COMMAND_DESCRIPTION = "remove volume metadata replica"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help="[<tenant name or uuid>]/<volume name or uuid>")
        parser.add_argument('device_id', help="Quobyte device id", type=int)

    def run(self, args):
        uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        request = {}
        request['volume_uuid'] = uuid
        request['remove_replica_device_id'] = args.device_id
        self._json_rpc.call("updateVolume", request)
        print("Success. Updated volume " + request['volume_uuid'])


class VolumeUpdateSetPreferred(Command):
    COMMAND_DESCRIPTION = "set preferred volume metadata replica"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help="[<tenant name or uuid>]/<volume name or uuid>")
        parser.add_argument('device_id', help="Quobyte device id", type=int)

    def run(self, args):
        uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        request = {}
        request['volume_uuid'] = uuid
        request['remove_preferred_primary_replica_device'] = False
        request['preferred_primary_replica_device_id'] = args.device_id
        self._json_rpc.call("updateVolume", request)
        print("Success. Updated volume " + request['volume_uuid'])


class VolumeUpdateRemovePreferred(Command):
    COMMAND_DESCRIPTION = "remove preferred volume metadata replica"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help="[<tenant name or uuid>]/<volume name or uuid>")
        parser.add_argument('device_id', help="Quobyte device id", type=int)

    def run(self, args):
        uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        request = {}
        request['volume_uuid'] = uuid
        request['remove_preferred_primary_replica_device'] = True
        request['preferred_primary_replica_device_id'] = args.device_id
        self._json_rpc.call("updateVolume", request)
        print("Success. Updated volume " + request['volume_uuid'])


def aes_encrypt(key, data, iv):
    try:
        from Cryptodome.Cipher import AES
    except ImportError:
        print_warning(0, "Some encryption features require 'cryptodomex' python package, "
                      "which cannot be imported correctly. "
                      "Please install with 'pip install cryptodomex'")

    assert len(key) == 32
    cipher = AES.new(key, AES.MODE_CBC, iv=iv)
    ciphertext = cipher.encrypt(data)
    return ciphertext

def aes_decrypt(key, data, iv):
    try:
        from Cryptodome.Cipher import AES
    except ImportError:
        print_warning(0, "Some encryption features require 'cryptodomex' python package, "
                      "which cannot be imported correctly. "
                      "Please install with 'pip install cryptodomex'")
    assert len(key) == 32
    cipher = AES.new(key, AES.MODE_CBC, iv=iv)
    return cipher.decrypt(data)

class VolumeEncryptionInitWithSlot(Command):
    COMMAND_DESCRIPTION = "Create and initialize keystore for volume encryption"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help="[<tenant name or uuid>]/<volume name or uuid>")
        parser.add_argument('slot_uuid', help="Slot UUID")
        parser.add_argument('slot_secret', help="Slot secret")

    def run(self, args):
        if not args.slot_secret:
            password = get_password_from_prompt()
        else:
            password = args.slot_secret

        volume_uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        new_keystore_slot_uuid = args.slot_uuid

        slot_infos = self._json_rpc.call(
                "getKeyStoreSlotWithoutHash", {
                "keystore_slot_uuid": new_keystore_slot_uuid})

        salt = base64.b64decode(
            slot_infos['keystore_slot']['encoded_slot_password_salt'])
        password_hash_iterations = int(
            slot_infos['keystore_slot']['keystore_slot_params']['password_hash_iterations'])

        password_hash_method = \
        slot_infos['existing_keystore_slot_data']['keystore_slot_params']['password_hash_method']
        if password_hash_method == 'PBKDF2WithHmacSHA512':
            hash_size = 512
        else:
            print_warning(1, "Unsupported hash method requested: " + \
            str(password_hash_method))
            return -1

        encoded_password_hash = base64.b64encode(
            get_pbkdf2_sha512_hash(
                password,
                salt,
                password_hash_iterations,
                hash_size // 8))

        # generate the volume secret key, and encrypt it with the slot's
        # password.
        key_iv = os.urandom(16)
        key_salt = os.urandom(8)
        key_iterations = int(slot_infos['keystore_slot'][
                                 'keystore_slot_params'][
                                 'key_derivation_iterations'])
        key_bits = 256

        volume_key = os.urandom(key_bits // 8)

        key_derivation_method = \
        slot_infos['keystore_slot']['keystore_slot_params']['key_derivation_method']
        if key_derivation_method != 'PBKDF2WithHmacSHA512':
            print_warning(1, "Unsupported key derivation method requested: " + \
                str(key_derivation_method))
            return -1

        kek = get_pbkdf2_sha512_hash(
            password,
            key_salt,
            key_iterations,
            key_bits // 8)

        encrypted_volume_key = aes_encrypt(kek, volume_key, key_iv)

        encoded_encrypted_volume_key = base64.b64encode(encrypted_volume_key)

        result = self._json_rpc.call(
                "setEncryptedVolumeKey", {
                "volume_uuid": volume_uuid,
                "key_version": 1,
                "new_encrypted_volume_key": {
                    "encoded_initialization_vector" : base64.b64encode(key_iv),
                    "encoded_key_derivation_salt": base64.b64encode(key_salt),
                    "encoded_encrypted_key": encoded_encrypted_volume_key
                },
                "new_keystore_slot_uuid": new_keystore_slot_uuid,
                "encoded_new_keystore_slot_password_hash": encoded_password_hash
            })

        print(result)

class VolumeEncryptionAddSlot(Command):
    COMMAND_DESCRIPTION = "Add a slot to keystore for volume encryption"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help="[<tenant name or uuid>]/<volume name or uuid>")
        parser.add_argument('new_slot_uuid', help="new slot UUID")
        parser.add_argument('existing_slot_uuid', help="Slot UUID")
        parser.add_argument('new_slot_secret', help="Slot secret", nargs='?')
        parser.add_argument('existing_slot_secret', help="Slot secret", nargs='?')

    def run(self, args):
        volume_uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        new_keystore_slot_uuid = args.new_slot_uuid
        existing_keystore_slot_uuid = args.existing_slot_uuid
        if not args.new_slot_secret:
            new_password = get_password_from_prompt()
            existing_password = get_password_from_prompt()
        else:
            new_password = args.new_slot_secret
            existing_password = args.existing_slot_secret

        slot_infos = self._json_rpc.call(
            "getAddKeySlotData", {
                "new_keystore_slot_uuid": new_keystore_slot_uuid,
                "existing_keystore_slot_uuid": existing_keystore_slot_uuid,
                "volume_uuid": volume_uuid
            })

        key_version = slot_infos['key_version']

        existing_password_salt_bytes = base64.b64decode(
            slot_infos['existing_keystore_slot_data']['encoded_slot_password_salt'])
        new_password_salt_bytes = base64.b64decode(
            slot_infos['new_keystore_slot_data']['encoded_slot_password_salt'])

        existing_password_hash_iterations = int(
            slot_infos['existing_keystore_slot_data']['keystore_slot_params']['password_hash_iterations'])
        new_password_hash_iterations = int(
            slot_infos['new_keystore_slot_data']['keystore_slot_params'][
                'password_hash_iterations'])

        existing_key_derivation_iterations = int(
            slot_infos['existing_keystore_slot_data']['keystore_slot_params']['key_derivation_iterations'])
        new_key_derivation_iterations = int(
            slot_infos['new_keystore_slot_data']['keystore_slot_params'][
                'key_derivation_iterations'])

        existing_hash_method = slot_infos['existing_keystore_slot_data']['keystore_slot_params']['password_hash_method']
        if  existing_hash_method == 'PBKDF2WithHmacSHA512':
            hash_size = 512
        else:
            print_warning(1, "Unsupported hash method requested: " + \
                  str(existing_hash_method))
            return -1

        encoded_existing_password_hash = base64.b64encode(
            get_pbkdf2_sha512_hash(
                existing_password,
                existing_password_salt_bytes,
                existing_password_hash_iterations,
                hash_size // 8))

        existing_key = self._json_rpc.call(
            "getEncryptedVolumeKey", {
                "keystore_slot_uuid": existing_keystore_slot_uuid,
                "encoded_slot_password_hash": encoded_existing_password_hash,
                "volume_uuid": volume_uuid,
                "key_version": key_version
            })
        encoded_initialization_vector = existing_key['encrypted_volume_key'][
            'encoded_initialization_vector']
        encoded_key_derivation_salt = existing_key['encrypted_volume_key'][
            'encoded_key_derivation_salt']
        encoded_encrypted_key = existing_key['encrypted_volume_key'][
            'encoded_encrypted_key']

        existing_key_bytes = base64.b64decode(encoded_encrypted_key)
        existing_iv_bytes = \
            base64.b64decode(encoded_initialization_vector)
        existing_key_derivation_salt = \
            base64.b64decode(encoded_key_derivation_salt)

        existing_key_derivation_method = slot_infos['existing_keystore_slot_data']['keystore_slot_params']['key_derivation_method']
        if  existing_key_derivation_method != 'PBKDF2WithHmacSHA512':
            print_warning(1, "Unsupported key derivation method requested: " + \
                  str(existing_key_derivation_method))
            return -1

        key_bits = 256  # we currently only support aes 256
        existing_kek = get_pbkdf2_sha512_hash(
            existing_password,
            existing_key_derivation_salt,
            existing_key_derivation_iterations,
            key_bits // 8)

        # the secrect to be re-encrypted
        volume_key = aes_decrypt(existing_kek, existing_key_bytes,
                                 existing_iv_bytes)

        new_hash_method = slot_infos['new_keystore_slot_data']['keystore_slot_params']['password_hash_method']
        if  new_hash_method == 'PBKDF2WithHmacSHA512':
            hash_size = 512
        else:
            print_warning(1, "Unsupported hash method requested: " + \
                  str(new_hash_method))
            return -1

        new_encoded_password_hash = base64.b64encode(
            get_pbkdf2_sha512_hash(
                new_password,
                new_password_salt_bytes,
                new_password_hash_iterations,
                hash_size // 8))
        # now encode the new key!

        new_key_derivation_method = \
        slot_infos['new_keystore_slot_data']['keystore_slot_params'][
            'key_derivation_method']
        if  new_key_derivation_method != 'PBKDF2WithHmacSHA512':
            print_warning(1, "Unsupported hash method requested: " + \
                  str(new_key_derivation_method))
        key_bits = 256

        new_key_salt = os.urandom(8)
        new_key_iv = os.urandom(16)

        new_kek = get_pbkdf2_sha512_hash(
            new_password,
            new_key_salt,
            new_key_derivation_iterations,
            key_bits // 8)

        new_encrypted_key_bytes = aes_encrypt(new_kek, volume_key, new_key_iv)
        new_encoded_encrypted_volume_key = base64.b64encode(
            new_encrypted_key_bytes)

        result = self._json_rpc.call(
                "setEncryptedVolumeKey", {
                "volume_uuid": volume_uuid,
                "key_version": key_version,
                "new_encrypted_volume_key": {
                    "encoded_initialization_vector" : base64.b64encode(new_key_iv),
                    "encoded_key_derivation_salt": base64.b64encode(new_key_salt),
                    "encoded_encrypted_key": new_encoded_encrypted_volume_key
                },
                "new_keystore_slot_uuid": new_keystore_slot_uuid,
                "encoded_new_keystore_slot_password_hash":
                    new_encoded_password_hash,
                "existing_keystore_slot_uuid": existing_keystore_slot_uuid,
                "encoded_existing_keystore_slot_password_hash": encoded_existing_password_hash
            })

        print(result)


class HealthManagerStatus(Command):
    COMMAND_DESCRIPTION = "show health manager status"

    def run(self, args):
        call = {}
        result = self._json_rpc.call("getHealthManagerStatus", call)['health_manager_status']

        _output_json_and_exit(args, result)

        system_health = ""
        system_health_reasons = ""
        for k in result:
            if k == "system_health_reason":
                for reason in result[k]:
                    system_health_reasons = system_health_reasons + "\n" + reason
                continue
            if k == "system_health":
                system_health = result[k]
            if k == "next_maintenance_window_start_ms":
                v = _print_millis_timestamp(result[k])
            else:
                v = result[k]
            print("{0:<35s} {1:}".format(k + ":", v))
        if system_health_reasons != "":
            print("Reasons for " + system_health + " system health:" + system_health_reasons)
        print()


class BucketList(Command):
    COMMAND_DESCRIPTION = "list all published S3 buckets"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_name_or_uuid', help="<tenant name or uuid>", nargs='?')
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        call = {}
        if args.tenant_name_or_uuid:
            uuid, _ = self._resolver.resolve_tenant(args.tenant_name_or_uuid)
            call['tenant_domain'] = uuid
        result = self._json_rpc.call("getVolumeList", call)
        vol_list = result['volume']
        new_vol_list = []
        for v in vol_list:
            if v['bucket_names']:
                for b in v['bucket_names']:
                    new_v = v.copy()
                    new_v['bucket_names'] = [b]
                    new_vol_list.append(new_v)
        printer = PrettyTable(
            args,
            new_vol_list,
            ['bucket_names', 'name', 'volume_uuid'],
            ["S3 Bucket Names", "Volume Name", "UUID"])
        print(printer.out())

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} tenant list --list-columns=name"


class BucketShow(Command):
    COMMAND_DESCRIPTION = "show detailed bucket information"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('bucket_name', help="bucket name")

    def run(self, args):
        bucket_name = args.bucket_name
        result = self._json_rpc.call("getVolumeList", {})
        vol_list = result['volume']
        bucket_volume = {}
        for volume in vol_list:
            if bucket_name in volume['bucket_names']:
                bucket_volume = volume
        if not bucket_volume:
            print_warning(1, "No such bucket: " + bucket_name)
        else:
            _output_json_and_exit(args, bucket_volume)

            tenant_name = self._resolver.lookup_tenant_uuid(bucket_volume['tenant_domain'])
            print(bucket_name + ':')
            bucket_type = 'exclusive' if bucket_volume['isExclusiveVolumeBucket'] else 'shared'
            print("  volume:", bucket_volume["name"], bucket_volume["volume_uuid"], bucket_type)
            print("  tenant:", tenant_name, bucket_volume["tenant_domain"])
        print()

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} bucket list --list-columns=bucket_names"


class Accounting(Command):

    def get_consumers(self, accounting_request):
        """
        Returns consumer map, which is a dictionary of type
         {<consumer_type>:
           {<identifier>: {<resource_type>: usage, <resource_type>: usage, ...},
            <identifier>: {<resource_type>: usage, <resource_type>: usage, ...},
            ...
           },
          <consumer_type>:
            {...},
          ...
         }
        """
        result = self._json_rpc.call("getAccounting", accounting_request)
        entity_usage_list = result["entity_usage"]
        consumers_map = {}
        for entity_usage in entity_usage_list:
            consumer_type = entity_usage["consumer"]["type"]
            identifier = entity_usage["consumer"]["identifier"]
            resources = entity_usage["usage"]
            if consumer_type not in consumers_map:
                consumers_map[consumer_type] = {}
            by_type_consumption = consumers_map[consumer_type]
            for resource in resources:
                resource_type = resource["type"]
                usage = resource["value"]
                entry = {"id": identifier}
                if identifier in by_type_consumption:
                    entry = by_type_consumption[identifier]
                entry[resource_type] = usage
                by_type_consumption[identifier] = entry
        return consumers_map

    def get_top_capacity_consumers(self, get_top_capacity_consumer_request):
        """
        Returns consumer map, which is a dictionary of type
         {<consumer_type>:
           {<identifier>: {<resource_type>: usage, <resource_type>: usage, ...},
            <identifier>: {<resource_type>: usage, <resource_type>: usage, ...},
            ...
           },
          <consumer_type>:
            {...},
          ...
         }
        """
        result = self._json_rpc.call("getTopCapacityConsumer", get_top_capacity_consumer_request)
        top_consumers = result["top_consumer"]
        consumers_map = {}
        for top_consumer in top_consumers:
            consumers = top_consumer["consumer"]
            consumer_type = top_consumer["consumer_type"]
            resource_type = top_consumer["resource_type"]
            if consumer_type not in consumers_map:
                consumers_map[consumer_type] = {}
            by_type_consumption = consumers_map[consumer_type]
            for consumer in consumers:
                identifier = consumer["identifier"]["identifier"]
                usage = consumer["usage"]
                entry = {"id": identifier}
                if identifier in by_type_consumption:
                    entry = by_type_consumption[identifier]
                entry[resource_type] = usage
                by_type_consumption[identifier] = entry
        return consumers_map

def _print_millis_timestamp(ts_millis):
    if isinstance(ts_millis, str):
        return ts_millis
    return time.strftime("%x %X", time.gmtime(ts_millis // 1000))

def _print_s_timestamp_full(ts):
    if isinstance(ts, str):
        return ts
    return time.strftime("%x %X", time.gmtime(ts))

def _print_s_timestamp(ts):
    if isinstance(ts, str):
        return ts
    return time.strftime("%X", time.gmtime(ts))

def _print_volume_details(args, volume, labels):
    tenant_domain = volume['tenant_domain']

    replica_devices = []
    for device in volume['replica_device_ids']:
        if 'primary_device_id' in volume and volume['primary_device_id'] == device:
            replica_devices.append("*" + str(device))
        else:
            replica_devices.append(str(device))

    if 'preferred_primary_replica_device_id' in volume:
        preferred_primary_replica_device_id = str(volume['preferred_primary_replica_device_id'])
    else:
        preferred_primary_replica_device_id = 'none'

    print(volume['name'])
    print("  uuid:                        " + volume['volume_uuid'])
    print("  tenant:                      " + tenant_domain)
    print("  disk used:                   " + human_readable_bytes(volume['used_disk_space_bytes']))
    if 'quota_disk_space_bytes' in volume:
        print("  disk space quota:            " + \
            human_readable_bytes(volume['quota_disk_space_bytes']))
    print("  files:                       " + str(volume['file_count']))
    print("  directories:                 " + str(volume['directory_count']))
    print("  metadata devices:            [%s]" % ", ".join(replica_devices))
    print("  preferred primary device:    " + preferred_primary_replica_device_id)
    if args.show_all:
        print("  storage device spread:       " + str(volume.get('device_spread', 'unknown')))
    print("  last successful scrub:       " + _print_millis_timestamp(
        volume.get('last_successful_scrub_ms', 'never')))
    last_access = volume.get('last_access_timestamp_s', 'unknown')
    print("  last access:                 " +\
          ('never' if last_access == 0 else _print_s_timestamp_full(last_access)))
    if volume.get('scheduled_for_deletion', None):
        print("  scheduled for deletion")
    if 'async_replication_source' in volume and volume['async_replication_source']:
        print("  mirrored from:               "\
            + ",".join(volume['async_replication_source']['OBSOLETE_remote_registry_target']) \
            + "/" + volume['async_replication_source']['remote_volume_uuid'])
        async_replication_progress = volume.get('async_replication_progress',\
            {'connected': False, 'files_in_progress': 0, 'in_sync_until_timestamp_s': "-"})
        print("  mirroring connected:         "\
            + str(async_replication_progress['connected']))
        print("  mirroring progress:          "\
            + str(max(0, volume['file_count']\
                  - async_replication_progress['files_in_progress']))\
            + "/" + str(volume['file_count']))
        mirrored_until = "-"
        if 'in_sync_until_timestamp_s' in async_replication_progress:
            mirrored_until = _print_s_timestamp_full(
                async_replication_progress['in_sync_until_timestamp_s'])
        print("  mirrored until:              " + mirrored_until)
        mirroring_in_sync = volume['file_count'] > 0 \
            and async_replication_progress['files_in_progress'] == 0 \
            and async_replication_progress['connected'] is True
        print("  mirroring in sync:           " + str(mirroring_in_sync))

    if len(labels) > 0:
        print("  labels:")
        for label in labels:
            print("     " + label['name'] + " = " + label['value'])
    if 'volume_encryption_context' in volume:
        c = volume['volume_encryption_context']
        print("  key owner:                   " +  c.get('key_owner', ""))
        print("  encryption method:           " \
              + c.get('file_encryption_method', "") \
              + " with " + c.get('key_derivation_method', ""))

class VolumeList(Command):
    COMMAND_DESCRIPTION = "list volumes"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('tenant_name_or_uuid', help="<tenant name or uuid>", nargs='?')
        parser.add_argument('-a', '--all', help="show all", action="store_true")
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        call = {}
        if args.tenant_name_or_uuid:
            call['tenant_domain'] = self._resolver.resolve_tenant(args.tenant_name_or_uuid)[0]
        result = self._json_rpc.call("getVolumeList", call)
        vol_list = result['volume']

        printer = PrettyTable(
            args,
            vol_list, ['name',
            'tenant_domain',
            'used_logical_space_bytes',
            'file_count',
            'bucket_names',
            ('async_replication_source', 'OBSOLETE_remote_registry_target'),
            ('async_replication_source', 'remote_volume_uuid')],
            ["Name", "Tenant", "Logical Usage", "File Count",
             "S3 buckets", "Mirrored From", "/"],
            sorting_depth=2
        )
        if args.all:
            printer.insert_column(1, 'volume_uuid', "UUID")
        else:
            printer.modify_column('bucket_names', lambda x: len(x) if x else "")
        printer.modify_column('tenant_domain', self._resolver.lookup_tenant_uuid)
        print(printer.out())

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} tenant list --list-columns=name"


class VolumeShow(Command):
    COMMAND_DESCRIPTION = "show detailed volume information"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)
        parser.add_argument('--show-all', help="show all fields")
        parser.add_argument('volume_handle', help="[<tenant name or uuid>]/<volume name or uuid>")

    def get_details(self, uuid):
        result = self._json_rpc.call("getVolumeList", {'volume_uuid': [uuid]})
        return result['volume']

    def get_effective_configuration(self, volume_uuid):
        result = self._json_rpc.call("getEffectiveVolumeConfiguration",
                                     {'volume_uuid': volume_uuid})
        return result['configuration']

    def run(self, args):
        volume_uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        vol_list = self.get_details(volume_uuid)
        assert vol_list

        _output_json_and_exit(args, vol_list)

        labels = self._json_rpc.call("getLabels",
                                     {'filter_entity_type': 'VOLUME',
                                      'filter_entity_id': volume_uuid})['label']
        _print_volume_details(args, vol_list[0], labels)
        # fetch device usage accounting
        accounting_request = {
            "entity": [{
                "type": "VOLUME",
                "identifier": volume_uuid
                }]
            }
        consumers_map = Accounting(self._json_rpc).get_consumers(accounting_request)
        if "VOLUME" in consumers_map:
            device_usage = []
            for entry in consumers_map["VOLUME"].values():
                if "HDD_PHYSICAL_DISK_SPACE" in entry:
                    device_usage.append(
                        {"id": "HDD", "physical_bytes": entry["HDD_PHYSICAL_DISK_SPACE"]})
                if "SSD_PHYSICAL_DISK_SPACE" in entry:
                    device_usage.append(
                        {"id": "SSD", "physical_bytes": entry["SSD_PHYSICAL_DISK_SPACE"]})
                if "NVME_PHYSICAL_DISK_SPACE" in entry:
                    device_usage.append(
                        {"id": "NVMe", "physical_bytes": entry["NVME_PHYSICAL_DISK_SPACE"]})
            if device_usage:
                print()
                print("DEVICE USAGE")
                printer = PrettyTable(
                    args,
                    device_usage,
                    ['id', 'physical_bytes'],
                    ["Device", "Physical"],
                    sort_by_key="physical_bytes",
                    reverse=True
                )
                print(printer.out())

        # fetch and show the Top Consumers
        get_top_capacity_consumer_request = \
                {"scope": "VOLUME",
                 "scope_identifier": volume_uuid,
                 "only_consumer_type": ["USER", "GROUP"],
                 "only_resource_type": ["LOGICAL_DISK_SPACE", "PHYSICAL_DISK_SPACE", "FILE_COUNT"]}
        consumers_map = Accounting(self._json_rpc).get_top_capacity_consumers(
                get_top_capacity_consumer_request)
        if "USER" in consumers_map:
            users = list(consumers_map["USER"].values())
            if users:
                print()
                print("CONSUMPTION BY USERS  (top 100)")
                printer = PrettyTable(
                    args,
                    users,
                    ['id', 'LOGICAL_DISK_SPACE', 'PHYSICAL_DISK_SPACE', 'FILE_COUNT'],
                    ["User", "Logical", "Physical", "Files"],
                    sort_by_key="LOGICAL_DISK_SPACE",
                    reverse=True
                )
                print(printer.out())
        if "GROUP" in consumers_map:
            groups = list(consumers_map["GROUP"].values())
            if groups:
                print()
                print("CONSUMPTION BY GROUPS (top 100)")
                printer = PrettyTable(
                    args,
                    groups,
                    ['id', 'LOGICAL_DISK_SPACE', 'PHYSICAL_DISK_SPACE', 'FILE_COUNT'],
                    ["Group", "Logical", "Physical", "Files"],
                    sort_by_key="LOGICAL_DISK_SPACE",
                    reverse=True
                )
                print(printer.out())
        print()

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class VolumeLookup(Command):
    COMMAND_DESCRIPTION = "look up name of a particular volume uuid"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_uuid', help="<volume uuid>")

    def run(self, args):
        volume_name = self._resolver.lookup_volume_uuid(args.volume_uuid)
        if not volume_name:
            raise InvalidInputException("No volume with uuid: " + args.volume_uuid)
        print(volume_name)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=volume_uuid"


class VolumeResolve(Command):
    COMMAND_DESCRIPTION = "resolve volume name to volume uuid"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)

    def run(self, args):
        volume_uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        print(volume_uuid)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class VolumeRepair(Command):
    COMMAND_DESCRIPTION = "enforce the replica placement for all files of the given volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        parser.add_argument('comment', help="task comment", nargs='?')

    def run(self, args):
        task_type = "ENFORCE_PLACEMENT"

        target, _, _ = self._resolver.resolve_volume(args.volume_handle)
        call = {'task_type': task_type,
                'restrict_to_volumes': [str(target)],
                'comment': get_comment(args)}

        result = self._json_rpc.call("createTask", call)
        task_id = result['task_id']
        if int(task_id) > 0:
            print("Success. Created new " + args.volume_handle \
                  + " task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class VolumeScrub(Command):
    COMMAND_DESCRIPTION = "scrub all files associated with the given volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        parser.add_argument('-eccc', action="store_true")
        parser.add_argument('comment', help="task comment", nargs='?')

    def run(self, args):
        task_type = "SCRUB"
        target, _, _ = self._resolver.resolve_volume(args.volume_handle)
        if "eccc" in args:
            call = {'task_type': task_type,
                    'restrict_to_volumes': [str(target)],
                    'scrub_settings': {'ec_consistency_check_only': True},
                    'comment': get_comment(args), }
        else:
            call = {'task_type': task_type,
                    'restrict_to_volumes': [str(target)],
                    'comment': get_comment(args), }

        result = self._json_rpc.call("createTask", call)
        task_id = result['task_id']
        if int(task_id) > 0:
            print("Success. Created new " + args.volume_handle\
                  + " task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class VolumePublish (Command):
    COMMAND_DESCRIPTION = "publish a volume as an S3 bucket"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('volume_handle', help=NameResolver.VOLUME_HANDLE_FORMAT)
        parser.add_argument('bucket_name', help="bucket name")

    def run(self, args):
        uuid, _, _ = self._resolver.resolve_volume(args.volume_handle)
        self._json_rpc.call(
            "publishBucketVolume",
            {'volume_uuid': uuid, 'bucket_name': args.bucket_name})
        print("Success, published volume", uuid, "as bucket", args.bucket_name)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class VolumeUnpublish (Command):
    COMMAND_DESCRIPTION = "unpublish an S3 bucket volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('bucket_name', help="bucket name")

    def run(self, args):
        uuid, _, _ = self._resolver.resolve_volume(args.bucket_name)
        self._json_rpc.call("unpublishBucketVolume", {'volume_uuid': uuid})
        print("Success, unpublished bucket volume " + uuid)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} volume list --list-columns=tenant_domain,name"


class DeviceShow(Command):
    COMMAND_DESCRIPTION = "show detailed device information"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="Device id", type=int)

    def get_device(self, device_id):
        try:
            result = self._json_rpc.call(
                "getDeviceList", {'device_id': [int(device_id)]})
            return result['device_list']['devices']
        except ValueError as e:
            raise InvalidInputException("Invalid device id: " + device_id) from e

    def run(self, args):
        dev_list = self.get_device(args.device_id)

        _output_json_and_exit(args, dev_list)

        if not dev_list:
            print_warning(1, "Wrong device id: %s" % args.device_id)
            return -2

        for dev in dev_list:
            print("Id:                      " + str(dev['device_id']))
            print("Host:                    " + dev['host_name'])
            print("Model:                   " + dev['device_model'])
            print("Serial#:                 " + dev['device_serial_number'])
            if 'detected_disk_type' in dev:
                print("Detected device type:    " + dev['detected_disk_type'])
            print("Mode:                    " + dev['device_status'])
            print("Last successful cleanup: " + _print_millis_timestamp(
                dev.get('last_cleanup_ms', 'never')))
            print("Active data:")
            print("  Files:                 " + str(dev.get('file_count', '0')))
            print("  Volume databases:      " + str(dev.get('volume_database_count', '0')))
            print("  Registry databases:    " + str(dev.get('registry_database_count', '0')))

            if dev['device_status'] == "DECOMMISSIONED" or dev['device_status'] == "DISCONNECTED":
                # Find last seen location of this device.
                if dev['host_name']:
                    last_seen_name = dev['host_name']
                else:
                    last_seen_name = "unknown / never registered"
                content_types = []
                for content in dev['content']:
                    content_types.append(content['content_type'])
                    if 'last_seen_service_name' in content:
                        last_seen_name = content['last_seen_service_name']
                print("Last seen at:            " + last_seen_name)
                print("Used for:                " + " ".join(sorted(content_types)))
            else:
                print("Local mount point:      ", dev.get('current_mount_path', 'unknown'))
                print("Mount state:            ", dev.get('mount_state', 'unknown'))
                if 'used_disk_space_bytes' in dev:
                    print("Usage:                   " \
                        + human_readable_bytes(dev['used_disk_space_bytes']) \
                        + " of " + \
                        human_readable_bytes(
                            dev['total_disk_space_bytes']) + " used")
                if 'object_loader_fetching_bytes' in dev:
                    print("Load Object Growth:      " \
                        + human_readable_bytes(dev['object_loader_fetching_bytes']))
                if 'is_empty' in dev and dev['is_empty']:
                    print("                         device is empty")

                sys.stdout.write("Services:                ")
                sys.stdout.flush()
                first_line = True
                for content in dev['content']:
                    # TODO(flangner): Add service uuid!
                    if 'service_uuid' in content:
                        service_uuid = content['service_uuid']
                    else:
                        service_uuid = None
                    if first_line:
                        first_line = False
                    else:
                        sys.stdout.write("                         ")
                        sys.stdout.flush()
                    if not content['available']:
                        hostname = dev['host_name']
                        if 'last_seen_service_name' in content:
                            hostname = content['last_seen_service_name']
                        if 'last_seen_service_uuid' in content:
                            service_uuid = content['last_seen_service_uuid']

                        if service_uuid:
                            service_content = content['content_type'] + "[" + service_uuid \
                                + "] not available"
                        else:
                            service_content = content[
                                'content_type'] + " not available"

                        if dev['device_status'] == "ONLINE" or dev['device_status'] == "DRAIN":
                            print(service_content + " (check service, " \
                                + "network and hardware on host " + \
                                hostname + ")")
                        else:
                            print(service_content + " (last seen on host " + hostname + ")")
                    else:
                        if service_uuid:
                            print(content['content_type'] + " for service " + service_uuid)
                        else:
                            print(content['content_type'] + " (not registered with any service yet)")

            if len(dev['device_tags']) > 0:
                print("Tags:                    " + " ".join(dev['device_tags']))
            print("CRC/IO error counts:     %d/%d" % (dev.get('crc_error_count', 0),
                                                      dev.get('io_error_count', 0)))
            print("Chassis LED status:     ", dev.get('led_status', 'unknown'))
            print("TRIM method:            ", dev.get('trim_device_method', 'unknown'))
            print("fsck before mount:      ", dev.get('filesystem_check_before_mount', 'unknown'))

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} device list --list-columns=device_id"


class DeviceRemove(Command):
    COMMAND_DESCRIPTION = "empty device and disconnect it from the system"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="Device id", type=int)
        parser.add_argument('comment', help="task comment", nargs='?')

    def run(self, args):
        task_type = "DRAIN"
        target = args.device_id
        call = {'task_type': task_type,
                'restrict_to_devices': [int(target)],
                'comment': get_comment(args), }

        result = self._json_rpc.call("createTask", call)
        task_id = result['task_id']
        if int(task_id) > 0:
            print("Success. Created new " + args.device_id\
                  + " task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} device list --list-columns=device_id"


class DeviceList(Command):
    COMMAND_DESCRIPTION = "show a list of devices and their current state"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument(
            'device_type',
            help="optional type filter, if set list only devices with selected type, "
                "can be D(ATA), M(ETADATA) or R(EGISTRY)",
            type=str.upper, choices=VALID_DEVICE_TYPES, nargs='?')
        parser.add_argument('-a', '--all', help='show decomissioned', action='store_true')
        PrettyTable.add_specific_arguments(parser)

    def get_list(self, args):
        call = {}
        if args.device_type:
            call['device_type'] = convert_device_type_input(args.device_type)

        result = self._json_rpc.call("getDeviceList", call)
        return result['device_list']['devices']

    def run(self, args):
        dev_list = self.get_list(args)
        if type(dev_list) == int and dev_list == -2:
            return

        to_print = []
        for dev in dev_list:
            if dev['device_status'] == "DECOMMISSIONED" and not args.all:
                continue
            dev['content'] = [c['content_type'] for c in dev['content']]
            to_print.append(dev)

        printer = PrettyTable(
            args,
            to_print,
            ['device_id', 'host_name', 'device_status',
             'used_disk_space_bytes', 'object_loader_fetching_bytes',
             'total_disk_space_bytes', 'content', 'led_status',
             'mount_state', 'filesystem_check_before_mount', 'device_tags'],
            ["Id", "Host", "Mode", "Disk Used",
             "Disk Usage Growth on Object Loading", "Disk Total",
             "Services", "LED Mode", "Mount State", "Check on Mount", "Tags"]
        )
        print(printer.out())

    @staticmethod
    def completion_cmd():
        return "printf 'DATA\\nMETADATA\\nREGISTRY\\n'"


class DeviceMode(Command):
    COMMAND_DESCRIPTION = "changes the device mode of a device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('device_mode', help="led status", choices=VALID_DEVICE_MODES,
                            type=str.upper)
        parser.add_argument('--comment', help="comment")

    def run(self, args):
        self._json_rpc.call(
            "updateDevice", {
                'device_id': args.device_id,
                'set_device_status': args.device_mode.upper(),
                'comment': get_comment(args)})
        print("Success. Updated device mode for device", args.device_id,
              "to", args.device_mode.upper())


class DeviceLEDStatus(Command):
    COMMAND_DESCRIPTION = "changes the LED status of a device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('led_status', help="led status", type=str.upper,
                            choices=VALID_DEVICE_LED_MODES)
        parser.add_argument('--comment', help="comment")

    def update_status(self, args):
        call = {
            'device_id': int(args.device_id),
            'set_led_status': args.led_status.upper(),
            'comment': get_comment(args)}
        self._json_rpc.call("updateDevice", call)

    def run(self, args):
        self.update_status(args)

        print("Success. Updated device led status for device",
              args.device_id, "to" , args.led_status.upper())


class DeviceMountStateChange(Command):
    COMMAND_DESCRIPTION = "changes the mount state of the device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('mount_state', type=str.upper, choices=VALID_MOUNT_STATES)
        parser.add_argument('--comment', help="comment")

    def update_status(self, args):
        call = {
            'device_id': args.device_id,
            'set_mount_state': args.mount_state.upper(),
            'comment': get_comment(args)}
        self._json_rpc.call("updateDevice", call)

    def run(self, args):
        self.update_status(args)
        print("Success. Updated device mount state for device",
              args.device_id, "to", args.mount_state.upper())


class FileSystemCheckBeforeMountStateChange(Command):
    COMMAND_DESCRIPTION = "changes the fsck policy of the device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('fsck_policy', type=str.upper, choices=VALID_FILESYSTEM_CHECK_STATES)
        parser.add_argument('--comment', help="comment")

    def update_status(self, args):
        call = {
            'device_id': args.device_id,
            'set_filesystem_check_before_mount': args.fsck_policy.upper(),
            'comment': get_comment(args)}

        self._json_rpc.call("updateDevice", call)

    def run(self, args):
        self.update_status(args)
        print("Success. Updated file system checking before mount for device",
            args.device_id, "to", args.fsck_policy.upper())


class DeviceTrimMethodChange(Command):
    COMMAND_DESCRIPTION = "changes the method to run TRIM on SSD and NVMe devices"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('trim_mode',
                            help="method to run TRIM on SSD and NVMe devices",
                            type=str.upper, choices=VALID_TRIM_METHODS)
        parser.add_argument('--comment', help="comment")

    def run(self, args):
        self._json_rpc.call(
            "updateDevice",
            {
                'device_id': args.device_id,
                'set_trim_device_method': args.trim_mode,
                'comment': get_comment(args)
            })
        print("Success. Updated TRIM method for device",
             args.device_id, "to", args.trim_mode.upper())


class DeviceAddTags(Command):
    COMMAND_DESCRIPTION = "add placement tags to device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('tag', help="tags to add", nargs='+')
        parser.add_argument('--comment', help="comment")

    def get_tags_list(self, device_id):
        call = {}
        call['device_id'] = [device_id]
        result = self._json_rpc.call("getDeviceList", call)
        return result['device_list']['devices'][0]['device_tags']

    def run(self, args):
        for tag in args.tag:
            if re.search(",", tag):
                print_warning(1, "Sorry, commas are not allowed in tags."
                              "Please provide a space separated tag list.")
                return -2
        old_tags = self.get_tags_list(args.device_id)

        call = {'device_id': args.device_id,
                'update_device_tags': True,
                'device_tags': old_tags + args.tag,
                'comment': get_comment(args)}

        try:
            self._json_rpc.call("updateDevice", call)
            print("Success. Added device tags for device ", args.device_id)
            try:
                for dev in DeviceShow(self._json_rpc).get_device(args.device_id):
                    if len(dev['device_tags']) > 0:
                        print("New tag list: " + " ".join(dev['device_tags']))
            except:
                pass
        except Exception as e:
            if re.search("PosixErrorException.+Not found: " + str(args.device_id), str(e)):
                print_warning(1, "Device not found: " + str(args.device_id))
            else:
                print_warning(1, e)


class DeviceRemoveTags(Command):
    COMMAND_DESCRIPTION = "remove placement tags to device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('tag', help="tags to add", nargs='+')
        parser.add_argument('--comment', help="comment")

    def get_tags_list(self, device_id):
        call = {}
        call['device_id'] = [device_id]
        result = self._json_rpc.call("getDeviceList", call)
        return result['device_list']['devices'][0]\
            ['device_tags']

    def run(self, args):
        for tag in args.tag:
            if re.search(",", tag):
                print_warning(1, "Sorry, commas are not allowed in tags."
                              "Please provide a space separated tag list.")
                return -2

        old_tags = self.get_tags_list(args.device_id)
        new_tags = []
        for tag in old_tags:
            if not tag in args.tag:
                new_tags.append(tag)

        call = {'device_id': args.device_id,
                'update_device_tags': True,
                'device_tags': new_tags,
                'comment': get_comment(args)}
        try:
            self._json_rpc.call("updateDevice", call)
            print("Success. Removed device tags for device", args.device_id)
            try:
                for dev in DeviceShow(self._json_rpc).get_device(args.device_id):
                    if len(dev['device_tags']) > 0:
                        print("New tag list: " + " ".join(dev['device_tags']))
            except:
                pass
        except Exception as e:
            if re.search("PosixErrorException.+Not found: " + str(args.device_id), str(e)):
                print_warning(1, "Device not found: " + str(args.device_id))
            else:
                print(e)


class DeviceAddType(Command):
    COMMAND_DESCRIPTION = "add device type to device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('device_type',
                            help="device type to add, "
                                + "one of the three device types D(ATA), M(ETADATA), R(EGISTRY)",
                            choices=VALID_DEVICE_TYPES, type=str.upper)
        parser.add_argument('--comment', help="comment")

    def run(self, args):
        device_type = convert_device_type_input(args.device_type)[0]

        call = {
            'device_id': int(args.device_id),
            'device_type': device_type,
            'comment': get_comment(args)}
        self._json_rpc.call("updateDevice", call)
        print("Success. Added device type", device_type, "for device", args.device_id)


class DeviceRemoveType(Command):
    COMMAND_DESCRIPTION = "remove device type from device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('device_type',
                            help="device type to remove, "
                                + "one of the three device types D(ATA), M(ETADATA), R(EGISTRY)",
                            choices=VALID_DEVICE_TYPES, type=str.upper)
        parser.add_argument('--comment', help="comment")

    def run(self, args):
        device_type = convert_device_type_input(args.device_type)[0]
        call = {
            'device_id': args.device_id,
            'device_type': device_type,
            'remove_device_type': True,
            'comment': get_comment(args)}
        self._json_rpc.call("updateDevice", call)
        print("Success. Removed device type ", device_type, "from device", args.device_id)


class DeviceHealthChange(Command):
    COMMAND_DESCRIPTION = "changes the health status of a device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)
        parser.add_argument('health_status', choices=VALID_HEALTH_STATUS,
                            type=str.upper)
        parser.add_argument('--comment', help="comment")

    def run(self, args):
        call = {
            'device_id': args.device_id,
            'set_device_health': {
                'health_status': args.health_status,
                'error_report': '',
            },
            'comment': get_comment(args)
        }
        self._json_rpc.call("updateDevice", call)
        print("Success. Updated device health of device ", args.device_id, "to", args.health_status)


class DeviceAddr(Command):
    COMMAND_DESCRIPTION = "show endpoints for device"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('device_id', help="device id", type=int)

    def run(self, args):
        result = self._json_rpc.call(
            "getDeviceNetworkEndpoints", {'device_id': args.device_id})

        _output_json_and_exit(args, result)

        endpoints = result['endpoints']
        if len(endpoints) > 0:
            print("Network endpoints for device", args.device_id)
            for endpoint in endpoints:
                print("  " + endpoint['hostname'] + ":" + str(endpoint['port']) + \
                    " (" + endpoint['device_type'] + ")")
        else:
            print_warning(
                0, "No network endpoints for device " + str(args.device_id) + " available.")

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} device list --list-columns=device_id"


class DeviceListClean(Command):
    COMMAND_DESCRIPTION = "list all unformatted devices"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('service_uuid', nargs='?')
        PrettyTable.add_specific_arguments(parser)

    def run(self, args):
        call = {}
        if args.service_uuid:
            call['service_uuid'] = args.service_uuid
        result = self._json_rpc.call("getUnformattedDevices", call)['unformatted_device']
        if result:
            printer = PrettyTable(
                args,
                result,
                ['handle_id', 'hostname', 'disk_name', 'size_in_bytes'],
                ["Handle", "Host", "Name", "Disk Size"])
            print(printer.out())
        elif args.service_uuid:
            print_warning(1, "No unformatted devices found for service_uuid: %s."
                                 % args.service_uuid)
        else:
            print("No unformatted devices found.")


class DeviceMake(Command):
    COMMAND_DESCRIPTION = "format and initialize clean devices as Quobyte devices"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('handle',
            help="comma separated clean device identifiers, identifiers can be "
                 "retrieved using \"device list-unformatted\" command")
        parser.add_argument('device_type', choices=VALID_DEVICE_TYPES, type=str.upper)
        parser.add_argument('fs_type',
            nargs='?', choices=SUPPORTED_FS_TYPES, default='xfs')
        parser.add_argument('--comment', help="comment")
        parser.add_argument('--tags', help="comma separated list of device tags"
                                           " (used for all the devices)")

    def run(self, args):
        initial_device_tags = []
        if args.tags:
            initial_device_tags = args.tags.split(",")

        devices = {}
        devices['device'] = []
        devices['comment'] = get_comment(args)

        handles = []
        if "," in args.handle:
            handles = args.handle.split(",")
        else:
            handles.append(args.handle)
        device_type = convert_device_type_input(args.device_type)[0]
        fs_type = args.fs_type.upper()
        for handle in handles:
            device = {
                'handle_id': handle.strip(),
                'device_type': device_type,
                'fs_type': fs_type,
                'initial_device_tag': initial_device_tags,
            }
            devices['device'].append(device)

        result = self._json_rpc.call("makeDevices", devices)
        uuid = result['task_id']
        print("Success. Started MAKE_DEVICE task with id %s." % (str(uuid)))

    @staticmethod
    def completion_cmd():
        return ("qmgmt${opts} device make ",
                "printf 'DATA\\nMETADATA\\nREGISTRY\\n'")


class TaskCancel(Command):
    COMMAND_DESCRIPTION = "cancels a pending or running task and removes completed tasks"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('task_id',
                            help="If no task_id is given, all running tasks are going to be canceled.",
                            nargs='?')
        parser.add_argument('--force')
        parser.add_argument('--delete')

    def do_cancel(self, task_id, force=False, delete=False):
        if delete:
            self._json_rpc.call("cancelTask", {'task_id': [task_id], 'delete': delete})
        elif force:
            self._json_rpc.call("cancelTask", {'task_id': [task_id], 'force': force})
        else:
            self._json_rpc.call("cancelTask", {'task_id': [task_id]})

    def do_cancel_all(self):
        result = self._json_rpc.call("getTaskList", {'only_root_tasks': True,
                                                     'only_processing': True})
        tasks = result['tasks']
        self._json_rpc.call("cancelTask", {'task_id': tasks})
        print("Cancelled " + len(tasks) + " tasks")

    def run(self, args):
        if not args.task_id:
            _confirmation_prompt("Really cancel all tasks?")
            self.do_cancel_all()
            print("Success. Cancelled all tasks")
        elif args.force or args.delete:
            _confirmation_prompt("Really cancel task " + args.task_id + "?")
            self.do_cancel(args.task_id, args.force, args.delete)
            print("Success. Cancelled task id " + args.task_id)
        else:
            _confirmation_prompt("Really cancel task " + args.task_id + "?")
            self.do_cancel(args.task_id)
            print("Success. Cancelled task id " + args.task_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} task list RUNNING,SCHEDULED --list-columns=task_id"


class TaskResume(Command):
    COMMAND_DESCRIPTION = "continue a failed/cancelled task from the last persistent checkpoint"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('task_id', help="task id")

    def do_resume(self, task_id):
        self._json_rpc.call("resumeTask", {'task_id': [str(task_id)]})

    def run(self, args):
        self.do_resume(args.task_id)
        print("Success. Resumed task id " + args.task_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} task list CANCELED,FAILED --list-columns=task_id"


class TaskRetry(Command):
    COMMAND_DESCRIPTION = "Reset the whole task state and rerun the given task"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('task_id', help="task id")

    def do_retry(self, task_id):
        self._json_rpc.call("retryTask", {'task_id': [str(task_id)]})

    def run(self, args):
        self.do_retry(args.task_id)
        print("Success. Retrying task id " + args.task_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} task list --list-columns=task_id"


class TaskCreate(Command):
    COMMAND_DESCRIPTION = "create (schedule) a new task"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('--comment', help="comment")
        parser.add_argument('--task_priority', help="task priority", default="NORMAL",
                            choices=['VERY_LOW', 'LOW', 'NORMAL', 'HIGH', 'VERY_HIGH'],
                            type=str.upper)
        parser.add_argument("--directory",
                            dest="deleting_directory", help="directory for delete tasks")
        parser.add_argument("--since", dest="timestamp", help="timestamp for catch-up tasks")
        parser.add_argument("--under",type=int,
                            help="rebalance task upper bound")
        parser.add_argument("--over", type=int,
                            help="rebalance task lower bound")

        task_type_help = ("One of the following task types:\n"
        + "    SCRUB                    verifies data checksums and repairs broken data, \n"
        + "                             blocks expects a target volume uuid\n"
        + "                             * optionally can be restricted to the given devices\n"
        + "    CHECK_FILES              checks files with errors, attempting to repair them\n"
        + "                             expects a target volume uuid\n"
        + "                             * optionally can be restricted to the given devices\n"
        + "    DRAIN                    removes all files and volumes from the target device\n"
        + "                             and replaces the replicas on other storage devices\n"
        + "    REGENERATE               restores the replica sets of files and volumes that\n"
        + "                             are unsaturated due to the removal of the given\n"
        + "                             target device\n"
        + "    CLEANUP                  cleans unreferenced data from given devices and frees\n"
        + "                             disk space\n"
        + "    REBALANCE_DATA           moves files between data devices for even distribu-\n"
        + "                             tion of files, doesn't require a target.\n"
        + "        [--under=<int>]      Underutilized threshold percentage for REBALANCE_DATA task\n"
        + "                             (default 50)\n"
        + "        [--over=<int>]       Overutilized threshold percentage for REBALANCE_DATA task\n"
        + "                             (default 75)\n"
        + "    REBALANCE_METADATA       moves metadata replicas between metadata devices,\n"
        + "                             trying to eliminate over- and underutilized metadata\n"
        + "                             devices and distributing replicas and primaries.\n"
        + "    ENFORCE_VOLUME_PLACEMENT restores replica sets and replaces replicas of volume\n"
        + "                             metadata according to configured policies.\n"
        + "                             * optionally can be restricted to the given volumes\n"
        + "    ENFORCE_PLACEMENT        restores replica sets and replaces replicas of volume\n"
        + "                             metadata and files according to configured policies.\n"
        + "                             * optionally can be restricted to the given volumes\n"
        + "    CATCH_UP                 synchronize replica sets and EC files that have been\n"
        + "                             modified in the last 24 hours at the given volume\n"
        + "                             * optionally can be restricted to the given devices\n"
        + "                             * possible secondary volume filter\n"
        + "        [--since=<arg>]      set downtime start time (other than 24 hours)\n"
        + "                             Accepts timestamps in seconds, milliseconds.\n"
        + "                             Or date in formats: yyyy-mm-dd, yyyy-mm-dd-hh:mm:ss\n"
        + "    FSTRIM                   trim a given device (run fstrim command on the\n"
        + "                             mounted path of the device)\n"
        + "    DELETE_FILES             deletes the directory recursively\n"
        + "        --directory=<path>   absolute directory path from the volume root\n"
        + "    ERASE_SNAPSHOTS          removes expired volume snapshots\n"
        + "  [--priority=<priority>]    lower priority tasks get preempted in favor of higher\n"
        + "                             priority tasks (default NORMAL). <priority> accepts\n"
        + "                             the values VERY_LOW, LOW, NORMAL, HIGH, and VERY_HIGH\n"
        + "  [--comment=<new comment>]  replace user and hostname comment with the given one\n"
        + "    UPDATE_FILE_SIZES        check and correct file size information expects a \n"
        + "                             target volume uuid\n")

        parser.add_argument('task_type', help=task_type_help)
        parser.add_argument('target_id', nargs='*')

    @staticmethod
    def translate_task_names(task_name):
        if task_name in ["SCRUB_VOLUME", "SCRUB"]:
            return "SCRUB"
        elif task_name in ["REPAIR_VOLUMES", "REPAIR_VOLUME", "REPAIR",
                           "CHECK", "CHECK_FILES", "CHECK_VOLUME"]:
            return "CHECK_FILES"
        elif task_name in ["DRAIN", "DRAIN_DEVICE", "DRAIN_DATA_DEVICE"]:
            return "DRAIN"
        elif task_name in ["CLEANUP_DEVICE", "CLEANUP", "CLEANUP_VOLUME"]:
            return "CLEANUP"
        elif task_name in ["REPLACE", "REPLACE_DEVICE", "REGENERATE"]:
            return "REGENERATE"
        elif task_name in ["REBALANCE", "REBALANCE_DATA"]:
            return "REBALANCE"
        elif task_name == "REBALANCE_METADATA":
            return "REBALANCE_METADATA_DEVICES"
        elif task_name == "CATCH_UP":
            return "CATCH_UP"
        elif task_name in ["ENFORCE_VOLUME_PLACEMENT", "ENFORCE_METADATA_PLACEMENT"]:
            return "ENFORCE_VOLUME_PLACEMENT"
        elif task_name in ["ENFORCE_PLACEMENT", "ENFORCE_FILE_PLACEMENT"]:
            return "ENFORCE_PLACEMENT"
        elif task_name == "FSTRIM":
            return "FSTRIM"
        elif task_name == "DELETE_FILES":
            return "DELETE_FILES_IN_VOLUMES"
        elif task_name == "ERASE_SNAPSHOTS":
            return "ERASE_SNAPSHOTS"
        elif task_name == "UPDATE_FILE_SIZES":
            return "UPDATE_FILE_SIZES"
        else:
            raise InvalidInputException("Unknown task type: " + task_name)

    @staticmethod
    def parse_to_timestamp(t):
        t = str(t)
        now = datetime.datetime.now()
        for timestamp in [t, t[:-3]]:
            try:
                date = datetime.datetime.fromtimestamp(int(timestamp))
                if 2000 <= date.year <= now.year:
                    return int(timestamp + "000")
            except ValueError:
                continue
        for formatstr in ['%Y/%m/%d', '%Y-%m-%d',
                       '%Y-%m-%d-%H:%M:%S', '%Y/%m/%d/%H:%M:%S']:
            try:
                return int(time.mktime(
                    datetime.datetime.strptime(t, formatstr).timetuple())*1000)
            except ValueError:
                continue
        print_warning(1, "Wrong time format.")
        sys.exit(-2)

    def task_settings(self, args, task_type, call):
        if task_type == "CATCH_UP":
            if not args.timestamp:
                now_ms = int(round(time.time() * 1000))
                # 24h back from now
                timestamp = now_ms - int(24 * 60 * 60 * 1000)
            else:
                timestamp = self.parse_to_timestamp(args.timestamp)
            call['catch_up_settings'] = {
                'downtime_begin_timestamp_ms': timestamp}
        elif task_type == "REBALANCE":
            call['rebalance_settings'] = {
                'underutilized_threshold_percentage': args.under or 50,
                'overutilized_threshold_percentage': args.over or 75
            }
        elif task_type == "DELETE_FILES_IN_VOLUMES" and args.deleting_directory:
            call['delete_files_settings'] = {
                'directory_path': args.deleting_directory
            }

    def get_targets(self,
                    args,
                    allow_volumes=True,
                    allow_devices=True,
                    require_device=False):
        volumes = []
        devices = []
        for arg in args.target_id:
            try:
                devices.append(int(arg))
            except ValueError:
                volumes.append(self._resolver.resolve_volume(arg)[0])
        if require_device and volumes and not devices:
            print_warning(1, "Task requires at least one device"
                             " as a target to use volume filter.")
            sys.exit(-2)
        if volumes and not allow_volumes:
            print_warning(1, "Task does not accept volume targets.")
            sys.exit(-2)
        if devices and not allow_devices:
            print_warning(1, "Task does not accept device targets.")
            sys.exit(-2)
        return volumes, devices

    def run(self, args):
        task_type = self.translate_task_names(args.task_type.upper())
        if task_type in ["FSTRIM", "DRAIN", "REBALANCE",
                         "REBALANCE_METADATA_DEVICES"]:
            restrict_to_volumes, restrict_to_devices = \
                self.get_targets(args, allow_volumes=False)
        elif task_type in ["ERASE_SNAPSHOTS", "ERASE_VOLUMES",
                           "ENFORCE_METADATA_PLACEMENT", "DELETE_FILES_IN_VOLUMES"]:
            restrict_to_volumes, restrict_to_devices = \
                self.get_targets(args, allow_devices=False)
        elif task_type in ["CATCH_UP"]:
            restrict_to_volumes, restrict_to_devices = \
                self.get_targets(args, require_device=True)
        else:
            restrict_to_volumes, restrict_to_devices = \
                self.get_targets(args)

        call = {'task_type': task_type,
                'comment': get_comment(args),
                'restrict_to_volumes': restrict_to_volumes,
                'restrict_to_devices': restrict_to_devices,
                'task_priority': args.task_priority}

        self.task_settings(args, task_type, call)

        task_id = self._json_rpc.call("createTask", call)['task_id']

        if int(task_id) > 0:
            print("Success. Created new " + args.task_type\
                  + " task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "printf 'SCRUB\\nCHECK_FILES\\nDRAIN\\nREGENERATE\\n" \
               "CLEANUP\\nCLEANUP_VOLUME\\nREBALANCE_DATA\\n" \
               "REBALANCE_METADATA\\n" \
               "ENFORCE_METADATA_PLACEMENT\\nENFORCE_PLACEMENT\\n" \
               "CATCH_UP\\nFSTRIM\\nUPDATE_FILE_SIZES\\n'"


class UsageShow(Command):
    COMMAND_DESCRIPTION = "resource usage for a given consumer"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)
        parser.add_argument(
            'type', help="task id", type=str.upper,
            choices=['SYSTEM', 'DEVICE', 'TENANT', 'VOLUME', 'VOLUME_SNAPSHOT',
                     'USER', 'GROUP', 'EXTERNAL_STORAGE'])
        parser.add_argument(
            'identifier', nargs='?',
            help="id like device id, tenant name or uuid, volume handle, user, group")
        parser.add_argument('tenant_id', nargs='?')
        parser.add_argument('volume_uuid', nargs='?')

    def do_show(self, args):
        entities = []
        if not args.identifier and args.type != 'SYSTEM' and args.type != 'EXTERNAL_STORAGE':
            raise InvalidInputException("Not enough arguments.")
        elif args.type == 'TENANT':
            uuid, _ = self._resolver.resolve_tenant(args.identifier)
            entities.append({'type': 'TENANT', 'identifier': uuid})
            volumes = self._json_rpc.call("getVolumeList",
                                          {'tenant_domain': uuid})['volume']
            for vol in volumes:
                entities.append({'type': 'VOLUME', 'identifier': vol['volume_uuid']})
        elif args.type == 'VOLUME':
            uuid, _, _ = self._resolver.resolve_volume(args.identifier)
            entities.append({'type': 'VOLUME', 'identifier': uuid})
        elif args.type in ('USER', 'GROUP'):
            if args.tenant_id:
                uuid, _ = self._resolver.resolve_tenant(args.tenant_id)
                entities.append({'type': args.type,
                                 'identifier': args.identifier,
                                 'tenant_id': uuid})
            else:
                tenants = self._json_rpc.call("getTenant", {})['tenant']
                for t in tenants:
                    entities.append({'type': args.type,
                                     'identifier': args.identifier,
                                     'tenant_id': t['tenant_id']})
        elif args.type == 'DEVICE':
            entities.append({'type': 'DEVICE', 'identifier': args.identifier})
        elif args.type == 'SYSTEM':
            entities.append({'type': 'SYSTEM'})
        elif args.type == 'VOLUME_SNAPSHOT':
            uuid, _, _ = self._resolver.resolve_volume(args.identifier)
            entities.append({'type': 'VOLUME_SNAPSHOT', 'identifier': uuid})
        elif args.type == 'EXTERNAL_STORAGE':
            entity = {'type': 'EXTERNAL_STORAGE'}
            if args.identifier:
                entity['identifier'] = args.identifier
            if args.tenant_id:
                entity['tenant_id'] = args.tenant_id
            if args.volume_uuid:
                entity['volume_id'] = args.volume.uuid
            entities.append(entity)
        else:
            return []
        # TODO(artari): adapt to use Accounting().getConsumers() call instead.
        result = self._json_rpc.call("getAccounting", {'entity': entities})
        return result['entity_usage']

    def run(self, args):
        consumer_types = []
        resource_types = []
        data = []
        for entity in self.do_show(args):
            consumer_types.append(entity['consumer']['type'])
            one_usage = {}
            one_usage[entity['consumer']['type']] = \
                entity['consumer'].get('identifier', "*")
            if entity['consumer'].get('tenant_id', None):
                consumer_types.append('TENANT')
                one_usage['TENANT'] = \
                    entity['consumer']['tenant_id']
            for resource in entity['usage']:
                resource_type = resource['type']
                # TODO(artari): remove "allocated" resource filter, when it is enabled in UI
                if "ALLOCATED" not in resource_type:
                    resource_types.append(resource_type)
                    one_usage[resource['type']] = resource['value']
            data.append(one_usage)
        if resource_types:
            types = consumer_types + resource_types
            printer = PrettyTable(
                args,
                data,
                [str(t) for i, t in enumerate(types) if t not in types[:i]],
                sorting_depth=2)
            if 'VOLUME' in types:
                printer.modify_column(
                    'VOLUME',
                    lambda uuid: self._resolver.lookup_volume_uuid(uuid) if uuid else None)
            if 'TENANT' in types:
                printer.modify_column(
                    'TENANT',
                    lambda uuid: self._resolver.lookup_tenant_uuid(uuid) if uuid else None)
            print(printer.out())
        else:
            print("No accounting to show.")

    @staticmethod
    def completion_cmd():
        return "printf 'SYSTEM\\nDEVICE\\nTENANT\\nVOLUME\\nUSER\\nGROUP\\n" \
               "EXTERNAL_STORAGE\\n'"


class TaskShow(Command):
    COMMAND_DESCRIPTION = "show detailed task information"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('task_id', help="task id")

    def do_list(self, task_id):
        result = self._json_rpc.call("getTaskList", {'task_id': [str(task_id)]})
        tasks = result['tasks']
        return tasks

    def print_value(self, field_display_name, field_name, task):
        if field_name in task:
            if isinstance(task[field_name], list):
                display_value = ", ".join(map(str, task[field_name]))
            else:
                display_value = str(task[field_name])
            if display_value == "ENFORCE_FILE_PLACEMENT":
                display_value = "REPAIR"
            elif display_value == "REPLACE_DEVICE":
                display_value = "REPLACE"
            elif field_name == "cleared_disk_space":
                display_value = human_readable_bytes(task[field_name])
            self.print_value2(field_display_name, display_value)

    def print_value2(self, field_display_name, display_value):
        print("  %-20s %s" % (field_display_name + ":", display_value))

    def print_value3(self, display_value):
        print("  %-20s %s" % ("", display_value))

    def print_message(self, message):
        print("  %-20s" % message)

    def run(self, args):
        tasks = self.do_list(args.task_id)
        for task in tasks:
            if task['task_id'] == args.task_id:
                print("Task " + task['task_id'])
                self.print_value("type", 'task_type', task)
                if task.get('owner_type') == 'HEALTH_MANAGER':
                    self.print_value2("owner", "Health Manager")
                else:
                    self.print_value("owner", 'owner_name', task)
                self.print_value("comment", 'comment', task)
                self.print_value2(
                    "time info",
                    "Created at " + _print_millis_timestamp(task.get('submission_timestamp_ms', "~")))
                self.print_value3("Total time in state \"Running\" is {}".format(
                    to_human_readable_ms(task.get('total_runtime_ms', 0))))
                task_state = task.get('state')
                if task_state in ('RUNNING','CANCELLING'):
                    self.print_value3("Current execution started on {}".format(
                        _print_millis_timestamp(task.get('begin_timestamp_ms', "~"))))
                elif task_state in ('QUEUED', 'SCHEDULED'):
                    self.print_value3("Task is waiting to be executed...")
                else:
                    self.print_value3("Latest run started on {} and ended on {}".format(
                        _print_millis_timestamp(task.get('begin_timestamp_ms', "~")),
                        _print_millis_timestamp(task.get('end_timestamp_ms', "~"))))

                for scope in task['scope']:
                    self.print_value(
                        "%s scope" % scope['type'].lower(),
                        "subject_id",
                        scope)
                self.print_value2("state", task_state)
                self.print_value("error message", 'error_message', task)
                progress = {}
                if "progress" in task:
                    progress = task['progress']
                    self.print_value2(
                        "success",
                        "%.02f %%" % float(progress['success_fraction'] * 100))
                    self.print_value2(
                        "failure",
                        "%.02f %%" % float(progress['failure_fraction'] * 100))
                    self.print_value2(
                        "time elapsed",
                        _print_s_timestamp(progress.get('time_elapsed_s', 0)))
                    try:
                        self.print_value2(
                            "time left",
                            _print_s_timestamp(progress.get('eta_s', "~")))
                    except:
                        self.print_value2("time left", "~")
                errors = False
                for performance in task['performance']:
                    processed = performance['processed']
                    total = performance.get('total')
                    if performance['type'] == "BYTE":
                        processed = human_readable_bytes(processed)
                        if total is not None:
                            total = human_readable_bytes(total)
                    if "per_second" in performance:
                        per_second = performance['per_second']
                        if performance['type'] == "BYTE":
                            per_second = human_readable_bytes(per_second) + " / s"
                        else:
                            per_second = "%.02f / s" % per_second
                        self.print_value2("%s rate" % performance['type'].lower(),
                                          per_second)
                    if total is None:
                        self.print_value2(
                            "%ss processed" % performance['type'].lower(),
                            "%s" % str(processed))
                    else:
                        self.print_value2(
                            "%ss processed" % performance['type'].lower(),
                            "%s / %s" % (str(processed), str(total)))
                    if "error" in performance and int(performance['error']) > 0:
                        errors = True
                        error = performance['error']
                        if performance['type'] == "BYTE":
                            error = human_readable_bytes(error)
                        self.print_value2(
                            "erroneous %ss" % performance['type'].lower(),
                            str(error))
                for long_running_operation in progress['long_running_operation']:
                    self.print_message("Operation %s for %s at %s running since %d" % (
                        str(long_running_operation['operation_id']),
                        str(long_running_operation['subject']),
                        str(long_running_operation['rpc_target']),
                        int(long_running_operation['start_timestamp_ms'])))
                if 'human_readable_summary' in progress:
                    self.print_value2(
                        "summary",
                        progress['human_readable_summary'])
                if errors:
                    print_warning(0, "The task has errors, please run " \
                        "'qmgmt task geterrors %s' to list them." % str(args.task_id))

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} task list --list-columns=task_id"


class TaskGeterrors(Command):
    COMMAND_DESCRIPTION = "list all errors collected by the given task"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument('task_id', help="task id")

    def do_list(self, task_ids):
        result = self._json_rpc.call("getTaskList", {'task_id': task_ids})
        tasks = result['tasks']
        return tasks

    def print_errors(self, task):
        for errors in task['error_details']:
            print(errors['description'])
            for item in errors['item']:
                print(item)

    def run(self, args):
        tasks = self.do_list([str(args.task_id)])

        _output_json_and_exit(args, tasks)

        if tasks and len(tasks) > 0:
            task = tasks[0]
            if 'error_details' in task and len(task['error_details']) > 0:
                self.print_errors(task)
                print()

            if 'scope' in task:
                for scope in task['scope']:
                    if str(scope['type']) == "TASK" and len(scope['subject_id']) > 0:
                        for task in self.do_list(scope['subject_id']):
                            if 'error_details' in task and len(task['error_details']) > 0:
                                print("Task %s:" % str(task['task_id']))
                                self.print_errors(task)
                                print()
        else:
            print_warning(1, "Task %s does not exist." % str(args.task_id))

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} task list --list-columns=task_id"


class TaskList(Command):
    COMMAND_DESCRIPTION = "show a list of all tasks (scheduled, running, completed)"

    @staticmethod
    def add_specific_arguments(parser):
        TasksTable.add_specific_arguments(parser)
        parser.add_argument('--limit')
        parser.add_argument('task_state', nargs='+', type=str.upper,
                            choices=["CANCELED", "CANCELLING", "FAILED",
                                     "FINISHED", "RUNNING", "QUEUED", "SCHEDULED"])

    def do_list(self, args):
        request = {"only_root_tasks": True}
        for state in args.task_state:
            request.setdefault("task_state", []).append(state)
        if args.limit:
            request["task_count_limit"] = args.limit

        tasks = self._json_rpc.call("getTaskList", request)['tasks']
        return tasks

    def run(self, args):
        tasks = self.do_list(args)
        if tasks == -2:
            return -2
        tasks = sorted(tasks, key=lambda k: k['task_id'], reverse=True)
        tasks_dict = {}

        for task in tasks:
            if task['task_type'] == "REPLACE_DEVICE":
                task['task_type'] = "REPLACE"
            tasks_dict[task['task_id']] = task

        printer = TasksTable(
            args,
            tasks,
            ['task_id',
             'task_type',
             'state',
             ('scope', 0, 'type'),
             ('scope', 0, 'subject_id')],
            ["Id", "Type", "Status", "Scope Type", "Targets"]
        )
        print(printer.out())

    @staticmethod
    def completion_cmd():
        return "printf 'CANCELED\\nCANCELLING\\nFAILED" \
               "\\nFINISHED\\nRUNNING\\nQUEUED\\nSCHEDULED\\n'"

class FilesTaskBase(Command):

    _directory_path_note = ("NOTE: directory_path must be an absolute directory path from "
                            "the volume root.\n"
                            "      File path or glob is not supported.\n")

    _cloud_object_override_note = ("NOTE: if an object with the same name already exists in the\n"
                                   "      external destination, it is overwritten during copy\n")

    @staticmethod
    def get_filters_help():
        filters_help = ("--filters=\"<filters>\" each filter of the format:\n"
                        "  size{<|<=|=|>=|>}<bytes>      (filter file size in bytes)\n"
                        "  atime{<|<=|=|>=|>}<duration>  (filter access time duration "
                        "- for example, 30m, 1h, 2d)\n"
                        "  mtime{<|<=|=|>=|>}<duration>  (filter modification time duration"
                        " - for example, 30m, 1h, 2d)\n"
                        "  example: --filters=\"size=10000&atime>2d\"")
        return filters_help

    def get_quobyte_location(self, location, is_target=False):
        if not location.startswith('quobyte:'):
            raise InvalidInputException("Invalid Quobyte location \"" + location + "\"")
        quobyte_location_address = location.split("quobyte:")
        #[registries]/tenant/volume/[path]
        split_location = quobyte_location_address[1].split("/")
        if len(split_location) < 4:
            raise InvalidInputException("Invalid Quobyte location \"" + location + "\"")

        quobyte_location = {}
        registry = split_location[0]
        if registry:
            registries = registry.split(",")
            if registries:
                quobyte_location["registry"] = registries
        # volume uuid resolution is not supported for remote cluster
        if is_target and registry and split_location[1].strip():
            raise InvalidInputException("Invalid Quobyte target location \"" + location + "\""
                                        ". Registry and tenant cannot be provided together.")
        # quobyte:/tenant/volume/ - local volume and can be resolved
        if split_location[1].strip():
            volume_uuid, _, _ = self._resolver.resolve_volume(
                split_location[1] + "/" + split_location[2])
        else: # quobyte:remote-registries//volume_uuid/ - remote volume cannot be resolved
            volume_uuid = split_location[2]
        if len(volume_uuid.strip()) == 0:
            raise InvalidInputException("Invalid Quobyte location \"" + location + "\"")

        quobyte_location["volume"] = volume_uuid
        path = "/".join(split_location[3:])
        if not path.startswith("/"):
            path = "/" + path
        quobyte_location["path"] = path
        return {"quobyte": quobyte_location}

    def get_azure_destination_location(self, location):
        if not location.startswith('azure:'):
            self._raise_invalid_azure_destination_error(location)

        azure_url = None
        access_key = os.getenv("AZURE_STORAGE_KEY")
        azure_address = location.split("azure:")[1].split('@')
        if len(azure_address) == 1:
            # azure:http(s)://<url>
            if not azure_address[0].startswith('http://') and \
            not azure_address[0].startswith('https://'):
                self._raise_invalid_azure_destination_error(location)
            else:
                azure_url = azure_address[0]
        elif len(azure_address) == 2 and (azure_address[0].startswith('http://')
                                          or azure_address[0].startswith('https://')):
            # azure:http(s)://<access_key>@<url>
            azure_address_split = azure_address[0].split('://')
            azure_url = azure_address_split[0] + "://" + azure_address[1]
            access_key = azure_address_split[1]
        else:
            self._raise_invalid_azure_destination_error(location)

        if not azure_url:
            self._raise_invalid_azure_destination_error(location)
        if not access_key:
            raise InvalidInputException(("Empty shared access key. Access key is not provided "
            "in Azure URL and AZURE_STORAGE_KEY environment variable is not set."))
        if ':' in access_key:
            raise InvalidInputException(("Invalid shared access key. Access key "
            "must not contain a ':' character."))

        azure_location = {}
        azure_location["url"] = azure_url
        azure_location["access_key"] = access_key
        return {'azure_container': azure_location}

    def get_external_destination_location(self, location):
        if not location.startswith('external:') or len(location.split("external:")) != 2:
            raise InvalidInputException("Invalid external destination location \"" + location
                                        + "\". Must have the format: "
                                        + "external:<external-storage-location-uuid>")
        external_location_uuid = location.split("external:")[1]
        return {"external_storage_location_uuid": external_location_uuid}

    def _raise_invalid_azure_destination_error(self, location):
        raise InvalidInputException("Invalid azure destination location \"" + location + "\". "
                                    + "Must have the format: "
                                    + "azure:http(s)://<access-key>@<container-host>"
                                    + " or azure:http(s)://<container-host>")

    def _raise_invalid_s3_error(self, location):
        raise InvalidInputException("Invalid s3 location \"" + location + "\". "
                                    + "Must have the format: "
                                    + "s3:http(s)://<access-key-id>:<access-key-secret>"
                                    + "@<bucket-host> or s3:http(s)://<bucket-host>")

    def get_s3_location(self, location):
        if not location.startswith('s3:'):
            self._raise_invalid_s3_error(location)

        s3_url = None
        access_key_id = os.getenv("AWS_ACCESS_KEY_ID")
        access_key_secret = os.getenv("AWS_SECRET_ACCESS_KEY")
        s3_address = location.split("s3:")[1].split('@')
        if len(s3_address) == 1:
            if not s3_address[0].startswith('http://') \
            and not s3_address[0].startswith('https://'):
                self._raise_invalid_s3_error(location)
            else:
                s3_url = s3_address[0]
        elif len(s3_address) == 2 and (s3_address[0].startswith('http://') \
          or s3_address[0].startswith('https://')):
            s3_address_key_secret = s3_address[0].split('://')
            if len(s3_address_key_secret) < 2 or ":" not in s3_address_key_secret[1]:
                self._raise_invalid_s3_error(location)
            s3_url = s3_address_key_secret[0] + "://" + s3_address[1]
            access_key_id = s3_address_key_secret[1].split(':')[0]
            access_key_secret = s3_address_key_secret[1].split(':')[1]
        else:
            self._raise_invalid_s3_error(location)

        if not s3_url:
            self._raise_invalid_s3_error(location)
        if not access_key_id or not access_key_secret:
            raise InvalidInputException("Empty S3 credentials. Not provided in S3 URL"
            " and environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY are not set.")

        s3_location = {}
        s3_location["url"] = s3_url
        s3_location["access_key_id"] = access_key_id
        s3_location["access_key_secret"] = access_key_secret
        return {'s3_bucket': s3_location}

    def append_filters(self, args, job):
        if args.filters:
            filters = []
            for filter_arg in args.filters.split("&"):
                filters.append(self.__get_filter(filter_arg))
            if filters:
                job["filter"] = filters

    def __get_filter(self, filter_arg):
        enums_by_operator = [
            ("<=", "SMALLER_THAN_OR_EQUAL_TO"),
            (">=", "LARGER_THAN_OR_EQUAL_TO"),
            ("<", "SMALLER_THAN"),
            (">", "LARGER_THAN"),
            ("=", "EQUALS")
        ]
        for operator, enum in enums_by_operator:
            if operator in filter_arg:
                result = {}
                result["operator"] = enum
                split_filter_arg = filter_arg.split(operator)
                if len(split_filter_arg) != 2:
                    raise InvalidInputException("Invalid filter format: " + filter_arg
                                                + " Must be \"<type>{<|<=|=|>=|>}<value>\".")
                filter_property = split_filter_arg[0]
                if filter_property == "size":
                    result["type"] = "CURRENT_FILE_SIZE"
                    result["value"] = int(split_filter_arg[1])
                elif filter_property == "atime":
                    result["type"] = "LAST_ACCESS_AGE_S"
                    result["value"] = \
                        int(to_seconds_from_human_readable_duration(split_filter_arg[1]))
                elif filter_property == "mtime":
                    result["type"] = "LAST_MODIFICATION_AGE_S"
                    result["value"] = \
                        int(to_seconds_from_human_readable_duration(split_filter_arg[1]))
                else:
                    raise InvalidInputException("Unknown filter type: " + filter_property)
                return result
        raise InvalidInputException("Unknown filter operator: " + filter_arg)

    def run(self, args):
        pass


class FilesDelete(FilesTaskBase):
    COMMAND_DESCRIPTION = "delete files in the volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("target",
            help=
                "<target> has the following format:\n"
                "  quobyte:/tenant/volume/[directory_path]\n"
                "    or\n"
                "  quobyte://volume_uuid/[directory_path]\n"
                + FilesTaskBase._directory_path_note)
        parser.add_argument(
            "--run-as-user", action="store_true", help="Runs task as the current user, not root")
        parser.add_argument("--comment")

    def run(self, args):
        if not args.target.startswith('quobyte:'):
            print("Invalid Quobyte target location \"" + args.target)
            return -2
        quobyte_location = args.target
        if not quobyte_location.startswith('quobyte:/'):
            quobyte_location = quobyte_location.replace('quobyte:', 'quobyte:/')
        job = {}
        job["source"] = self.get_quobyte_location(quobyte_location)
        call = {"task_type": "DELETE_FILES_IN_VOLUMES",
                "comment": get_comment(args),
                "restrict_to_volumes": [job["source"]["quobyte"]["volume"]]
                }
        if "path" in job["source"]["quobyte"]:
            deleting_directory = job["source"]["quobyte"]["path"]
        else:
            deleting_directory = "/"
        call['delete_files_settings'] = {
            "directory_path": deleting_directory,
            "run_as_user": args.run_as_user
            }
        task_id = self._json_rpc.call("createTask", call)['task_id']
        if int(task_id) > 0:
            print("Success. Created new delete files task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class FilesRecode(FilesTaskBase):
    COMMAND_DESCRIPTION = "recode files in the volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("target",
            help=(
                "<target> has the following format:\n"
                "  quobyte:/tenant/volume/[directory_path]\n"
                "    or\n"
                "  quobyte://volume_uuid/[directory_path]\n"
                + FilesTaskBase._directory_path_note))
        parser.add_argument("--filters", action="store_true", help=FilesTaskBase.get_filters_help())
        parser.add_argument("--comment")
        parser.add_argument("--ignore-modified-files-errors",
                                action="store_true",
                                help="Recently modified files are not counted as"
                                 " errors and instead are skipped",
                                dest="do_not_fail_task_on_modified_files")

    def run(self, args):
        jobs = []
        restrict_to_volumes = []
        source = args.target
        if source.startswith('quobyte'):
            job = {
                'source': self.get_quobyte_location(source),
                'do_not_fail_on_modified_files': args.do_not_fail_task_on_modified_files,
            }
            restrict_to_volumes.append(job["source"]["quobyte"]["volume"])
            self.append_filters(args, job)
            jobs.append(job)
        elif source == "-": # recode supports batch jobs provided via stdin
            line_num = 0
            for line in sys.stdin:
                line_num += 1
                row = line.rstrip('\n').strip()
                if len(row) == 0:
                    continue
                row = row.split(",")
                if len(row) != 3:
                    raise InvalidInputException("Unexpected volume recode entry %s at line %d" %
                     (row, line_num))
                tenant = ""
                volume = ""
                if len(row[0].strip()) > 0:
                    tenant = row[0].strip()
                if len(row[1].strip()) > 0:
                    volume = row[1].strip()
                directory_path = "/"
                if len(row[2]) > 0:
                    directory_path = row[2]
                if not directory_path.startswith('/'):
                    raise InvalidInputException("Requires absolute path. Path %s at line %d" \
                      " is not an absolute path." % (directory_path, line_num))
                if volume is None or len(volume) == 0:
                    raise InvalidInputException("Invalid source volume %s at %d" \
                        % (volume, line_num))
                quobyte_source = "quobyte:/"
                quobyte_source += tenant
                quobyte_source += "/" + volume
                quobyte_source += directory_path
                job = {
                    'source': self.get_quobyte_location(quobyte_source),
                    'do_not_fail_on_modified_files': args.do_not_fail_task_on_modified_files,
                }
                restrict_to_volumes.append(job["source"]["quobyte"]["volume"])
                self.append_filters(args, job)
                jobs.append(job)
        else:
            raise InvalidInputException("Unknown source format for the recode command")
        settings = {"job": jobs}
        call = {"task_type": "RECODE_FILES",
                "comment": get_comment(args),
                "restrict_to_volumes": restrict_to_volumes,
                "copy_files_settings": settings
                }

        task_id = self._json_rpc.call("createTask", call)['task_id']
        if int(task_id) > 0:
            print("Success. Created a new recode files task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class FilesCopy(FilesTaskBase):
    COMMAND_DESCRIPTION = "copy files"

    @staticmethod
    def add_specific_arguments(parser):
        FilesCopy._add_arguments('copy', parser)

    @staticmethod
    def _add_arguments(command, parser):
        source_help=(
            "<source> has the following format:\n"
            "quobyte:/tenant/volume/[directory_path]\n"
            "  or\n"
            "quobyte://volume_uuid/[directory_path]\n"
            + FilesTaskBase._directory_path_note)
        if command == 'copy':
            source_help += (
            "  or\n"
            "s3:http(s)://<access-key-id>:<access-key-secret>@<bucket-host>\n"
            "  or\n"
            "s3:http(s)://<bucket-host> - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from\n"
            "the environment are used as access key and secret.\n")

        destination_help = (
            "<destination> has the following format:\n"
            "If source and destination volumes are in the same cluster:\n"
            "  quobyte:/tenant/volume/[<directory_path>]\n"
            "    or\n"
            "  quobyte://volume_uuid/[<directory_path>]\n"
            "If source and destination volumes are NOT in the same cluster:\n"
            "  quobyte:registry//volume_uuid/[<directory_path>]\n"
            "    registry can be SRV record or registry1:port1,...,registryN:portN\n")
        if command == 'copy':
            destination_help += (
            "  or\n"
            "s3:http(s)://<access-key-id>:<access-key-secret>@<bucket-host>\n"
            "  or\n"
            "s3:http(s)://<bucket-host> - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from\n"
            "the environment are used as access key and secret.\n"
            "  or\n"
            "azure:http(s)://<access-key>@<container-host>\n"
            "  or\n"
            "azure:http(s)://<container-host> - AZURE_STORAGE_KEY from the environment is used "
            "as the access key.\n"
             + FilesTaskBase._cloud_object_override_note)
        parser.add_argument("source", help=source_help)
        parser.add_argument("destination", help=destination_help)

        parser.add_argument("--filters", action="store_true", help=FilesTaskBase.get_filters_help())
        parser.add_argument("--comment")
        parser.add_argument("--ignore-modified-files-errors",
                            action="store_true",
                            help=
                                "Recently modified files are not counted as"
                                " errors and instead are skipped",
                            dest="do_not_fail_task_on_modified_files")
        parser.add_argument("--run-as-user",
                            action="store_true", help="Runs task as the current user, not root\n"
                            "(supported with only local cluster copy, move and copy from external "
                            "location tasks)")
        parser.add_argument(
            "--change-ownership", action="store_true",
            help=
                "SUPER_USER task running as root can change destination\n"
                " files ownership to root:root by using this flag. See\n"
                " documentation for more information.\n")
        parser.add_argument(
            "--destination-create-behavior", type=str.upper,
            choices=["FAIL_IF_FILE_EXISTS", "OVERWRITE_EXISTING_FILE",
                     "OVERWRITE_EXISTING_FILE_IF_OLDER"])

        if command == "copy":
            parser.add_argument("--snapshot-version", type=int)

    def validate_quobyte_source_and_quobyte_destination(self, settings):
        quobyte_source = ""
        quobyte_destination = ""
        if "quobyte" in settings["job"][0]["source"]:
            quobyte_source = [settings["job"][0]["source"]["quobyte"]["volume"]]
        if "quobyte" in settings["job"][0]["destination"]:
            quobyte_destination = [settings["job"][0]["destination"]["quobyte"]["volume"]]
        if quobyte_source == quobyte_destination:
            raise InvalidInputException("Source and destination must not have same volume UUID")

    def get_copy_file_settings(self, args):
        settings = {}
        job = {}
        source = args.source
        job["do_not_fail_on_modified_files"] = args.do_not_fail_task_on_modified_files
        if source.startswith('s3:'):
            job["source"] = self.get_s3_location(source)
        elif source.startswith('quobyte:'):
            job["source"] = self.get_quobyte_location(source)
        else:
            raise InvalidInputException("Invalid source location")
        if source.startswith('quobyte:') and hasattr(args, 'snapshot_version') and \
              args.snapshot_version:
            job['source']['quobyte']['snapshot_version'] = args.snapshot_version
        destination = args.destination
        destination_file_settings = {}
        if destination.startswith('quobyte:'):
            job["destination"] = self.get_quobyte_location(args.destination, True)
            destination_file_settings["change_ownership"] = args.change_ownership
            if args.destination_create_behavior:
                destination_file_settings["create_behavior"] = args.destination_create_behavior
        elif destination.startswith('s3:'):
            job["destination"] = self.get_s3_location(args.destination)
            destination_file_settings["create_behavior"] = 'OVERWRITE_EXISTING_FILE'
        elif destination.startswith('azure:'):
            job["destination"] = self.get_azure_destination_location(args.destination)
            destination_file_settings["create_behavior"] = 'OVERWRITE_EXISTING_FILE'
        else:
            raise InvalidInputException("destination must be of Quobyte, S3, Azure or external"
                                        " location type")
        self.append_filters(args, job)
        job["destination_file_settings"] = destination_file_settings
        settings = {"job": [job],
                    "run_as_user": args.run_as_user
                    }
        self.validate_quobyte_source_and_quobyte_destination(settings)
        return settings

    def run(self, args):
        settings = self.get_copy_file_settings(args)
        restrict_to_volumes = ""
        if "quobyte" in settings["job"][0]["source"]:
            restrict_to_volumes = settings["job"][0]["source"]["quobyte"]["volume"]
        else:
            restrict_to_volumes = settings["job"][0]["destination"]["quobyte"]["volume"]
        call = {"task_type": "COPY_FILES",
                "comment": get_comment(args),
                "restrict_to_volumes": [restrict_to_volumes],
                "copy_files_settings": settings
                }
        task_id = self._json_rpc.call("createTask", call)['task_id']
        if int(task_id) > 0:
            print("Success. Created a new copy files task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class FilesMove(FilesCopy):
    COMMAND_DESCRIPTION = "move files between Quobyte volumes"

    @staticmethod
    def add_specific_arguments(parser):
        FilesCopy._add_arguments('move', parser)

    def run(self, args):
        destination = args.destination
        if not destination.startswith('quobyte:'):
            raise InvalidInputException('Files move only supports Quobyte destination')
        settings = self.get_copy_file_settings(args)
        call = {"task_type": "MOVE_FILES",
                "comment": get_comment(args),
                "restrict_to_volumes": [settings["job"][0]["source"]["quobyte"]["volume"]],
                "copy_files_settings": settings
                }
        task_id = self._json_rpc.call("createTask", call)['task_id']
        if int(task_id) > 0:
            print("Success. Created a new move files task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class FilesPush(FilesTaskBase):
    COMMAND_DESCRIPTION = "push files to an external location"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("source",
            help=
                "<target> has the following format:\n"
                "  quobyte:/tenant/volume/[directory_path]\n"
                "    or\n"
                "  quobyte://volume_uuid/[directory_path]\n"
                + FilesTaskBase._directory_path_note)
        parser.add_argument("destination",
                            help="external:<external-location-uuid>\n"
                            + FilesTaskBase._cloud_object_override_note)
        parser.add_argument("--comment")

    def run(self, args):
        source = args.source
        if not source.startswith('quobyte:'):
            print("Files push command requires Quobyte source.")
            return -2
        split_source = source.split('quobyte:')[1].split('/')
        if len(split_source) < 4:
            print("Invalid Quobyte source.")
            return -2

        destination = args.destination
        if not destination.startswith('external:'):
            print("Files push command requires external location destination.")
            return -2

        job = {}
        job["source"] = self.get_quobyte_location(args.source)
        job["destination"] = self.get_external_destination_location(args.destination)
        settings = {"job": [job]}
        call = {"task_type": "PUSH_DATA",
                "comment": get_comment(args),
                "restrict_to_volumes": [job["source"]["quobyte"]["volume"]],
                "copy_files_settings": settings
                }

        task_id = self._json_rpc.call("createTask", call)['task_id']
        if int(task_id) > 0:
            print("Success. Created a new push files task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class FilesPull(FilesTaskBase):
    COMMAND_DESCRIPTION = "pull files back into Quobyte volume"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("source",
            help="<target> has the following format:\n"
                 "  quobyte:/tenant/volume/[directory_path]\n"
                 "    or\n"
                 "  quobyte://volume_uuid/[directory_path]\n"
                 + FilesTaskBase._directory_path_note)
        parser.add_argument("--comment")

    def run(self, args):
        source = args.source
        if not source.startswith('quobyte:'):
            print("Files pull requires Quobyte source.")
            return -2
        split_source = source.split('quobyte:')[1].split('/')
        if len(split_source) < 4:
            print("Invalid Quobyte source.")
            return -2

        job = {}
        job["source"] = self.get_quobyte_location(args.source)
        settings = {"job": [job]}
        call = {"task_type": "PULL_DATA",
                "comment": get_comment(args),
                "restrict_to_volumes": [job["source"]["quobyte"]["volume"]],
                "copy_files_settings": settings
                }

        task_id = self._json_rpc.call("createTask", call)['task_id']
        if int(task_id) > 0:
            print("Success. Created a new pull files task with task id " + task_id)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class ClientList(Command):
    COMMAND_DESCRIPTION = "list all known clients"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def do_list(self):
        result = self._json_rpc.call("getClientList", {})
        client = result['client']
        return client

    def run(self, args):
        clients = self.do_list()
        printer = PrettyTable(
            args,
            clients,
            ['hostname', 'mounted_volume_uuid', 'mount_user_name',
             'client_software_version', 'local_mount_point'],
            ["Hostname", "Volume UUID", "User ID", "Version", "Local Mount"]
        )
        print(printer.out())


class AuditLogList(Command):
    COMMAND_DESCRIPTION = "list audit log entries"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)
        parser.add_argument("--oldest-first", action="store_true")
        parser.add_argument("--limit", type=int, default=100)
        parser.add_argument("subject_type", nargs='?', type=str.upper,
                            choices=["DEVICE", "VOLUME", "TASK", "CONFIGURATION",
                                     "USER", "VOLUME_CONFIGURATION",
                                     "QUOTA", "RULE", "POLICY_RULE", "KEY_STORE"])
        parser.add_argument("subject_id",
                            nargs='?',
                            help="Depending on the subject type, e.g. a device id or volume uuid")

    def run(self, args):
        request = {
            "logs_limit": args.limit,
            "oldest_log_first": args.oldest_first
        }
        if args.subject_type:
            if not args.subject_id:
                print("No subject id given")
                sys.exit(-2)
            request["only_subject_type"] = args.subject_type
            request["only_subject_id"] = args.subject_id
        audit_events = self._json_rpc.call("getAuditLog", request)["audit_event"]
        printer = PrettyTable(
            args,
            audit_events,
            ['timestamp_ms', 'username', 'subject_type', 'subject_id', 'action', 'comment'],
            ['Timestamp', "User", "Subject Type", "Subject ID", "Action", "Comment"]
        )
        print(printer.out())


class AlertList(Command):
    COMMAND_DESCRIPTION = "list active alerts"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def do_list(self):
        result = self._json_rpc.call("getFiringRules", {})
        rules = result['rule']
        return rules

    def run(self, args):
        rules = self.do_list()
        alerts = []
        for rule in rules:
            alert = dict(rule)
            silenced_until = rule.get("silenced_until_timestamp_s", 0)
            if silenced_until == 0:
                alert["status"] = "Active"
            elif silenced_until == sys.maxsize:
                alert["status"] = "Acknowledged"
            else:
                silent_for_ms = (silenced_until - time.time()) * 1000
                if silent_for_ms <= 0:
                    alert["status"] = "Active"
                else:
                    alert["status"] = "Silenced for " + to_human_readable_ms(silent_for_ms)
            alerts.append(alert)

        alerts.sort(
            key=lambda x: (self.__get_status_order(x["status"]),
                           self.__get_severity_order(x["severity"])))
        printer = PrettyTable(
            args,
            alerts,
            ['alert_identifier', 'status', 'severity', 'user_message', 'user_suggested_action'],
            ['Alert Id', "Status", "Severity", "User message", "Suggested action"],
            sorting_depth=0
        )
        print(printer.out())

    @staticmethod
    def __get_status_order(status):
        if status == "Active":
            return 1
        if status == "Acknowledged":
            return 3
        return 2 # Silenced

    @staticmethod
    def __get_severity_order(status):
        if status == "INFO":
            return 3
        if status == "ERROR":
            return 1
        return 2 # Warning


class AlertSilence(Command):
    COMMAND_DESCRIPTION = "silence an active alert"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("alert_id")
        parser.add_argument(
            "duration",
            help="duration to silence the alert with time unit (e.g. 20m, 2h, or 2d")

    def run(self, args):
        silence_for_s = to_seconds_from_human_readable_duration(args.duration)

        # call API
        self._json_rpc.call(
                   "silenceAlert",
                   {"alert_identifier": args.alert_id,
                    "silence_for_s": silence_for_s})

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} alert list --list-columns=alert_identifier"


class AlertAcknowledge(Command):
    COMMAND_DESCRIPTION = "acknowledge an active alert"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("alert_id")

    def run(self, args):
        self._json_rpc.call("acknowledgeAlert", {"alert_identifier": args.alert_id})

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} alert list --list-columns=alert_identifier"


class RegistryAddReplica (Command):
    COMMAND_DESCRIPTION = "add a replica to the registry service"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("device_id", type=int)
        parser.add_argument("--comment")

    def run(self, args):
        self._json_rpc.call(
            "addRegistryReplica", {
                'device_id': args.device_id,
                'comment': get_comment(args)})
        print("Success, added replica ", args.device_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} device list REGISTRY --list-columns=device_id"


class RegistryRemoveReplica (Command):
    COMMAND_DESCRIPTION = "remove a replica from the registry service"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("device_id", type=int)
        parser.add_argument("--comment")

    def run(self, args):
        self._json_rpc.call(
            "removeRegistryReplica", {'device_id': args.device_id,
                                      'comment': get_comment(args)})
        print("Success, removed replica " + args.device_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} device list REGISTRY --list-columns=device_id"


class RegistryListReplicas (Command):
    COMMAND_DESCRIPTION = "shows the current active registry replica set"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def list_replicas(self):
        result = self._json_rpc.call("listRegistryReplicas", {})
        return result['device_ids']

    def get_list(self):
        result = self._json_rpc.call("getDeviceList", {'device_type': ["REGISTRY"]})
        return result['device_list']['devices']

    def run(self, args):
        dev_list = self.get_list()
        replica_set = self.list_replicas()
        printer = PrettyTable(
            args,
            [dev for dev in dev_list if str(dev['device_id']) in replica_set],
            ['is_primary', 'device_id', 'host_name', 'device_status'],
            ["Primary", "Id", "Host", "Mode"])
        print(printer.out())


class ServiceDelete (Command):
    COMMAND_DESCRIPTION = "delete an unavailable service or client"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("uuid", help="service or client uuid")

    def run(self, args):
        self._json_rpc.call(
            "deleteService", {'service_uuid': args.uuid})
        print("Success, deleted service", args.uuid, "from Registry")

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} service list --list-columns=service_uuid"


class ServiceList (Command):
    COMMAND_DESCRIPTION = "show a list of all services"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def list_services(self):
        return self._json_rpc.call('getServices', {})['service']

    def run(self, args):
        service_list = self.list_services()
        printer = PrettyTable(
            args,
            service_list,
            ['service_name', 'service_type', 'service_uuid', 'is_available'],
            ["Name", "Type", "UUID", "Availability"])
        printer.modify_column('is_available', lambda x: "*" if x else "(unavailable)")
        print(printer.out())


class DatabaseRegenerate (Command):
    COMMAND_DESCRIPTION = "Regenerate internal databases"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("type",
                            help="service or client uuid" , type=str.upper,
                            choices=['UPDATE_SCHEMA', 'VOLUME_ACCOUNTING'])
        parser.add_argument("volume_uuid")

    def regenerate_database(self, index, target):
        return self._json_rpc.call("regenerateDatabase",
                                   {'databaseType': index, 'database_identifier': target})

    def run(self, args):
        target = args.volume_uuid
        if args.type == "UPDATE_SCHEMA":
            _confirmation_prompt(
                "Volume '%s' will not be available until the "
                "database schema update is completed. Do you "
                "want to continue?" % (target))
        self.regenerate_database(args.type, target)
        print(
            "Success, database regeneration action '%s' "
            "has been scheduled for volume '%s'" % (args.type, target))

    @staticmethod
    def completion_cmd():
        return "printf 'VOLUME_ACCOUNTING\\n'"


class AddCA (Command):
    COMMAND_DESCRIPTION = "add certificate authority"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("name", help="name of the CA")
        parser.add_argument("public_key",
                            help="Will be generated if not given. Can be string or filename.")
        parser.add_argument("private_key",
                             help="can be either the path to a PEM file or a PEM string. "
                                  + "Will be generated if not given")

    def addCa(self, name, public_key, private_key):
        certificate = {}
        if public_key is not None:
            certificate["certificate"] = public_key
        if private_key is not None:
            certificate["private_key"] = private_key
        ca = {"name": name, "certificate": certificate}
        return self._json_rpc.call("addCa",
                                   {"name": name,
                                    "certificate_authority": ca})

    def run(self, args):
        public_key = None
        private_key = None
        if args.public_key:
            public_key = get_string_or_filecontent(args.public_key)
        if args.private_key:
            private_key = get_string_or_filecontent(args.private_key)
        self.addCa(args.name, public_key, private_key)
        print("Success, added CA " + args.name)

    @staticmethod
    def completion_cmd():
        return ("echo __files__",
                "echo __files__")

class ListCA (Command):
    COMMAND_DESCRIPTION = "list certificate authorities"

    def get_cas(self):
        return self._json_rpc.call("listCa", {})["ca"]

    def run(self, args):
        result = self.get_cas()

        _output_json_and_exit(args, result)

        for ca in result:
            print(ca["name"] + ":\n" + ca["certificate"]["certificate"] + "\n")


class DeleteCA (Command):
    COMMAND_DESCRIPTION = "delete certificate authority"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("name", help="name of the CA")

    def delete_ca(self, name):
        return self._json_rpc.call("deleteCa", {'name': name})

    def run(self, args):
        _confirmation_prompt("Really delete CA " + args.name + "?")
        self.delete_ca(args.name)
        print("Success, deleted CA " + args.name)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} ca list --list-columns=name"


class ExportCA (Command):
    COMMAND_DESCRIPTION = "export CA certificate"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("name", help="name of the CA")
        parser.add_argument("output", help="Optional output file name", nargs='?')

    def get_ca(self, name):
        cas = self._json_rpc.call("listCa", {})["ca"]
        for ca in cas:
            if ca["name"] == name:
                return ca["certificate"]["certificate"]
        return None

    def run(self, args):
        ca = self.get_ca(args.name)
        if ca is None:
            print_warning(1, "Unknown CA")
            return -2
        if not args.output:
            print(ca)
        else:
            with open(args.output, 'w', encoding="utf-8") as f:
                f.write(ca)
            print("Success, exported CA certificate to " + args.output)
        return 0

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} ca list --list-columns=name"


class AddCsr (Command):
    COMMAND_DESCRIPTION = "add certificate signing request (CSR)"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("csr", help="as string or path to file")

    def add_csr(self, csr):
        return self._json_rpc.call("addCsr", {"csr": {"csr_description": csr}})["csr_id"]

    def run(self, args):
        csr = get_string_or_filecontent(args.csr)
        csr_id = self.add_csr(csr)
        print("Success, added CSR with id", csr_id)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class ListCsr (Command):
    COMMAND_DESCRIPTION = "list certificate signing requests"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("state", choices=['PENDING', 'APPROVED', 'REJECTED'], nargs='?',
                            type=str.upper)
        PrettyTable.add_specific_arguments(parser)

    def list_csr(self, state):
        request = {}
        if state:
            request["state"] = state
        return self._json_rpc.call("listCsr", request)["csr"]

    def run(self, args):
        csrs = self.list_csr(args.state)
        if len(csrs) > 0:
            printer = PrettyTable(
                args,
                csrs,
                ['csr_id', 'state', 'subject', 'certificate_fingerprint'],
                ["Id", "State", "Subject", "Certificate Fingerprint"]
            )
            print(printer.out())
        else:
            if args.output != "comp":
                print_warning(1, "No certificate signing requests found")

    @staticmethod
    def completion_cmd():
        return "printf 'PENDING\\nAPPROVED\\nREJECTED\\n'"


class ApproveCsr (Command):
    COMMAND_DESCRIPTION = "approve certificate signing request"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("csr_id", type=int)
        parser.add_argument("subject", help="Overwrite subject", nargs='?')

    def run(self, args):
        req = {}
        req["csr_id"] = args.csr_id
        if args.subject:
            req["effective_subject"] = args.subject
        req["approve"] = True
        self._json_rpc.call("decideCsr", req)
        print("Success, approved CSR with id", args.csr_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} csr list --list-columns=csr_id"


class RejectCsr (Command):
    COMMAND_DESCRIPTION = "reject certificate signing request"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("csr_id", type=int)

    def run(self, args):
        req = {}
        req["csr_id"] = args.csr_id
        req["approve"] = False
        self._json_rpc.call("decideCsr", req)
        print("Success, rejected CSR with id", args.csr_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} csr list --list-columns=csr_id"


class DeleteCsr (Command):
    COMMAND_DESCRIPTION = "delete certificate signing request"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("csr_id", type=int)

    def run(self, args):
        req = {}
        req["csr_id"] = args.csr_id
        self._json_rpc.call("deleteCsr", req)
        print("Success, deleted CSR with id", args.csr_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} csr list --list-columns=csr_id"


class AddCertificate (Command):
    COMMAND_DESCRIPTION = "add certificate"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("certificate", help="certficate itself or filename containing one")
        parser.add_argument("csr_id", nargs='?', type=int)

    def run(self, args):
        certificate = get_string_or_filecontent(args.certificate)
        req = {"certificate": {"certificate": certificate}}
        if args.csr_id:
            req["csr_id"] = args.csr_id
        fingerprint = self._json_rpc.call("addCertificate", req)["fingerprint"]
        print("Sucess, added certificate with fingerprint", fingerprint)

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class DeleteCertificate (Command):
    COMMAND_DESCRIPTION = "delete certificate"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("fingerprint", help="certficate fingerprint")

    def deleteCertificate(self, fingerprint):
        return self._json_rpc.call("deleteCertificate", {"fingerprint": fingerprint})

    def run(self, args):
        self.deleteCertificate(args.fingerprint)
        print("Success, deleted certificate with fingerprint " + args.fingerprint)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} certificate list --list-columns=fingerprint"


class ListCertificate (Command):
    COMMAND_DESCRIPTION = "list certificates"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)

    def listCertificates(self):
        return self._json_rpc.call("listCertificates", {})["certificate"]

    def run(self, args):
        certificates = self.listCertificates()
        if len(certificates) > 0:
            printer = PrettyTable(
                args,
                certificates,
                ['fingerprint', 'last_seen_from_host',
                 'last_seen_timestamp_seconds', 'subject_string'],
                ["Fingerprint", "Last seen from host",
                 "Last seen timestamp", "Subject"]
            )
            print(printer.out())
        else:
            if args.output != "comp":
                print_warning(1, "No certificates found")


class ExportCertificate (Command):
    COMMAND_DESCRIPTION = "export certificate as PEM"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("fingerprint", help="certficate fingerprint")
        parser.add_argument("output_file", help="output file", nargs='?')

    def exportCertificate(self, fingerprint):
        return self._json_rpc.call(
            "exportCertificate",
            {"fingerprint": fingerprint})["certificate"]

    def run(self, args):
        certificate = self.exportCertificate(args.fingerprint)
        if not args.output_file:
            print(certificate)
        else:
            with open(args.output_file, 'w', encoding="utf-8") as f:
                f.write(certificate)
            print("Success, exported certificate to " + args.output_file)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} certificate list --list-columns=fingerprint"


class CertificateConfigShow(Command):
    COMMAND_DESCRIPTION = "Show certificate configuration"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("fingerprint", help="certficate fingerprint")

    def getCertificateConfig(self, fingerprint):
        return self._json_rpc.call("getCertificateSubject", {"fingerprint": fingerprint})

    def run(self, args):
        print(self.getCertificateConfig(args.fingerprint))


class CertificateConfigSet(Command):
    COMMAND_DESCRIPTION = "Set certificate config"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("fingerprint", help="certficate fingerprint")
        parser.add_argument("config", help="path to file or string, in json")

    def setCertificateConfig(self, fingerprint, config):
        return self._json_rpc.call("setCertificateSubject",
                                   {"fingerprint": fingerprint,
                                    "subject": config.get("subject", config)})

    def run(self, args):
        config = get_string_or_filecontent(args.config)
        self.setCertificateConfig(args.fingerprint, json.loads(config))


class CertificateConfigEdit (Command):
    COMMAND_DESCRIPTION = "Edit certificate subject"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("fingerprint", help="certficate fingerprint")

    def getCertificateConfig(self, fingerprint):
        return self._json_rpc.call("getCertificateSubject", {"fingerprint": fingerprint})

    def setCertificateConfig(self, fingerprint, config):
        return self._json_rpc.call("setCertificateSubject",
                                   {"fingerprint": fingerprint,
                                    "subject": config.get("subject", config)})
    def run(self, args):
        # Read
        config = json.dumps(self.getCertificateConfig(args.fingerprint))
        try:
            after = DataDumpHelper.edit_json(config)
            if after != "":
                self.setCertificateConfig(args.fingerprint, json.loads(after))
                print("Success. Updated configuration ")
            else:
                print("Success. Configuration was not modified.")
        except IOError as e:
            print_warning(1, "Failed. Configuration was not updated: %s" % str(e))


class GetLabel (Command):
    COMMAND_DESCRIPTION = "retrieve one or more labels"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("entity_type", choices=['VOLUME', 'TENANT', 'SERVICE'], nargs='?',
                            type=str.upper)
        parser.add_argument("entity_id", nargs='?')
        parser.add_argument("name", nargs='?')

    def run(self, args):
        rpc_args = {}
        if args.entity_type:
            rpc_args['filter_entity_type'] = args.entity_type
        if args.entity_id:
            rpc_args['filter_entity_id'] = args.entity_id
        if args.name:
            rpc_args['label_name'] = args.name

        rpc_args['namespace'] = 'SYSTEM'

        data = self._json_rpc.call("getLabels", rpc_args)['label']

        _output_json_and_exit(args, data)

        if len(data) > 0:
            for entity in data:
                label_string = entity['entity_type'] + " " + entity['entity_id'] + ": " + \
                      entity['name']
                if entity['value']:
                    label_string += " = " + entity['value']
                print(label_string)
        else:
            print_warning(1, "No labels found")

    @staticmethod
    def completion_cmd():
        return "printf 'VOLUME\\nTENANT\\nSERVICE\\n'"


class SetLabel (Command):
    COMMAND_DESCRIPTION = "set a label"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("entity_type", choices=['VOLUME', 'TENANT', 'SERVICE'],
                            type=str.upper)
        parser.add_argument("entity_id")
        parser.add_argument("name")
        parser.add_argument("value", nargs='?')

    def run(self, args):
        rpc_args = {}
        rpc_args['namespace'] = 'SYSTEM'
        rpc_args['entity_type'] = args.entity_type
        rpc_args['entity_id'] = args.entity_id
        rpc_args['name'] = args.name
        if args.value:
            rpc_args['value'] = args.value

        self._json_rpc.call("setLabels", {"label": [rpc_args]})

        print("Set label " + rpc_args['name'] + " on " +\
              rpc_args['entity_type'] + "/" + rpc_args['entity_id'])

    @staticmethod
    def completion_cmd():
        return "printf 'VOLUME\\nTENANT\\nSERVICE\\n'"


class DeleteLabel (Command):
    COMMAND_DESCRIPTION = "delete a label"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("entity_type", choices=['VOLUME', 'TENANT', 'SERVICE'],
                            type=str.upper)
        parser.add_argument("entity_id", help="UUID or id of entity")
        parser.add_argument("name", help="Label name")

    def run(self, args):
        rpc_args = {}
        rpc_args['namespace'] = 'SYSTEM'
        rpc_args['entity_type'] = args.entity_type
        rpc_args['entity_id'] = args.entity_id
        rpc_args['name'] = args.name

        self._json_rpc.call("deleteLabels", {"label": [rpc_args]})

        print("Deleted label " + rpc_args['name'] + " on " +\
              rpc_args['entity_type'] + "/" + rpc_args['entity_id'])

    @staticmethod
    def completion_cmd():
        return "printf 'VOLUME\\nTENANT\\nSERVICE\\n'"


class CreateAccessKey(Command):
    COMMAND_DESCRIPTION = "create an access key for user authentication"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("type", type=str.upper,
                            choices=['DATA_ACCESS_KEY',
                                     'MANAGEMENT_ACCESS_KEY',
                                     'GENERAL_ACCESS_KEY'])
        parser.add_argument("username")
        parser.add_argument("--tenant",
                            help="tenant membership, "
                                 "required for DATA_ACCESS_KEY and GENERAL_ACCESS_KEY")
        parser.add_argument("--validity-days", type=int, default=0,
                            help="Key is valid for this many days")

    def run(self, args):
        request = {
            "access_key_type": args.type,
            "user_name": args.username,
            "validity_days": args.validity_days
            }

        if args.type in ("DATA_ACCESS_KEY", "GENERAL_ACCESS_KEY"):
            if not args.tenant:
                print_warning(1, "For access key type " + args.type + " tenant is required.")
                print()
                return -2
            else:
                tenant_uuid, _ = self._resolver.resolve_tenant(args.tenant)
                request["tenant_id"] = tenant_uuid
        elif args.type == "MANAGEMENT_ACCESS_KEY":
            tenant_uuid = None
        else:
            raise InvalidInputException("Unsupported access key type: " + args.type)

        result = self._json_rpc.call("createAccessKeyCredentials", request)
        print("Created access key with id:", result['access_key_credentials']['access_key_id'])

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} accesskey create"


class DeleteAccessKey(Command):
    COMMAND_DESCRIPTION = "delete access key credentials"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("username")
        parser.add_argument("access_key_id",
                            help="if not specified, all keys for this user will be deleted",
                            nargs='?')

    def run(self, args):
        if not args.access_key_id:
            try:
                user_details = self._json_rpc.call(
                    "getUsers", {"user_id": [args.username]})['user_configuration'][0]
            except:
                raise InvalidInputException("Wrong user id: %s." % args.username)
            if not user_details['access_key_credentials']:
                raise Exception(
                    "User '%s' has no assigned access key credentials."
                    % args.username)
            else:
                for key in user_details['access_key_credentials']:
                    self._json_rpc.call("deleteAccessKeyCredentials",
                                        {"user_name": user_details['id'],
                                         "access_key_id": key['access_key_id']})
                    print("Deleted S3 access key credentials with id: %s" \
                          % key['access_key_id'])
        else:
            self._json_rpc.call("deleteAccessKeyCredentials",
                                {"user_name": args.username,
                                 "access_key_id": args.access_key_id})
            print("Deleted access key with id: %s" % args.access_key_id)

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} accesskey delete"


class ListAccessKey(Command):
    COMMAND_DESCRIPTION = "list access key credentials"

    @staticmethod
    def add_specific_arguments(parser):
        PrettyTable.add_specific_arguments(parser)
        parser.add_argument("username", nargs='?')

    def run(self, args):
        if not args.username:
            users = self._json_rpc.call("getUsers", {})['user_configuration']
        else:
            users = self._json_rpc.call(
                "getUsers", {"user_id": [args.username]})['user_configuration']

        printer = PrettyTable(
            args,
            [{'id': user['id'], 'key': key} for user in users
             for key in user['access_key_credentials']],
            ['id',
             ('key', 'access_key_id'),
             ('key', 'secret_access_key'),
             ('key', 'type'),
             ('key', 'tenant_id'),
             ('key', 'valid_until_timestamp_ms')],
            ["User", "Access Key Id", "Secret Access Key", "Type", "Tenant", "Valid Until"]
        )
        print(printer.out())

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} accesskey list"


class ImportAccessKey(Command):
    COMMAND_DESCRIPTION = "import access key credentials"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("inputfile",
            help="The csv file <access-keys-file.csv> should have a header line followed by the"
                 " line(s) of access key parameters. The header specifies the access key type."
                 " The format of the line with access key parameters depends on the specified"
                 " access key type:"
                 " MANAGEMENT_ACCESS_KEY"
                 " <user_name>,<access_key_id>,<access_key_secret>"
                 "[,<validity_days>]"
                 " DATA_ACCESS_KEY, GENERAL_ACCESS_KEY"
                 " <user_name>,<access_key_id>,<access_key_secret>,<tenant>"
                 "[,<validity_days>]"
                 " Examples for csv file entries:"
                 "             MANAGEMENT_ACCESS_KEY"
                 "             user1,accessKeyId1,secretKey1,365"
                 "             user2,accessKeyId2,secretKey2"
                 " or"
                 "             DATA_ACCESS_KEY"
                 "             user1,accessKeyId1,secretKey1,myTenantId,45"
                 "             user2,accessKeyId2,secretKey2,myTenantId")
        parser.add_argument("--format", choices=["S3_KEY_IMPORT_FORMAT"])

    def run(self, args):
        with open(args.inputfile, "r", encoding="utf8") as csv_file:
            reader = csv.reader(csv_file, delimiter=',')
            access_key_type = None
            line_number = 0
            if args.format == "S3_KEY_IMPORT_FORMAT":
                # use old format without header line
                access_key_type = "DATA_ACCESS_KEY"
                line_number = 1
            for key_entries in reader:
                if line_number == 0:
                    access_key_type = key_entries[0]
                    if access_key_type not in (
                            "DATA_ACCESS_KEY","GENERAL_ACCESS_KEY", "MANAGEMENT_ACCESS_KEY"):
                        raise InvalidInputException(
                            "Unsupported access key type: " + access_key_type)
                    line_number += 1
                    continue
                # check number of parameters
                if access_key_type in ("DATA_ACCESS_KEY", "GENERAL_ACCESS_KEY"):
                    min_entry_number = 4
                    max_entry_number = 5
                else:
                    assert access_key_type == "MANAGEMENT_ACCESS_KEY"
                    min_entry_number = 3
                    max_entry_number = 4
                if len(key_entries) < min_entry_number or len(key_entries) > max_entry_number:
                    print_warning(1, "Row {0}: Unsupported format.".format(reader.line_num))
                    return -2
                if len(key_entries) == min_entry_number:
                    key_entries.append('0')

                try:
                    user_name = key_entries[0]
                    access_key_id = key_entries[1]
                    secret_access_key = key_entries[2]
                    if access_key_type == "MANAGEMENT_ACCESS_KEY":
                        validity_days = key_entries[3]
                        access_key_details = self.__create_access_key_details(
                            access_key_type=access_key_type,
                            access_key_id=access_key_id,
                            secret_access_key=secret_access_key,
                            days=validity_days
                        )
                    else:
                        assert access_key_type == "DATA_ACCESS_KEY" \
                            or access_key_type == "GENERAL_ACCESS_KEY"
                        tenant_id = key_entries[3]
                        validity_days = key_entries[4]
                        access_key_details = self.__create_access_key_details(
                            access_key_type=access_key_type,
                            access_key_id=access_key_id,
                            secret_access_key=secret_access_key,
                            tenant_id=tenant_id,
                            days=validity_days
                        )
                    print(user_name, access_key_details)
                    self._json_rpc.call(
                        "importAccessKeys",
                        {"user_name": user_name, "access_key_details": [access_key_details]})
                    print("Access key with id '{0}' has been imported.".format(access_key_id))
                except Exception as e:
                    print("Row {0}: Import failed for access key with id {1} due to {2}".format(
                        reader.line_num, access_key_id, str(e)))

    def __create_access_key_details(
            self,
            access_key_type,
            access_key_id,
            secret_access_key,
            tenant_id = None,
            days = "0") -> dict:
        validity_days = int(days)
        if tenant_id:
            tenant_id, _ = self._resolver.resolve_tenant(tenant_id)
        access_key_details = {
            "access_key_id": access_key_id,
            "type": access_key_type,
            "secret_access_key": secret_access_key,
            "validity_days": validity_days
        }
        if tenant_id:
            access_key_details["tenant_id"] = tenant_id
        return access_key_details

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} accesskey import"


class KeyStoreCreateSystemSlot (Command):
    COMMAND_DESCRIPTION = "create system-owned key store slot"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("name")
        parser.add_argument("password", nargs='?')

    def run(self, args):
        if not args.password:
            password = get_password_from_prompt()
        else:
            password = args.password

        if len(password) < 20:
            print_warning(0, "You chose a rather short password."
                          "We recommend to use at least 20 characters")
        self._json_rpc.call(
            "createMasterKeystoreSlot",
            {"master_keystore_slot_password": password,
             "master_keystore_slot_name": args.name})
        print("Success. Key store initialized.")

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} encryptkeystoreion init"


class KeyStoreStatus (Command):
    COMMAND_DESCRIPTION = "show key store status"

    def run(self, args):
        result = self._json_rpc.call("getEncryptStatus", {})
        print("Status: " + str(result['status']))

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} key store status"


class KeyStoreUnlock (Command):
    COMMAND_DESCRIPTION = "unlock system key store after cluster cold-start"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("password", nargs='?')

    def run(self, args):
        if not args.password:
            args.password = getpass.getpass(
                prompt="Please enter key store master password: ")

        self._json_rpc.call(
            "unlockMasterKeystoreSlot",
            {"master_keystore_slot_password": args.password})

        print("Success.")

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} user config list --list-columns=id"


class ListSystemKeystoreSlots (Command):
    COMMAND_DESCRIPTION = "list system-owned key slots"

    def run(self, args):
        result = self._json_rpc.call("getMasterKeystoreSlots", {})
        printer = PrettyTable(
            args,
            result["slot"],
            ["uuid",
             "name",
             "created_timestamp_ms",
             "created_by_username"],
            ["UUID", "Name", "Created", "Created By"])
        print(printer.out())

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} keystore list-system-slots"


class DeleteSystemKeystoreSlot(Command):
    COMMAND_DESCRIPTION = "delete system-owned key store slot"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("slot_uuid")

    def run(self, args):
        result = self._json_rpc.call(
            "removeMasterKeystoreSlot", {
                "keystore_slot_uuid": args.slot_uuid
            })
        print(result)


class CreateUserKeystoreSlot (Command):
    COMMAND_DESCRIPTION = "create a new user slot"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("slot_password", nargs='?')

    def run(self, args):
        if not args.slot_password:
            password = get_password_from_prompt()
        else :
            password = args.slot_password

        defaults = self._json_rpc.call("getDefaultKeyStoreSlotParams", {})
        if 'default_keystore_slot_params' not in defaults:
            print("Cannot retrieve default key store slot params from API.")
            return -1

        iterations = defaults['default_keystore_slot_params']\
            .get("password_hash_iterations")

        hash_method = defaults['default_keystore_slot_params']\
            .get("password_hash_method")
        if hash_method != "PBKDF2WithHmacSHA512":
            print_warning(1, "Default hash method not supported")
            return -1
        hash_bits = 512
        salt = os.urandom(8)
        encoded_salt = base64.b64encode(salt)
        password_hash = get_pbkdf2_sha512_hash(
            password,
            salt,
            iterations,
            hash_bits // 8)
        encoded_password_hash = base64.b64encode(password_hash)
        result = self._json_rpc.call(
                "createNewUserKeystoreSlot", {
                "encoded_new_keystore_slot_password_hash": encoded_password_hash,
                "encoded_new_keystore_slot_password_salt": encoded_salt
            })

        print("Success. Created new key store slot with id: " +\
              result['keystore_slot_uuid'])


class DeleteUserKeystoreSlot(Command):
    COMMAND_DESCRIPTION = "delete user slot"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("slot_uuid")

    def run(self, args):
        result = self._json_rpc.call(
            "removeKeystoreSlot", {
                "keystore_slot_uuid": args.slot_uuid
            })
        print(result)


class FileDumpMetadata(Command):
    COMMAND_DESCRIPTION = "dump a file's metadata"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("global_file_id", help='form <volume_uuid>:<file_id>')
        parser.add_argument("segment_offset", nargs='?')
        parser.add_argument("stripe_number", nargs='?')
        parser.add_argument("-a", "--show-all", action="store_true")

    def run(self, args):
        request = {}
        volume_and_file = args.global_file_id.split(':')
        if len(volume_and_file) != 2:
            print_warning(1, "invalid parameter is given: ", args.global_file_id)
            return -3
        request['volume_uuid'] = volume_and_file[0]
        request['file_id'] = int(volume_and_file[1])

        if args.segment_offset:
            request['include_object_dumps'] = True
            request['segment_start_offset'] = int(args.segment_offset)
            if args.stripe_number:  # stripe number
                request['stripe_number'] = int(args.stripe_number)

            if args.show_all:
                print("Warning: since you have specified the \"segment offset\", the --show-all"
                      " parameter will be ignored")
        else:
            request['include_object_dumps'] = args.show_all

        # api call
        result = self._json_rpc.call("getFileMetadataDump", request)

        # dump results
        file_metadata_dump = result['file_metadata_dump']
        if 'file_metadata' in file_metadata_dump and \
           'file_id' in file_metadata_dump:
            volume_uuid = file_metadata_dump['volume_uuid']
            file_id = file_metadata_dump['file_id']
            file_metadata = file_metadata_dump['file_metadata']
            stripe_metadata_dumps = result['stripe_metadata_dump']
            last_segment_start_offset = None
            last_stripe_number = None
            if 'last_segment_start_offset' in result:
                last_segment_start_offset = result['last_segment_start_offset']
            if 'last_stripe_number' in result:
                last_stripe_number = result['last_stripe_number']

            # Make directory to write dump files
            dirpath = '/var/tmp/quobyte/support/'
            os.makedirs(dirpath, exist_ok=True)

            # Metadata Dump
            metadata_file_name = dirpath + '%s,%s,metadata.txt' % (volume_uuid, file_id)
            with open(metadata_file_name, 'w', encoding="utf-8") as mdfile:
                mdfile.write(file_metadata)
                if 'dynamic_file_metadata' in file_metadata_dump:
                    dynamic_file_metadata = file_metadata_dump['dynamic_file_metadata']
                    mdfile.write(dynamic_file_metadata)

            # Segment Dumps
            sdfile = None
            if len(stripe_metadata_dumps) > 0:
                filename = volume_uuid + ',' + str(file_id)
                if 'segment_start_offset' in request:
                    filename = filename + ',' + str(request['segment_start_offset'])
                if 'stripe_number' in request:
                    filename = filename + ',' + str(request['stripe_number'])
                filename = filename + ',segments.txt'
                with open(dirpath + filename, 'w', encoding="utf-8") as sdfile:
                    for stripe_metadata_dump in stripe_metadata_dumps:
                        sdfile.write(stripe_metadata_dump)
                        sdfile.write("\n")

            print("Created file metadata dumps. "
                  "Please send the following dumps to Quobyte Support.")
            print("file metadata dump: " + os.path.abspath(mdfile.name))
            if sdfile is not None:
                print("file segment dump: " + os.path.abspath(sdfile.name))
            if last_segment_start_offset is not None:
                # NOTE(jeehoon): in this case last_stripe_number is not None
                print(' '.join(['Segment dumps could not be retrieved from'
                                ' start offset {}'.format(last_segment_start_offset),
                                '(stripe number: {})'.format(last_stripe_number),
                                'as the size of the dump exceeded threshold.']))
                print('Please specify segment start offset and/or stripe number(s) to '
                      'retrieve segment dumps further.')
        else:
            print("file %s at volume %s is not found" % (
                request['file_id'] if 'file_id' in request else request['file'],
                request['volume_uuid']))

    @staticmethod
    def completion_cmd():
        return ("qmgmt${opts} volume list --list-columns=tenant_domain,name",
                "echo __files__")


class FileResolveGlobalId(Command):
    COMMAND_DESCRIPTION = "resolve global file id into a file with volume uuid"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("global_file_id", help='form <volume_uuid>:<file_id>')

    def run(self, args):
        result = self._json_rpc.call(
                "resolveGlobalFileId", {
                    "global_file_id": args.global_file_id
                    })
        if 'file' in result:
            print("volume uuid: " + result['volume_uuid'])
            print("file: " + result['file'])
        else:
            print_warning(1, "file not found.")


class SupportDumpGenerate(Command):
    COMMAND_DESCRIPTION = "schedule asynchronous support dump generation"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("support_ticket_id", nargs='?',
            help="support generate-dump [<support-ticket-id> or <support-ticket-number>]"
            "   schedules a support dump generation. The generated dump can be downloaded to"
            "   local machine with \"qmgmt support download-dump\" until validity expires,"
            "   or automatically attached to Quobyte Support Ticket if a ticket number is"
            "   given."
            "    support-ticket-id      Support ticket ID as four or five digits."
            "    support-ticket-number  Support ticket number as seven digits."
            "    * If ticket ID or number is given, the support dump will be automatically"
            "      uploaded to Quobyte support infrastructure after successful generation.")

    def run(self, args):
        request = {}

        # parse ticket ID or number if given
        if args.support_ticket_id:
            ticket_id_length = len(args.support_ticket_id)
            if ticket_id_length not in (4, 5, 7):
                print("Ticket ID or number should be four, five, or seven digits integer")
                return -2
            try:
                support_ticket_id_or_number = int(args.support_ticket_id)
            except ValueError:
                print("support-ticket-number should be a seven digits integer")
                return -2

            request['support_ticket_id'] = support_ticket_id_or_number

        result = self._json_rpc.call("generateAsyncSupportDump", request)
        if 'is_scheduled' in result and result['is_scheduled']:
            print("Async support dump generation is scheduled.")
        else:
            print_warning(1, "Failed to schedule async support dump generation.")


class SupportDumpStatus(Command):
    COMMAND_DESCRIPTION = "shows status of support dump generation"

    def run(self, args):
        # check support dump status, and start downloading if generation is done.
        response = self._json_rpc.call("getSupportDumpStatus", {})
        support_ticket_id = None
        status = None
        if 'status' in response:
            status = response['status']
        if status == 'NOT_FOUND':
            print("* No support dump generation is in progress.")
        elif status == 'GENERATING':
            print("* Support dump generation is in progress.")
        elif status == 'DONE':
            print("* Support dump is ready for download.")

            if 'support_ticket_id' in response:
                # Upload is requested. Show the upload result.
                support_ticket_id = response['support_ticket_id']
                response_code = response['s3_upload_response_code']
                if response_code == 200:
                    print("* Support dump has been successfully uploaded to Quobyte" +\
                          " support infrastructure.")
                    print("* Support ticket ID or number: %s" % (support_ticket_id))
                else:
                    print("* Support dump could not be uploaded to Quobyte support infrastructure.")
                    print("* Please check API service log for details.")
        else:
            print_warning(1, "Unknown status: " + status)
            return -2


class SupportDumpDownload(Command):
    COMMAND_DESCRIPTION = "download support dump file if ready"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("output_file")

    def run(self, args):
        # check support dump status, and start downloading if generation is done.
        response = self._json_rpc.call("getSupportDumpStatus", {})
        status = None
        support_dump_id = None
        if 'status' in response:
            status = response['status']
        if 'support_dump_id' in response:
            support_dump_id = response['support_dump_id']
        if status == 'NOT_FOUND':
            print_warning(1,
                          "Support dump generation is not scheduled. "
                          "Schedule asynchronous support dump generation with "
                          "'qmgmt support support-dump-generate'")
            return -2
        elif status == 'GENERATING':
            print_warning(1,
                          "Support dump generation is not finished. "
                          "Please try again after a few minutes.")
            return -2
        elif status != 'DONE':
            print_warning(1, "Unknown status: " + status)
            return -2

        path = args.output_file

        # check if given filename ends with .zip
        dirpath = os.path.abspath(path)
        is_directory = os.path.isdir(dirpath)
        if not is_directory:
            print_warning(1, "given path " + path + " is not an existing directory.")
            return -2

         # check if file can be written at given directory
        if not os.access(dirpath, os.W_OK):
            print_warning(1, "have no write permissions to " + dirpath)
            return -2

        timestamp_s = int(support_dump_id) // 1000
        filename = datetime.datetime.fromtimestamp(timestamp_s).strftime(
            'support_data_%Y%m%d-%H%M%S.zip')
        filepath = dirpath + '/' + filename

        # rpc call
        result = self._json_rpc.call(
                "getSupportDump", {
                    "support_dump_id": support_dump_id})
        if 'bytes' in result:
            # decode base64-encoded string
            decoded_bytes = base64.b64decode(result['bytes'])
            with open(filepath, 'wb') as f:
                f.write(decoded_bytes)
            print("Downloaded support dump at: " + filepath)
        else:
            print_warning(1, "Support dump file not found for given id " + str(support_dump_id))
            return -2

    @staticmethod
    def completion_cmd():
        return "echo __files__"


class ServiceDumpGenerate(Command):
    COMMAND_DESCRIPTION = "generate service dump"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("service_uuid")
        parser.add_argument("support_ticket_id", nargs='?',
            help="support generate-dump [<support-ticket-id> or <support-ticket-number>]"
            " schedules a support dump generation. The generated dump can be downloaded to"
            " local machine with \"qmgmt support download-dump\" until validity expires,"
            " or automatically attached to Quobyte Support Ticket if a ticket number is"
            " given."
            "   support-ticket-id      Support ticket ID as four or five digits."
            "   support-ticket-number  Support ticket number as seven digits."
            "  * If ticket ID or number is given, the support dump will be automatically"
            "    uploaded to Quobyte support infrastructure after successful generation.")

    def run(self, args):
        service_uuid = args.service_uuid

        # parse ticket ID if given
        request = {'service_uuid': service_uuid}
        if args.support_ticket_id:
            ticket_id_length = len(str(args.support_ticket_id))
            if ticket_id_length not in (4, 5, 7):
                print("Ticket ID or number should be four, five, or seven digits integer")
                return -2
            try:
                support_ticket_id_or_number = int(args.support_ticket_id)
            except ValueError:
                print("support-ticket-number should be a seven digits integer")
                return -2

            request['support_ticket_id'] = support_ticket_id_or_number

        # Make directory to write dump file
        dirpath = '/var/tmp/quobyte/support/'
        os.makedirs(dirpath, exist_ok=True)

        # rpc call
        result = self._json_rpc.call("getServiceDump", request)
        if 'bytes' in result:
            filename = 'service_state_%s.zip' % (service_uuid)
            filepath = dirpath + filename

            # decode base64-encoded string and write to the zip file
            decoded_bytes = base64.b64decode(result['bytes'])
            with open(filepath, 'wb') as f:
                f.write(decoded_bytes)

            # show result
            print("Created service state dump at: " + filepath)
            if 'support_ticket_id' in request and 's3_upload_response_code' in result:
                # Upload is requested. Show the upload result.
                response_code = result['s3_upload_response_code']
                if response_code == 200:
                    print("Service dump has been uploaded to the Quobyte Support infrastructure.")
                    print("Ticket ID or number: %s" % (support_ticket_id_or_number))
                else:
                    print("Service dump could not be uploaded to the Quobyte support " + \
                        "infrastructure.")
                    print("Please check API service log for details.")
        else:
            print_warning(1, "Service state dump could not be downloaded for " + str(service_uuid)
                    + ". Please try again or find service log for details.")
            return -2

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} service list --list-columns=service_uuid"


class AbstractQuery(Command):

    def __init__(self, json_rpc):
        super().__init__(json_rpc)
        self.query_id = None
        self.files_total = 0

    def process_result_rows(self, rows):
        assert False, "implement me"

    def drive_query(self, query_id : str, output_progress_to=sys.stderr):
        self.query_id = query_id
        signal.signal(signal.SIGINT, self.interrupt)

        while True:
            progress_response = self._json_rpc.call(
                "getQueryProgress", {'query_id' : self.query_id})

            no_data_in_response = True
            wrote_stdout_output = False
            if 'result_row' in progress_response and len(progress_response['result_row']) > 0:
                if sys.stdout.isatty():
                    print("\r", file=output_progress_to)
                wrote_stdout_output = self.process_result_rows(progress_response['result_row'])
                no_data_in_response = False

            self.files_total = progress_response['items_total']
            files_done = progress_response['items_processed']
            volumes_total = progress_response['volumes_total']
            volumes_done = progress_response['volumes_done']
            if wrote_stdout_output and sys.stdout.isatty():
                # Do not overwrite result line on stdout, if any
                print("", file=output_progress_to)
            print("\rProgress: " +
                  str(files_done) + "/" + str(self.files_total) + " nodes, " +
                  str(volumes_done) + "/" + str(volumes_total) + " volumes",
                  end='', file=output_progress_to)

            if progress_response['query_status'] != 'RUNNING':
                return progress_response['query_status']

            if no_data_in_response:
                time.sleep(0.25)

    def interrupt(self, signal, frame):
        if self.query_id:
            self._json_rpc.call("cancelQuery", {'query_id' : self.query_id})
            print("\rInterrupted running query")


    def _get_global_file_id(self, path : str):
        """ Returns (volume_uuid, file_id) of node """
        try:
            value = os.getxattr(path, 'quobyte.file_id')
            return value.decode().split(':')
        except OSError as e:
            if e.errno in (61, 95):
                raise InvalidInputException("Not in a quobyte volume: " + path) from e
            raise InvalidInputException("Problem with path: " + str(e)) from e

    def _determine_volume(self, relpath : str):
        """ Returns (volume_mount_point, volume_uuid, path_in_volume) """
        path = os.path.abspath(relpath)
        while True:
            volume_uuid, _ = self._get_global_file_id(path)

            value = os.getxattr(path, 'quobyte.info')
            if volume_uuid in value.decode():
                return path, volume_uuid, os.path.relpath(os.path.abspath(relpath), path)

            path = os.path.dirname(path)

    def get_query_expression_for_path(self, path):
        """ Compute query restriction for given local path """
        volume_mount_point, volume_uuid, path_in_volume = self._determine_volume(path)
        query = 'volume_uuid=%s' % volume_uuid
        if path_in_volume != '.':
            # Only restrict if we don't start from volume root
            query += ' AND directory*="%s/*"' % path_in_volume
        return query, volume_mount_point


class QueryAnalyzeVolumes(AbstractQuery):
    COMMAND_DESCRIPTION = "analyze cluster, tenants or volumes and create report"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("--tenants", help="comma-separated list of tenants (names or UUIDs)")
        parser.add_argument("--volumes", help="comma-separated list of volumes (names or UUIDs)")
        parser.add_argument(
            "-o", "--output_file", default="analyze_report.html",
            help="where to write the html report to")

    def run(self, args):
        request = {}

        tenant_uuids = comma_separated_tenant_list_to_uuid_array(self._resolver, args.tenants)
        volume_uuids = comma_separated_volume_list_to_uuid_array(self._resolver, args.volumes)
        if tenant_uuids:
            request["restrict_to_tenants"] = tenant_uuids
        if volume_uuids:
            request["restrict_to_volumes"] = volume_uuids
        response = self._json_rpc.call("analyzeVolumes", request)

        self.drive_query(response['query_id'])
        if not self.result_row:
            print("No report in response")
            return -2

        report = self.result_row[0]['column'][0]

        with open(args.output_file, 'wb') as f:
            f.write(report.encode())
        print("\rWrote report to", args.output_file, "                  ")

    def process_result_rows(self, rows):
        self.result_row = rows
        return False  # did not write to stdout


class QueryAnalyzeFiles(AbstractQuery):
    COMMAND_DESCRIPTION = "analyze all accessible files in a directory tree"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument(
            "-o", "--output_file", default="analyze_report.html",
            help="where to write the html report to")
        parser.add_argument(
            'path', nargs='?',
            help="Directory to analyze recursively")

    def run(self, args):
        if args.path:
            query, _ = self.get_query_expression_for_path(args.path)
            request = {"query" : query}
        else:
            request = {}

        response = self._json_rpc.call("analyzeFiles", request)

        self.drive_query(response['query_id'])
        if not self.result_row:
            print("No report in response")
            return -2

        report = self.result_row[0]['column'][0]

        with open(args.output_file, 'wb') as f:
            f.write(report.encode())
        print("\rWrote report to", args.output_file, "                  ")

    def process_result_rows(self, rows):
        self.result_row = rows
        return False  # did not write to stdout


class QueryFind(AbstractQuery):
    COMMAND_DESCRIPTION = "query file metadata with find syntax"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument(
            "--summary", action="store_true",
            help="Only output counts, not list of files")
        parser.add_argument(
            "-newer",
            help="Find files whose mtime is more recent than NEWER's")
        parser.add_argument(
            "-size",
            help="Find files that are greater than (+), smaller than (-) or equal SIZE")
        parser.add_argument(
            "-name",
            help="Find files whose name match the glob pattern NAME")
        parser.add_argument(
            'path', default='.', nargs='?',
            help="Directory where to start the search")

    def _build_query(self, args) -> str:
        """ Build query from parameters """
        if not args.path:
            args.path = '.'
        query, volume_mount_point = self.get_query_expression_for_path(args.path)
        self._base_path = volume_mount_point

        if args.newer:
            try:
                mtime_secs = int(os.stat(args.newer).st_mtime)
                query += ' AND mtime > %d' % mtime_secs
            except OSError as e:
                raise InvalidInputException("Problem with newer parameter: " + str(e)) from e

        if args.size:
            if args.size[0] == '+':
                operator = '>'
                args.size = args.size[1:]
            elif args.size[0] == '-':
                operator = '<'
                args.size = args.size[1:]
            else:
                operator = '='

            try:
                if args.size[-1] == 'c':
                    size_value = int(args.size[:-1])
                elif args.size[-1] == 'b':
                    size_value = int(args.size[:-1]) * 512
                elif args.size[-1] == 'w':
                    size_value = int(args.size[:-1]) * 2
                elif args.size[-1] == 'k':
                    size_value = int(args.size[:-1]) * 1024
                elif args.size[-1] == 'M':
                    size_value = int(args.size[:-1]) * 1024 * 1024
                elif args.size[-1] == 'G':
                    size_value = int(args.size[:-1]) * 1024 * 1024 * 1024
                else:
                    size_value = int(args.size[:-1]) * 512
            except ValueError as ve:
                raise InvalidInputException("Invalid -size parameter: " + str(ve)) from ve

            query += ' AND size%s%d' % (operator, size_value)

        if args.name:
            query += ' AND name*="%s"' % args.name

        return query

    def run(self, args):
        query = self._build_query(args)

        if args.summary:
            self._only_summary = True
            select_property = 'COUNT(id)'
        else:
            self._only_summary = False
            select_property = 'path'

        response = self._json_rpc.call(
            "queryFiles",
            {'query' : query,
             'select_property' : [select_property],
             'iterate_all' : False})

        self.drive_query(
            response['query_id'], output_progress_to=open(os.devnull, "w"))
        if self._only_summary:
            print('/%d' % self.files_total)

    def process_result_rows(self, rows):
        if self._only_summary:
            print(rows[0]['column'][0], end='')
        else:
            for row in rows:
                columns = os.path.join(self._base_path, row['column'][0])
                sys.stdout.write(columns + "\n")
        return True


class QueryDiskUsage(AbstractQuery):
    COMMAND_DESCRIPTION = "query disk usage of accessible files"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument(
            '--raw', action='store_true',
            help="Output as plain bytes")
        parser.add_argument(
            'path', default='.', nargs='?',
            help="Directory for which to compute the disk usage:\n"
                 "    file size, allocated bytes (excl. redundancy), "
                 "physical bytes (incl. redundancy)\n\n"
                 "Note that this only counts files in directories "
                 "that the user is allowed to access.")

    def run(self, args):
        if not args.path:
            args.path = '.'
        if not os.path.isdir(args.path):
            print(args.path, "is not a directory")
            return 2
        query, _ = self.get_query_expression_for_path(args.path)
        self._human_readable = not args.raw
        select_properties = ['SUM(size)', 'SUM(allocated_size)', 'SUM(physical_size)']

        response = self._json_rpc.call(
            "queryFiles",
            {'query' : query,
             'select_property' : select_properties,
             'iterate_all' : False})

        terminal_status = self.drive_query(
            response['query_id'], output_progress_to=open(os.devnull, "w"))

    def process_result_rows(self, rows):
        size, allocated, physical = \
            int(rows[0]['column'][0]), int(rows[0]['column'][1]), int(rows[0]['column'][2])
        if self._human_readable:
            print(human_readable_bytes(size).replace(' ', ''),
                  human_readable_bytes(allocated).replace(' ', ''),
                  human_readable_bytes(physical).replace(' ', ''))
        else:
            print(size, allocated, physical)
        return True


class QueryFiles(AbstractQuery):
    COMMAND_DESCRIPTION = "query file metadata"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument(
            '-o', '--output-file',
            help="where to write the result list to, otherwise stdout")
        parser.add_argument(
            "-a", "--all",
            action="store_true",
            help="iterate also unlinked files and files in snapshots"
                " (slower and SUPERUSER only)")
        parser.add_argument(
            "--group-by",
            help="Group query by specified comma-separated values")
        parser.add_argument(
            "--list-columns",
            help="Limit listing to specified comma-separated property columns")
        parser.add_argument(
            'expression',
            help="expression of predicates over file properties")
        parser.add_argument(
            'path', nargs='?',
            help="Directory where to start the search")

    def run(self, args):
        result = self._json_rpc.call("whoAmI", {})
        if len(result['group']) == 0 and not args.all:
            print(
                "According to Quobyte's user directory, your user "
                + result['user_id'] + " is not member of any groups. "
                "They query might not return items which you are only allowed to list"
                " per groups permissions.",
                file=sys.stderr)

        select_properties = [p.strip() for p in args.list_columns.split(",")] \
            if args.list_columns else ['volume_uuid_path']
        group_by_properties = [p.strip() for p in args.group_by.split(",")] \
            if args.group_by else []

        if args.path:
            query_from_path, _ = self.get_query_expression_for_path(args.path)
            args.expression = '(' + args.expression + ') AND ' + query_from_path

        response = self._json_rpc.call(
            "queryFiles",
            {'query' : args.expression,
             'select_property' : select_properties,
             'group_by_property' : group_by_properties,
             'iterate_all' : args.all})

        if args.output_file:
            self.output_file = open(args.output_file, 'w', encoding="utf8")
        else:
            self.output_file = None

        self.row_count = 0
        terminal_status = self.drive_query(response['query_id'])

        if self.output_file:
            self.output_file.close()
            print("\rWrote", self.row_count, "rows to", args.output_file, 30 * " ",
                  file=sys.stderr)
        elif terminal_status == 'DONE':
            print("\33[2K\r", file=sys.stderr)
        elif terminal_status == 'CANCELLED':
            print("\33[2K\rCancelled", file=sys.stderr)

    def process_result_rows(self, rows):
        if self.output_file:
            effective_file = self.output_file
        else:
            effective_file = sys.stdout
        for row in rows:
            columns = ','.join(self.escape_value_for_csv(column) for column in row['column'])
            effective_file.write(columns + "\n")
            self.row_count += 1
        return rows and effective_file == sys.stdout

    def escape_value_for_csv(self, column):
        enclose_in_double_quotes = False
        # RFC 4180
        if '"' in column:
            column = column.replace('"', '\"')
            enclose_in_double_quotes = True
        if ',' in column:
            enclose_in_double_quotes = True
        if enclose_in_double_quotes:
            column = '"' + column + '"'
        return column


class QueryCancel(Command):
    COMMAND_DESCRIPTION = "cancel a running query"

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("query_id")

    def run(self, args):
        self._json_rpc.call("cancelQuery", {'query_id': args.query_id})


class NetworkTestStart(Command):
    COMMAND_DESCRIPTION = "start a network test"

    def run(self, args):
        self._json_rpc.call("startNetworkTest", {})
        print("Started network test, please check "
              "primary Registry status page for result")


class NetworkTestCancel(Command):
    COMMAND_DESCRIPTION = "cancel a network test"

    def run(self, args):
        self._json_rpc.call("cancelNetworkTest", {})
        print("Canceled network test")


class NetworkTestShow(Command):
    COMMAND_DESCRIPTION = "show network test results"

    def run(self, args):
        status_result = self._json_rpc.call("getNetworkTestResult", {})
        if 'status' in status_result:
            if status_result['status'] == 'NOT_SCHEDULED':
                print("No network test running")
            elif status_result['status'] == 'IN_PROGRESS':
                print("Network test is still in progress, please try later")
            elif status_result['status'] == 'DONE':
                print("Started network test, please check "
                    "primary Registry status page for result")
        else:
            print_warning(1, "Failed to retrieve network test result.")


class WhoAmI(Command):
    COMMAND_DESCRIPTION = "show effective user credentials"

    def run(self, args):
        result = self._json_rpc.call("whoAmI", {})

        _output_json_and_exit(args, result)
        print("id:                " + result['user_id'])
        print("name:              " + result['user_name'])
        print("primary group:     " + str(result.get('primary_group', 'none')))
        print("groups:            " + str(result['group']))
        print("management role:   " + str(result.get('management_role', 'none')))
        print("member of tenants: " + str(result.get('member_of_tenant', 'none')))
        print("admin of tenants: " + str(result.get('admin_of_tenant', 'none')))


class ShellCompletion(Command):
    COMMAND_DESCRIPTION = (
        "output shell auto-completion, can be used as $source <(qmgmt completion bash)\n"
        "or qmgmt completion bash > /etc/bash_completion.d/qmgmt\n")

    @staticmethod
    def add_specific_arguments(parser):
        parser.add_argument("shell_type", choices=['bash', 'zsh'])

    @staticmethod
    def get_header():
        return """# Autogenerated code.
# Copyright 2019-2025 Quobyte Inc. All rights reserved.

"""

    def generate_bash(self):
        out = """
if ! type _get_comp_words_by_ref &> /dev/null; then
_get_comp_words_by_ref ()
{
    words=("${COMP_WORDS[@]}")
}
fi

__qmgmt_complete_from() {
    COMPREPLY=( $(compgen -W "$1" -- "${COMP_WORDS[$COMP_CWORD]}") )
}

__qmgmt_complete_from_cmd() {
    local RESPONSE
    local IFS=$\'\\n\'
    RESPONSE=`eval $1`
    local RESULT=$?
    if [ "${RESPONSE}" = "__files__" ]; then
        COMPREPLY=( $(compgen -f "${cur}") )
        compopt -o filenames 2>/dev/null
    elif [ "${RESULT}" = "0" ]; then
        COMPREPLY=( $(compgen -W "${RESPONSE}" -- "${COMP_WORDS[$COMP_CWORD]}") )
        for (( i=0; i<$((${#COMPREPLY[@]})); i++ ));
        do
            COMPREPLY[$i]=$(printf %q "${COMPREPLY[$i]}")
        done
    else
        COMPREPLY=()
    fi
}

"""

        for key in COMMANDS.keys():
            try:
                comp_cmd = COMMANDS[key](None).completion_cmd()
                if comp_cmd is None:
                    raise TypeError()
            except TypeError:
                out += "_qmgmt_" + key.replace(" ", "") + "() {\n    :\n}\n\n"
                continue
            out += "_qmgmt_" + key.replace(" ", "") + "() {\n"
            if isinstance(comp_cmd, str):
                out += "    if [ \"${prev}\" = \"%s\" ];  then\n" % key.split(" ")[-1] +\
                       "        __qmgmt_complete_from_cmd \"%s\"\n" % comp_cmd +\
                       "    fi\n"
            elif isinstance(comp_cmd, tuple):
                out += "    if [ \"${preprev}\" = \"%s\" ]; then\n" % key.split(" ")[-1] +\
                       "        __qmgmt_complete_from_cmd \"%s\"\n" % comp_cmd[1] +\
                       "    elif [ \"${prev}\" = \"%s\" ]; then\n" % key.split(" ")[-1] +\
                       "        __qmgmt_complete_from_cmd \"%s\"\n" % comp_cmd[0] +\
                       "    fi\n"
            elif isinstance(comp_cmd, dict):
                out += "    case \"${prev}\" in\n"\
                       "    %s)\n" % key.split(" ")[-1] +\
                       "        __qmgmt_complete_from \"%s\"\n" % " ".join(comp_cmd.keys()) + \
                       "        ;;\n"
                for subcom in comp_cmd.keys():
                    out += "    %s)\n" % subcom +\
                           "        __qmgmt_complete_from_cmd \""
                    if isinstance(comp_cmd[subcom], str):
                        out += (comp_cmd[subcom] or " ") + "\"\n"
                    elif isinstance(comp_cmd[subcom], tuple):
                        out += (comp_cmd[subcom][0] or " ") + "\"\n"
                    else:
                        out += " \"\n"
                    out += "        ;;\n"
                tuples = [c for c in comp_cmd.keys() if isinstance(comp_cmd[c], tuple)]
                if tuples:
                    out += "    *)\n"
                    for subcom in tuples:
                        out += "        if [ \"${preprev}\" = \"%s\" ]; then\n" \
                               % subcom + \
                               "            __qmgmt_complete_from_cmd \"%s\"\n" \
                               % comp_cmd[subcom][1] + \
                               "        fi\n"
                    out += "        ;;\n"
                out += "    esac\n"
            out += "}\n\n"

        commands = {}
        for key in COMMANDS.keys():
            if " " in key:
                com, subcom = key.split(" ", 1)
                if com not in commands:
                    commands[com] = [subcom]
                else:
                    if commands[com]:
                        commands[com].append(subcom)
                    else:
                        commands[com] = [subcom]
            else:
                commands[key] = None

        for com in commands.keys():
            if commands[com]:
                out += "_qmgmt_%s() {\n" % com + \
                       "    __qmgmt_complete_from \"%s\"\n" % " ".join(commands[com]) + \
                       "}\n\n"
            out += "__qmgmt_%s_wrapper() {\n" % com
            if commands[com]:
                out += "    _qmgmt_%s\"$1\"\n" % com
            else:
                try:
                    comp_cmd = COMMANDS[com](None).completion_cmd()
                    out += "    if [ \"$1\" != \"\" ]; then\n" \
                           "        _qmgmt_%s\n" % com + \
                           "    else\n" \
                           "        __qmgmt_complete_from \"%s\"\n"\
                           % " ".join(comp_cmd.keys()) + \
                           "    fi\n"
                except:
                    out += "    _qmgmt_%s\n" % com
            out += "}\n\n"


        out +=\
"""__qmgmt__wrapper() {
    __qmgmt_complete_from "%s"
}

_qmgmt_opts() {
    qmgmt -h | grep -Ewo -- '-[[:alnum:]]|--[[:alnum:]]*|\
--[[:alnum:]]*-[[:alnum:]]*|--[[:alnum:]]*-[[:alnum:]]*-[[:alnum:]]*'
}

__qmgmt_completion() {
    local cur prev preprev opts command subcommand words i
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    preprev="${COMP_WORDS[COMP_CWORD-2]}"
    opts=" -f --output=comp"
    command=""
    subcommand=""
    words=COMP_WORDS

    _get_comp_words_by_ref -n := words
    for (( i=1; i<$((${#words[@]})); i++ ));
    do
        if [ "${words[$i]}" = "-u" ]; then
            opts="${opts} -u ${words[$i+1]}"
        elif [[ ${words[$i]} == --url=* ]]; then
            opts="${opts} ${words[$i]}"
        elif [ "${words[$i]}" = "-a" ] || [ "${words[$i]}" = "--all" ]; then
            opts="${opts} -a"
        elif [[ ${words[$i]} == --ca=* ]]; then
            opts="${opts} ${words[$i]}"
        elif [ "${words[$i]}" = "--verify-server-certificate" ]; then
            opts="${opts} ${words[$i]}"
        elif [ "${words[$i-1]}" != "-u" ] && [[ "${words[$i]}" != "-*" ]]; then
            if [ "$cur" != "${words[$i]}" ]; then
                command="${words[$i]}"
            fi
            if [ "$cur" != "${words[$i+1]}" ]; then
                subcommand="${words[$i+1]}"
            fi
            break
        fi
    done
    case "${cur}" in
    -*)

        __qmgmt_complete_from_cmd "_qmgmt_opts"
        ;;
    *)
        if [ "${prev}" != "-u" ]; then
            __qmgmt_${command}_wrapper "$subcommand" 2>/dev/null
        fi
        ;;
    esac

}

complete -F __qmgmt_completion qmgmt
""" % " ".join(set([com.split(" ")[0] for com in COMMANDS.keys()]))

        return out

    def generate_zsh(self):
        return self.generate_bash()

    def run(self, args):
        if args.shell_type == "bash":
            print(self.get_header() + self.generate_bash())
        elif args.shell_type == "zsh":
            print(self.get_header() + self.generate_zsh())
        else:
            print_warning(1, "Unsupported shell type.")
            return -2

    @staticmethod
    def completion_cmd():
        return "printf 'bash\\nzsh\\n'"

def strToList(line, separator = ","):
    return [ item.strip() for item in line.split(separator) if len(item.strip()) > 0 ]

def get_string_or_filecontent(string_or_file):
    try:
        with open(string_or_file, 'r', encoding="utf8") as file:
            result = file.read()
            return result
    except IOError:
        return string_or_file


def sort_host_names(value):
    if value['host_name'] == "not registered":
        return ("zzzzzzzzz", int(value['device_id']))
    else:
        return (value['host_name'].lower(), int(value['device_id']))


def sort_services(value):
    return (value['service_name'], value['service_type'])


def sort_clean_devices(value):
    return value['handle_id']


def service_type_to_human(service_type):
    return SERVICE_TYPE_MAP.get(service_type, 'unknown')


def convert_device_type_input(types):
    device_types = []
    for arg in types.upper().split():
        if arg not in VALID_DEVICE_TYPES:
            raise InvalidInputException("Invalid device type.")
        elif arg == "D":
            device_types.append("DATA")
        elif arg == "R":
            device_types.append("REGISTRY")
        elif arg == "M":
            device_types.append("METADATA")
        else:
            device_types.append(arg)

    return device_types


def to_seconds_from_human_readable_duration(duration):
    try:
        duration_value = int(duration[:-1])
        duration_unit = duration[-1:]
    except (SyntaxError, ValueError):
        raise InvalidInputException("Invalid input for duration: %s" % (duration))
    if duration_value < 0:
        raise InvalidInputException("Invalid duration value: %s. Duration value must not be < 0 " \
          % (duration_value))

    supported_units = ['s', 'm', 'h', 'd']
    if duration_unit not in supported_units:
        raise InvalidInputException("Invalid duration unit: %s. Supported duration units are %s" \
          % (duration_unit, supported_units))

    # convert to seconds
    if duration_unit == 's':
        duration_s = duration_value
    elif duration_unit == 'm':
        duration_s = duration_value * 60
    elif duration_unit == 'h':
        duration_s = duration_value * 60 * 60
    elif duration_unit == 'd':
        duration_s = duration_value * 60 * 60 * 24
    else:
        duration_s = duration_value

    return duration_s


def to_human_readable_ms(time_ms):
    try:
        millis = int(time_ms)
    except (SyntaxError, ValueError):
        raise InvalidInputException("Invalid milliseconds input: %s" % time_ms)
    if millis < 0:
        raise InvalidInputException("Invalid milliseconds input: %s" % time_ms)
    seconds = millis / 1000
    if seconds < 60:
        return '{}{}'.format(seconds, "s")
    minutes = int(seconds / 60)
    if minutes < 60:
        return '{}{}'.format(minutes, "m")
    hours = int(minutes / 60)
    if hours < 48:
        return '{}{}'.format(hours, "h")
    else:
        days = int(hours / 24)
        return '{}{}'.format(days, "d")


def human_readable_bytes(num):
    unit_metric = BYTES_UNIT == 'metric'
    factor = 1000.0 if unit_metric else 1024.0
    for x in ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']:
        if num < factor:
            x = x if unit_metric else x.replace('B', 'iB')
            return "%.0f %s" % (num, x)
        num /= factor


def human_writable_bytes(str_bytes):
    try:
        match = re.match(
            r"^(?P<num>[0-9]*)\.?(?P<frac>[0-9]*)(?P<unit>[^0-9]+)?", str_bytes)
        number = ""
        if match.group("num"):
            number += match.group("num")
        if match.group("frac"):
            number += "." + match.group("frac")

        result_num = float(number)
        unit = match.group("unit")
        if not unit:
            return int(result_num)

        factor = 1024 if 'i' in match.group("unit") else 1000

        for prefix in ['K', 'M', 'G', 'T', 'P']:
            result_num *= factor
            if unit[0] == prefix:
                return int(result_num)

        raise InvalidInputException("Invalid unit: " + match.group("unit")
                                    + " in input: " + str_bytes)
    except ValueError:
        raise InvalidInputException("Invalid size in bytes: " + str_bytes)


def is_valid_service_device_id(device):
    return re.match('^[0-9]+$', str(device))


def is_valid_uuid(uuid):
    return re.match(
        r'^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z',
        uuid,
        re.I)

def get_comment(args):
    default_message = "Via qmgmt run by " + str(getpass.getuser()) + "@" \
                  + str(socket.gethostname()) + "."
    if not args.comment:
        return default_message
    else:
        return args.comment


def parse_labels(labels_csv: str):
    """ Parse CSV of name:value """
    label_pairs =  labels_csv.split(',')
    result = []
    for label_pair in label_pairs:
        if not label_pair:
            continue
        label_parts = label_pair.split(':')
        if len(label_parts) != 2:
            raise InvalidInputException(
                "Invalid label (use <name>:<value>): " + str(label_parts))
        result.append({
            "name": label_parts[0],
            "value": label_parts[1]
        })
    return result


def comma_separated_tenant_list_to_uuid_array(resolver, comma_separated_tenant_list):
    """
    Converts string with comma separated tenants (tenant names or tenant UUIDs) to an array
    of tenant UUIDs.
    """
    array = []
    if comma_separated_tenant_list:
        tenants = comma_separated_tenant_list.split(",")
        for tenant in tenants:
            tenant = tenant.strip()
            if tenant:
                tenant_id, _ = resolver.resolve_tenant(tenant)
                array.append(tenant_id)
    if len(array) > 1:
        # remove possible duplicates
        array = list(dict.fromkeys(array))
    return array


def comma_separated_volume_list_to_uuid_array(resolver, comma_separated_volume_list):
    """
    Converts string with comma separated volumes (volume names or volume UUIDs) to an array
    of volume UUIDs.
    """
    array = []
    if comma_separated_volume_list:
        volumes = comma_separated_volume_list.split(",")
        for volume in volumes:
            volume = volume.strip()
            if volume:
                resolved, _, _ = resolver.resolve_volume(volume)
                if resolved:
                    array.append(resolved.strip())
    if len(array) > 1:
        # remove possible duplicates
        array = list(dict.fromkeys(array))
    return array


def _confirmation_prompt(message):
    if FORCE:
        return
    while True:
        char = input(message + " <y|n>:").lower()
        if char in ["y", "n"]:
            break
        else:
            print("Please type \"y\" or \"n\"")
    if char == "y":
        return
    else:
        print("Operation aborted.")
        sys.exit(-2)


COMMANDS = {
    "user login": UserLogin,
    "user logout": UserLogout,

    "user config add": UserConfigurationAdd,
    "user config set-password": UserConfigurationSetPassword,
    "user config list": UserConfigurationList,
    "user config export": UserConfigurationExport,
    "user config import": UserConfigurationImport,
    "user config delete": UserConfigurationDelete,
    "user config edit": UserConfigurationEdit,

    "policy-rule list": PolicyRuleList,
    "policy-rule filter": PolicyRuleFilter,
    "policy-rule create": PolicyRuleCreate,
    "policy-rule update": PolicyRuleUpdate,
    "policy-rule priority": PolicyRulePriority,
    "policy-rule delete": PolicyRuleDelete,
    "policy-rule edit": PolicyRuleEdit,
    "policy-rule export": PolicyRuleExport,
    "policy-rule import": PolicyRuleImport,
    "policy-preset list": PolicyPresetList,

    "device addr": DeviceAddr,
    "device list": DeviceList,
    "device show": DeviceShow,
    "device update mode": DeviceMode,
    "device update led-status": DeviceLEDStatus,
    "device update mount": DeviceMountStateChange,
    "device update fsck": FileSystemCheckBeforeMountStateChange,
    "device update trim-method": DeviceTrimMethodChange,
    "device update add-tags": DeviceAddTags,
    "device update remove-tags": DeviceRemoveTags,
    "device update add-type": DeviceAddType,
    "device update remove-type": DeviceRemoveType,
    "device update health": DeviceHealthChange,
    "device remove": DeviceRemove,
    "device list-unformatted": DeviceListClean,
    "device make": DeviceMake,

    "tenant create": TenantCreate,
    "tenant create-with-id": TenantCreateWithId,
    "tenant update name": TenantUpdateName,
    "tenant update restrict_to_network": TenantUpdateRestrictToNetwork,
    "tenant update add_volume_access": TenantUpdateAddVolumeAccess,
    "tenant update remove_volume_access": TenantUpdateRemoveVolumeAccess,
    "tenant delete": TenantDelete,
    "tenant list": TenantList,
    "tenant show": TenantShow,
    "tenant lookup": TenantLookup,
    "tenant resolve": TenantResolve,
    "volume create": VolumeCreate,
    "volume erase": VolumeErase,
    "volume delete": VolumeDelete,

    "volume update name": VolumeUpdateName,
    "volume update add_replica_device": VolumeUpdateAddReplicaDevice,
    "volume update remove_replica_device": VolumeUpdateRemoveReplicaDevice,
    "volume update set_preferred_primary_replica_device": VolumeUpdateSetPreferred,
    "volume update remove_preferred_primary_replica_device": VolumeUpdateRemovePreferred,

    "volume encryption init": VolumeEncryptionInitWithSlot,
    "volume encryption addslot": VolumeEncryptionAddSlot,

    "volume list": VolumeList,
    "volume lookup": VolumeLookup,
    "volume resolve": VolumeResolve,
    "volume repair": VolumeRepair,
    "volume scrub": VolumeScrub,
    "volume show": VolumeShow,
    "volume publish": VolumePublish,
    "volume unpublish": VolumeUnpublish,
    "volume mirror": VolumeMirror,
    "volume disconnect": VolumeDisconnect,

    "snapshot create": SnapshotCreate,
    "snapshot update": SnapshotUpdate,
    "snapshot erase": SnapshotErase,
    "snapshot delete": SnapshotDelete,
    "snapshot list": SnapshotList,
    "usage show": UsageShow,

    "external-locations list": ExternalStorageLocationsConfigurationList,
    "external-locations create": ExternalStorageLocationsConfigurationCreate,
    "external-locations edit": ExternalStorageLocationsConfigurationEdit,
    "external-locations delete": ExternalStorageLocationsConfigurationDelete,
    "external-locations import": ExternalStorageLocationsConfigurationImport,
    "external-locations export": ExternalStorageLocationsConfigurationExport,

    "systemconfig export": SystemConfigurationExport,
    "systemconfig import": SystemConfigurationImport,
    "systemconfig delete": SystemConfigurationDelete,
    "systemconfig edit": SystemConfigurationEdit,

    "failuredomains export": FailureDomainsConfigurationExport,
    "failuredomains import": FailureDomainsConfigurationImport,
    "failuredomains delete": FailureDomainsConfigurationDelete,
    "failuredomains edit": FailureDomainsConfigurationEdit,

    "quota list": QuotaConfigurationList,
    "quota export": QuotaConfigurationExport,
    "quota import": QuotaConfigurationImport,
    "quota delete": QuotaConfigurationDelete,
    "quota edit": QuotaConfigurationEdit,
    "quota create": QuotaConfigurationCreate,

    "rules list": RulesConfigurationList,
    "rules export": RulesConfigurationExport,
    "rules import": RulesConfigurationImport,
    "rules delete": RulesConfigurationDelete,
    "rules edit": RulesConfigurationEdit,

    "nfs-virtualips list": NfsVirtualIpsConfigurationList,
    "nfs-virtualips export": NfsVirtualIpsConfigurationExport,
    "nfs-virtualips import": NfsVirtualIpsConfigurationImport,
    "nfs-virtualips delete": NfsVirtualIpsConfigurationDelete,
    "nfs-virtualips edit": NfsVirtualIpsConfigurationEdit,

    "nfs-exports list": NfsExportsConfigurationList,
    "nfs-exports export": NfsExportsConfigurationExport,
    "nfs-exports import": NfsExportsConfigurationImport,
    "nfs-exports delete": NfsExportsConfigurationDelete,
    "nfs-exports edit": NfsExportsConfigurationEdit,

    "task cancel": TaskCancel,
    "task resume": TaskResume,
    "task retry": TaskRetry,
    "task create": TaskCreate,
    "task list": TaskList,
    "task show": TaskShow,
    "task geterrors": TaskGeterrors,

    "files copy": FilesCopy,
    "files move": FilesMove,
    "files recode": FilesRecode,
    "files delete": FilesDelete,
    "files push": FilesPush,
    "files pull": FilesPull,

    "registry add": RegistryAddReplica,
    "registry remove": RegistryRemoveReplica,
    "registry list": RegistryListReplicas,

    "service delete": ServiceDelete,
    "service list": ServiceList,
    "client list": ClientList,

    "alert list": AlertList,
    "alert silence": AlertSilence,
    "alert acknowledge": AlertAcknowledge,

    "bucket list": BucketList,
    "bucket show": BucketShow,

    "ca add": AddCA,
    "ca list": ListCA,
    "ca delete": DeleteCA,
    "ca export": ExportCA,
    "csr add": AddCsr,
    "csr approve": ApproveCsr,
    "csr reject": RejectCsr,
    "csr list": ListCsr,
    "csr delete": DeleteCsr,
    "certificate add": AddCertificate,
    "certificate delete": DeleteCertificate,
    "certificate list": ListCertificate,
    "certificate export": ExportCertificate,
    "certificate config show": CertificateConfigShow,
    "certificate config set": CertificateConfigSet,
    "certificate config edit": CertificateConfigEdit,

    "label get": GetLabel,
    "label set": SetLabel,
    "label delete": DeleteLabel,

    "accesskey create": CreateAccessKey,
    "accesskey delete": DeleteAccessKey,
    "accesskey list": ListAccessKey,
    "accesskey import": ImportAccessKey,

    "keystore create-system-slot": KeyStoreCreateSystemSlot,
    "keystore list-system-slots": ListSystemKeystoreSlots,
    "keystore delete-system-slot": DeleteSystemKeystoreSlot,
    "keystore status": KeyStoreStatus,
    "keystore unlock": KeyStoreUnlock,
    "keystore create-slot": CreateUserKeystoreSlot,
    "keystore delete-slot": DeleteUserKeystoreSlot,

    "file dump-metadata": FileDumpMetadata,
    "file resolve-global-id": FileResolveGlobalId,

    "support generate-dump": SupportDumpGenerate,
    "support dump-status": SupportDumpStatus,
    "support download-dump": SupportDumpDownload,
    "support generate-service-dump": ServiceDumpGenerate,

    "query analyze-volumes": QueryAnalyzeVolumes,
    "query analyze": QueryAnalyzeFiles,
    "query files": QueryFiles,
    "query disk-usage": QueryDiskUsage,
    "query find": QueryFind,
    "query cancel": QueryCancel,

    "networktest start": NetworkTestStart,
    "networktest cancel": NetworkTestCancel,
    "networktest show": NetworkTestShow,

    "license import": LicenseKeyImport,
    "audit-log list": AuditLogList,
    "database regenerate": DatabaseRegenerate,
    "healthmanager status": HealthManagerStatus,
    "whoami": WhoAmI,
    "completion": ShellCompletion
    }

def main():
    # print_warning function checks for COMMAND_NAME global variable
    global COMMAND_NAME
    COMMAND_NAME = None
    if sys.version_info < (3,5):
        print_warning(1, "qmgmt requires python 3.5 or later."
                      "Please make sure that your local PATH "
                      "references a suitable Python interpreter.")
        sys.exit(2)

    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)

    # General arguments
    parser.add_argument("--version",
                        action="store_true",
                        dest="print_version",
                        help="Print qmgmt release version to stdout.")
    parser.add_argument("-f", "--force",
                        action="store_true",
                        dest="force",
                        help="Do not ask for confirmation.")
    parser.add_argument("--verbose", "-v",
                        action="store_true",
                        dest="verbose",
                        help="Additional output, including JSON-RPC requests.")

    # API ommunication arguments
    parser.add_argument("-u", "--url",
                        action="store",
                        help="The hostname or URL of an API HTTP(S) service endpoint.\n"
                          "In case only a hostname or URL without port are given, the "
                          "address is with default Quobyte ports.\n"
                          "If not set, the environment variable QUOBYTE_API is used instead.\n"
                          "If that is not set, an API on localhost is configured.")
    parser.add_argument("--ca",
                        action="store",
                        dest="ca_path",
                        help="Path to custom CA certificate (default: system CAs are used).")
    parser.add_argument("--no-check-certificate",
                        action="store_true",
                        help="Ignore server certificate.")
    parser.add_argument("-r", "--retry",
                        action="store_true",
                        dest="retry",
                        default=not sys.stdin.isatty(),
                        help="Retry forever, defaults for non-interactive shells.")
    parser.add_argument("--interactive",
                        action="store_false",
                        dest="retry",
                        help="Don't retry requests.")

    # Output arguments
    parser.add_argument("--output", "-o",
                        action="store",
                        metavar="format",
                        dest="list_output",
                        choices=['table', 'csv', 'json', 'keys'],
                        help="Choose output format.\n"
                             "table: formatted table (default).\n"
                             "csv: comma separated list.\n"
                             "json: json format, non-human readable.\n"
                             "keys: list all comma separated keys, "
                             "to use with columns options.")
    parser.add_argument("--unit",
                        help="Output bytes in metric (1000, default) units (GB, TB) or "
                             "IEC (1024) units (GiB, TiB)",
                        choices=['metric', 'iec'],
                        default="metric")

    # Command-specific arguments
    levels = ['action', 'property']
    subparser_by_parent = {
        '' : parser.add_subparsers(dest="subject", metavar="command")
        }
    for name, command_class in COMMANDS.items():
        parts = name.split(' ')
        for i in range(len(parts)):
            parent = ' '.join(parts[0:i])
            prefix = ' '.join(parts[0:i+1])
            cmd = parts[i]
            is_leave = i == len(parts) - 1

            if not is_leave:
                if prefix not in subparser_by_parent:
                    subject_parser = subparser_by_parent[parent].add_parser(
                        cmd,
                        help= cmd + " operations",
                        formatter_class=argparse.RawTextHelpFormatter)
                    parent_subparsers = subject_parser.add_subparsers(
                        dest=levels[i], metavar="command")
                    subparser_by_parent[prefix] = parent_subparsers
                else:
                    parent_subparsers = subparser_by_parent[prefix]
            else:
                parent_subparsers = subparser_by_parent[parent]
                command_parser = parent_subparsers.add_parser(
                    cmd,
                    formatter_class=argparse.RawTextHelpFormatter,
                    help=command_class.COMMAND_DESCRIPTION)
                command_class.add_specific_arguments(command_parser)

    args = parser.parse_args()

    global FORCE
    FORCE = args.force
    global VERBOSE
    VERBOSE = args.verbose
    global BYTES_UNIT
    BYTES_UNIT = args.unit

    if not args.url:
        if os.getenv("QUOBYTE_API"):
            args.url = os.getenv("QUOBYTE_API")
        else:
            args.url = "localhost:7860"

    if not re.search("://", args.url):
        protocol = "http://"
        if args.ca_path:
            protocol = "https://"
        args.url = protocol + args.url

    if not re.search(":[0-9]+$", args.url):
        if args.url.startswith("https://"):
            args.url += ":8443"
        else:
            args.url += ":7860"

    if args.print_version:
        print(RELEASE_VERSION_LONG)
        sys.exit(0)

    try:
        default_user_credentials = BasicAuthCredentials("admin", "quobyte")
        credentials_provider = InteractiveCredentialsProvider(
            default_user_credentials, FORCE)
        rpc_interface = JsonRpc(url=args.url,
                                session_cookie_handler=SessionCookieHandler(),
                                credentials_provider=credentials_provider,
                                fail_fast=not args.retry,
                                ca=args.ca_path,
                                ignore_server_certificate=args.no_check_certificate)

        cmd_parts = []
        if hasattr(args, 'subject') and args.subject:
            cmd_parts.append(args.subject)
        if hasattr(args, 'action') and args.action:
            cmd_parts.append(args.action)
        if hasattr(args, 'property') and args.property:
            cmd_parts.append(args.property)
        command_name = ' '.join(cmd_parts)

        if not cmd_parts:
            parser.print_help()
            sys.exit(2)

        COMMAND_NAME = command_name
        if command_name not in COMMANDS:
            print("Missing subcommand", command_name, ", try -h")
            sys.exit(2)
        return_value = COMMANDS[command_name](rpc_interface).run(args)
        if return_value != 0:
            sys.exit(return_value)
    except KeyboardInterrupt:
        print_warning(1, "Operation cancelled by user.")
        sys.exit(-4)
    except InvalidInputException as e:
        print_warning(1, "Operation failed due to invalid input: %s" % e)
        sys.exit(-6)
    except ServerErrorException as e:
        print_warning(1, "Operation failed: %s" % e)
        sys.exit(-7)
    except CommunicationException as e:
        print_warning(1, "Operation failed because connection to API service at %s failed "
                      "(set API URL with -u <url>): %s" % (args.url, e))
        sys.exit(-8)
    except AccessDeniedException as e:
        print_warning(1, "Operation failed because login failed: %s" % e)
        sys.exit(-9)
    except LookupError as e:
        print_warning(1, "Operation failed because: %s" % e)
        traceback.print_tb(sys.exc_info()[2])
        sys.exit(-10)
    except MalformedJsonException as e:
        print_warning(1, "Operation failed, server %s did not serve well-formed JSON: %s"
            % (args.url, e))
        sys.exit(-11)
    except UnicodeEncodeError as e:
        print_warning(1, "Encoding error: %s" % e,
                      "Try executing: 'export PYTHONIOENCODING=UTF-8'")
        sys.exit(-12)
    except Exception as e:
        print_warning(1, "Unexpected error, please report to support@quobyte.com:",
                      type(e).__name__,
                      e,
                      "Please copy this stacktrace ---",
                      traceback.print_tb(sys.exc_info()[2]),
                      "---")
        sys.exit(-5)

if __name__ == "__main__":
    main()
