#!/usr/bin/env python3
#
# Copyright 2013-2022 Quobyte Inc. All rights reserved.
#

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 optparse import OptionParser, SUPPRESS_HELP
from sys import exit as sysexit
from textwrap import TextWrapper
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': 'Metadata (M)',
    'STORAGE_SERVICE': 'Data (D)',
    'DIRECTORY_SERVICE': 'Registry (R)',
    'CLIENT': 'FUSE Client (C)',
    'API_PROXY': 'API Proxy (A)',
    'NFS_PROXY': 'NFS Proxy',
    'WEBCONSOLE': 'Web Console',
    'S3_PROXY': 'S3 Proxy (S)',
}

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 = "3.25 (4fbbd59204)"

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(data):
    if LIST_OUTPUT == "json":
        print(json.dumps(data))
        sys.exit(0)


class SessionCookieHandler(object):
    COOKIE_FILE = ".qmgmt_session_cookie"

    def __init__(self):
        self._cookie = None
        home = 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_session_cookie(self):
        if os.path.exists(self._cookie_path):
            with open(self._cookie_path, "r") 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') 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(object):

    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):
        username = os.environ.get('QUOBYTE_USER')
        password = os.environ.get('QUOBYTE_PASSWORD')
        if not username or not password:
            return
        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(object):

    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_home_directory(error=""):
    home = os.path.expanduser("~")
    if home == "~":
        raise InvalidInputException(
            'Cannot determine user home directory. ', error)
    return home


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


def get_comment():
    default_message = "Via qmgmt run by " + str(getpass.getuser()) + "@" \
                  + str(socket.gethostname()) + "."
    if not COMMENT:
        return default_message
    else:
        return COMMENT


def is_uuid(name_or_uuid):
    try:
        UUID(name_or_uuid)
        return True
    except ValueError:
        return False


class HTTPSConnectionWithCaVerification(httplib.HTTPConnection):

    "In addition to core HTTPSConnection, the server certificate is verified against a given CA certificate."

    default_port = httplib.HTTPS_PORT

    def __init__(self, host, port=None, key_file=None, cert_file=None,
                 ca_file=None,
                 strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
        httplib.HTTPConnection.__init__(self, host, port, strict, timeout)
        self.key_file = key_file
        self.cert_file = cert_file
        self.ca_file = ca_file

    def connect(self):
        "Connect to a host on a given (SSL) port."

        sock = socket.create_connection((self.host, self.port), self.timeout)
        if self._tunnel_host:
            self.sock = sock
            self._tunnel()
        self.sock = ssl.wrap_socket(sock, keyfile=self.key_file, certfile=self.cert_file,
                                    ca_certs=self.ca_file, cert_reqs=ssl.CERT_REQUIRED)

    httplib.__all__.append("HTTPSConnectionWithCaVerification")


class JsonRpc(object):

    login_method_string = "qmgmt"

    def __init__(self, url, session_cookie_handler, credentials_provider, fail_fast,
                 ca, require_cert_verify):
        self._ca_file = ca
        if require_cert_verify and not ca:
            crt_paths = [
                "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
                "/etc/pki/tls/certs/ca-bundle.crt",
                "/etc/ssl/certs/ca-certificates.crt",
                "/etc/ssl/ca-bundle.pem"
            ]
            for crt in crt_paths:
                if os.path.isfile(crt):
                    self._ca_file = crt
                    break

            if not self._ca_file:
                raise InvalidInputException("No CA certificate found, but server certificate"
                                            " verification is enforced. Please make sure"
                                            " to provide path to the CA certificate"
                                            " with '--ca' option.")
        self._setup_connection(url)
        self._id = time.time()
        self._fail_fast = fail_fast
        self._session_cookie_handler = session_cookie_handler
        self._credentials_provider = credentials_provider
        self._require_cert_verify = require_cert_verify
        self._tried_default_credentials = False
        self._tried_env_credentials = False
        self._disabled_cert_verification = False

    def _setup_connection(self, url):
        parsedUrl = urlparse(url)
        self._url = parsedUrl.geturl()
        self._netloc = parsedUrl.netloc
        if parsedUrl.scheme == 'https':
            if self._ca_file:
                self._connection = HTTPSConnectionWithCaVerification(self._netloc,
                                                                     ca_file=self._ca_file)
            else:
                self._connection = httplib.HTTPSConnection(
                    self._netloc,
                    context=ssl._create_unverified_context()
                )
                print_warning(0, "Cannot verify the server certificate of the API service"
                              " because the CA certificate is not available."
                              " Please provide path to the CA certificate"
                              " with '--ca' option.")
        else:
            self._connection = httplib.HTTPConnection(self._netloc)

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

    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 = dict()
                # 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()
                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()

                if VERBOSE:
                    print("> POST")
                    print("> " + self._url + path)
                    print("> " + str(call_body))
                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.")

                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 as e:
                        content = content.replace('\0', '\\u0000')  # issue 3863
                        try:
                            result = json.loads(content.decode("utf-8"))
                        except ValueError as e:
                            raise MalformedJsonException(e)

                    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
            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
                if not self._disabled_cert_verification and not self._require_cert_verify:
                    print_warning(0, "Could not verify server certificate of API service using"
                                  " local CA certificate.")
                    self._connection.close()
                    # Core HTTPSConnection does no certificate verification.
                    self._connection = httplib.HTTPSConnection(self._netloc)
                    self._disabled_cert_verification = True
                else:
                    raise CommunicationException(
                        "Client SSL subsystem returned error: " + str(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.")
            except (httplib.HTTPException, socket.error) as e:
                if self._fail_fast:
                    raise CommunicationException(str(e))
                else:
                    print_warning(0, "Encountered error, retrying:", e)
                    time.sleep(1)


class PrettyTable(object):
    """ 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,
                 data,
                 columns,
                 header=None,
                 style="table",
                 sorting_depth=1,
                 sort_by_key=None,
                 reverse=False):
        self._header = header
        if LIST_COLUMNS:
            self._columns = LIST_COLUMNS.split(",")
            self._header = 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 ADD_COLUMNS:
                self._columns.extend(ADD_COLUMNS.split(","))
                if self._header:
                    self._header.extend(ADD_COLUMNS.split(","))
        if LIST_OUTPUT:
            self._style = 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 _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):
                list = [self.encode_with_fallback(self._get_nested(row, keys))
                        for row in self._data]
                if self._header:
                    list.append(self._header[i])
                else:
                    list.append(self._columns[i])
                len_max = len(max(list, key=len)) + 1
                if len_max > 70:
                    len_max = 70
                self._format_string += u'{%i:<%s} ' % (i, len_max)
        elif self._style == "csv":
            self._format_string = \
                u','.join(["{%i}" % i for i, key in enumerate(self._columns)])
        elif self._style == "comp":
            self._format_string = \
                u'/'.join(["{%i}" % i for i, key in enumerate(self._columns)])

    def rename_columns(self, header):
        if not LIST_COLUMNS:
            if ADD_COLUMNS:
                header.extend(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 u",".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 = u''

        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):
    def _get_sorting_keys(self, v):
        for part in self._get_nested(v, self._columns[0]).split("."):
            try:
                yield int(part)
            except:
                print_warning(0, "Unexpected task id format.")
                yield part


class DataDumpHelper(object):

    @staticmethod
    def exportToFile(data_dump, file_name):
        with open(file_name, "w") as file:
            file.write(data_dump)

    @staticmethod
    def importFromFile(file_name):
        with open(file_name) as file:
            return file.read()

    @staticmethod
    def importFromStdin():
       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.exportToFile(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.importFromFile(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


class Command(object):

    def print_help(self, args):
        pass

    def run(self, args):
        pass

    @staticmethod
    def completion_cmd():
        pass


class UserLogin(Command):

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

    def print_help(self, args):
        print("login [<username*>] [<password*>]")
        print("  username*   read from std-in if not given.")
        print("  password*   read from std-in if not given.")

    def run(self, args):
        if len(args) < 1:
            tmp_provider = InteractiveCredentialsProvider(None, False)
            tmp_provider.prompt_for_credentials()
            credentials = tmp_provider.get_credentials()
        elif len(args) == 1:
            credentials = BasicAuthCredentials(args[0], getpass.getpass())
            JsonRpc.login_method_string = "positional argument and prompted password"
        else:
            credentials = BasicAuthCredentials(args[0], args[1])
            JsonRpc.login_method_string = "positional arguments"

        cookiehandler = SessionCookieHandler()
        if cookiehandler.get_session_cookie():
            print_warning(2, "Cleaning up existing session.")
            cookiehandler.delete_session_cookie()

        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 (using %s). %s"
              % (credentials.get_username(), JsonRpc.login_method_string, userrole))
        print("Created authenticated session via session cookie stored in %s. "
              "The session will expire after a timeout or by executing 'qmgmt user logout'."
                  % SessionCookieHandler.COOKIE_FILE)


class UserLogout(Command):

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

    def print_help(self, args):
        print("logout")
        print("  no parameters required")

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


class TenantLookup(Command):

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

    def print_help(self, args):
        print("tenant lookup <tenant id>")

    def do_lookup(self, uuid):
        global TENANT_LOOKUP_CACHE
        if uuid in TENANT_LOOKUP_CACHE:
            return TENANT_LOOKUP_CACHE[uuid]
        result = self._json_rpc.call(
            "getTenant", {'tenant_id': [uuid]})
        ten_list = result['tenant']
        if ten_list:
            TENANT_LOOKUP_CACHE[uuid] = ten_list[0]['name']
            return ten_list[0]['name']
        return None

    def run(self, args):
        if len(args) == 0 or not is_valid_uuid(args[0]):
            print_warning(1, "Tenant uuid expected.")
            return -2
        tenant_name = self.do_lookup(args[0])
        if not tenant_name:
            raise InvalidInputException("No tenant with uuid: " + args[0])
        print(tenant_name)
        return 0

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


class TenantResolve(Command):

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

    def print_help(self, args):
        print("tenant resolve <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 == e.ENTITY_NOT_FOUND or e.code == e.INVALID_ARGUMENTS:
                raise InvalidInputException("No such tenant: " + tenant)
            else:
                raise

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Expected at least one argument")
            return -2
        uuid = self.resolve_string(args[0])
        print(uuid)

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


class TenantCreate(Command):

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

    def print_help(self, args):
        print("tenant create <name> [<restrict_to_network>] [<labels>]")
        print("  name                human readable tenant name")
        print("  restrict_to_network list of one or more IP networks belonging to the domain.")
        print("                      Notation: <address>/<netmask length>")
        print("  labels              comma-separated list of labels with the format: <name>:<value>")

    def do_create(self, args):
        name = ""
        restrict_to_network = []
        labelTuples = []
        if len(args) > 0:
            name = args[0]
        if len(args) > 1:
            restrict_to_network = strToList(args[1])
        if len(args) > 2:
            labelTuples = args[2].split(',')
        labels = []
        for labelTuple in labelTuples:
            splitLabelTuple = labelTuple.split(':')
            if len(splitLabelTuple) != 2:
                raise InvalidInputException("Invalid label: " + str(labelTuple))
            labels.append({
                "name": splitLabelTuple[0],
                "value": splitLabelTuple[1]
            })
        tenant = {'name': name,
                  'restrict_to_network': restrict_to_network}
        call = {'tenant': tenant,
                'on_create_label': labels}
        create_result = self._json_rpc.call("setTenant", call)
        return create_result['tenant_id']

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Expected at least one argument.")
            return -2
        uuid = self.do_create(args)
        print("Success. Created new tenant with tenant id " + uuid)


class TenantCreateWithId(Command):

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

    def print_help(self, args):
        print("tenant create-with-id <name> <tenant_id> [<restrict_to_network>] [<labels>]")
        print("  name                human readable tenant name")
        print("  tenant_id           a predefined id to be used as the tenants uuid")
        print("  restrict_to_network list of one or more IP networks belonging to the domain.")
        print("                      Notation: <address>/<netmask length>")
        print("  labels              comma-separated list of labels with the format: <name>:<value>")

    def do_create(self, args):
        name = ""
        restrict_to_network = []
        labelTuples = []
        if len(args) > 0:
            name = args[0]
        if len(args) > 1:
            uuid = args[1]
        if len(args) > 2:
            restrict_to_network = strToList(args[2])
        if len(args) > 3:
            labelTuples = args[3].split(',')
        labels = []
        for labelTuple in labelTuples:
            splitLabelTuple = labelTuple.split(':')
            if len(splitLabelTuple) != 2:
                raise InvalidInputException("Invalid label: " + str(labelTuple))
            labels.append({
                "name": splitLabelTuple[0],
                "value": splitLabelTuple[1]
            })
        tenant = {'name': name,
                  'tenant_id': uuid,
                  'restrict_to_network': restrict_to_network}
        call = {'tenant': tenant,
                'on_create_label': labels}
        create_result = self._json_rpc.call("setTenant", call)
        return create_result['tenant_id']

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Expected at least two arguments.")
            return -2
        uuid = self.do_create(args)
        print("Success. Registered new tenant with tenant id " + uuid)


class TenantUpdate(Command):

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

    def print_help(self, args):
        print("tenant update name <tenant uuid or name> <new name>")
        print("tenant update restrict_to_network <tenant uuid or name>" \
              " <restrict_to_network(s)>")
        print("    restrict_to_network(s) list of comma separated subnets")
        print("tenant update add_volume_access <tenant uuid or name>" \
              " <volume uuid or name> <read_only> <restrict_to_network>")
        print("tenant update remove_volume_access <tenant uuid or name>" \
              " <volume uuid or name> [<restrict_to_network>]")

    def run(self, args):
        if len(args) == 0:
            print_warning(1, "Subcommand expected.")
            return -2
        sub_command = args[0].lower()
        if len(args) < 3:
            print_warning(1, "Expected at least two arguments.")
            return -2
        uuid = TenantResolve(self._json_rpc).resolve_string(args[1])
        result = self._json_rpc.call("getTenant", {'tenant_id': [uuid]})
        tenant = result['tenant'][0]
        if sub_command == "name":
            tenant['name'] = args[2]
        elif sub_command == "restrict_to_network":
            if len(args) >= 2:
                tenant['restrict_to_network'] = strToList(args[2])
            else:
                raise InvalidInputException("Command requires subnet(s)")
        elif sub_command == "add_volume_access":
            if 3 <= len(args) < 5:
                print_warning(
                    1,
                    sub_command + "operation needs both <read_only> and <restrict_to_network>"
                                  " parameters.\n"
                                  "Use 'true' or 'false' value for <read_only> and"
                                  " <ip address>/<prefix length> format for <restrict_to_network>")
                return -2
            volume_access = {}
            volume_uuid = VolumeResolve(self._json_rpc) \
                .resolve_string(args[1] + '/' + args[2])
            volume_access['volume_uuid'] = volume_uuid
            if len(args) > 3:
                if args[3].lower() == 'true':
                    volume_access['read_only'] = True
                elif args[3].lower() == 'false':
                    volume_access['read_only'] = False
                else:
                    print_warning(1, "Please use 'true' or 'false'"
                                  " for <read_only> parameter.")
                    return -2
            if len(args) > 4:
                volume_access_network = args[4]
                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)
        elif sub_command == "remove_volume_access":
            try:
                volume_uuid = VolumeResolve(self._json_rpc) \
                    .resolve_string(args[1] + '/' + args[2])
            except InvalidInputException:
                # Allow removal of network restrictions for deleted volumes if given a UUID.
                volume_uuid = args[2]
                if not is_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 len(args) > 3 and item.get('restrict_to_network') != args[3]:
                    continue
                volume_access_list.remove(item)
        else:
            print_warning(1, "Unknown subcommand.")
            return -2

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

    @staticmethod
    def completion_cmd():
        return {
            "name": "qmgmt${opts} tenant list --list-columns=name",
            "restrict_to_network":
                "qmgmt${opts} tenant list --list-columns=name",
            "add_volume_access":
                ("qmgmt${opts} tenant list --list-columns=name",
                 "qmgmt${opts} volume list ${prev}"
                 " --list-columns=name"),
            "remove_volume_access":
                ("qmgmt${opts} tenant list --list-columns=name",
                 "qmgmt${opts} volume list ${prev} --list-columns=name")
        }


class TenantDelete(Command):

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

    def print_help(self, args):
        print("tenant delete <tenant uuid or name>")

    def do_delete(self, uuid):
        self._json_rpc.call("deleteTenant", {'tenant_id': uuid})

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Tenant uuid or name expected.")
            return -2
        if is_valid_uuid(args[0]):
            uuid = args[0]
            tenant_name = TenantLookup(self._json_rpc).do_lookup(uuid)
        else:
            uuid = TenantResolve(self._json_rpc).resolve_string(args[0])
            tenant_name = args[0]

        name_str = '' if tenant_name is None else " (" + tenant_name + ")"
        _confirmation_prompt("Really delete tenant " + uuid + name_str + "?")
        self.do_delete(uuid)
        print("Success, deleted tenant " + args[0])

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


class TenantList(Command):

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

    def print_help(self, args):
        print("tenant list")
        print("tenant list volume_access <tenant name or uuid>")

    def get_tenant_list(self):
        call = {}
        result = self._json_rpc.call("getTenant", call)
        return result['tenant']

    def print_all_tenants(self):
        ten_list = self.get_tenant_list()
        if ten_list:
            printer = PrettyTable(
                ten_list,
                ['name', 'tenant_id', 'restrict_to_network'],
                ["Name", "UUID", "restrict_to_network"])
            print(printer.out())
        else:
            print_warning(0, "Tenant list is empty.")

    def print_volume_access(self, 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(
                    vol_list,
                    ['volume_uuid', 'read_only', 'restrict_to_network'],
                    ["Name", "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):
        if len(args) < 1:
            self.print_all_tenants()
        elif len(args) > 1 and args[0] == 'volume_access':
            if len(args) < 2:
                print_warning(1, "Tenant uuid or name expected.")
                return -2
            uuid = TenantResolve(self._json_rpc).resolve_string(args[1])
            self.print_volume_access(uuid)
        else:
            print_warning(1, "Unknown subcommand.")
            return -2

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


class VolumeCreate(Command):

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

    def print_help(self, args):
        print("volume create [<tenant name or uuid>]/<volume name> <user> <group>" \
              "  [<access_mode>] [<labels>] [<encryption_profile>]")

        print("  tenant name or uuid human readable tenant name or uuid")
        print("  volume name         human readable volume name")
        print("  user                user name of owner of the root directory (string)")
        print("  group               group name of owner of the root directory (string)")
        print("  access_mode         numeric POSIX access mode for the volume root directory ")
        print("                      e.g. 0770. Default: 0700")
        print("  labels              comma-separated list of labels with the format: <name>:<value>")

    def do_create(self, args):
        if len(args) < 3:
            raise InvalidInputException(
                "Volume name, User, and Group must not be empty.")
        name = args[0]
        owner_uid = args[1]
        owner_gid = args[2]
        access_mode = '0700'

        labelTuples = []
        if len(args) > 4:
            access_mode = args[3]
            labelTuples = args[4].split(',')
        elif len(args) > 3:
            access_mode = args[3]
        labels = []
        for labelTuple in labelTuples:
            splitLabelTuple = labelTuple.split(':')
            if len(splitLabelTuple) != 2:
                raise InvalidInputException("Invalid label: " + str(labelTuple))
            labels.append({
                "name": splitLabelTuple[0],
                "value": splitLabelTuple[1]
            })

        (tenant_uuid, volume_name) = VolumeResolve(self._json_rpc)\
            .resolve_name_string(name)

        call = {'name': volume_name,
                'root_user_id': owner_uid,
                'root_group_id': owner_gid,
                'access_mode': int(access_mode),
                'tenant_id': tenant_uuid,
                'label': labels}

        create_result = self._json_rpc.call("createVolume", call)
        return create_result['volume_uuid']

    def run(self, args):
        if len(args) < 3:
            print_warning(1, "Expected at least three arguments")
            return -2
        uuid = self.do_create(args)
        print("Success. Created new volume with volume uuid " + uuid)

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


class VolumeMirror(Command):

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

    def print_help(self, args):
        print("volume mirror [<tenant name or uuid>]/<volume name>" \
              " <remote registry>[,<remote registry>]*/<remote volume uuid> [<configuration_name>]")
        print("  tenant name or uuid human readable tenant name or uuid")
        print("  volume name         human readable volume name")
        print("  remote registry     remote registry URL(s), including ALL replicas")
        print("  remote volume       remote volume uuid")
        print("  configuration_name  name of the volume configuration to use")

    def create_mirror(self, args):
        local_volume = args[0]
        remote_volume = args[1]
        configuration_name = 'BASE'
        if len(args) > 2:
            configuration_name = args[2]

        if '/' in local_volume:
            local_tenant = TenantResolve(self._json_rpc)\
                .resolve_string(local_volume.split("/", 1)[0])
            local_volume_name = local_volume.split("/", 1)[1]
        else:
            tenants = self._json_rpc.call("getTenant", {})['tenant']
            if len(tenants) == 1:
                local_tenant = tenants[0]['tenant_id']
            else:
                raise InvalidInputException(
                    "Please specify tenant name or uuid.")
            local_volume_name = local_volume

        # TODO(tilman): Change registry URLs to single cluster uuid #10428
        remote_registry_target = remote_volume.split("/")[0].split(",")
        remote_volume_uuid = remote_volume.split("/")[1]
        if not is_valid_uuid(remote_volume_uuid):
            raise InvalidInputException("No valid uuid: " + remote_volume_uuid)

        call = {'local_volume_name': local_volume_name,
                'local_configuration_name': configuration_name,
                'local_tenant_id': local_tenant,
                'remote_volume_uuid': remote_volume_uuid,
                'OBSOLETE_remote_registry_target': remote_registry_target}
        return self._json_rpc.call("createMirroredVolume", call)

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Expected two arguments")
            return -2
        self.create_mirror(args)
        print("Success. Created new mirrored volume")

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


class VolumeDisconnect(Command):

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

    def print_help(self, args):
        print("volume disconnect [<tenant name or uuid>]/<volume name or uuid>")

    def do_disconnect(self, uuid):
        self._json_rpc.call("disconnectMirroredVolume", {'volume_uuid': uuid})

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Volume uuid or name expected.")
            return -2
        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        volume_name = VolumeLookup(self._json_rpc).do_lookup(uuid)

        name_str = '' if volume_name is None else " (" + volume_name + ")"
        _confirmation_prompt("Really disconnect mirrored volume " + uuid + name_str + "?")
        self.do_disconnect(uuid)
        print("Success, disconnected volume " + args[0])

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


class VolumeErase(Command):

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

    def print_help(self, args):
        print("volume erase [<tenant name or uuid>]/<volume name or uuid> [force|cancel]")
        print("  tenant name or uuid human readable tenant name or uuid")
        print("  volume name or uuid human readable volume name or uuid")
        print("  force               erase volume immediately, not after grace period or in maintenance window")
        print("  cancel              cancel scheduled or running volume erasure")

    def do_delete(self, uuid, force):
        self._json_rpc.call("eraseVolume", {'volume_uuid': uuid, 'force': force})

    def cancel_erasure(self, uuid):
        self._json_rpc.call("cancelVolumeErasure", {'volume_uuid': uuid})

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Volume uuid or name expected.")
            return -2
        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        force = False
        cancel = False
        if len(args) >= 2:
            if args[1] == "force":
                force = True
            elif args[1] == "cancel":
                cancel = True
            else:
                print_warning(1, "Unknown option: " + args[1])
                return -2
        volume_name = VolumeLookup(self._json_rpc).do_lookup(uuid)
        result = self._json_rpc.call("getVolumeList", {'volume_uuid': [uuid]})
        tenant_uuid = result['volume'][0]['tenant_domain']
        tenant_name = TenantLookup(self._json_rpc).do_lookup(tenant_uuid)

        if cancel:
            self.cancel_erasure(uuid)
            print("Success, cancelled erasure of volume " + args[0] + ".")
        else:
            _confirmation_prompt(
                    "Really erase volume {0} ({1}) "
                    "for tenant {2} ({3}) ?"
                            .format(uuid, volume_name, tenant_uuid, tenant_name ))
            self.do_delete(uuid, force)
            print("Success, requested erasure of volume " + args[0] + ".")

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


class VolumeDelete(Command):

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

    def print_help(self, args):
        print("volume delete [<tenant name or uuid>]/<volume name or uuid>")

    def do_delete(self, uuid):
        self._json_rpc.call("deleteVolume", {'volume_uuid': uuid})

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Volume uuid or name expected.")
            return -2
        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        volume_name = VolumeLookup(self._json_rpc).do_lookup(uuid)
        result = self._json_rpc.call("getVolumeList", {'volume_uuid': [uuid]})
        tenant_uuid = result['volume'][0]['tenant_domain']
        tenant_name = TenantLookup(self._json_rpc).do_lookup(tenant_uuid)
        _confirmation_prompt(
                "Really delete volume %s (%s) "
                "for tenant %s (%s) ?"
                        % (uuid, volume_name, tenant_uuid, tenant_name ))
        self.do_delete(uuid)
        print("Success, deleted volume " + args[0])

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


class SnapshotCreate(Command):

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

    def print_help(self, args):
        print("snapshot create [<tenant>]/<volume> <snapshot_name> [--comment=<comment>]"
              " [--pin=<true|false>]")
        print("  Create snapshot for a volume.")
        print("  tenant                 tenant ID or tenant name")
        print("  volume                 volume UUID or volume name")
        print("  snapshot_name          name for the snapshot")
        print("  --comment=<comment>    comment for the snapshot. If not specified, comment will")
        print("                         be generated automatically")
        print("  --pin=<true|false>     specifies if snapshot should be excluded from automatic")
        print("                         cleanup process (true) or not (false). If not specified,")
        print("                         it will be set to 'true'")
        print()
        print("  Examples: snapshot create myTenant/myVolume mySnapshotName")
        print("            snapshot create myTenant/myVolume mySnapshotName --pin=false")
        print("            snapshot create myTenant/myVolume mySnapshotName --comment=\"created"
              " manually by user admin\" --pin=false")
        print()

    def do_create(self, uuid, name, comment, pinned):
        self._json_rpc.call("createSnapshot", {'volume_uuid': uuid,
                                               'name': name,
                                               'comment': comment,
                                               'pinned': pinned})

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Volume uuid or name and snapshot name expected.")
            self.print_help(args)
            return -2

        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])

        snapshot_name = args[1]

        self.do_create(uuid, snapshot_name, get_comment(), True if options.pin == "true" else False)
        print("Success, created snapshot " + snapshot_name + " on volume " + uuid)

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


class SnapshotErase(Command):

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

    def print_help(self, args):
        print("snapshot erase [<tenant name or uuid>]/<volume name or uuid> <snapshot_name>")

    def do_delete(self, uuid, name):
        self._json_rpc.call("eraseSnapshot", {'volume_uuid': uuid, 'name': name})

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Volume uuid or name and snapshot name expected")
            return -2

        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        volume_name = VolumeLookup(self._json_rpc).do_lookup(uuid)

        name = args[1]
        name_str = '' if volume_name is None else " (" + volume_name + ")"
        _confirmation_prompt("Really erase snapshot " + name + " on volume " + uuid + name_str + "?")
        self.do_delete(uuid, name)
        print("Success, snapshot " + name + " on volume " + uuid + " gets 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):

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

    def print_help(self, args):
        print("snapshot delete [<tenant name or uuid>]/<volume name or uuid> <snapshot_name>")

    def do_delete(self, uuid, name):
        self._json_rpc.call("deleteSnapshot", {'volume_uuid': uuid, 'name': name})

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Volume uuid or name and snapshot name expected")
            return -2

        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        volume_name = VolumeLookup(self._json_rpc).do_lookup(uuid)

        name = args[1]
        name_str = '' if volume_name is None else " (" + volume_name + ")"
        _confirmation_prompt("Really delete snapshot " + name + " on volume " + uuid + name_str + "?")
        self.do_delete(uuid, name)
        print("Success, snapshot " + name + " on volume " + uuid + " is deleted in background")

    @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):

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

    def print_help(self, args):
        print("snapshot list [<tenant name or uuid>]/<volume name or uuid>")

    def get_snapshots(self, uuid):
        snapshots = self._json_rpc.call("listSnapshots", {'volume_uuid': uuid})['snapshot']
        return snapshots

    def print_snapshots(self, snapshots):
        printer = PrettyTable(
            snapshots,
            ['version', 'name', 'timestamp', 'comment', 'pinned'],
            ["Version", "Name", "Created", "Comment", "Pinned"])
        print(printer.out())


    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Volume uuid or name expected.")
            return -2

        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])

        snapshots = self.get_snapshots(uuid)
        self.print_snapshots(snapshots)

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


class ConfigurationBase(Command):

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

    def print_help(self, args):
        if self._name_field:
            print("%s edit <%s>" % (self._configuration_cmd, self._name_field))
        else:
            print("%s edit" % self._configuration_cmd)
        print("  opens configuration in text editor")
        if self._name_field:
            print("%s export <%s> [<to file name>]" % (self._configuration_cmd, self._name_field))
        else:
            print("%s export [<to file name>]" % self._configuration_cmd)
        if self._name_field:
            print("%s import <%s> [<configuration_dump*>]" % (self._configuration_cmd, self._name_field))
        else:
            print("%s import [<configuration_dump*>]" % self._configuration_cmd)
        print("  configuration_dump*  read from std-in if not given.")
        if self._name_field and not self._delete_disabled:
            print("%s delete <%s>" % (self._configuration_cmd, self._name_field))
        elif not self._name_field and not self._delete_disabled:
            print("%s delete" % self._configuration_cmd)
        if self._name_field and self._configuration_cmd != 'quota':
            print("%s list" % self._configuration_cmd)
        if self._configuration_cmd == 'quota':
            print("quota list [<Value>] list all quotas with optional\n" \
                  "                     filtering by any value (name," \
                  " uuid, quota id etc).")
            print("quota create <type> <identifier> <resource_type>" \
                  " <resource_value> [<tenant_id>]")
        if self._configuration_cmd == 'user config':
            print("user config add <user_id> [--email=<email>] [--role=<user role>]"
                  " [--member-of-tenant=<tenant list>] [--admin-of-tenant=<tenant list>]"
                  " [--primary-group=<group name>] [--member-of-group=<group list>]"
                  " [--password=<password>]")
            print("user config set-password [<user_id>] [--password=<password>]")
        print()

    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(self, args):
        if len(args) == 0:
            print_warning(1, "Subcommand expected")
            return -2

        sub_command = args[0].lower()
        if sub_command not in ["list"] and len(args) < 2:
            print_warning(1, "Expected at least two arguments")
            return -2
        request = {}
        request['configuration_type'] = self._configuration_type

        if sub_command == "export":
            config_name = args[1]
            request['configuration_type'] = self._configuration_type
            request['configuration_name'] = config_name
            result = self._json_rpc.call("exportConfiguration", request)
            dump = str(result['proto_dump'])
            if len(args) == 3:
                DataDumpHelper.exportToFile(dump, args[2])
            else:
                print(dump)
        elif sub_command == "import":
            config_name = args[1]
            request['configuration_type'] = self._configuration_type
            request['configuration_name'] = config_name
            if len(args) == 3:
                dump = DataDumpHelper.importFromFile(args[2])
            else:
                dump = DataDumpHelper.importFromStdin()
            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
            config_name = args[1]
            request['configuration_type'] = self._configuration_type
            request['configuration_name'] = 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")
                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(
                    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'),
                                TenantLookup(self._json_rpc).do_lookup
                            )
                        elif consumer_type == "VOLUME":
                            printer.modify_cell(
                                row_number,
                                ('consumer', 0, 'identifier'),
                                VolumeLookup(self._json_rpc).do_lookup
                            )

                    # Modify cell: Tenant Lookup
                    if tenant_id is not None:
                        printer.modify_cell(
                            row_number,
                            ('consumer', 0, 'tenant_id'),
                            TenantLookup(self._json_rpc).do_lookup
                        )
                if len(args) > 1:
                    printer.filter_values(
                        args[1],
                        TenantLookup(self._json_rpc).do_lookup(args[1]),
                        VolumeLookup(self._json_rpc).do_lookup(args[1]))
                print(printer.out())
            else:
                for config in self.get_config_list():
                    print(config[self._name_field])

        elif sub_command == "edit":
            config_name = args[1]
            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"] = "printf 'DEVICE\\nGROUP\\nSYSTEM\\nTENANT\\nUSER\\nVOLUME'"

        return r


class SystemConfiguration(ConfigurationBase):

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

    def run(self, args):
        args = args[0:1] + ["ALL"] + args[1:]
        print()
        return ConfigurationBase.run(self, args)


class LicenseKeyImport(Command):

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

    def print_help(self, args):
        print("license import [license_file|license_string]")
        print("  If none of the following is given data is read from stdin.")
        print("  license_file        path to a file containing a license key")
        print("  license_string      a license string provided in command line")

    def do_import(self, args):
        if len(args) != 1 or len(args[0]) == 0:
            raise InvalidInputException("No license data provided")
        key_data = args[0]
        if os.path.isfile(key_data):
            try:
                key_data = open(key_data).readline().strip()
            except:
                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: {}"
                  .format(call_result["verification_result"]))
        return status_code

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


class FailureDomainsConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        ConfigurationBase.__init__(self, 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

    def run(self, args):
        args = args[0:1] + ["ALL"] + args[1:]
        print()
        return ConfigurationBase.run(self, args)


class QuotaConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        ConfigurationBase.__init__(self, 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

    def run(self, args):
        return ConfigurationBase.run(self, args)


class QuotaCreate(Command):

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

    def print_help(self, args):
        print("quota create <type> <identifier> <resource_type> <resource_value> [<tenant_id>]")
        print("type            quote type: DEVICE GROUP SYSTEM TENANT USER VOLUME")
        print("identifier      quota target identifier (uuid, name etc.)")
        print("resource_type   DIRECTORY_COUNT FILE_COUNT")
        print("                PHYSICAL_DISK_SPACE LOGICAL_DISK_SPACE")
        print("                HDD_PHYSICAL_DISK_SPACE HDD_LOGICAL_DISK_SPACE")
        print("                SSD_PHYSICAL_DISK_SPACE SSD_LOGICAL_DISK_SPACE")
        print("resource_value  integer number for resource limit")
        print("tenant_id       needs to be specified for USER GROUP VOLUME consumer type")

    @staticmethod
    def tenant_id_check(args):
        if args[0] in ['USER', 'GROUP', 'VOLUME']:
            return True

    def run(self, args):
        if len(args) < 1 or \
                (self.tenant_id_check(args) and len(args) < 5) or len(args) < 4:
            print_warning(1, "Not enough arguments.")
            return -2
        consumer_type = args[0]
        if consumer_type not in\
                ['DEVICE', 'GROUP', 'SYSTEM', 'TENANT', 'USER', 'VOLUME']:
            print_warning(1, "Unknown consumer type.")
            return -2
        identifier = args[1]
        resource_type = args[2]
        if resource_type not in\
                ['DIRECTORY_COUNT', 'FILE_COUNT',
                 'LOGICAL_DISK_SPACE', 'PHYSICAL_DISK_SPACE',
                 'SSD_LOGICAL_DISK_SPACE', 'SSD_PHYSICAL_DISK_SPACE',
                 'HDD_LOGICAL_DISK_SPACE', 'HDD_PHYSICAL_DISK_SPACE']:
            print_warning(1, "Unknown resource type.")
            return -2
        resource_value = args[3]

        if consumer_type == 'TENANT':
            identifier = TenantResolve(self._json_rpc).resolve_string(identifier)
        elif consumer_type == 'VOLUME':
            identifier = VolumeResolve(self._json_rpc).resolve_string(identifier)

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

        if self.tenant_id_check(args):
            tenant_id = TenantResolve(self._json_rpc).resolve_string(args[4])
            quota['consumer'][0].update({'tenant_id': tenant_id})

        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: %s" % quotas['quotas'][0]['id'])


class RulesConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        ConfigurationBase.__init__(self, 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

    def run(self, args):
        return ConfigurationBase.run(self, args)


class NfsVirtualIpsConfiguration(ConfigurationBase):

    def __init__(self, json_rpc):
        ConfigurationBase.__init__(self, 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

    def run(self, args):
        return ConfigurationBase.run(self, args)


class UserConfiguration(ConfigurationBase):

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

    def print_help(self, args):
        if len(args) == 0:
            ConfigurationBase.print_help(self, args)
            print()
        elif args[0].lower() == "set-password":
            print("user config set-password [<user_id>] [--password=<password>]")
            print()
            print("  Sets the password for the built-in database user")
            print("  user_id               (optional) username; prompted if not given")
            print("  --password=<password> (optional) password; prompted if not given")
            print()

        elif args[0].lower() == "add":
            print("user config add <user_id> [--email=<email>] [--role=<user role>]"
                  " [--member-of-tenant=<tenant list>] [--admin-of-tenant=<tenant list>]"
                  " [--primary-group=<group name>] [--member-of-group=<group list>]"
                  " [--password=<password>]")
            print()
            print("  Adds a new user to the built-in user database.")
            print("  user_id                login name of the user")
            print("  --email=email          email address of the user")
            print("  --password=password    password for the user to be passed on the command line"
                  ".")
            print("                         Do not use this option, if you want to be prompted for")
            print("                         the password.")
            print("  --role=role            role of the user. The following roles are currently")
            print("                         supported:")
            print("                         SUPER_USER, SUPER_USER_READONLY, HARDWARE_OPERATOR,")
            print("                         FILESYSTEM_ADMIN, FILESYSTEM_ADMIN_READONLY.")
            print("                         If no role is defined, the user will have no privileges"
                  ".")
            print("                         To define the tenant membership or administration use")
            print("                         --member-of-tenant and --admin-of-tenant options,")
            print("                         respectively.")
            print("  --admin-of-tenant=tenants")
            print("                         comma-separated list of tenants (names or IDs), which")
            print("                         can be administrated by the user.")
            print("  --member-of-tenant=tenants")
            print("                         comma-separated list of tenants (names or IDs), of")
            print("                         which the user is a member.")
            print("  --primary-group=<group name>")
            print("                         name of the user's primary group; required for the")
            print("                         creation of the file system and S3 access keys.")
            print("  --member-of-group=groups")
            print("                         comma-separated list of groups, of which the user is")
            print("                          a member.")
            print()
            print("  Example: user config add myUser --role=FILESYSTEM_ADMIN --email=myUser@mail"
                  " --member-of-tenant=\"My Tenant, 64c25306-6caf-4b25-9dc7-f0efa5fcb3b5,tenant\""
                  " --admin-of-tenant=\"My Tenant\" --primary-group=\"users\""
                  "--member-of-group=\"users,dev,console\" --password=secret")
            print()

    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:
                    resolved = TenantResolve(self._json_rpc).resolve_string(tenant)
                    if resolved:
                        array.append(resolved.strip())
        if len(array) > 1:
            # remove possible duplicates
            array = list(dict.fromkeys(array))

    def run(self, args):
        if len(args) == 0:
            print_warning(1, "Subcommand expected")
            return -2

        sub_command = args[0].lower()
        if (sub_command != "list" and sub_command != "set-password") and len(args) < 2:
            print_warning(1, "\"" + sub_command + "\" command expects at least one argument")
            return -2
        request = {}
        # Subcommand "list"
        if sub_command == "list":
            request['configuration_type'] = self._configuration_type
            result = self._json_rpc.call("getConfiguration", request)
            printer = PrettyTable(
                result['user_configuration'],
                ['id', 'email', 'role'],
                ["Id", "Email", "Role"]
            )
            print(printer.out())

        # Subcommand "set-password"
        elif sub_command == "set-password":
            # get username
            if len(args) < 2:
                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[1]
            # get password
            new_password = options.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)

        # Subcommand "add"
        elif sub_command == "add":
            user_id = args[1]
            email = options.email
            password = options.password
            role = options.role
            g_member = options.g_member
            t_member = options.t_member
            t_admin = options.t_admin
            primary_group = options.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
            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)
        # Subcommands from ConfigurationBase: "edit, delete, ..."
        elif sub_command == "edit":
            user_id = args[1]
            response = self._json_rpc.call("getUsers", {"user_id": [user_id]})
            if "user_configuration" in response:
                help_message = (
                    "# Update metadata 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(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)))
        else:
            ConfigurationBase.run(self, args)


class PolicyRuleList(Command):

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

    def print_help(self, args):
        print("policy-rules list")
        print("  lists all policy rules briefly.")

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


class PolicyRuleEdit(Command):

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

    def print_help(self, args):
        print("policy-rules edit")
        print("  edit policy rules in text editor.")

    def run(self, args):
        if len(args) != 0:
          print_warning(1, "policy-rules edit does not take further arguments")
          return
        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):

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

    def print_help(self, args):
        print("policy-rules export [</path/to/file>]")
        print("  prints to stdout if no file is given.")

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

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


class PolicyRuleImport(Command):

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

    def print_help(self, args):
        print("policy-rules import [</path/to/file>]")
        print("  reads from stdin if no file is given.")
        print("  If a single policy rule is provided:")
        print("  - Create it if no UUID is defined.")
        print("  - Update it otherwise.")
        print("  If multiple policy rules are provided, update all existing rules accordingly:")
        print("  - Create provided rules with missing UUIDs.")
        print("  - Update rules to provided ones with matching UUIDs.")
        print("  - Delete rules which are no longer provided.")

    def run(self, args):
        call = {}
        if len(args) == 1:
            dump = DataDumpHelper.importFromFile(args[0])
        else:
            dump = DataDumpHelper.importFromStdin()
        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):

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

    def print_help(self, args):
        print("policy-rules filter <format> <subject>")
        print("  Filter effective policy rules and/or policies for a subject.")
        print("  <format> can be either of:")
        print("    rules     Lists effective policy rules with their effective policies.")
        print("    policies  Lists flat effective policies without containing policy rules.")
        print("  <subject> can be either of:")
        print("    global")
        print("    tenant <tenant name or uuid>")
        print("    volume [<tenant name or uuid>]/<volume name or uuid>")
        print("    file [<tenant name or uuid>]/<volume name or uuid> </path/to/file>")
        print("    client [<tenant name or uuid>]/<volume name or uuid> </path/to/file>"
              " <client_ip_address> <client-type>")
        print("      <client-type> can by either of: NATIVE S3 NFS")

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Missing format and/or subject.")
            return -2
        call = {}

        format = args[0]
        if format == "rules":
            call['policies_only'] = False
        elif format == "policies":
            call['policies_only'] = True
        else:
            print_warning(1, "Unknown format: " + format)
            return -2

        subject = args[1]
        policy_subject = {}
        if subject == "global":
            policy_subject['global'] = True
        elif subject == "tenant":
            if len(args) < 3:
                print_warning(1, "Missing tenant UUID.")
                return -2
            policy_subject['tenant'] = {
                'uuid': TenantResolve(self._json_rpc).resolve_string(args[2])}
        elif subject == "volume":
            if len(args) < 3:
                print_warning(1, "Missing volume UUID.")
                return -2
            policy_subject['volume'] = {
                'uuid': VolumeResolve(self._json_rpc).resolve_string(args[2])}
        elif subject == "file":
            if len(args) < 4:
                print_warning(1, "Missing volume UUID and/or file path.")
                return -2
            policy_subject['volume'] = {
                'uuid': VolumeResolve(self._json_rpc).resolve_string(args[2])}
            policy_subject['file'] = {'path': args[3]}
        elif subject == "client":
            if len(args) < 6:
                print_warning(1, "Missing parameter(s).")
                return -2
            client_subject = {
                "client_ip_address": args[4],
                "client_type": args[5]
            }
            policy_subject['volume'] = {
                'uuid': VolumeResolve(self._json_rpc).resolve_string(args[2])}
            policy_subject['file'] = {'path': args[3]}
            policy_subject['client'] = client_subject
        else:
            print_warning(1, "Unknown subject: " + subject)
            return -2
        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):

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

    def print_help(self, args):
        print("policy-rule create")
        print("  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):

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

    def print_help(self, args):
        print("policy-rule update <policy rule name or uuid>")
        print("  update a policy rule in text editor.")

    def run(self, args):
        if len(args) == 0:
            print_warning(1, "Policy rule name or UUID expected")
            return -2
        uuidOrName = str(args[0])
        try:
            uuid = str(UUID(uuidOrName, version=4))
        except:
            uuid = PolicyRuleResolve(self._json_rpc).resolve_name(uuidOrName)
        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):

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

    def print_help(self, args):
        print("policy-rule priority {increase|decrease} <policy rule name or uuid>")
        print("  increase or decrease a policy rule's priority.")

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Missing arguments.")
            return -2

        change = str(args[0])
        if change == "increase":
            changeEnum = "INCREASE"
        elif change == "decrease":
            changeEnum = "DECREASE"
        else:
            print_warning(1, "Use either \"increase\" or \"decrease\"")
            return -2
        uuidOrName = str(args[1])
        try:
            uuid = str(UUID(uuidOrName, version=4))
        except:
            uuid = PolicyRuleResolve(self._json_rpc).resolve_name(uuidOrName)
        try:
            call = {"policy_rule_uuid": uuid, "priority_change":  changeEnum}
            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):

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

    def print_help(self, args):
        print("policy-rule delete <policy rule name or uuid>")
        print("  delete a policy rule.")

    def run(self, args):
        if len(args) == 0:
            print_warning(1, "Policy rule name or UUID expected")
            return -2
        uuidOrName = str(args[0])
        try:
            uuid = str(UUID(uuidOrName, version=4))
        except:
            uuid = PolicyRuleResolve(self._json_rpc).resolve_name(uuidOrName)
        _confirmation_prompt("Really delete policy rule %s?" % (uuidOrName))
        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):

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

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

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

    def print_help(self, args):
        print("policy-preset list")

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


class VolumeUpdate(Command):

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

    def print_help(self, args):
        print("volume update name [<tenant name or uuid>]/<volume name or uuid> <new name>")
        print("volume update add_replica_device " \
              "[<tenant name or uuid>]/<volume name or uuid> <metadata device id>")
        print("volume update remove_replica_device " \
              "[<tenant name or uuid>]/<volume name or uuid> <metadata device id>")
        print("volume update set_preferred_primary_replica_device " \
              "[<tenant name or uuid>]/<volume name or uuid> <metadata device id>")
        print("volume update remove_preferred_primary_replica_device " \
              "[<tenant name or uuid>]/<volume name or uuid>")
        print("volume update set_mirror_source " \
              "[<tenant name or uuid>]/<volume name or uuid> <registry URL>")

    def run(self, args):
        if len(args) == 0:
            print_warning(1, "Subcommand expected")
            return -2

        sub_command = args[0].lower()
        if sub_command == "publish":
            print_warning(1, "Not supported anymore, use 'volume publish' instead.")
            return -2
        request = {}
        if sub_command == "name":
            if len(args) < 3:
                print_warning(1, "Expected at least three arguments.")
                return -2
            uuid = VolumeResolve(self._json_rpc).resolve_string(args[1])
            request['volume_uuid'] = uuid
            request['name'] = args[2]
        elif sub_command == "add_replica_device":
            if len(args) < 3:
                print_warning(1, "Expected at least three arguments.")
                return -2
            uuid = VolumeResolve(self._json_rpc).resolve_string(args[1])
            try:
                request['volume_uuid'] = uuid
                request['add_replica_device_id'] = int(args[2])
            except ValueError:
                raise InvalidInputException("Invalid device id: " + args[2])
        elif sub_command == "remove_replica_device":
            if len(args) < 3:
                print_warning(1, "Expected at least three arguments.")
                return -2
            uuid = VolumeResolve(self._json_rpc).resolve_string(args[1])
            try:
                request['volume_uuid'] = uuid
                request['remove_replica_device_id'] = int(args[2])
            except ValueError:
                raise InvalidInputException("Invalid device id: " + args[2])
        elif sub_command == "set_preferred_primary_replica_device":
            if len(args) < 3:
                print_warning(1, "Expected at least three arguments.")
                return -2
            uuid = VolumeResolve(self._json_rpc).resolve_string(args[1])
            try:
                request['volume_uuid'] = uuid
                request['remove_preferred_primary_replica_device'] = False
                request['preferred_primary_replica_device_id'] = int(args[2])
            except ValueError:
                raise InvalidInputException("Invalid device id: " + args[2])
        elif sub_command == "remove_preferred_primary_replica_device":
            if len(args) < 2:
                print_warning(1, "Expected at least two arguments.")
                return -2
            uuid = VolumeResolve(self._json_rpc).resolve_string(args[1])
            try:
                request['volume_uuid'] = uuid
                request['remove_preferred_primary_replica_device'] = True
            except ValueError:
                raise InvalidInputException("Invalid device id: " + args[2])
        elif sub_command == "set_mirror_source":
            if len(args) < 3:
                print_warning(1, "Expected at least three arguments.")
                return -2
            uuid = VolumeResolve(self._json_rpc).resolve_string(args[1])
            try:
                request['volume_uuid'] = uuid
                request['OBSOLETE_remote_registry_target'] = args[2].split(",")
            except ValueError:
                raise InvalidInputException("Invalid registry url: " + args[2])
        else:
            print_warning(1, "Unknown subcommand.")
            return -2
        self._json_rpc.call("updateVolume", request)
        print("Success. Updated volume " + request['volume_uuid'])

    @staticmethod
    def completion_cmd():
        return dict((k, "qmgmt${opts} volume list --list-columns=tenant_domain,name")
            for k in ["name", "add_replica_device", "remove_replica_device",
            "set_preferred_primary_replica_device", "remove_preferred_primary_replica_device",
            "set_mirror_source"])

class VolumeEncryption(Command):

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

    def print_help(self, args):
        print("Commands:")
        EncryptionInitWithSlot(self._json_rpc).print_help([])
        print()
        EncryptionAddSlot(self._json_rpc).print_help([])
        print()

    def run(self, args):
        if not args:
            self.print_help(args)
            return -2

        if args[0] == "init-with-slot":
            return EncryptionInitWithSlot(self._json_rpc).run(args[1:])
        elif args[0] == "add-slot":
            return EncryptionAddSlot(self._json_rpc).run(args[1:])
        else:
            print_warning(1, "Unknown subcommand.")
            return -2

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

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 EncryptionInitWithSlot (Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("volume encryption init-with-slot [<tenant name or uuid>]/" \
              "<volume name or uuid> <slot uuid> [<slot password>]")

    def run(self, args):
        if len(args) < 3:
            password = get_password_from_prompt()
        else:
            password = args[2]

        volume_uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        new_keystore_slot_uuid = args[1]

        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 EncryptionAddSlot(Command):

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

    def print_help(self, args):
        print("volume encryption add-slot [<tenant name or uuid>]/" \
              "<volume name or uuid> <new slot uuid> " \
              "[<new slot password>] <existing slot uuid> [<existing slot " \
              "password>]")

    def run(self, args):
        volume_uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        new_keystore_slot_uuid = args[1]
        if len(args) < 4:
            new_password = get_password_from_prompt()
            existing_password = get_password_from_prompt()
            existing_keystore_slot_uuid = args[2]
        else:
            new_password = args[2]
            existing_keystore_slot_uuid = args[3]
            existing_password = args[4]

        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)


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


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


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


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

    replica_devices = list()
    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 SHOW_ALL:
        print("  storage device spread:       " + str(volume.get('device_spread', 'unknown')))
    print("  last successful scrub:       " + _printMillisTimestamp(
        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 _printSTimestampFull(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 = _printSTimestampFull(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):

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

    def print_help(self, args):
        print("volume list [<tenant name or uuid>]")

    def run(self, args):
        call = {}
        if len(args) > 0:
            uuid = TenantResolve(self._json_rpc).resolve_string(args[0])
            call['tenant_domain'] = uuid
        result = self._json_rpc.call("getVolumeList", call)
        vol_list = result['volume']

        printer = PrettyTable(
            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 SHOW_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', TenantLookup(self._json_rpc).do_lookup)
        print(printer.out())

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


class HealthManagerStatus(Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("healthmanager status")

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

        _output_json_and_exit(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 = _printMillisTimestamp(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):

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

    def print_help(self, args):
        print("bucket list [<tenant name or uuid>]")

    def run(self, args):
        call = {}
        if len(args) > 0:
            tenant_name = args[0]
            uuid = TenantResolve(self._json_rpc).resolve_string(tenant_name)
            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(
            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):

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

    def print_help(self, args):
        print("bucket show <bucket name>")

    def run(self, args):
        if len(args) == 0:
            print_warning(1, "Expected at least one argument")
            return -2
        bucket_name = str(args[0])
        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(bucket_volume)

            tenant_name = TenantLookup(self._json_rpc).do_lookup(
                bucket_volume['tenant_domain'])
            print(bucket_name + ':')
            print('  volume: %s (%s) (%s)' % (bucket_volume['name'],
                                              bucket_volume['volume_uuid'],
                                              'exclusive'
                                              if bucket_volume['isExclusiveVolumeBucket']
                                              else 'shared'))
            print('  tenant: %s (%s) ' % (tenant_name,
                                          bucket_volume['tenant_domain']))
        print()

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


class Accounting(Command):

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

    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


class VolumeShow(Command):

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

    def print_help(self, args):
        print("volume show [<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):
        if len(args) == 0:
            print_warning(1, "volume uuid or name expected")
            return -2
        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        vol_list = self.get_details(uuid)

        _output_json_and_exit(vol_list)

        if len(vol_list) == 0:
            print_warning(1, "No such volume: " + args[0])
        else:
            labels = self._json_rpc.call("getLabels",
                                                  {'filter_entity_type': 'VOLUME',
                                                   'filter_entity_id': uuid})['label']
            _printVolumeDetails(vol_list[0], labels)
            # fetch device usage accounting
            accounting_request = {
                "entity": [{
                    "type": "VOLUME",
                    "identifier": 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 len(device_usage) != 0:
                    print()
                    print("DEVICE USAGE")
                    printer = PrettyTable(
                        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": 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 len(users) != 0:
                    print()
                    print("CONSUMPTION BY USERS  (top 100)")
                    printer = PrettyTable(
                        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 len(groups) != 0:
                    print()
                    print("CONSUMPTION BY GROUPS (top 100)")
                    printer = PrettyTable(
                        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):

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

    def print_help(self, args):
        print("volume lookup <volume uuid>")

    def do_lookup(self, 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 run(self, args):
        if len(args) == 0 or not is_valid_uuid(args[0]):
            print_warning(1, "Volume uuid expected.")
            return -2
        volume_name = self.do_lookup(args[0])
        if not volume_name:
            raise InvalidInputException("No volume with uuid: " + args[0])
        print(volume_name)
        return 0

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


class VolumeResolve(Command):

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

    def print_help(self, args):
        print("volume resolve [<tenant name or uuid>]/<volume name or uuid>")

    def resolve_name_string(self, name):
        try:
            tenant = name.split("/", 1)[0]
            tenant_uuid = TenantResolve(self._json_rpc).resolve_string(tenant)
            volume = name.split("/", 1)[1]
        except (InvalidInputException, IndexError):
            tenant_uuid = ""
            volume = name
        return tenant_uuid, volume

    def resolve_string(self, name):

        if name.startswith("/"):
            name = name[1::]

        if VolumeLookup(self._json_rpc).do_lookup(name):
            return name

        (tenant_uuid, volume_name) = self.resolve_name_string(name)

        if VolumeLookup(self._json_rpc).do_lookup(volume_name):
            return volume_name

        # resolveVolumeName returns only one volume uuid
        result = self._json_rpc.call(
            "getVolumeList", {'tenant_domain': tenant_uuid})['volume']

        candidates = []
        for volume in result:
            if volume['name'] == volume_name:
                candidates.append(volume)
        if len(candidates) < 1:
            raise InvalidInputException("No such volume: %s" % volume_name)
        elif len(candidates) > 1:
            raise InvalidInputException("Volume name '%s' exists"
                                        " for multiple tenants" % volume_name)
        else:
            return candidates[0]['volume_uuid']

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Expected at least one argument")
            return -2
        uuid = self.resolve_string(args[0])
        print(uuid)

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


class VolumeRepair(Command):

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

    def print_help(self, args):
        print("volume repair [<tenant name or uuid>]/<volume name or uuid>")
        print("    [--comment=<new comment>] replace default user and hostname" \
              " comment with the given one.")

    def run(self, args):
        task_type = "ENFORCE_PLACEMENT"
        if len(args) < 1:
            print_warning(1, "Expected a volume name or uuid.")
            return -2

        target = VolumeResolve(self._json_rpc).resolve_string(args[0])
        call = {'task_type': task_type,
                'restrict_to_volumes': [str(target)],
                'comment': get_comment(), }

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

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


class VolumeScrub(Command):

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

    def print_help(self, args):
        print("volume scrub [<tenant name or uuid>]/<volume name or uuid>")
        print("    [--comment=<new comment>] replace default user and hostname" \
              " comment with the given one.")

    def run(self, args):
        task_type = "SCRUB"
        if len(args) < 1:
            print_warning(1, "Expected a volume name or uuid.")
            return -2
        target = VolumeResolve(self._json_rpc).resolve_string(args[0])
        if "eccc" in args:
            call = {'task_type': task_type,
                    'restrict_to_volumes': [str(target)],
                    'scrub_settings': {'ec_consistency_check_only': True},
                    'comment': get_comment(), }
        else:
            call = {'task_type': task_type,
                    'restrict_to_volumes': [str(target)],
                    'comment': get_comment(), }

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

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


class VolumePublish (Command):

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

    def print_help(self, args):
        print("volume publish [<tenant name or uuid>]/<volume name or uuid> <bucket_name>")
        print("  Publishes the volume as a bucket under the given name.")

    def do_publish(self, uuid, name):
        self._json_rpc.call("publishBucketVolume", {'volume_uuid': uuid,
                                                    'bucket_name': name})

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Volume identifier and bucket name expected.")
            return -2
        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        self.do_publish(uuid, args[1])
        print("Success, published volume " + uuid + " as bucket " + args[0])

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


class VolumeUnpublish (Command):

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

    def print_help(self, args):
        print("volume unpublish [<tenant name or uuid>]/<volume name or uuid>")
        print("  Unpublishes a specific exclusive bucket volume.")

    def do_unpublish(self, uuid):
        self._json_rpc.call("unpublishBucketVolume", {'volume_uuid': uuid})

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Volume uuid or name expected.")
            return -2
        uuid = VolumeResolve(self._json_rpc).resolve_string(args[0])
        self.do_unpublish(uuid)
        print("Success, unpublished bucket volume " + uuid)

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


class DeviceShow(Command):

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

    def print_help(self, args):
        print("device show <device_id>")

    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:
            raise InvalidInputException("Invalid device id: " + device_id)

    def run(self, args):
        if len(args) != 1:
            print_warning(1, "Expected exactly one argument: device_id")
            return -2

        dev_list = self.get_device(args[0])

        _output_json_and_exit(dev_list)

        if not dev_list:
            print_warning(1, "Wrong device id: %s" % args[0])
            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: " + _printMillisTimestamp(
                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):

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

    def print_help(self, args):
        print("device remove <device_id>")
        print("    [--comment=<new comment>] replace default user and hostname" \
              " comment with the given one.")

    def run(self, args):
        task_type = "DRAIN"
        if len(args) < 1:
            print_warning(1, "Expected a device id")
            return -2
        target = args[0]
        call = {'task_type': task_type,
                'restrict_to_devices': [int(target)],
                'comment': get_comment(), }

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

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


class DeviceList(Command):

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

    def print_help(self, args):
        print("device list [type]")
        print("  type      optional type filter, if set list only devices with selected type")
        print("            can be D(ATA), M(ETADATA) or R(EGISTRY)")
        print("  --all, -a\n" \
              "            show decommissioned devices")

    def get_list(self, args):
        call = {}
        if len(args) > 0:
            if args[0].upper() not in VALID_DEVICE_TYPES:
                print_warning(1, "Invalid device type.")
                return -2
            call['device_type'] = convert_device_type_input(args[0].upper())

        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 SHOW_ALL:
                continue
            dev['content'] = [c['content_type'] for c in dev['content']]
            to_print.append(dev)

        printer = PrettyTable(
            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 DeviceUpdate(Command):

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

    def print_help(self, args):
        print("Commands:")
        print("  device update mode         changes the mode of a device")
        print("  device update led-status   changes the LED status of a device")
        print("  device update add-tags     adds custom tags for this device")
        print("  device update remove-tags  removes custom tags for this device")
        print("  device update add-type     adds a new device type to an existing device")
        print("  device update remove-type  removes a device type from an existing device")
        print("  device update health       changes the health status of the device")
        print("  device update mount        changes the mount state of the device")
        print("  device update fsck         changes the file system check before mount state of the device")
        print("  device update trim-method  changes the method used to TRIM the device")
        print("  [--comment=<new comment>] replace default user and hostname" \
              " comment with the given one.")
        print()
        print("Usage:")
        DeviceMode(self._json_rpc).print_help([])
        print()
        DeviceLEDStatus(self._json_rpc).print_help([])
        print()
        DeviceAddTags(self._json_rpc).print_help([])
        print()
        DeviceRemoveTags(self._json_rpc).print_help([])
        print()
        DeviceAddType(self._json_rpc).print_help([])
        print()
        DeviceRemoveType(self._json_rpc).print_help([])
        print()
        DeviceHealthChange(self._json_rpc).print_help([])
        print()
        DeviceMountStateChange(self._json_rpc).print_help([])
        print()
        FileSystemCheckBeforeMountStateChange(self._json_rpc).print_help([])
        print()
        DeviceTrimMethodChange(self._json_rpc).print_help([])

    def run(self, args):
        if not args:
            self.print_help(args)
            return -2

        if args[0] == "mode":
            return DeviceMode(self._json_rpc).run(args[1:])
        elif args[0] == "led-status":
            return DeviceLEDStatus(self._json_rpc).run(args[1:])
        elif args[0] == "add-tags":
            return DeviceAddTags(self._json_rpc).run(args[1:])
        elif args[0] == "remove-tags":
            return DeviceRemoveTags(self._json_rpc).run(args[1:])
        elif args[0] == "add-type":
            return DeviceAddType(self._json_rpc).run(args[1:])
        elif args[0] == "remove-type":
            return DeviceRemoveType(self._json_rpc).run(args[1:])
        elif args[0] == "health":
            return DeviceHealthChange(self._json_rpc).run(args[1:])
        elif args[0] == 'mount':
            return DeviceMountStateChange(self._json_rpc).run(args[1:])
        elif args[0] == 'fsck':
            return FileSystemCheckBeforeMountStateChange(
                self._json_rpc).run(args[1:])
        elif args[0] == 'trim-method':
            return DeviceTrimMethodChange(self._json_rpc).run(args[1:])
        else:
            print_warning(1, "Unknown subcommand.")
            return -2

    @staticmethod
    def completion_cmd():
        return {
            "mode": ("qmgmt${opts} device list --list-columns=device_id",
                       "printf '%s\\n'" % "\\n".join(VALID_DEVICE_MODES)),
            "led-status": ("qmgmt${opts} device list --list-columns=device_id",
                           "printf '%s\\n'" % "\\n".join(VALID_DEVICE_LED_MODES)),
            "add-tags": "qmgmt${opts} device list --list-columns=device_id",
            "remove-tags": ("qmgmt${opts} device list --list-columns=device_id",
                            "qmgmt${opts} device show ${prev} | grep Tags: |"
                            "cut -d \":\" -f 2 | tr ' ' '\\n'   "),
            "add-type": ("qmgmt${opts} device list --list-columns=device_id",
                         "printf 'DATA\\nMETADATA\\nREGISTRY\\n'"),
            "remove-type": ("qmgmt${opts} device list --list-columns=device_id",
                            "printf 'DATA\\nMETADATA\\nREGISTRY\\n'"),
            "health": ("qmgmt${opts} device list --list-columns=device_id",
                       "printf 'HEALTHY\\nDEFECTIVE\\n'"),
            "mount": ("qmgmt${opts} device list --list-columns=device_id",
                       "printf '%s\\n'" % "\\n".join(VALID_MOUNT_STATES)),
            "fsck": ("qmgmt${opts} device list --list-columns=device_id",
                      "printf '%s\\n'" % "\\n".join(VALID_FILESYSTEM_CHECK_STATES)),
            "trim-method": ("qmgmt${opts} device list --list-columns=device_id",
                      "printf '%s\\n'" % "\\n".join(VALID_TRIM_METHODS))
        }


class DeviceLEDStatus(Command):

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

    def print_help(self, args):
        wrapper = TextWrapper(subsequent_indent="                ", width=80)
        print("device update led-status <device_id> <led-status>")
        print(wrapper.fill(
            "  device_id     global device identifier of the device to change, see device list"))
        print(wrapper.fill("  led-status        new LED status of the device, must be one of " +
                           ", ".join(VALID_DEVICE_LED_MODES)))

    def update_status(self, args):
        try:
            call = {
                'device_id': int(args[0]),
                'set_led_status': args[1].upper(),
                'comment': get_comment()}
            self._json_rpc.call("updateDevice", call)
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

    def run(self, args):
        if len(args) != 2 or args[1].upper() not in VALID_DEVICE_LED_MODES:
            if len(args) != 2:
                print_warning(1, "Expected two arguments.")
            else:
                print_warning(1, "Invalid status.")
            return -2

        self.update_status(args)

        print("Success. Updated device led status for device " + args[0] + " to " + args[1].upper())


class DeviceMode(Command):

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

    def print_help(self, args):
        wrapper = TextWrapper(subsequent_indent="                ", width=80)
        print("device update mode <device_id> <mode>")
        print(wrapper.fill(
            "  device_id     global device identifier of the device to change, see device list"))
        print(wrapper.fill("  mode          new mode of the device, must be one of " +
                           ", ".join(VALID_DEVICE_MODES)))

    def update_mode(self, args):
        try:
            call = {
                'device_id': int(args[0]),
                'set_device_status': args[1].upper(),
                'comment': get_comment()}
            self._json_rpc.call("updateDevice", call)
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

    def run(self, args):
        if len(args) != 2 or args[1].upper() not in VALID_DEVICE_MODES:
            if len(args) != 2:
                print_warning(1, "Expected two arguments.")
            else:
                print_warning(1, "Invalid mode.")
            return -2

        self.update_mode(args)

        print("Success. Updated device mode for device " + args[0] + " to " + args[1].upper())


class DeviceMountStateChange(Command):

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

    def print_help(self, args):
        wrapper = TextWrapper(subsequent_indent="                ", width=80)
        print("device update mount <device_id> <state>")
        print(wrapper.fill(
            "  device_id     global device identifier of the device to change, see device list"))
        print(wrapper.fill("  state        new mount state of the device, must be one of " +
                           ", ".join(VALID_MOUNT_STATES)))

    def update_status(self, args):
        try:
            if args[1].upper() == "UNMOUNTED":
                call = {
                    'device_id': int(args[0]),
                    'set_mount_state': "UNMOUNTED",
                    'comment': get_comment()}
            elif args[1].upper() == "MOUNTED":
                call = {
                    'device_id': int(args[0]),
                    'set_mount_state': "MOUNTED",
                    'comment': get_comment()}
            self._json_rpc.call("updateDevice", call)
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

    def run(self, args):
        if len(args) != 2 or args[1].upper() not in VALID_MOUNT_STATES:
            if len(args) != 2:
                print_warning(1, "Expected two arguments.")
            else:
                print_warning(1, "Invalid status.")
            return -2

        self.update_status(args)

        print("Success. Updated device mount state for device " + args[0] + " to " + args[1].upper())


class FileSystemCheckBeforeMountStateChange(Command):

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

    def print_help(self, args):
        wrapper = TextWrapper(subsequent_indent="                ", width=80)
        print("device update fsck <device_id> <state>")
        print(wrapper.fill(
            "  device_id     global device identifier of the device to change, see device list"))
        print(wrapper.fill(
            "  state         run file system checks on mounting, must be one of " +
                           ", ".join(VALID_FILESYSTEM_CHECK_STATES)))

    def update_status(self, args):
        try:
            if args[1].upper() == "ENABLED":
                call = {
                    'device_id': int(args[0]),
                    'set_filesystem_check_before_mount': "ENABLED",
                    'comment': get_comment()}
            elif args[1].upper() == "DISABLED":
                call = {
                    'device_id': int(args[0]),
                    'set_filesystem_check_before_mount': "DISABLED",
                    'comment': get_comment()}
            self._json_rpc.call("updateDevice", call)
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

    def run(self, args):
        if len(args) != 2 or args[1].upper() not in VALID_FILESYSTEM_CHECK_STATES:
            if len(args) != 2:
                print_warning(1, "Expected two arguments.")
            else:
                print_warning(1, "Invalid status.")
            return -2

        self.update_status(args)

        print("Success. Updated file system checking before mount for device " \
            + args[0] + " to " + args[1].upper())

class DeviceTrimMethodChange(Command):

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

    def print_help(self, args):
        wrapper = TextWrapper(subsequent_indent="                ", width=80)
        print("device update trim-method <device_id> <method>")
        print(wrapper.fill(
            "  device_id     device identifier of the device to change, see device list"))
        print(wrapper.fill(
            "  method        method to run TRIM on SSD and NVMe devices, one of " +
                           ", ".join(VALID_TRIM_METHODS)))

    def run(self, args):
        if len(args) != 2:
            raise InvalidInputException("Expected two arguments.")
        if args[1].upper() not in VALID_TRIM_METHODS:
            raise InvalidInputException(
                "Invalid method " + args[1].upper()
                    + ", expecting one of " + ", ".join(VALID_TRIM_METHODS))
        try:
            int(args[0])
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

        self._json_rpc.call(
            "updateDevice",
            {
                'device_id': int(args[0]),
                'set_trim_device_method': args[1].upper(),
                'comment': get_comment()
            })

        print("Success. Updated TRIM method for device " \
            + args[0] + " to " + args[1].upper())


class DeviceAddTags(Command):

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

    def print_help(self, args):
        print("device update add-tags <device_id> <tag> [<tag> ...]")
        wrapper = TextWrapper(subsequent_indent="              ", width=80)
        print(wrapper.fill("  device_id   global device identifier of the device to change, "
                           + "see device list"))
        print(wrapper.fill("  tag         a space separated list of keywords describing " +
                           "characteristics of this device that are used for tiering. "))

    def get_tags_list(self, device_id):
        call = {}
        call['device_id'] = [int(device_id)]

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

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Expected at least one argument")
            return -2
        try:
            int(args[0])
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

        for tag in args[1:]:
            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[0])

        call = {'device_id': int(args[0]),
                'update_device_tags': True,
                'device_tags': (old_tags + args[1:]),
                'comment': get_comment()}

        try:
            self._json_rpc.call("updateDevice", call)
            print("Success. Added device tags for device " + args[0] + ".")
            try:
                for dev in DeviceShow(self._json_rpc).get_device(args[0]):
                    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: " + args[0], str(e)):
                print_warning(1, "Device not found: " + args[0])
            else:
                print_warning(1, e)


class DeviceRemoveTags(Command):

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

    def print_help(self, args):
        print("device update remove-tags <device_id> <tag> [<tag> ...]")
        wrapper = TextWrapper(subsequent_indent="              ", width=80)
        print(wrapper.fill("  device_id   global device identifier of the device to change, "
                           + "see device list"))
        print(wrapper.fill("  tag         a space separated list of keywords describing " +
                           "characteristics of this device that are used for tiering. "))

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

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Expected at least one argument")
            return -2

        try:
            int(args[0])
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

        for tag in args[1:]:
            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[0])
        new_tags = list()
        for tag in old_tags:
            if tag not in args[1:]:
                new_tags.append(tag)

        call = {'device_id': int(args[0]),
                'update_device_tags': True,
                'device_tags': new_tags,
                'comment': get_comment()}
        try:
            self._json_rpc.call("updateDevice", call)
            print("Success. Removed device tags for device " + args[0] + ".")
            try:
                for dev in DeviceShow(self._json_rpc).get_device(args[0]):
                    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: " + args[0], str(e)):
                print_warning(1, "Device not found: " + args[0])
            else:
                print(e)


class DeviceAddType(Command):

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

    def print_help(self, args):
        print("device update add-type <device_id> <type>")
        print("adds a new type to an existing device.")
        print("  device_id     global device id of the device to change, see device list")
        print("  type          one of the three device types D(ATA), M(ETADATA), R(EGISTRY)")

    def run(self, args):
        if len(args) != 2 or args[1].upper() not in VALID_DEVICE_TYPES:
            if len(args) != 2:
                print_warning(1, "Expected exactly two arguments.")
            else:
                print_warning(1, "Invalid device type.")
            return -2

        try:
            int(args[0])
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

        device_type = convert_device_type_input(args[1])[0]

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


class DeviceRemoveType(Command):

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

    def print_help(self, args):
        print("device update remove-type <device_id> <type>")
        print("removes a type from an existing device.")
        print("  device_id     global device id of the device to change, see device list")
        print("  type          one of the three device types D(ATA), M(ETADATA), R(EGISTRY)")

    def run(self, args):
        if len(args) != 2 or args[1].upper() not in VALID_DEVICE_TYPES:
            if len(args) != 2:
                print_warning(1, "Expected exactly two arguments.")
            else:
                print_warning(1, "Invalid device type.")
            return -2

        try:
            int(args[0])
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

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


class DeviceHealthChange(Command):

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

    def print_help(self, args):
        print("device update health <device_id> <status>")
        print("changes the health status of an existing device.")
        print("  device_id     global device id of the device to update see device list")
        print("  status        one of HEALTHY, DEFECTIVE")

    def run(self, args):
        if len(args) != 2 or args[1].upper() not in VALID_HEALTH_STATUS :
            if len(args) != 2:
                print_warning(1, "Expected exactly two arguments.")
            else:
                print_warning(1, "Invalid health status.")
            return -2

        try:
            int(args[0])
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

        call = {
            'device_id': int(args[0]),
            'set_device_health': {
                'health_status': args[1].upper(),
                'error_report': '',
            },
            'comment': get_comment()
        }
        self._json_rpc.call("updateDevice", call)
        print("Success. Updated device health of device " + args[0] + " to " + args[1].upper())


class DeviceAddr(Command):

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

    def print_help(self, args):
        print("device addr <device_id> ")
        print("  device_id   global device identifier of the device to show endpoints for")

    def run(self, args):
        if len(args) != 1:
            print_warning(1, "Expected one argument: device_id")
            return -2

        try:
            int(args[0])
        except ValueError:
            raise InvalidInputException("Invalid device id: " + args[0])

        result = self._json_rpc.call(
            "getDeviceNetworkEndpoints", {'device_id': int(args[0])})

        _output_json_and_exit(result)

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

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


class DeviceListClean(Command):

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

    def print_help(self, args):
        print("device list-unformatted [<hostname>]")
        print("  hostname   optional hostname filter.")

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


class DeviceMake(Command):

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

    def print_help(self, args):
        print("device make <handle> <type> [<fs_type>]")
        print("  handle      clean device identifier, which can be found by running")
        print("              \"device list-unformatted\"")
        print("  type        one of the three device types D(ATA), M(ETADATA), R(EGISTRY)")
        print("  fs_type     one of the supported filesystem types ext4, xfs. By default xfs")
        print("              is set. Currently, other filesystem types are not supported.")
        print("  [--comment=<new comment>] replace default user and hostname" \
              " comment with the given one.")
        print("  [--tags=<comma separated list of device tags>]  Example: --tags=hdd,fast")

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Expected at least two arguments")
            return -2
        if args[1].upper() not in VALID_DEVICE_TYPES:
            print_warning(1, "Invalid device type: %s" % args[1])
            return -2

        fs_type = "XFS"
        if len(args) > 2:
            if args[2].upper() in SUPPORTED_FS_TYPES:
               fs_type = args[2].upper()
            else:
               print_warning(1, "Unknown file system type: %s" % args[2])
               return -2

        initial_device_tags = []
        if options.tags:
            initial_device_tags = options.tags.split(",")

        call = {}
        call['handle_id'] = args[0]
        call['device_type'] = convert_device_type_input(args[1])[0]
        call['comment'] = get_comment()
        call['fs_type'] = fs_type.upper()
        call['initial_device_tag'] = initial_device_tags
        result = self._json_rpc.call("makeDevice", call)
        uuid = result['task_id']
        hostname = result['host_name']
        device_name = result['device_name']
        print("Success. Making the device %s a Quobyte device" \
              " at %s (MAKE_DEVICE task with id %s)" % (device_name, hostname, str(uuid)))

    @staticmethod
    def completion_cmd():
        return ("qmgmt${opts} device list-unformatted --list-columns=handle_id",
                "printf 'DATA\\nMETADATA\\nREGISTRY\\n'")


class TaskCancel(Command):

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

    def print_help(self, args):
        print("task cancel [<task_id>]")
        print("cancels a pending or running task and removes completed tasks")
        print("  task_id id of the task to be cancelled.")
        print("          If no task_id is given, all running tasks are going to be canceled.")

    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 len(args) < 1:
            _confirmation_prompt("Really cancel all tasks?")
            self.do_cancel_all()
            print("Success. Cancelled all tasks")
        elif len(args) != 1:
            if len(args) > 2 or not (args[1] == "force" or args[1] == "delete"):
              print_warning(1, "Expected one argument: task_id")
              return -2
            else:
              task_id = args[0]
              _confirmation_prompt("Really cancel task " + task_id + "?")
              self.do_cancel(task_id, args[1] == "force", args[1] == "delete")
              print("Success. Cancelled task id " + task_id)
        else:
            task_id = args[0]
            _confirmation_prompt("Really cancel task " + task_id + "?")
            self.do_cancel(task_id)
            print("Success. Cancelled task id " + task_id)

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


class TaskResume(Command):

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

    def print_help(self, args):
        print("task resume [<task_id>]")
        print("Continue a failed/cancelled task from the last persistent checkpoint.")
        print("  task_id id of the task to be resumed.")

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

    def run(self, args):
        if len(args) != 1:
            print_warning(1, "Expected one argument: task_id")
            return -2
        else:
            task_id = args[0]
            self.do_resume(task_id)
            print("Success. Resumed task id " + task_id)

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


class TaskRetry(Command):

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

    def print_help(self, args):
        print("task retry [<task_id>]")
        print("Reset the whole task state and rerun the given task.")
        print("  task_id id of the task to be retried.")

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

    def run(self, args):
        if len(args) != 1:
            print_warning(1, "Expected one argument: task_id")
            return -2
        else:
            task_id = args[0]
            self.do_retry(task_id)
            print("Success. Retrying task id " + task_id)

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


class TaskCreate(Command):

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

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

    @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 in["ANALYZE_VOLUME","ANALYZE_VOLUMES"]:
            return "ANALYZE_VOLUMES"
        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 format 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, format).timetuple())*1000)
            except ValueError:
                continue
        print_warning(1, "Wrong time format.")
        sys.exit(-2)

    def task_settings(self, task_type, call):
        if task_type == "CATCH_UP":
            if not 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(TIMESTAMP)
            call['catch_up_settings'] = {
                'downtime_begin_timestamp_ms': timestamp}
        elif task_type == "REBALANCE":
            call['rebalance_settings'] = {
                'underutilized_threshold_percentage': UNDER or 50,
                'overutilized_threshold_percentage': OVER or 75
            }
        elif task_type == "DELETE_FILES_IN_VOLUMES" and DELETING_DIRECTORY is not None:
            call['delete_files_settings'] = {
                'directory_path': DELETING_DIRECTORY
            }

    def get_targets(self,
                    args,
                    allow_volumes=True,
                    allow_devices=True,
                    require_device=False):
        volumes = []
        devices = []
        for arg in args[1:]:
            try:
                devices.append(int(arg))
            except:
                volumes.append(
                    VolumeResolve(self._json_rpc).resolve_string(arg))
        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):
        if len(args) == 0:
            print_warning(1, "Expected at least one argument")
            return -2
        task_type = self.translate_task_names(args[0].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(),
                'restrict_to_volumes': restrict_to_volumes,
                'restrict_to_devices': restrict_to_devices,
                'task_priority': str(PRIORITY or 'NORMAL').upper()}

        self.task_settings(task_type, call)

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

        if int(id) > 0:
            print("Success. Created new " + args[0]\
                  + " task with task id " + 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):

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

    def print_help(self, args):
        print("usage show <type> [<identifier>] [<tenant_id>]")
        print()
        print("usage show SYSTEM")
        print("    resource usage for the whole system")
        print()
        print("usage show DEVICE <device_id>")
        print("    resource usage for the device")
        print()
        print("usage show TENANT <tenant name or uuid>")
        print("    resource usage for the tenant and its volumes")
        print()
        print("usage show VOLUME <tenant name or uuid>/<volume name or uuid>")
        print("    resource usage for volume")
        print()
        print("usage show VOLUME_SNAPSHOT <tenant name or uuid>/<volume name or uuid>")
        print("    resource usage for volume's snapshot(s)")
        print()
        print("usage show USER <user name> [<tenant name or uuid>]")
        print("usage show GROUP <group name> [<tenant name or uuid>]")
        print("    resource usage for group or user, can be limited to one tenant")

    def do_show(self, args):
        entities = []
        if len(args) < 2 and args[0] != 'SYSTEM':
            raise InvalidInputException("Not enough arguments.")
        elif args[0] == 'TENANT':
            uuid = TenantResolve(self._json_rpc).resolve_string(args[1])
            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[0] == 'VOLUME':
            uuid = VolumeResolve(self._json_rpc).resolve_string(args[1])
            entities.append({'type': 'VOLUME', 'identifier': uuid})
        elif args[0] == 'USER' or args[0] == 'GROUP':
            if len(args) > 2:
                uuid = TenantResolve(self._json_rpc).resolve_string(args[2])
                entities.append({'type': args[0],
                                 'identifier': args[1],
                                 'tenant_id': uuid})
            else:
                tenants = self._json_rpc.call("getTenant", {})['tenant']
                for t in tenants:
                    entities.append({'type': args[0],
                                     'identifier': args[1],
                                     'tenant_id': t['tenant_id']})
        elif args[0] == 'DEVICE':
            entities.append({'type': 'DEVICE', 'identifier': args[1]})
        elif args[0] == 'SYSTEM':
            entities.append({'type': 'SYSTEM'})
        elif args[0] == 'VOLUME_SNAPSHOT':
            uuid = VolumeResolve(self._json_rpc).resolve_string(args[1])
            entities.append({'type': 'VOLUME_SNAPSHOT', 'identifier': uuid})
        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):
        if len(args) == 0:
            print_warning(1, "Expected at least one argument")
            return -2

        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(
                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 x: VolumeLookup(self._json_rpc).do_lookup(x) if x else None)
            if 'TENANT' in types:
                printer.modify_column(
                    'TENANT',
                    lambda x: TenantLookup(self._json_rpc).do_lookup(x) if x else None)
            print(printer.out())
        else:
            print("No accounting to show.")

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


class TaskShow(Command):

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

    def print_help(self, args):
        print("task show <task_id>")
        print("shows detailed information of the task identified by 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):
        if len(args) != 1:
            print_warning(1, "Expected exactly one argument: task_id")
            return -2

        tasks = self.do_list(args[0])
        for task in tasks:
            if task['task_id'] == args[0]:
                print("Task " + task['task_id'])
                self.print_value("type", 'task_type', task)
                self.print_value("comment", 'comment', task)
                self.print_value2(
                    "time info",
                    "Created at " + _printMillisTimestamp(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 == 'RUNNING' or task_state == 'CANCELLING':
                    self.print_value3("Current execution started on {}".format(
                        _printMillisTimestamp(task.get('begin_timestamp_ms', "~"))))
                elif task_state == 'QUEUED' or task_state == 'SCHEDULED':
                    self.print_value3("Task is waiting to be executed...")
                else:
                    self.print_value3("Latest run started on {} and ended on {}".format(
                        _printMillisTimestamp(task.get('begin_timestamp_ms', "~")),
                        _printMillisTimestamp(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",
                        _printSTimestamp(progress.get('time_elapsed_s', 0)))
                    try:
                        self.print_value2(
                            "time left",
                            _printSTimestamp(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[0]))

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


class TaskGeterrors(Command):

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

    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):
        if len(args) != 1:
            print_warning(1, "Expected exactly one argument: task_id")
            return -2

        tasks = self.do_list([str(args[0])])

        _output_json_and_exit(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[0]))

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


class TaskList(Command):

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

    def print_help(self, args):
        print("task list <state> [--limit=<number>]")
        print()
        print("  Prints a list of tasks for the selected state(s).")
        print("  state           comma-separated list of states:")
        print("                  CANCELED CANCELLING FAILED FINISHED RUNNING QUEUED SCHEDULED")
        print("  --limit=number  limit the number of displayed tasks. Optional")
        print()
        print("  Example: task list RUNNING,FAILED --limit=1000")
        print()

    def do_list(self, args):
        if len(args) == 0:
            print_warning(1, "Expected an argument: <state>")
            return -2

        request = {"only_root_tasks": True}
        if len(args) > 0:
            states = args[0].split(",")
            for state in states:
                if state not in ["CANCELED", "CANCELLING", "FAILED",
                                 "FINISHED", "RUNNING", "QUEUED", "SCHEDULED"]:
                    print_warning(1, "Unknown state: " + state)
                    self.print_help([])
                    return -2
                request.setdefault("task_state", []).append(state)
        if LIMIT is not None:
            request["task_count_limit"] = 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(
            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 FilesTask(Command):

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

    def print_help(self, args):
        print("files <command> <source> [<destination>] [--filters=\"filter1&filter2"
              "&...&filterN\"] [--ignore-modified-files-errors] [--ignore-all-errors]"
              " [--destination-create-behavior=<arg>]")
        print("  <command> one of [copy, move, recode, delete]")
        print("  <source> has the following format:")
        print("    \"quobyte:/<volume_uuid>/[directory_path]\"")
        print("             or")
        print("    - to provide batch of recode targets from the stdin (each line should be of the")
        print("      form <volume_uuid>[,absolute_directory_path])")
        print("  [<destination>] has the following format:")
        print("    (not required for recode/delete command)")
        print("    \"quobyte:[<registry>]/<volume_uuid>/[<directory_path>]\"")
        print("        registry can be SRV record or registry1:port1,...,registryN:portN")
        print("  --ignore-modified-files-errors Recently modified files are not counted as")
        print("    errors and instead are skipped (not supported by delete command)")
        print("  --ignore-all-errors Task will not abort on error threshold")
        print("    and runs until the finish (not supported by delete command)")
        print("  --filters=\"<filters>\" each filter of format:")
        print("    (filters are not supported with delete command)")
        print("    size{<|<=|=|>=|>}<bytes>      (filter file size in bytes)")
        print("    atime{<|<=|=|>=|>}<duration>  (filter access time duration"
              " - for example, 30m, 1h, 2d)")
        print("    mtime{<|<=|=|>=|>}<duration>  (filter modification time duration"
              " - for example, 30m, 1h, 2d)")
        print("      example: --filters=\"size=10000&atime>2d\"")
        print("  [--destination-create-behavior=<arg>] Used only for copy and move tasks."
              " Arg is one of:")
        print("    FAIL_IF_FILE_EXISTS, OVERWRITE_EXISTING_FILE, OVERWRITE_EXISTING_FILE_IF_OLDER.")
        print("    If not given, defaults to FAIL_IF_FILE_EXISTS.")
        print("  [--comment=<new comment>] replace default user and hostname"
              " comment with the given one.")

    def getSourceLocation(self, location):
        if not location.startswith('quobyte:'):
            raise InvalidInputException("Invalid source location \"" + location + "\". " \
                                        + "Must have the format: " \
                                        + "quobyte:/<volume_uuid>/")
        splitLocation = location.split("quobyte:")
        return self.getQuobyteSourceLocation(splitLocation[1])

    def getQuobyteSourceLocation(self, location):
        splitLocation = location.split("/")
        if len(splitLocation) < 3:
            raise InvalidInputException("Invalid Quobyte source \"" + location + "\". " \
                                        + "Must have the format: " \
                                        + "/<volume_uuid>/[<directory_path>]")
        volume = splitLocation[1]
        path = "/".join(splitLocation[2:])
        result = {}
        result["volume"] = volume
        if path:
            result["path"] = "/" + path
        return { "quobyte" : result }

    def getDestinationLocation(self, location):
        if not location.startswith('quobyte:'):
            raise InvalidInputException("Invalid destination location \"" + location + "\". " \
                                        + "Must have the format: " \
                                        + "quobyte:[<registry>]/<volume_uuid>/[<directory_path>]")
        splitLocation = location.split("quobyte:")
        return self.getQuobyteDestinationLocation(splitLocation[1])

    def getQuobyteDestinationLocation(self, location):
        splitLocation = location.split("/")
        if len(splitLocation) < 3:
            raise InvalidInputException("Invalid Quobyte destination \"" + location + "\". " \
                                        + "Must have the format: " \
                                        + "[<registry>]/<volume_uuid>/[<directory_path>]")
        registry = splitLocation[0]
        volume = splitLocation[1]
        path = "/".join(splitLocation[2:])
        result = {}
        if registry:
            registries = registry.split(",")
            if registries:
                result["registry"] = registries
        result["volume"] = volume
        if path and self.command != "recode":
            result["path"] = "/" + path
        return { "quobyte" : result }

    def getFilter(self, filterArg):
        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 filterArg:
                result = {}
                result["operator"] = enum
                splitFilterArg = filterArg.split(operator)
                if len(splitFilterArg) != 2:
                    raise InvalidInputException("Invalid filter format: " + filterArg \
                                                + " Must be \"<type>{<|=|>}<value>\".")
                type = splitFilterArg[0]
                if type == "size":
                    result["type"] = "CURRENT_FILE_SIZE"
                    result["value"] = int(splitFilterArg[1])
                elif type == "atime":
                    result["type"] = "LAST_ACCESS_AGE_S"
                    result["value"] = \
                        int(to_seconds_from_human_readable_duration(splitFilterArg[1]))
                elif type == "mtime":
                    result["type"] = "LAST_MODIFICATION_AGE_S"
                    result["value"] = \
                        int(to_seconds_from_human_readable_duration(splitFilterArg[1]))
                else:
                    raise InvalidInputException("Unknown filter type: " + type)
                return result
        raise InvalidInputException("Unknown filter operator: " + filterArg)

    def run(self, args):
        self.command = COMMAND_NAME.split(' ')[1]
        if self.command == "delete" and (FILTERS is not None or DO_NOT_FAIL_COPY_TASK_ON_ANY_ERRORS
            or DO_NOT_FAIL_COPY_TASK_ON_MODIFIED_FILES):
            print("Given one or more unsupported delete options. Files delete command does not")
            print("  support --filters, --ignore-modified-files-errors and")
            print("  --ignore-all-errors options.")
            self.print_help([])
            return -2
        if self.command == "recode" or self.command == "delete":
            if len(args) < 1:
                print("Missing Quobyte source")
                self.print_help([])
                return -2
            elif len(args) > 1:
                print("Files recode/delete command supports only <source> argument")
                self.print_help([])
                return -2
        elif len(args) < 2:
            print("Missing source and/or destination.")
            self.print_help([])
            return -2

        filters = []
        if self.command != "delete" and FILTERS:
            for filterArg in FILTERS.split("&"):
                filters.append(self.getFilter(filterArg))

        jobs = []
        restrict_to_volumes = []
        source = args[0];
        if source.startswith('quobyte'):
            job = {
                'source': self.getSourceLocation(source),
                'filter': filters,
                'do_not_fail_on_modified_files': DO_NOT_FAIL_COPY_TASK_ON_MODIFIED_FILES,
                'do_not_fail_on_any_error': DO_NOT_FAIL_COPY_TASK_ON_ANY_ERRORS
            }
            restrict_to_volumes.append(job["source"]["quobyte"]["volume"])
            if self.command == "delete":
                pass
            elif self.command == 'recode':
                # source and destination are same for recode
                job["destination"] = self.getDestinationLocation(source)
                # recode does not support destination path
                job["destination"]["quobyte"].pop('path', None)
            else:
                job["destination"] = self.getDestinationLocation(args[1])

            if DESTINATION_CREATE_BEHAVIOR and (self.command == "copy" or self.command == "move"):
                job["destination_file_settings"] = {'create_behavior': DESTINATION_CREATE_BEHAVIOR}
            if self.command == "move":
                job["commit_action"] = "DELETE_SOURCE_FILE"
            jobs.append(job)
        elif source == "-" and self.command == 'recode': # recode supports batch jobs
            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) > 2:
                    raise InvalidInputException("Unexpected volume recode entry %s at line %d" %
                     (row, line_num))
                quobyte_source = "quobyte:/"
                volume_uuid = row[0]
                directory_path = "/"
                if len(row) == 2:
                    directory_path = row[1]
                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_uuid == None or len(volume_uuid) == 0:
                    raise InvalidInputException("Invalid source volume %s at %d" \
                        % (volume_uuid, line_num))
                quobyte_source += volume_uuid
                quobyte_source += directory_path
                job = {
                    'source': self.getSourceLocation(quobyte_source),
                    'destination': self.getDestinationLocation(quobyte_source),
                    'filter': filters,
                    'do_not_fail_on_modified_files': DO_NOT_FAIL_COPY_TASK_ON_MODIFIED_FILES,
                    'do_not_fail_on_any_error': DO_NOT_FAIL_COPY_TASK_ON_ANY_ERRORS
                }
                restrict_to_volumes.append(job["source"]["quobyte"]["volume"])
                # recode does not support destination path
                job["destination"]["quobyte"].pop('path', None)
                jobs.append(job)
        else:
            raise InvalidInputException("Unknown source format for the %s command" % (self.command))
        settings = {"job": jobs}
        if self.command != "delete":
            call = {"task_type": "COPY_FILES",
                    "comment": get_comment(),
                    "restrict_to_volumes": restrict_to_volumes,
                    "copy_files_settings": settings
                    }
        else:
            if len(jobs) > 1:
                raise InvalidInputException("Delete files command supports only single volume job")
            job = jobs[0]
            call = { "task_type": "DELETE_FILES_IN_VOLUMES",
                     "comment": get_comment(),
                     "restrict_to_volumes": restrict_to_volumes
                    }
            if "path" in job["source"]["quobyte"]:
                deleting_directory = job["source"]["quobyte"]["path"]
            else:
                deleting_directory = "/"
            call['delete_files_settings'] = {
                "directory_path": deleting_directory
            }
        id =  self._json_rpc.call("createTask", call)['task_id']
        if int(id) > 0:
            print("Success. Created new task with task id " + id)

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


class ClientList(Command):

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

    def print_help(self, args):
        print("client list")
        print("  prints a list of all registered clients.")

    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(
            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):

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

    def print_help(self, args):
        print("audit-log list <oldest first> [<subject type> <subject id>] --limit=<number>")
        print()
        print("  oldest first    true to list oldest entries at the top of the list, "
              "false otherwise.")
        print("  subject type    One of:")
        print("                  DEVICE, VOLUME, TASK, CONFIGURATION, USER, VOLUME_CONFIGURATION,")
        print("                  QUOTA, RULE, POLICY_RULE, KEY_STORE")
        print("  subject id      Depending on the subject type, e.g. a device id or volume uuid.")
        print("  --limit=number  limit the number of displayed entries. Must not be 0; must be "
              "provided.")
        print()
        print("  Example: audit-log list false DEVICE 3 --limit=1000")
        print()

    def run(self, args):
        if len(args) != 1 and len(args) != 3:
            print_warning(1, "Wrong number of arguments.")
            return -2
        if LIMIT is None or LIMIT == 0:
            print_warning(1, "--limit parameter must be set and must not be 0")
            return -2
        request = {
            "logs_limit": LIMIT,
            "oldest_log_first": args[0].lower() == "true"
        }
        if len(args) == 3:
            request["only_subject_type"] = str(args[1])
            request["only_subject_id"] = str(args[2])
        audit_events = self._json_rpc.call("getAuditLog", request)["audit_event"]
        printer = PrettyTable(
            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):

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

    def print_help(self, args):
        print("alert list")
        print("  prints a list of all active alerts.")

    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(
            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):

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

    def print_help(self, args):
        print("alert silence <alert id> <silence time>")
        print("  alert id     id of the alert to silence")
        print("  duration     duration to silence the alert with time unit")
        print("               (e.g. 20m, 2h, or 2d)")

    def run(self, args):
        # check and parse arguments
        if len(args) < 2:
            print_warning(1, "alert id and silence time should be given.")
            return -2
        alert_id = args[0]
        silence_for_s = to_seconds_from_human_readable_duration(args[1])

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

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


class AlertAcknowledge(Command):

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

    def print_help(self, args):
        print("alert acknowledge <alert id>")
        print("  alert id         id of the alert to acknowledge")

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "alert id should be given.")
            return -2
        alert_id = args[0]
        self._json_rpc.call("acknowledgeAlert", {"alert_identifier": alert_id})

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


class RegistryAddReplica (Command):

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

    def print_help(self, args):
        print("registry add <device ID>")
        print("  adds a replica to the registry service.")
        print("    [--comment=<new comment>] replace default user and hostname" \
              " comment with the given one.")

    def do_add_replica(self, device_id):
        result = self._json_rpc.call(
            "addRegistryReplica", {
                'device_id': device_id,
                'comment': get_comment()})

    def run(self, args):
        if len(args) != 1:
            print_warning(1, "Device ID expected.")
            return -2

        self.do_add_replica(args[0])
        print("Success, added replica " + args[0])

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


class RegistryRemoveReplica (Command):

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

    def print_help(self, args):
        print("registry remove <device ID>")
        print("  removes a replica from the registry service.")

    def do_remove_replica(self, device_id):
        result = self._json_rpc.call(
            "removeRegistryReplica", {'device_id': device_id,
                                      'comment': get_comment()})

    def run(self, args):
        if len(args) != 1:
            print_warning(1, "Device ID expected.")
            return -2

        self.do_remove_replica(args[0])
        print("Success, removed replica " + args[0])

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


class RegistryListReplicas (Command):

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

    def print_help(self, args):
        print("registry list")
        print("  Shows the current active registry replica set.")
        print("  To list all the registry devices in the cluster," \
              " use 'qmgmt device list R'")

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

    def get_list(self, args):
        call = {'device_type': ["REGISTRY"]}

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

    def run(self, args):
        dev_list = self.get_list(args)
        replica_set = self.list_replicas()
        if type(dev_list) == int and dev_list == -2:
            return
        printer = PrettyTable(
            [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 ServiceDeregister (Command):

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

    def print_help(self, args):
        print("service deregister <service uuid>")
        print("  Deregisters a specific service.")

    def deregisterService(self, uuid):
        result = self._json_rpc.call(
            "deregisterService", {'service_uuid': uuid})

    def run(self, args):
        if len(args) != 1:
            print_warning(1, "Service uuid expected.")
            return -2

        repl_list = self.deregisterService(args[0])
        print("Success, deregisterd service " + args[0])

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


class ServiceList (Command):

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

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

    def run(self, args):
        service_list = self.listServices()
        printer = PrettyTable(
            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):

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

    def print_help(self, args):
        print("database regenerate <type> [target]")
        print("  type  one of the following database types:")
        print("          UPDATE_SCHEMA       update the Metadata Service database schema of a")
        print("                              given <target> volume")
        print("          VOLUME_ACCOUNTING   clears and recalculates the accounting data of a")
        print("                              given <target> volume")

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

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Database type expected.")
            return -2
        if args[0] not in ["VOLUME_ACCOUNTING", "UPDATE_SCHEMA"]:
            print_warning(1, "Unknown database type '%s'" % (args[0]))
            return -2
        if len(args) != 2:
            print_warning(1, "Target database UUID expected.")
            return -2
        target = ""
        if len(args) == 2:
            target = args[1]
        if args[0] == "UPDATE_SCHEMA":
            _confirmation_prompt(
                "Volume '%s' will not be available until the "
                "database schema update is completed. Do you "
                "want to continue?" % (target))
        self.regenerateDatabase(args[0], target)
        print(
            "Success, database regeneration action '%s' "
            "has been scheduled for volume '%s'" % (args[0], target))

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


class AddCA (Command):

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

    def print_help(self, args):
        print("ca add <name> [public key] [private key]")
        print("  Adds a new CA certificate. Adding a new CA without any key will generate a new")
        print("  CA with public and private key to sign new certificates. Adding a public key")
        print("  without a private key is necessary to import existing certificates later. Public and")
        print("  private keys can be either the path to a PEM file or a PEM string")

    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):
        if len(args) < 1:
            print_warning(1, "CA name expected")
            return -2
        name = args[0]
        public_key = None
        private_key = None
        if len(args) >= 2:
            public_key = get_string_or_filecontent(args[1])
        if len(args) >= 3:
            private_key = get_string_or_filecontent(args[2])
        self.addCa(name, public_key, private_key)
        print("Success, added CA " + name)

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

class ListCA (Command):

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

    def print_help(self, args):
        print("ca list")
        print("  List existing certificate authorities")

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

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

        _output_json_and_exit(result)

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


class DeleteCA (Command):

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

    def print_help(self, args):
        print("ca delete <name>")
        print("  Delete existing certificate authority")

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

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "CA name expected.")
            return -2
        name = args[0]
        _confirmation_prompt("Really delete CA " + name + "?")
        self.deleteCa(name)
        print("Success, deleted CA " + name)

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


class ExportCA (Command):

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

    def print_help(self, args):
        print("ca export <name> [output]")
        print("  Export CA certificate")

    def getCA(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):
        if len(args) < 1:
            print_warning(1, "CA name expected.")
            return -2
        name = args[0]
        ca = self.getCA(name)
        if ca is None:
            print_warning(1, "Unknown CA")
            return -2
        if len(args) < 2:
            print(ca)
        else:
            output = args[1]
            f = open(output, "w")
            f.write(ca)
            f.close()
            print("Success, exported CA certificate to " + output)

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


class AddCsr (Command):

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

    def print_help(self, args):
        print("csr add <csr>")
        print("  Add certificate signing request, <csr> can be string or path to file")

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

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "CSR expected.")
            return -2
        csr = get_string_or_filecontent(args[0])
        csrId = self.addCsr(csr)
        print("Success, added CSR with Id " + str(csrId))

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


class ListCsr (Command):

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

    def print_help(self, args):
        print("csr list [state]")
        print("  List certificate signing requests. ")
        print("  Possible states are PENDING, APPROVED or REJECTED.")

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

    def run(self, args):
        state = None
        if len(args) >= 1:
            if args[0] not in ['PENDING', 'APPROVED', 'REJECTED']:
                print_warning(1, "Unknown state.")
                return -2
            state = args[0]
        csrs = self.listCsr(state)
        if len(csrs) > 0:
            printer = PrettyTable(
                csrs,
                ['csr_id', 'state', 'subject', 'certificate_fingerprint'],
                ["Id", "State", "Subject", "Certificate Fingerprint"]
            )
            print(printer.out())
        else:
            if LIST_OUTPUT != "comp":
                print_warning(1, "No certificate signing requests found")

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


class ApproveCsr (Command):

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

    def print_help(self, args):
        print("csr approve <csrId> [subject]")
        print("  Approve certificate signing request")
        print("  Certificate subject can be overwritten optionally")

    def approveCsr(self, csrId, subject=None):
        req = {}
        req["csr_id"] = csrId
        if subject is not None:
            req["effective_subject"] = subject
        req["approve"] = True
        return self._json_rpc.call("decideCsr", req)

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "csrId expected")
            return -2
        csrId = int(args[0])
        subject = None
        if len(args) > 2:
            subject = args[1]
        self.approveCsr(csrId, subject)
        print("Success, approved CSR with Id " + str(csrId))

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


class RejectCsr (Command):

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

    def print_help(self, args):
        print("csr reject <csrId>")
        print("  Reject certificate signing request")

    def rejectCsr(self, csrId):
        req = {}
        req["csr_id"] = csrId
        req["approve"] = False
        return self._json_rpc.call("decideCsr", req)

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "csrId expected")
            return -2
        csrId = int(args[0])
        self.rejectCsr(csrId)
        print("Success, rejected CSR with Id " + str(csrId))

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


class DeleteCsr (Command):

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

    def print_help(self, args):
        print("csr delete <csrId>")
        print("  Delete certificate signing request")

    def deleteCsr(self, csrId):
        req = {}
        req["csr_id"] = csrId
        return self._json_rpc.call("deleteCsr", req)

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "csrId expected")
            return -2
        csrId = int(args[0])
        self.deleteCsr(csrId)
        print("Success, deleted CSR with Id " + str(csrId))

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


class AddCertificate (Command):

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

    def print_help(self, args):
        print("certificate add <certificate> [csrId]")
        print("  Import certificate")
        print("  Optionally assign to pending CSR")

    def addCertificate(self, certificate, csrId=None):
        req = {"certificate": {"certificate": certificate}}
        if csrId is not None:
            req["csr_id"] = csrId
        return self._json_rpc.call("addCertificate", req)["fingerprint"]

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "certificate expected")
            return -2
        csrId = int(args[1]) if len(args) > 1 else None
        certificate = get_string_or_filecontent(args[0])
        fingerprint = self.addCertificate(certificate, csrId)
        print("Sucess, added certificate with fingerprint " + fingerprint)

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


class DeleteCertificate (Command):

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

    def print_help(self, args):
        print("certificate delete <fingerprint>")
        print("  Delete certificate")

    def deleteCertificate(self, fingerprint):
        return self._json_rpc.call("deleteCertificate", {"fingerprint": fingerprint})

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "fingerprint expected")
            return -2
        fingerprint = args[0]
        self.deleteCertificate(fingerprint)
        print("Success, deleted certificate with fingerprint " + fingerprint)

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


class ListCertificate (Command):

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

    def print_help(self, args):
        print("certificate list")
        print("  List certificates")

    def listCertificates(self):
        return self._json_rpc.call("listCertificates", {})["certificate"]

    def run(self, args):
        certificates = self.listCertificates()
        if len(certificates) > 0:
            printer = PrettyTable(
                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 LIST_OUTPUT != "comp":
                print_warning(1, "No certificates found")


class ExportCertificate (Command):

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

    def print_help(self, args):
        print("certificate export <fingerprint> [output file]")
        print("  Export certificate as PEM string")

    def exportCertificate(self, fingerprint):
        return self._json_rpc.call(
            "exportCertificate",
            {"fingerprint": fingerprint})["certificate"]["certificate"]

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "fingerprint expected")
            return -2
        fingerprint = args[0]
        certificate = self.exportCertificate(fingerprint)
        if len(args) < 2:
            print(certificate)
        else:
            output = args[1]
            f = open(output, "w")
            f.write(certificate)
            f.close()
            print("Success, exported certificate to " + output)

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


class CertificateConfig (Command):

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

    def print_help(self, args):
        print("certificate config show <fingerprint>")
        print("  Show certificate configuration if exists.")
        print("certificate config set <fingerprint> <config>")
        print("  Set certificate config. <config> is path to config file")
        print("  or string with certificate configuration in json format:")
        print('  {"service_type": [],')
        print('   "restrict_to_hosts": [],')
        print('   "restrict_to_subjects": [{"forbid_root": true,')
        print('                             "groups": [],')
        print('                             "read_only": true,')
        print('                             "tenant": "",')
        print('                             "user": "",')
        print('                             "volume": ""')
        print('                            }]}')
        print("certificate config edit <fingerprint>")
        print("  Edit certificate subject")

    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):
        if len(args) < 2:
            print_warning(1, "missing arguments")
            return -2
        subcommand = args[0].lower()
        fingerprint = args[1]
        if subcommand == "show":
            print(self.getCertificateConfig(fingerprint))
        elif subcommand == "set":
            if len(args) < 3:
                print_warning(1, "missing config")
            else:
                config = get_string_or_filecontent(args[2])
                self.setCertificateConfig(fingerprint, json.loads(config))
        elif subcommand == "edit":
            # Read
            config = json.dumps(self.getCertificateConfig(fingerprint))
            try:
                after = DataDumpHelper.edit(config)
                if after != "":
                    self.setCertificateConfig(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))
        else:
            print_warning(1, "unknown subcommand")
            return -2

    @staticmethod
    def completion_cmd():
        return dict((k, "qmgmt${opts} certificate list --list-columns=fingerprint")
            for k in ["show", "set", "edit"])


class GetLabel (Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("label get [entity_type] [entity_id] [name]")
        print("  Returns the labels for one or more entities matching the search criteria ")
        print("  entity_type can be VOLUME, TENANT, SERVICE (optional)")
        print("  entity_id is the (uu)id of the entity (optional)")
        print("  name is the label name (optional)")

    def run(self, args):
        rpc_args = {}
        if len(args) >= 1:
            if args[0].upper() not in ['VOLUME', 'TENANT', 'SERVICE']:
                print_warning(1, "Unknown entity_type.")
                return -2
            rpc_args['filter_entity_type'] = args[0].upper()

        if len(args) >= 2:
            rpc_args['filter_entity_id'] = args[1]
        if len(args) >= 3:
            rpc_args['label_name'] = args[2]

        rpc_args['namespace'] = 'SYSTEM'

        data = self._json_rpc.call("getLabels", rpc_args)['label']

        _output_json_and_exit(data)

        if len(data) > 0:
            for entity in data:
                labelString = entity['entity_type'] + " " + entity['entity_id'] + ": " + \
                      entity['name']
                if entity['value']:
                    labelString += " = " + entity['value']
                print(labelString)
        else:
            print_warning(1, "No labels found")

    @staticmethod
    def completion_cmd():
        return "printf 'VOLUME\\nTENANT\\nSERVICE\\n'"


class SetLabel (Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("label set <entity_type> <entity_id> <name> [value]")
        print("  Sets the label on an entity, which must exist in the system")
        print("  entity_type can be VOLUME, TENANT, SERVICE")
        print("  entity_id must be the (uu)id of an existing entity in the system")

    def completion_str(self):
        return '_set_label() {'\
               ''

    def run(self, args):
        if len(args) < 3:
            print_warning(1, "Expect at least three argument.")
            self.print_help(args)
            return -2
        if args[0].upper() not in ['VOLUME', 'TENANT', 'SERVICE']:
            print_warning(1, "Unknown entity_type.")
            return -2
        rpc_args = {}
        rpc_args['namespace'] = 'SYSTEM'
        rpc_args['entity_type'] = args[0].upper()
        rpc_args['entity_id'] = args[1]
        rpc_args['name'] = args[2]
        if len(args) == 4:
          rpc_args['value'] = args[3]

        data = 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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("label delete <entity_type> <entity_id> <name>")
        print("  Deletes the label on an entity")
        print("  entity_type can be VOLUME, TENANT, SERVICE")
        print("  entity_id must be the (uu)id of an entity")

    def run(self, args):
        if len(args) < 3:
            print_warning(1, "Expect at least three argument.")
            self.print_help(args)
            return -2
        if args[0].upper() not in ['VOLUME', 'TENANT', 'SERVICE']:
            print_warning(1, "Unknown entity_type.")
            return -2
        rpc_args = {}
        rpc_args['namespace'] = 'SYSTEM'
        rpc_args['entity_type'] = args[0].upper()
        rpc_args['entity_id'] = args[1]
        rpc_args['name'] = args[2]

        data = 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 CreateS3AccessKey(Command):
    """This class is deprecated. Use CreateAccessKey."""
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("\"s3-key create\" option is deprecated and may not be supported in the future. "
              "Use \"accesskey create\" option.")

    def run(self, args):
        new_format_args = []
        if len(args) >= 2:
            new_format_args.append("DATA_ACCESS_KEY")
            new_format_args.append(args[1])
            options.tenant = args[0]
            if len(args) == 3:
                options.days = args[2]
        CreateAccessKey(self._json_rpc).run(new_format_args)


class DeleteS3AccessKey(Command):
    """This class is deprecated. Use DeleteAccessKey."""
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("\"s3-key delete\" option is deprecated and may not be supported in the future. "
              "Use \"accesskey delete\" option.")

    def run(self, args):
        DeleteAccessKey(self._json_rpc).run(args)


class ListS3AccessKey(Command):
    """This class is deprecated. Use ListAccessKey."""
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("\"s3-key list\" option is deprecated and may not be supported in the future. "
              "Use \"accesskey list\" option.")

    def run(self, args):
        ListAccessKey(self._json_rpc).run(args)


class ImportS3AccessKey(Command):
    """This class is deprecated. Use ImportAccessKey."""
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("\"s3-key import\" option is deprecated and may not be supported in the future. "
              "Use \"accesskey import\" option.")

    def run(self, args):
        options.comment = "S3_KEY_IMPORT_FORMAT"
        ImportAccessKey(self._json_rpc).run(args)


class CreateAccessKey(Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("accesskey create <accesskey type> <username> [--tenant=<tenant>]"
              " [--validity-days=<days>]")
        print("  Create access keys for a user.")
        print("  accesskey type    DATA_ACCESS_KEY (restricts access to the file system and S3),")
        print("                    MANAGEMENT_ACCESS_KEY (restricts access to API and Webconsole),")
        print("                    GENERAL_ACCESS_KEY (all uses),")
        print("  username          user in the User Directory")
        print("  --tenant=tenant   tenant (id or name) for file system and S3 access: required")
        print("                    only for DATA_ACCESS_KEY and GENERAL_ACCESS_KEY types")
        print("  --validity-days=days")
        print("                    number of days for access key validity; if not specified, the")
        print("                    key is persistent")
        print()
        print("  Examples: accesskey create DATA_ACCESS_KEY myUser --tenant=\"My Tenant\"")
        print("            accesskey create MANAGEMENT_ACCESS_KEY myUser")
        print("            accesskey create GENERAL_ACCESS_KEY myUser --tenant=\"My Tenant\""
              " --validity-days=365")
        print()

    def run(self, args):
        if len(args) < 2:
            print_warning(1, "Expect at least two arguments.")
            self.print_help(args)
            return -2

        access_type = None
        user_name = None
        tenant_uuid = options.tenant
        days_parameter = options.validity_days if options.validity_days else "0"
        if len(args) >= 2:
            access_type = args[0]
            user_name = args[1]
            if access_type == "DATA_ACCESS_KEY" or access_type == "GENERAL_ACCESS_KEY":
                if not tenant_uuid:
                    print_warning(1, "For access key type " + access_type + " tenant is required.")
                    print()
                    return -2
                else:
                    if not is_valid_uuid(tenant_uuid):
                        tenant_uuid = TenantResolve(self._json_rpc).resolve_string(tenant_uuid)
            elif access_type == "MANAGEMENT_ACCESS_KEY":
                tenant_uuid = None
            else:
                raise InvalidInputException("Unsupported access key type: " + access_type)
        try:
            validity_days = int(days_parameter)
        except ValueError:
            raise InvalidInputException("validity_days argument should be an integer.")

        request = {
            "access_key_type": access_type,
            "user_name": user_name,
            "validity_days": validity_days
        }
        if tenant_uuid:
            request["tenant_id"] = tenant_uuid

        result = self._json_rpc.call("createAccessKeyCredentials", request)
        print("Created access key with id: %s"
              % result['access_key_credentials']['access_key_id'])

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} accesskey create"


class DeleteAccessKey(Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("accesskey delete <user_name> [access_key_id]")
        print("  Delete access key for a user.")
        print("  user_name         user in the User Directory")
        print("  access_key_id     access key id; if not specified, all keys for this user")
        print("                    will be removed")
        print()

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Expect at least one argument.")
            self.print_help(args)
            return -2
        if len(args) == 1:
            try:
                user_details = self._json_rpc.call(
                    "getUsers", {"user_id": [args[0]]})['user_configuration'][0]
            except:
                raise InvalidInputException("Wrong user id: %s." % args[0])
            if not user_details['access_key_credentials']:
                raise Exception(
                    "User '%s' has no assigned access key credentials."
                    % args[0])
            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[0],
                                 "access_key_id": args[1]})
            print("Deleted access key with id: %s" % args[1])

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


class ListAccessKey(Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("accesskey list [user_name]")
        print("  List access keys for all or specified user.")
        print("  user_name         user in the User Directory")
        print()

    def run(self, args):
        if len(args) < 1:
            users = self._json_rpc.call("getUsers", {})['user_configuration']
        else:
            users = self._json_rpc.call(
                "getUsers", {"user_id": [args[0]]})['user_configuration']

        printer = PrettyTable(
            [{'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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("accesskey import <access-keys-file.csv>")
        print("  Imports access keys from the csv file.")
        print("  The csv file <access-keys-file.csv> should have a header line followed by the")
        print("  line(s) of access key parameters. The header specifies the access key type.")
        print("  The format of the line with access key parameters depends on the specified")
        print("  access key type:")
        print("    MANAGEMENT_ACCESS_KEY")
        print("                    <user_name>,<access_key_id>,<access_key_secret>"
              "[,<validity_days>]")
        print("    DATA_ACCESS_KEY, GENERAL_ACCESS_KEY")
        print("                    <user_name>,<access_key_id>,<access_key_secret>,<tenant>"
              "[,<validity_days>]")
        print()
        print("    Examples for csv file entries:")
        print("             MANAGEMENT_ACCESS_KEY")
        print("             user1,accessKeyId1,secretKey1,365")
        print("             user2,accessKeyId2,secretKey2")
        print("    or")
        print("             DATA_ACCESS_KEY")
        print("             user1,accessKeyId1,secretKey1,myTenantId,45")
        print("             user2,accessKeyId2,secretKey2,myTenantId")
        print()

    def run(self, args):
        if len(args) < 1:
            print_warning(1, "Expects a file with access key details")
            self.print_help(args)
            return -2

        with open(args[0]) as csv_file:
            reader = csv.reader(csv_file, delimiter=',')
            access_key_type = None;
            line_number = 0
            if options.comment == "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 != "DATA_ACCESS_KEY" \
                            and access_key_type != "GENERAL_ACCESS_KEY" \
                            and access_key_type != "MANAGEMENT_ACCESS_KEY":
                        raise InvalidInputException(
                            "Unsupported access key type: " + access_key_type)
                    line_number += 1
                    continue
                # check number of parameters
                if access_key_type == "DATA_ACCESS_KEY" or access_key_type == "GENERAL_ACCESS_KEY":
                    min_entry_number = 4
                    max_entry_number = 5
                elif 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))
                    self.print_help(args)
                    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(
                            self,
                            access_key_type=access_key_type,
                            access_key_id=access_key_id,
                            secret_access_key=secret_access_key,
                            days=validity_days
                        )
                    elif 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(
                            self,
                            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)))

    @staticmethod
    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 and not is_valid_uuid(tenant_id):
            tenant_id = TenantResolve(self._json_rpc).resolve_string(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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("keystore create-system-slot <name> [password]")
        print("  Create System-Owned Key Store Slot. ")
        print("  Initializes file encryption if this is the first key. ")

    def run(self, args):
        if len(args) == 0:
            print_warning(1, "Expect <name>")
            return -2
        elif len(args) == 1:
            name = args[0]
            password = get_password_from_prompt()
        elif len(args) == 2:
            name = args[0]
            password = args[1]

        if len(password) < 20:
            print_warning(0, "You chose a rather short password."
                          "We recommend to use at least 20 characters")
        result = self._json_rpc.call(
            "createMasterKeystoreSlot",
            {"master_keystore_slot_password": password,
             "master_keystore_slot_name": name})
        print("Success. Key store initialized.")

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} encryptkeystoreion init"

class KeyStoreStatus (Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("keystore status")
        print("  Prints encryption and key store state.")

    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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("keystore unlock [master_password]")
        print("  Unlocks Encryption Key Store after cluster restarts. ")

    def run(self, args):
        if len(args) < 1:
            master_keystore_slot_password =  getpass.getpass(
                prompt="Please enter key store master password: ")
        else :
            master_keystore_slot_password = args[0]

        result = self._json_rpc.call(
                "unlockMasterKeystoreSlot",
            {"master_keystore_slot_password": master_keystore_slot_password})

        print("Success.")

    @staticmethod
    def completion_cmd():
        return "qmgmt${opts} user config list --list-columns=id"

class ListSystemKeystoreSlots (Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("keystore list-system-slots")
        print("  Lists system-owned key slots. ")

    def run(self, args):
        result = self._json_rpc.call(
                "getMasterKeystoreSlots",
            {})
        printer = PrettyTable(
            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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("keystore delete-system-slot <slot uuid>")

    def run(self, args):
        if len(args) != 1:
            self.print_help(args)
            return -2

        keystore_slot_uuid = args[0]

        result = self._json_rpc.call(
            "removeMasterKeystoreSlot", {
                "keystore_slot_uuid": keystore_slot_uuid
            })
        print(result)


class CreateUserKeystoreSlot (Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("keystore create-slot [<slot password>]")

    def run(self, args):
        if len(args) < 1:
            password = get_password_from_prompt()
        else :
            password = args[0]

        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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("keystore delete-slot <slot uuid>")

    def run(self, args):
        if len(args) != 1:
            self.print_help(args)
            return -2

        keystore_slot_uuid = args[0]

        result = self._json_rpc.call(
            "removeKeystoreSlot", {
                "keystore_slot_uuid": keystore_slot_uuid
            })
        print(result)


class FileDumpMetadata(Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("file dump-metadata <global_file_id>" \
              " [<segment offset> [<stripe number>]]")
        print("    global_file_id  is of the form <volume_uuid>:<file_id>")
        print("        volume_uuid      uuid of volume that contains the file")
        print("        file_id          numeric local file id (not a global file id)")
        print("    file path        absolute path of the file from the volume")
        print("    segment offset   start offset of a segment")
        print("    stripe number    index of a stripe in the specified segment")
        print("  * use -a or --show-all to include all segment blocks.")

    def run(self, args):
        if len(args) < 1:
            self.print_help(args)
            return -2

        request = dict()
        volume_and_file = args[0].split(':')
        if (len(volume_and_file) != 2):
            print_warning(1, "invalid parameter is given: ", args[0])
            return -3
        request['volume_uuid'] = volume_and_file[0]
        request['file_id'] = int(volume_and_file[1])

        if len(args) > 1:
            request['include_object_dumps'] = True
            try:  # segment start offset
                request['segment_start_offset'] = int(args[1])
                if len(args) > 2:  # stripe number
                    request['stripe_number'] = int(args[2])
            except ValueError as e:
                print_warning(1, "invalid parameter is given: ", e)
                return -2
            if SHOW_ALL:
                print("Warning: since you have specified the \"segment offset\", the --show-all"
                      " parameter will be ignored")
        else:
            request['include_object_dumps'] = 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
            mdfile = open(dirpath + '%s,%s,metadata.txt' % (volume_uuid, file_id), 'w')
            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)
            mdfile.close()

            # 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'
                sdfile = open(dirpath + filename, 'w')
                for stripe_metadata_dump in stripe_metadata_dumps:
                    sdfile.write(stripe_metadata_dump)
                    sdfile.write("\n")
                sdfile.close()

            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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("file resolve-global-id <global_file_id>")
        print("    global_file_id  is of the form <volume_uuid>:<file_id>")
        print("        volume_uuid      uuid of volume that contains the file")
        print("        file_id          numeric local file id (not a global file id)")

    def run(self, args):
        if len(args) != 1:
            self.print_help(args)
            return -2

        global_file_id = args[0]

        result = self._json_rpc.call(
                "resolveGlobalFileId", {
                    "global_file_id": 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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("support generate-dump [<support-ticket-id> or <support-ticket-number>]")
        print("   schedules a support dump generation. The generated dump can be downloaded to")
        print("   local machine with \"qmgmt support download-dump\" until validity expires,")
        print("   or automatically attached to Quobyte Support Ticket if a ticket number is")
        print("   given.")
        print()
        print("    support-ticket-id      Support ticket ID as four or five digits.")
        print("    support-ticket-number  Support ticket number as seven digits.")
        print()
        print("    * If ticket ID or number is given, the support dump will be automatically")
        print("      uploaded to Quobyte support infrastructure after successful generation.")
        print()

    def run(self, args):
        if len(args) > 1:
            self.print_help(args)
            return -2

        request = dict()

        # parse ticket ID or number if given
        if len(args) == 1:
            ticket_id_length = len(str(args[0]))
            if not (ticket_id_length == 4 or ticket_id_length == 5 or ticket_id_length == 7):
                print("Ticket ID or number should be four, five, or seven digits integer")
                self.print_help(args)
                return -2
            try:
                support_ticket_id_or_number = int(args[0])
            except ValueError:
                print("support-ticket-number should be a seven digits integer")
                self.print_help(args)
                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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("support get-dump-status")
        print("   shows status of support dump requested")
        print("")

    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
        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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("support download-dump <path to download>")
        print()
        print("  path to download    path to an existing directory")
        print()

    def run(self, args):
        if len(args) != 1:
            self.print_help(args)
            return -2

        # check support dump status, and start downloading if generation is done.
        response = self._json_rpc.call("getSupportDumpStatus", {})
        support_ticket_id = "Unknown"
        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[0]

        # 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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("support generate-service-dump <service-uuid> or <client-uuid> " + \
                "[<support-ticket-id> or <support-ticket-number>]")
        print("    Generate state dump of the service. The dump contains more detailed")
        print("    information of the service's states than the support dump.")
        print()
        print("    service-uuid           UUID of the Quobyte service to generate the dump")
        print("    client-uuid            UUID of the Quobyte client to generate the dump")
        print("    support-ticket-id      Support ticket ID as four or five digits.")
        print("    support-ticket-number  Support ticket number as seven digits.")
        print()
        print("    * If ticket ID or number is given, the service dump will be automatically")
        print("      uploaded to Quobyte support infrastructure after successful generation.")
        print()

    def run(self, args):
        if not(len(args) == 1 or len(args) == 2):
            self.print_help(args)
            return -2

        service_uuid = args[0]

        # parse ticket ID if given
        request = {'service_uuid': service_uuid}
        if len(args) == 2:
            ticket_id_length = len(str(args[1]))
            if not (ticket_id_length == 4 or ticket_id_length == 5 or ticket_id_length == 7):
                print("Ticket ID or number should be four, five, or seven digits integer")
                self.print_help(args)
                return -2
            try:
                support_ticket_id_or_number = int(args[1])
            except ValueError:
                print("support-ticket-number should be a seven digits integer")
                self.print_help(args)
                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):
        self._json_rpc = json_rpc

    def drive_query(self, query_id):
        self.query_id = query_id
        signal.signal(signal.SIGINT, self.interrupt)

        row_count = 0
        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=sys.stderr)
                wrote_stdout_output = self.process_result_rows(progress_response['result_row'])
                no_data_in_response = False

            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=sys.stderr)
            print("\rProgress: " +
                  str(files_done) + "/" + str(files_total) + " nodes, " +
                  str(volumes_done) + "/" + str(volumes_total) + " volumes",
                  end='', file=sys.stderr)

            if progress_response['query_status'] != 'RUNNING':
                return progress_response['query_status']

            if no_data_in_response:
                time.sleep(0.25)

    def interrupt(self, singal, frame):
        if self.query_id:
            self._json_rpc.call("cancelQuery", {'query_id' : self.query_id})
            print("\rInterrupted running query")


class QueryAnalyzeAllVolumes(AbstractQuery):
    def __init__(self, json_rpc):
        super().__init__(json_rpc)

    def print_help(self, args):
        print("query analyze-volumes [--tenants=<tenant list>] [--volumes=<volume list>]"
              " <output-file>")
        print("  To analyze ALL volumes, skip optional tenant and volume selection")
        print()
        print("  --tenants=tenants      comma-separated list of tenants (names or IDs), to")
        print("                         account for a volume report.")
        print("  --volumes=volumes      comma-separated list of volumes (names or UUIDs), to")
        print("                         account for a volume report.")
        print("  output-file            where to write the html report to")
        print()

    def run(self, args):
        if len(args) != 1:
            self.print_help(args)
            return -2

        tenants_arg = options.tenant_list
        volumes_arg = options.volume_list
        tenant_uuids = convert_comma_separated_tenant_list_to_uuid_array(self, tenants_arg)
        volume_uuids = convert_comma_separated_volume_list_to_uuid_array(self, volumes_arg)
        request = {}
        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[0], 'wb') as f:
            f.write(report.encode())
        print("\rWrote report to", args[0], "                  ")

    def process_result_rows(self, rows):
        self.result_row = rows
        return False  # did not write to stdout


class QueryFiles(AbstractQuery):
    def __init__(self, json_rpc):
        super().__init__(json_rpc)

    def print_help(self, args):
        print("query files [--list-columns=col1,col2] [--group-by=col1,col2] <predicate> [<output-file>]")
        print()
        print("  -a               iterate also unlinked files and files in snapshots")
        print("                   (slower and SUPERUSER only)")
        print("  --list-columns   set result columns or colunn aggregation functions")
        print("  --group-by       group by columns")
        print("  predicate        simple predicate over file properties")
        print("  output-file      where to write the result list to, otherwise stdout")
        print()

    def run(self, args):
        if len(args) == 0:
            self.print_help(args)
            return -2
        elif len(args) == 1:
            output_file_name = None
        elif len(args) > 1:
            output_file_name = args[1]

        result = self._json_rpc.call("whoAmI", {})
        if len(result['group']) == 0 and not SHOW_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 LIST_COLUMNS.split(",")] if LIST_COLUMNS else ['volume_uuid_path']
        group_by_properties = [p.strip() for p in GROUP_BY_COLUMNS.split(",")] if GROUP_BY_COLUMNS else []

        response = self._json_rpc.call(
            "queryFiles",
            {'query' : args[0],
             'select_property' : select_properties,
             'group_by_property' : group_by_properties,
             'iterate_all' : SHOW_ALL})

        if output_file_name:
            self.output_file = open(output_file_name, 'w')
        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", output_file_name, 30 * " ",
                  file=sys.stderr)
        elif terminal_status == 'DONE':
            print("\33[2K\rDone", 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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("query cancel <query id>")
        print()

    def run(self, args):
        if len(args) != 1:
            self.print_help(args)
            return -2

        self._json_rpc.call("cancelQuery", {'query_id': args[0]})


class NetworkTestStart(Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def run(self, args):
        result = self._json_rpc.call("startNetworkTest", {})
        print("Started network test, please check "
              "primary Registry status page for result")


class NetworkTestCancel(Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def run(self, args):
        result = self._json_rpc.call("cancelNetworkTest", {})
        print("Canceled network test")


class NetworkTestShow(Command):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

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

        _output_json_and_exit(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:   " + result['management_role'])
        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):
    def __init__(self, json_rpc):
        self._json_rpc = json_rpc

    def print_help(self, args):
        print("completion <shell_type>")
        print("  Output completion code for <shell_type> (bash or zsh)")
        print("  Can be used as $source <(qmgmt completion bash)")
        print("  or $qmgmt completion bash > /etc/bash_completion.d/qmgmt.")
        print("  For zsh first autoload/exec compinit and bashcompinit.")


    @staticmethod
    def get_header():
        return """# Autogenerated code.
# Copyright 2019 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(" ")
                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 len(args) < 1:
            self.print_help(args)
            return -2
        if args[0] == "bash":
            print(self.get_header() + self.generate_bash())
        elif args[0] == "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') 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

    return duration_s


def to_human_readable_ms(time_ms):
    try:
        millis = int(time_ms)
    except (SyntaxError, ValueError):
        raise InvalidInputException("Invalid milliseconds input: %s" % millis)
    if millis < 0:
        raise InvalidInputException("Invalid milliseconds input: %s" % millis)
    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)
        return int(result_num)
    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 convert_comma_separated_tenant_list_to_uuid_array(self, 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:
                resolved = TenantResolve(self._json_rpc).resolve_string(tenant)
                if resolved:
                    array.append(resolved.strip())
    if len(array) > 1:
        # remove possible duplicates
        array = list(dict.fromkeys(array))
    return array


def convert_comma_separated_volume_list_to_uuid_array(self, 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 = VolumeResolve(self._json_rpc).resolve_string(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)


def print_command_help(command_filter=""):
    print("Commands:")
    prev = None
    for command in filter(lambda i: i.startswith(command_filter), get_command_help_tuple()) \
            or get_command_help_tuple():
        if prev and prev != command.split(" ")[0]:
            print()
        print("  " + command)
        prev = command.split(" ")[0]


def get_command_help_tuple():
    return (
      "help <command name> shows a detailed help text for a command",
      "user login               create a new authenticated session",
      "user logout              destroy the current authenticated session",
      "user config              manage users",
      "policy-rule list         list policy rules briefly",
      "policy-rule filter       filter effective policy rules and/or policies for a subject",
      "policy-rule create       create a policy rule from a template in text editor",
      "policy-rule update       update a policy rule in text editor",
      "policy-rule priority     increase or decrease a policy rule's priority",
      "policy-rule delete       delete a policy rule",
      "policy-rule edit         edit all policy rules in text editor",
      "policy-rule export       export policy rules to stdout or a file",
      "policy-rule import       import policy rule(s) from stdin or a file",
      "policy-preset list       list available policy presets with their policies",
      "tenant create            create a new tenant",
      "tenant create-with-id    create a new tenant with predefined tenant id",
      "tenant update            update settings on a tenant",
      "tenant delete            delete a tenant",
      "tenant list              list all tenants",
      "tenant lookup            look up name of a particular tenant uuid",
      "tenant resolve           resolve tenant name to tenant id",
      "volume analyze           analyze volume and create report",
      "volume list              list all volumes (within tenant)",
      "volume create            create a new volume",
      "volume update            update settings on a volume",
      "volume erase             erase a volume with all its files from data devices",
      "volume show              show detailed volume information",
      "volume lookup            look up name of a particular volume uuid",
      "volume resolve           resolve volume name to volume uuid",
      "volume repair            enforce the replica placement for all files of the given volume",
      "volume scrub             scrub all files associated with the given volume",
      "volume publish           publish a volume as an S3 bucket",
      "volume unpublish         unpublish an S3 bucket volume",
      "volume mirror            create mirrored volume",
      "volume disconnect        disconnect mirrored volume",
      "volume encryption        manage volume encryption",
      "snapshot create          create a new snapshot",
      "snapshot erase           erase a snapshot with all unreferenced files from data devices",
      "snapshot delete          delete a snapshot without erasing any files from data devices",
      "snapshot list            list all volume snapshots",
      "device list              show a list of devices and their current state",
      "device show              show detailed device information",
      "device addr              show the network endpoints for a device",
      "device update            update settings for a device",
      "device remove            empty device and disconnect it from the system",
      "device list-unformatted  list all unformatted devices",
      "device make              make a clean device into a Quobyte device",
      "task list                show a list of all tasks (scheduled, running, completed)",
      "task show                show detailed task information",
      "task geterrors           list all errors collected by the given task",
      "task create              create (schedule) a new task",
      "task cancel              cancel a running or scheduled task",
      "task resume              resume a failed or canceled task",
      "task retry               retry a terminated task from the beginning",
      "files copy               copy files between Quobyte volumes",
      "files move               move files between Quobyte volumes",
      "files recode             recode files in the volume",
      "files delete             delete files in the volume",
      "usage show               resource usage for a given consumer",
      "nfs-virtualips           manage NFS virtual ips configuration",
      "quota                    manage quota pool configurations",
      "failuredomains           manage failure domain configuration",
      "rules                    manage automation rule configurations",
      "systemconfig             manage general system configuration",
      "license import           import license key",
      "registry add             add a replica to the registry service",
      "registry remove          remove a replica from the registry service",
      "registry list            list all registry service replicas",
      "service deregister       deregister a service",
      "service list             show a list of all services",
      "bucket list              list all published S3 buckets",
      "bucket show              show detailed bucket information",
      "client list              list all known clients",
      "audit-log list           list audit log entries",
      "alert list               list active alerts",
      "alert silence            silence an active alert",
      "alert acknowledge        acknowledge an active alert",
      "database regenerate      update, clear and regenerate database types",
      "ca add                   add certificate authority",
      "ca list                  list certificate authorities",
      "ca delete                delete certificate authority",
      "ca export                export ca certificate",
      "csr add                  add certificate signing request",
      "csr approve              approve certificate signing request",
      "csr reject               reject certificate signing request",
      "csr list                 list certificate signing requests",
      "csr delete               delete certificate signing request",
      "certificate add          add certificate",
      "certificate delete       delete certificate",
      "certificate list         list certificates",
      "certificate export       export certificate",
      "certificate config       config certificate subject",
      "label get                retrieve one or more labels",
      "label set                set a label",
      "label delete             delete a label",
      "healthmanager status     show health manager status",
      "accesskey create        create access key credentials",
      "accesskey delete        delete access key credentials",
      "accesskey list          list access key credentials",
      "accesskey import        import access key credentials",
      "keystore status          show key store status",
      "keystore unlock          unlock system key store",
      "keystore create-slot     create a new user slot",
      "keystore delete-slot     delete user slot",
      "keystore create-system-slot create system-owned key store slot",
      "keystore delete-system-slot delete system-owned key store slot",
      "keystore list-system-slots list system-owned key slots",
      "file dump-metadata       dump a file's metadata",
      "file resolve-global-id   resolve global file id into a file with volume uuid",
      "support generate-dump          schedule asynchronous support dump generation",
      "support dump-status            shows status of support dump generation",
      "support download-dump          download support dump file if ready",
      "support generate-service-dump  dump a service's state",
      "query analyze-volumes          analyze volumes and create report",
      "query files                    query file metadata",
      "query cancel                   cancel a running query",
#      "network-test start       start network test",
#      "network-test cancel      cancel network test",
#      "network-test show        show results of network test",
      "completion               output shell completion code (bash or zsh)",
    )

COMMANDS = {
    "user login": UserLogin,
    "user logout": UserLogout,
    "user config": UserConfiguration,
    "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": DeviceUpdate,
    "device remove": DeviceRemove,
    "device list-unformatted": DeviceListClean,
    "device make": DeviceMake,
    "tenant create": TenantCreate,
    "tenant create-with-id": TenantCreateWithId,
    "tenant update": TenantUpdate,
    "tenant delete": TenantDelete,
    "tenant list": TenantList,
    "tenant lookup": TenantLookup,
    "tenant resolve": TenantResolve,
    "volume create": VolumeCreate,
    "volume erase": VolumeErase,
    "volume delete": VolumeDelete,
    "volume update": VolumeUpdate,
    "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,
    "volume encryption": VolumeEncryption,
    "snapshot create": SnapshotCreate,
    "snapshot erase": SnapshotErase,
    "snapshot delete": SnapshotDelete,
    "snapshot list": SnapshotList,
    "usage show": UsageShow,
    "quota": QuotaConfiguration,
    "quota create": QuotaCreate,
    "failuredomains": FailureDomainsConfiguration,
    "rules": RulesConfiguration,
    "systemconfig": SystemConfiguration,
    "nfs-virtualips": NfsVirtualIpsConfiguration,
    "license import": LicenseKeyImport,
    "task cancel": TaskCancel,
    "task resume": TaskResume,
    "task retry": TaskRetry,
    "task create": TaskCreate,
    "task list": TaskList,
    "task show": TaskShow,
    "task geterrors": TaskGeterrors,
    "files copy": FilesTask,
    "files move": FilesTask,
    "files recode": FilesTask,
    "files delete": FilesTask,
    "registry add": RegistryAddReplica,
    "registry remove": RegistryRemoveReplica,
    "registry list": RegistryListReplicas,
    "service deregister": ServiceDeregister,
    "service list": ServiceList,
    "client list": ClientList,
    "audit-log list": AuditLogList,
    "alert list": AlertList,
    "alert silence": AlertSilence,
    "alert acknowledge": AlertAcknowledge,
    "database regenerate": DatabaseRegenerate,
    "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": CertificateConfig,
    "label get": GetLabel,
    "label set": SetLabel,
    "label delete": DeleteLabel,
    "healthmanager status": HealthManagerStatus,
    "accesskey create": CreateAccessKey,
    "accesskey delete": DeleteAccessKey,
    "accesskey list": ListAccessKey,
    "accesskey import": ImportAccessKey,
    # "s3-key ..." commands are deprecated, use accesskey instead
    "s3-key create": CreateS3AccessKey,
    "s3-key delete": DeleteS3AccessKey,
    "s3-key list": ListS3AccessKey,
    "s3-key import": ImportS3AccessKey,
    "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": QueryAnalyzeAllVolumes,
    "query files": QueryFiles,
    "query cancel": QueryCancel,
    "networktest start": NetworkTestStart,
    "networktest cancel": NetworkTestCancel,
    "networktest show": NetworkTestShow,
    "whoami": WhoAmI,
    "completion": ShellCompletion
}


def deduce_command_name(positional_args):
    if len(positional_args) < 1:
        option_parser.print_help()
        print_command_help()
        sys.exit(2)

    if len(positional_args) > 1 and positional_args[0].lower() + " " + positional_args[1].lower() in COMMANDS:
        return positional_args[0].lower() + " " + positional_args[1].lower(), 2
    if positional_args[0].lower() in COMMANDS:
        return positional_args[0].lower(), 1

    print_warning(1, "Unknown command: " + positional_args[0])
    print_command_help(positional_args[0])
    sys.exit(2)


def print_version():
    print(RELEASE_VERSION_LONG)


if __name__ == "__main__":
    # print_warning function checks for COMMAND_NAME global variable
    global COMMAND_NAME
    COMMAND_NAME = None
    if (sys.version_info.major, sys.version_info.minor) < (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)

    # TODO(artari): migrate from optparse to argparse
    option_parser = OptionParser(
        "usage: %prog [options] command [parameters]",
        add_help_option=False)
    option_parser.add_option("-h", "--help",
                             action="store_true",
                             dest="print_help_message",
                             default=False,
                             help="Print full help or a single command help. "
                                  "With '-a' always shows full help.")
    option_parser.add_option("-u", "--url",
                             action="store",
                             dest="api_service_url",
                             help="The hostname or URL of an API HTTP service endpoint. "
                               "In case only a hostname or URL without port are given, the "
                               "address is completed to match any certificate or CA parameters. "
                               "If not set, the environment variable QUOBYTE_API is used instead. "
                               "If that is not set, an API on localhost is configured.")
    option_parser.add_option("-f", "--force",
                             action="store_true",
                             dest="force",
                             default=False,
                             help="Do not ask for confirmation.")
    option_parser.add_option("-r", "--retry",
                             action="store_true",
                             dest="retry",
                             default=not sys.stdin.isatty(),
                             help="Retry forever. Defaults to True for non-interactive shells.")
    option_parser.add_option("--interactive",
                             action="store_false",
                             dest="retry",
                             help="Don't retry requests.")
    option_parser.add_option("--ca",
                             action="store",
                             metavar="/path/to/ca",
                             dest="ca_path",
                             help="Path to CA certificate.")
    option_parser.add_option("--verify-server-certificate",
                             action="store_true",
                             dest="verify_server_certificate",
                             default=False,
                             help="Abort if the server certificate cannot be verified.")
    option_parser.add_option("--show-all", "-a",
                             action="store_true",
                             dest="show_all",
                             default=False,
                             help="Retrieve additional information where possible. "
                                  "Shows: decommissioned devices in device list, "
                                  "device spread in volume show, "
                                  "bucket names and volume uuids in volume list. "
                                  "Dumps: all segment dumps in file dump-metadata.")
    option_parser.add_option("--verbose", "-v",
                             action="store_true",
                             dest="verbose",
                             default=False,
                             help="Additional output for debugging,"
                                  " like detailed requests information.")
    option_parser.add_option("--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.")
    option_parser.add_option("--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.")
    option_parser.add_option("--group-by",
                             action="store",
                             metavar="STR",
                             dest="group_by_columns",
                             help="Group query by specified comma-separated"
                                  " values.")
    option_parser.add_option("--output", "-o",
                             action="store",
                             metavar="STR",
                             dest="list_output",
                             help="Choose output format. "
                                  "table: (default) formatted table. "
                                  "csv: no formatting, comma separated. "
                                  "json: json format, non-human readable. "
                                  "keys: list all comma separated keys, "
                                  "to use with columns options.")
    option_parser.add_option("--comment",
                             action="store",
                             dest="comment",
                             metavar="STR",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--under",
                             action="store",
                             type="int",
                             dest="underutilized_threshold_percentage",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--over",
                             action="store",
                             type="int",
                             dest="overutilized_threshold_percentage",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--priority",
                             action="store",
                             type="str",
                             dest="task_priority",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--directory",
                             action="store",
                             type="str",
                             dest="deleting_directory",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--since",
                             action="store",
                             type="str",
                             dest="timestamp",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--unit",
                             help="Output bytes in metric (1000, default) units (GB, TB) or "
                                  "IEC (1024) units (GiB, TiB)",
                             choices=['metric', 'iec'],
                             default="metric")
    option_parser.add_option("--limit",
                             action="store",
                             type="int",
                             metavar="number",
                             dest="limit",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--tags",
                             action="store",
                             dest="tags",
                             metavar="STR",
                             help=SUPPRESS_HELP)

    option_parser.add_option("--version",
                             action="store_true",
                             dest="print_version",
                             default=False,
                             help="Print qmgmt release version to stdout.")
    option_parser.add_option("--filters",
                              action="store",
                              type="str",
                              dest="filters",
                              help=SUPPRESS_HELP)
    option_parser.add_option("--ignore-modified-files-errors",
                              action="store_true",
                              default=False,
                              dest="do_not_fail_task_on_modified_files",
                              help=SUPPRESS_HELP)
    option_parser.add_option("--ignore-all-errors",
                              action="store_true",
                              default=False,
                              dest="do_not_fail_task_on_any_error",
                              help=SUPPRESS_HELP)
    option_parser.add_option("--destination-create-behavior",
                              action="store",
                              default=None,
                              type="str",
                              dest="destination_create_behavior",
                              help=SUPPRESS_HELP)
    option_parser.add_option("--tenants",
                             action="store",
                             type="str",
                             dest="tenant_list",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--volumes",
                             action="store",
                             type="str",
                             dest="volume_list",
                             help=SUPPRESS_HELP)

    # User config options
    option_parser.add_option("--email",
                             action="store",
                             type="str",
                             dest="email",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--role",
                             action="store",
                             type="str",
                             dest="role",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--member-of-tenant",
                             action="store",
                             type="str",
                             dest="t_member",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--admin-of-tenant",
                             action="store",
                             type="str",
                             dest="t_admin",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--member-of-group",
                             action="store",
                             type="str",
                             dest="g_member",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--primary-group",
                             action="store",
                             type="str",
                             dest="primary_group",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--password",
                             action="store",
                             type="str",
                             dest="password",
                             help=SUPPRESS_HELP)

    # Accesskey options
    option_parser.add_option("--tenant",
                             action="store",
                             type="str",
                             dest="tenant",
                             help=SUPPRESS_HELP)
    option_parser.add_option("--validity-days",
                             action="store",
                             type="str",
                             dest="validity_days",
                             help=SUPPRESS_HELP)

    # Snapshot options
    option_parser.add_option("--pin",
                             action="store",
                             type="choice", choices=("true", "false"),
                             default="true",
                             dest="pin",
                             help=SUPPRESS_HELP)

    if len(sys.argv) == 1:
        option_parser.print_help()
        print_command_help()
        sys.exit(2)
    options, positional_args = option_parser.parse_args()

    if not options.api_service_url:
        if os.getenv("QUOBYTE_API"):
            options.api_service_url = os.getenv("QUOBYTE_API")
        else:
            options.api_service_url = "localhost:7860"

    if not re.search("://", options.api_service_url):
        protocol = "http://"
        if options.ca_path or options.verify_server_certificate:
            protocol = "https://"
        options.api_service_url = protocol + options.api_service_url

    if not re.search(":[0-9]+$", options.api_service_url):
        if options.api_service_url.startswith("https://"):
            options.api_service_url += ":8443"
        else:
            options.api_service_url += ":7860"

    if options.print_version:
        print_version()
        sys.exit(0)

    global FORCE
    FORCE = options.force
    global SHOW_ALL
    SHOW_ALL = options.show_all
    global ADD_COLUMNS
    ADD_COLUMNS = options.add_columns
    global LIST_COLUMNS
    LIST_COLUMNS = options.list_columns
    global GROUP_BY_COLUMNS
    GROUP_BY_COLUMNS = options.group_by_columns
    global LIST_OUTPUT
    LIST_OUTPUT = options.list_output
    global VERBOSE
    VERBOSE = options.verbose
    global COMMENT
    COMMENT = options.comment
    global BYTES_UNIT
    BYTES_UNIT = options.unit
    if ADD_COLUMNS and LIST_COLUMNS:
        print_warning(1, "add-columns and list-columns are "
                      "mutually exclusive options.")
        sys.exit(-6)
    global UNDER
    UNDER = options.underutilized_threshold_percentage
    global OVER
    OVER = options.overutilized_threshold_percentage
    global PRIORITY
    PRIORITY = options.task_priority
    global DELETING_DIRECTORY
    DELETING_DIRECTORY = options.deleting_directory
    global FILTERS
    FILTERS = options.filters
    global DO_NOT_FAIL_COPY_TASK_ON_MODIFIED_FILES
    DO_NOT_FAIL_COPY_TASK_ON_MODIFIED_FILES = options.do_not_fail_task_on_modified_files
    global DO_NOT_FAIL_COPY_TASK_ON_ANY_ERRORS
    DO_NOT_FAIL_COPY_TASK_ON_ANY_ERRORS = options.do_not_fail_task_on_any_error
    global DESTINATION_CREATE_BEHAVIOR
    DESTINATION_CREATE_BEHAVIOR = options.destination_create_behavior
    global TIMESTAMP
    TIMESTAMP = options.timestamp
    global LIMIT
    LIMIT = options.limit
    try:
        default_user_credentials = BasicAuthCredentials("admin", "quobyte")
        credentials_provider = InteractiveCredentialsProvider(
            default_user_credentials, FORCE)
        rpc_interface = JsonRpc(url=options.api_service_url,
                                session_cookie_handler=SessionCookieHandler(),
                                credentials_provider=credentials_provider,
                                fail_fast=not options.retry,
                                ca=options.ca_path,
                                require_cert_verify=options.verify_server_certificate)
        if len(positional_args) == 0:
            option_parser.print_help()
            print_command_help()
            sys.exit(2)
        elif positional_args[0].lower() == "help":
            command_name, parts = deduce_command_name(positional_args[1:])
            args = positional_args[1 + parts:]
            COMMANDS[command_name](rpc_interface).print_help(args)
        else:
            command_name, parts = deduce_command_name(positional_args)
            args = positional_args[parts:]
            COMMAND_NAME = command_name
            if options.print_help_message is True:
                COMMANDS[command_name](rpc_interface).print_help(args)
                sys.exit(0)
            return_value = COMMANDS[command_name](rpc_interface).run(args)
            if return_value != 0:
                sys.exit(return_value)
    except KeyboardInterrupt as e:
        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 failed: %s" % 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 did not serve well-formed JSON: %s" % 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)
