xref: /6.0.3/couchbase-cli/cbmgr.py (revision 2ddc1a08)
1"""A Couchbase  CLI subcommand"""
2
3import getpass
4import inspect
5import json
6import os
7import platform
8import random
9import re
10import string
11import subprocess
12import sys
13import urlparse
14import time
15
16from argparse import ArgumentError, ArgumentParser, HelpFormatter, Action, SUPPRESS
17from cluster_manager import ClusterManager
18from pbar import TopologyProgressBar
19
20COUCHBASE_DEFAULT_PORT = 8091
21
22BUCKET_PRIORITY_HIGH_INT = 8
23BUCKET_PRIORITY_HIGH_STR = "high"
24BUCKET_PRIORITY_LOW_INT = 3
25BUCKET_PRIORITY_LOW_STR = "low"
26
27BUCKET_TYPE_COUCHBASE = "membase"
28BUCKET_TYPE_MEMCACHED = "memcached"
29
30CB_BIN_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "bin"))
31
32# On MacOS the config is store in the users home directory
33if platform.system() == "Darwin":
34    CB_CFG_PATH = os.path.expanduser("~/Library/Application Support/Couchbase/var/lib/couchbase")
35else:
36    CB_CFG_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "var", "lib", "couchbase"))
37
38CB_MAN_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "share"))
39
40if os.name == "nt":
41    CB_MAN_PATH = os.path.join(CB_MAN_PATH, "html")
42else:
43    CB_MAN_PATH = os.path.join(CB_MAN_PATH, "man", "man1")
44
45def check_cluster_initialized(rest):
46    """Checks to see if the cluster is initialized"""
47    initialized, errors = rest.is_cluster_initialized()
48    if not initialized:
49        _exitIfErrors(["Cluster is not initialized, use cluster-init to initialize the cluster"])
50    elif errors:
51        _exitIfErrors(errors)
52
53def index_storage_mode_to_param(value, default="plasma"):
54    """Converts the index storage mode to what Couchbase understands"""
55    if value == "default":
56        return default
57    elif value == "memopt":
58        return "memory_optimized"
59    else:
60        return value
61
62def process_services(services, enterprise):
63    """Converts services to a format Couchbase understands"""
64    sep = ","
65    if services.find(sep) < 0:
66        #backward compatible when using ";" as separator
67        sep = ";"
68    svc_set = set([w.strip() for w in services.split(sep)])
69    svc_candidate = ["data", "index", "query", "fts", "eventing", "analytics"]
70    for svc in svc_set:
71        if svc not in svc_candidate:
72            return None, ["`%s` is not a valid service" % svc]
73        if not enterprise and svc in ["eventing", "analytics"]:
74            return None, ["{0} service is only available on Enterprise Edition".format(svc)]
75
76    if not enterprise:
77        # Valid CE node service configuration
78        ce_svc_30 = set(["data"])
79        ce_svc_40 = set(["data", "index", "query"])
80        ce_svc_45 = set(["data", "index", "query", "fts"])
81        if svc_set not in [ce_svc_30, ce_svc_40, ce_svc_45]:
82            return None, ["Invalid service configuration. Community Edition only supports nodes with the following"
83                          " combinations of services: '{0}', '{1}' or '{2}'".format(''.join(ce_svc_30),
84                                                                                    ','.join(ce_svc_40),
85                                                                                    ','.join(ce_svc_45))]
86
87    services = ",".join(svc_set)
88    for old, new in [[";", ","], ["data", "kv"], ["query", "n1ql"], ["analytics", "cbas"]]:
89        services = services.replace(old, new)
90    return services, None
91
92def find_subcommands():
93    """Finds all subcommand classes"""
94    clsmembers = inspect.getmembers(sys.modules[__name__], inspect.isclass)
95    subclasses = [cls for cls in clsmembers if issubclass(cls[1], (Subcommand, LocalSubcommand)) and cls[1] not in [Subcommand, LocalSubcommand]]
96
97    subcommands = []
98    for subclass in subclasses:
99        name = '-'.join([part.lower() for part in re.findall('[A-Z][a-z]*', subclass[0])])
100        subcommands.append((name, subclass[1]))
101    return subcommands
102
103def _success(msg):
104    print "SUCCESS: " + msg
105
106def _deprecated(msg):
107    print "DEPRECATED: " + msg
108
109def _warning(msg):
110    print "WARNING: " + msg
111
112def _exitIfErrors(errors):
113    if errors:
114        for error in errors:
115            print "ERROR: " + error
116        sys.exit(1)
117
118def _exit_on_file_write_failure(fname, to_write):
119    try:
120        wfile = open(fname, 'w')
121        wfile.write(to_write)
122        wfile.close()
123    except IOError, error:
124        _exitIfErrors([error])
125
126def _exit_on_file_read_failure(fname, toReport = None):
127    try:
128        rfile = open(fname, 'r')
129        read_bytes = rfile.read()
130        rfile.close()
131        return read_bytes
132    except IOError, error:
133        if toReport is None:
134            _exitIfErrors([error.strerror + " `" + fname + "`"])
135        else:
136            _exitIfErrors([toReport])
137
138def apply_default_port(nodes):
139    """
140    Adds the default port if the port is missing.
141
142    @type  nodes: string
143    @param nodes: A comma seprated list of nodes
144    @rtype:       array of strings
145    @return:      The nodes with the port postfixed on each one
146    """
147    nodes = nodes.split(',')
148    def append_port(node):
149        if re.match('.*:\d+$', node):
150            return node
151        return node + ':8091'
152    return [append_port(x) for x in nodes]
153
154class CLIHelpFormatter(HelpFormatter):
155    """Format help with indented section bodies"""
156
157    def __init__(self, prog, indent_increment=2, max_help_position=30, width=None):
158        HelpFormatter.__init__(self, prog, indent_increment, max_help_position, width)
159
160    def add_argument(self, action):
161        if action.help is not SUPPRESS:
162
163            # find all invocations
164            get_invocation = self._format_action_invocation
165            invocations = [get_invocation(action)]
166            for subaction in self._iter_indented_subactions(action):
167                invocations.append(get_invocation(subaction))
168
169            # update the maximum item length
170            invocation_length = max([len(s) for s in invocations])
171            action_length = invocation_length + self._current_indent + 2
172            self._action_max_length = max(self._action_max_length,
173                                          action_length)
174
175            # add the item to the list
176            self._add_item(self._format_action, [action])
177
178    def _format_action_invocation(self, action):
179        if not action.option_strings:
180            metavar, = self._metavar_formatter(action, action.dest)(1)
181            return metavar
182        else:
183            parts = []
184            if action.nargs == 0:
185                parts.extend(action.option_strings)
186                return ','.join(parts)
187            else:
188                default = action.dest
189                args_string = self._format_args(action, default)
190                for option_string in action.option_strings:
191                    parts.append(option_string)
192                return ','.join(parts) + ' ' +  args_string
193
194
195class CBDeprecatedAction(Action):
196    """Indicates that a specific option is deprecated"""
197
198    def __call__(self, parser, namespace, values, option_string=None):
199        _deprecated('Specifying ' + '/'.join(self.option_strings) + ' is deprecated')
200        if self.nargs == 0:
201            setattr(namespace, self.dest, self.const)
202        else:
203            setattr(namespace, self.dest, values)
204
205
206class CBHostAction(Action):
207    """Allows the handling of hostnames on the command line"""
208
209    def __call__(self, parser, namespace, values, option_string=None):
210        parsed = urlparse.urlparse(values)
211
212        # If the netloc is empty then it means that there was no scheme added
213        # to the URI and we are parsing it as a path. In this case no scheme
214        # means HTTP so we can add that scheme to the hostname provided.
215        if parsed.netloc == "":
216            parsed = urlparse.urlparse("http://" + values)
217
218        if parsed.scheme == "":
219            parsed = urlparse.urlparse("http://" + values)
220
221        if parsed.path != "" or parsed.params != "" or parsed.query != "" or parsed.fragment != "":
222            raise ArgumentError(self, "%s is not an accepted hostname" % values)
223
224        scheme = parsed.scheme
225        port = None
226        if scheme in ["http", "couchbase"]:
227            if not parsed.port:
228                port = 8091
229            if scheme == "couchbase":
230                scheme = "http"
231        elif scheme in ["https", "couchbases"]:
232            if not parsed.port:
233                port = 18091
234            if scheme == "couchbases":
235                scheme = "https"
236        else:
237            raise ArgumentError(self, "%s is not an accepted scheme" % scheme)
238
239        if parsed.port:
240            setattr(namespace, self.dest, (scheme + "://" + parsed.netloc))
241        else:
242            setattr(namespace, self.dest, (scheme + "://" + parsed.netloc + ":" + str(port)))
243
244
245class CBEnvAction(Action):
246    """Allows the custom handling of environment variables for command line options"""
247
248    def __init__(self, envvar, required=True, default=None, **kwargs):
249        if not default and envvar:
250            if envvar in os.environ:
251                default = os.environ[envvar]
252        if required and default:
253            required = False
254        super(CBEnvAction, self).__init__(default=default, required=required,
255                                          **kwargs)
256
257    def __call__(self, parser, namespace, values, option_string=None):
258        setattr(namespace, self.dest, values)
259
260
261class CBNonEchoedAction(CBEnvAction):
262    """Allows an argument to be specified by use of a non-echoed value passed through
263    stdin, through an environment variable, or as a value to the argument"""
264
265    def __init__(self, envvar, prompt_text="Enter password:", confirm_text=None,
266                 required=True, default=None, nargs='?', **kwargs):
267        self.prompt_text = prompt_text
268        self.confirm_text = confirm_text
269        super(CBNonEchoedAction, self).__init__(envvar, required=required, default=default,
270                                                nargs=nargs, **kwargs)
271
272    def __call__(self, parser, namespace, values, option_string=None):
273        if values == None:
274            values = getpass.getpass(self.prompt_text)
275            if self.confirm_text is not None:
276                confirm = getpass.getpass(self.prompt_text)
277                if values != confirm:
278                    raise ArgumentError(self, "Passwords entered do not match, please retry")
279        super(CBNonEchoedAction, self).__call__(parser, namespace, values, option_string=None)
280
281
282class CBHelpAction(Action):
283    """Allows the custom handling of the help command line argument"""
284
285    def __init__(self, option_strings, klass, dest=SUPPRESS, default=SUPPRESS, help=None):
286        super(CBHelpAction, self).__init__(option_strings=option_strings, dest=dest,
287                                           default=default, nargs=0, help=help)
288        self.klass = klass
289
290    def __call__(self, parser, namespace, values, option_string=None):
291        if option_string == "-h":
292            parser.print_help()
293        else:
294            CBHelpAction._show_man_page(self.klass.get_man_page_name())
295        parser.exit()
296
297    @staticmethod
298    def _show_man_page(page):
299        exe_path = os.path.abspath(sys.argv[0])
300        base_path = os.path.dirname(exe_path)
301
302        if os.name == "nt":
303            try:
304                subprocess.call(["rundll32.exe", "url.dll,FileProtocolHandler", os.path.join(CB_MAN_PATH, page)])
305            except OSError, e:
306                _exitIfErrors(["Unable to open man page using your browser, %s" % e])
307        else:
308            try:
309                subprocess.call(["man", os.path.join(CB_MAN_PATH, page)])
310            except OSError:
311                _exitIfErrors(["Unable to open man page using the 'man' command, ensure it " +
312                               "is on your path or install a manual reader"])
313
314
315class CliParser(ArgumentParser):
316
317    def __init__(self, *args, **kwargs):
318        super(CliParser, self).__init__(*args, **kwargs)
319
320    def error(self, message):
321        self.exit(2, ('ERROR: %s\n') % (message))
322
323
324class Command(object):
325    """A Couchbase CLI Command"""
326
327    def __init__(self):
328        self.parser = CliParser(formatter_class=CLIHelpFormatter, add_help=False)
329
330    def parse(self, args):
331        """Parses the subcommand"""
332        if len(args) == 0:
333            self.short_help()
334
335        return self.parser.parse_args(args)
336
337    def short_help(self, code=0):
338        """Prints the short help message and exits"""
339        self.parser.print_help()
340        self.parser.exit(code)
341
342    def execute(self, opts):
343        """Executes the subcommand"""
344        raise NotImplementedError
345
346    @staticmethod
347    def get_man_page_name():
348        """Returns the man page name"""
349        raise NotImplementedError
350
351    @staticmethod
352    def get_description():
353        """Returns the command description"""
354        raise NotImplementedError
355
356
357class CouchbaseCLI(Command):
358    """A Couchbase CLI command"""
359
360    def __init__(self):
361        super(CouchbaseCLI, self).__init__()
362        self.parser.prog = "couchbase-cli"
363        subparser = self.parser.add_subparsers(title="Commands", metavar="")
364
365        for (name, klass) in find_subcommands():
366            if klass.is_hidden():
367                subcommand = subparser.add_parser(name)
368            else:
369                subcommand = subparser.add_parser(name, help=klass.get_description())
370            subcommand.set_defaults(klass=klass)
371
372        group = self.parser.add_argument_group("Options")
373        group.add_argument("-h", "--help", action=CBHelpAction, klass=self,
374                           help="Prints the short or long help message")
375
376    def parse(self, args):
377        if len(sys.argv) == 1:
378            self.parser.print_help()
379            self.parser.exit(1)
380
381        if not args[1] in ["-h", "--help"] and  args[1].startswith("-"):
382            _exitIfErrors(["Unknown subcommand: '{0}'. The first argument has to be a subcommand like 'bucket-list' or"
383                           " 'rebalance', please see couchbase-cli -h for the full list of commands and"
384                           " options".format(args[1])])
385
386
387        l1_args = self.parser.parse_args(args[1:2])
388        l2_args = l1_args.klass().parse(args[2:])
389        setattr(l2_args, 'klass', l1_args.klass)
390        return l2_args
391
392    def execute(self, opts):
393        opts.klass().execute(opts)
394
395    @staticmethod
396    def get_man_page_name():
397        """Returns the man page name"""
398        return "couchbase-cli" + ".1" if os.name != "nt" else ".html"
399
400    @staticmethod
401    def get_description():
402        return "A Couchbase cluster administration utility"
403
404
405class Subcommand(Command):
406    """
407    A Couchbase CLI Subcommand: This is for subcommand that interact with a remote Couchbase Server over the REST API.
408    """
409
410    def __init__(self, deprecate_username=False, deprecate_password=False, cluster_default=None):
411        super(Subcommand, self).__init__()
412        self.parser = CliParser(formatter_class=CLIHelpFormatter, add_help=False)
413        group = self.parser.add_argument_group("Cluster options")
414        group.add_argument("-c", "--cluster", dest="cluster", required=(cluster_default==None),
415                           metavar="<cluster>", action=CBHostAction, default=cluster_default,
416                           help="The hostname of the Couchbase cluster")
417
418        if deprecate_username:
419            group.add_argument("-u", "--username", dest="username",
420                               action=CBDeprecatedAction, help=SUPPRESS)
421        else:
422            group.add_argument("-u", "--username", dest="username", required=True,
423                               action=CBEnvAction, envvar='CB_REST_USERNAME',
424                               metavar="<username>", help="The username for the Couchbase cluster")
425
426        if deprecate_password:
427            group.add_argument("-p", "--password", dest="password",
428                               action=CBDeprecatedAction, help=SUPPRESS)
429        else:
430            group.add_argument("-p", "--password", dest="password", required=True,
431                               action=CBNonEchoedAction, envvar='CB_REST_PASSWORD',
432                               metavar="<password>", help="The password for the Couchbase cluster")
433
434        group.add_argument("-o", "--output", dest="output", default="standard", metavar="<output>",
435                           choices=["json", "standard"], help="The output type (json or standard)")
436        group.add_argument("-d", "--debug", dest="debug", action="store_true",
437                           help="Run the command with extra logging")
438        group.add_argument("-s", "--ssl", dest="ssl", const=True, default=False,
439                           nargs=0, action=CBDeprecatedAction,
440                           help="Use ssl when connecting to Couchbase (Deprecated)")
441        group.add_argument("--no-ssl-verify", dest="ssl_verify", action="store_false", default=True,
442                           help="Skips SSL verification of certificates against the CA")
443        group.add_argument("--cacert", dest="cacert", default=True,
444                           help="Verifies the cluster identity with this certificate")
445        group.add_argument("-h", "--help", action=CBHelpAction, klass=self,
446                           help="Prints the short or long help message")
447
448
449    def execute(self, opts):
450        super(Subcommand, self).execute(opts)
451
452    @staticmethod
453    def get_man_page_name():
454        return Command.get_man_page_name()
455
456    @staticmethod
457    def get_description():
458        return Command.get_description()
459
460    @staticmethod
461    def is_hidden():
462        """Whether or not the subcommand should be hidden from the help message"""
463        return False
464
465class LocalSubcommand(Command):
466    """
467    A Couchbase CLI Localcommand: This is for subcommands that interact with the local Couchbase Server via the
468    filesystem or a local socket.
469    """
470
471    def __init__(self):
472        super(LocalSubcommand, self).__init__()
473        self.parser = CliParser(formatter_class=CLIHelpFormatter, add_help=False)
474        group = self.parser.add_argument_group(title="Local command options",
475                                               description="This command has to be execute on the locally running" +
476                                                           " Couchbase Server.")
477        group.add_argument("-h", "--help", action=CBHelpAction, klass=self,
478                           help="Prints the short or long help message")
479        group.add_argument("--config-path", dest="config_path", metavar="<path>",
480                           default=CB_CFG_PATH, help=SUPPRESS)
481
482    def execute(self, opts):
483        super(LocalSubcommand, self).execute(opts)
484
485    @staticmethod
486    def get_man_page_name():
487        return Command.get_man_page_name()
488
489    @staticmethod
490    def get_description():
491        return Command.get_description()
492
493    @staticmethod
494    def is_hidden():
495        """Whether or not the subcommand should be hidden from the help message"""
496        return False
497
498class AdminRoleManage(Subcommand):
499    """The administrator role manage subcommand (Deprecated)"""
500
501    def __init__(self):
502        super(AdminRoleManage, self).__init__()
503        self.parser.prog = "couchbase-cli admin-role-manage"
504
505        group = self.parser.add_argument_group("Admin role manage options")
506        group.add_argument("--my-roles", dest="my_roles", action="store_true",
507                           help="Show the current users roles")
508        group.add_argument("--get-roles", dest="get_roles", action="store_true",
509                           help="Show all valid users and roles")
510        group.add_argument("--set-users", dest="set_users", metavar="<user_list>",
511                           help="A comma-delimited list of user ids to set acess-control roles for")
512        group.add_argument("--set-names", dest="set_names", metavar="<name_list>",
513                           help="A optional quoted, comma-delimited list names, one for each " +
514                           "specified user id ")
515        group.add_argument("--roles", dest="roles", metavar="<role_list>",
516                           help="A comma-delimited list of roles to set for users")
517        group.add_argument("--delete-users", dest="delete_users", metavar="<user_list>",
518                           help="A comma-delimited list of users to remove from access control")
519
520    def execute(self, opts):
521        # Deprecated in 5.0
522        _deprecated("Please use the user-manage command instead")
523        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
524                              opts.cacert, opts.debug)
525        check_cluster_initialized(rest)
526
527        if opts.my_roles is None and opts.get_roles is None and \
528            opts.set_users is None and opts.delete_users is None:
529            _exitIfErrors(["You must specify either '--my-roles', '--get-roles', " +
530                           "'--set-users', or '--delete-users'"])
531
532        if opts.my_roles and (opts.get_roles or opts.set_users or opts.roles or opts.delete_users):
533            _exitIfErrors(["The '--my-roles' option may not be used with any other" +
534                           " option."])
535
536        if opts.get_roles and (opts.my_roles or opts.set_users or opts.roles or opts.delete_users):
537            _exitIfErrors(["The '--get-roles' option may not be used with any " +
538                           "other option."])
539
540        if (opts.set_users and opts.roles is None) or (opts.set_users is None and opts.roles):
541            _exitIfErrors(["You must specify lists of both users and roles for those" +
542                           " users.\n--set-users=[comma delimited user list] " +
543                           "--roles=[comma-delimited list of one or more from admin," +
544                           " ro_admin, cluster_admin, replication_admin, " +
545                           "bucket_admin[bucket name or '*'], views_admin[bucket" +
546                           " name or '*']"])
547
548        if opts.my_roles:
549            data, errors = rest.my_roles()
550            _exitIfErrors(errors)
551            print json.dumps(data, indent=2)
552        elif opts.get_roles:
553            data, errors = rest.list_rbac_users()
554            _exitIfErrors(errors)
555            print json.dumps(data, indent=2)
556        elif opts.set_users:
557            data, errors = rest.setRoles(opts.set_users, opts.roles, opts.set_names)
558            _exitIfErrors(errors)
559            _success("New users and roles added")
560        else:
561            data, errors = rest.deleteRoles(opts.delete_users)
562            _exitIfErrors(errors)
563            _success("Users deleted")
564
565    @staticmethod
566    def get_man_page_name():
567        return "couchbase-cli-admin-role-manage" + ".1" if os.name != "nt" else ".html"
568
569    @staticmethod
570    def get_description():
571        return "Set access-control roles for users (Deprecated)"
572
573
574class ClusterInit(Subcommand):
575    """The cluster initialization subcommand"""
576
577    def __init__(self):
578        super(ClusterInit, self).__init__(True, True, "http://127.0.0.1:8091")
579        self.parser.prog = "couchbase-cli cluster-init"
580        group = self.parser.add_argument_group("Cluster initialization options")
581        group.add_argument("--cluster-username", dest="username", required=True,
582                           metavar="<username>", help="The cluster administrator username")
583        group.add_argument("--cluster-password", dest="password", required=True,
584                           metavar="<password>", help="Only compact the data files")
585        group.add_argument("--cluster-port", dest="port", type=(int),
586                           metavar="<port>", help="The cluster administration console port")
587        group.add_argument("--cluster-ramsize", dest="data_mem_quota", type=(int),
588                           metavar="<quota>", help="The data service memory quota in megabytes")
589        group.add_argument("--cluster-index-ramsize", dest="index_mem_quota", type=(int),
590                           metavar="<quota>", help="The index service memory quota in megabytes")
591        group.add_argument("--cluster-fts-ramsize", dest="fts_mem_quota", type=(int),
592                           metavar="<quota>",
593                           help="The full-text service memory quota in Megabytes")
594        group.add_argument("--cluster-eventing-ramsize", dest="eventing_mem_quota", type=(int),
595                           metavar="<quota>",
596                           help="The Eventing service memory quota in Megabytes")
597        group.add_argument("--cluster-analytics-ramsize", dest="cbas_mem_quota", type=(int),
598                           metavar="<quota>",
599                           help="The analytics service memory quota in Megabytes")
600        group.add_argument("--cluster-name", dest="name", metavar="<name>", help="The cluster name")
601        group.add_argument("--index-storage-setting", dest="index_storage_mode",
602                           choices=["default", "memopt"], metavar="<mode>",
603                           help="The index storage backend (Defaults to \"default)\"")
604        group.add_argument("--services", dest="services", default="data", metavar="<service_list>",
605                           help="The services to run on this server")
606
607    def execute(self, opts):
608        # We need to ensure that creating the REST username/password is the
609        # last REST API that is called because once that API succeeds the
610        # cluster is initialized and cluster-init cannot be run again.
611
612        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
613                              opts.cacert, opts.debug)
614
615        initialized, errors = rest.is_cluster_initialized()
616        _exitIfErrors(errors)
617        if initialized:
618            _exitIfErrors(["Cluster is already initialized, use setting-cluster to change settings"])
619
620        enterprise, errors = rest.is_enterprise()
621        _exitIfErrors(errors)
622
623        if not enterprise and opts.index_storage_mode == 'memopt':
624            _exitIfErrors(["memopt option for --index-storage-setting can only be configured on enterprise edition"])
625
626        services, errors = process_services(opts.services, enterprise)
627        _exitIfErrors(errors)
628
629        if 'kv' not in services.split(','):
630            _exitIfErrors(["Cannot set up first cluster node without the data service"])
631
632        if opts.data_mem_quota or opts.index_mem_quota or opts.fts_mem_quota or opts.cbas_mem_quota \
633                or opts.eventing_mem_quota or opts.name is not None:
634            _, errors = rest.set_pools_default(opts.data_mem_quota, opts.index_mem_quota, opts.fts_mem_quota,
635                                               opts.cbas_mem_quota, opts.eventing_mem_quota, opts.name)
636        _exitIfErrors(errors)
637
638        # Set the index storage mode
639        if not opts.index_storage_mode and 'index' in services.split(','):
640            opts.index_storage_mode = "default"
641
642        default = "plasma"
643        if not enterprise:
644            default = "forestdb"
645
646        if opts.index_storage_mode:
647            param = index_storage_mode_to_param(opts.index_storage_mode, default)
648            _, errors = rest.set_index_settings(param, None, None, None, None, None)
649            _exitIfErrors(errors)
650
651        # Setup services
652        _, errors = rest.setup_services(services)
653        _exitIfErrors(errors)
654
655        # Enable notifications
656        _, errors = rest.enable_notifications(True)
657        _exitIfErrors(errors)
658
659        # Setup Administrator credentials and Admin Console port
660        _, errors = rest.set_admin_credentials(opts.username, opts.password,
661                                               opts.port)
662        _exitIfErrors(errors)
663
664        _success("Cluster initialized")
665
666    @staticmethod
667    def get_man_page_name():
668        return "couchbase-cli-cluster-init" + ".1" if os.name != "nt" else ".html"
669
670    @staticmethod
671    def get_description():
672        return "Initialize a Couchbase cluster"
673
674
675class BucketCompact(Subcommand):
676    """The bucket compact subcommand"""
677
678    def __init__(self):
679        super(BucketCompact, self).__init__()
680        self.parser.prog = "couchbase-cli bucket-compact"
681        group = self.parser.add_argument_group("Bucket compaction options")
682        group.add_argument("--bucket", dest="bucket_name", metavar="<name>",
683                           help="The name of bucket to compact")
684        group.add_argument("--data-only", dest="data_only", action="store_true",
685                           help="Only compact the data files")
686        group.add_argument("--view-only", dest="view_only", action="store_true",
687                           help="Only compact the view files")
688
689    def execute(self, opts):
690        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
691                              opts.cacert, opts.debug)
692        check_cluster_initialized(rest)
693
694        bucket, errors = rest.get_bucket(opts.bucket_name)
695        _exitIfErrors(errors)
696
697        if bucket["bucketType"] != BUCKET_TYPE_COUCHBASE:
698            _exitIfErrors(["Cannot compact memcached buckets"])
699
700        _, errors = rest.compact_bucket(opts.bucket_name, opts.data_only, opts.view_only)
701        _exitIfErrors(errors)
702
703        _success("Bucket compaction started")
704
705    @staticmethod
706    def get_man_page_name():
707        return "couchbase-cli-bucket-compact" + ".1" if os.name != "nt" else ".html"
708
709    @staticmethod
710    def get_description():
711        return "Compact database and view data"
712
713
714class BucketCreate(Subcommand):
715    """The bucket create subcommand"""
716
717    def __init__(self):
718        super(BucketCreate, self).__init__()
719        self.parser.prog = "couchbase-cli bucket-create"
720        group = self.parser.add_argument_group("Bucket create options")
721        group.add_argument("--bucket", dest="bucket_name", metavar="<name>", required=True,
722                           help="The name of bucket to create")
723        group.add_argument("--bucket-type", dest="type", metavar="<type>", required=True,
724                           choices=["couchbase", "ephemeral", "memcached"],
725                           help="The bucket type (couchbase, ephemeral, or memcached)")
726        group.add_argument("--bucket-ramsize", dest="memory_quota", metavar="<quota>", type=(int),
727                           required=True, help="The amount of memory to allocate the bucket")
728        group.add_argument("--bucket-replica", dest="replica_count", metavar="<num>",
729                           choices=["0", "1", "2", "3"],
730                           help="The replica count for the bucket")
731        group.add_argument("--bucket-priority", dest="priority", metavar="<priority>",
732                           choices=[BUCKET_PRIORITY_LOW_STR, BUCKET_PRIORITY_HIGH_STR],
733                           help="The bucket disk io priority (low or high)")
734        group.add_argument("--bucket-eviction-policy", dest="eviction_policy", metavar="<policy>",
735                           choices=["valueOnly", "fullEviction", "noEviction", "nruEviction"],
736                           help="The bucket eviction policy")
737        group.add_argument("--conflict-resolution", dest="conflict_resolution", default=None,
738                           choices=["sequence", "timestamp"], metavar="<type>",
739                           help="The XDCR conflict resolution type (timestamp or sequence)")
740        group.add_argument("--max-ttl", dest="max_ttl", default=None, type=(int), metavar="<seconds>",
741                           help="Set the maximum TTL the bucket will accept")
742        group.add_argument("--compression-mode", dest="compression_mode",
743                           choices=["off", "passive", "active"], metavar="<mode>",
744                           help="Set the compression mode of the bucket")
745        group.add_argument("--enable-flush", dest="enable_flush", metavar="<0|1>",
746                           choices=["0", "1"], help="Enable bucket flush on this bucket (0 or 1)")
747        group.add_argument("--enable-index-replica", dest="replica_indexes", metavar="<0|1>",
748                           choices=["0", "1"], help="Enable replica indexes (0 or 1)")
749        group.add_argument("--wait", dest="wait", action="store_true",
750                           help="Wait for bucket creation to complete")
751
752    def execute(self, opts):
753        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
754                              opts.cacert, opts.debug)
755        check_cluster_initialized(rest)
756
757        enterprise, errors = rest.is_enterprise()
758        _exitIfErrors(errors)
759
760        if opts.max_ttl and not enterprise:
761            _exitIfErrors(["Maximum TTL can only be configured on enterprise edition"])
762        if opts.compression_mode and not enterprise:
763            _exitIfErrors(["Compression mode can only be configured on enterprise edition"])
764
765        if opts.type == "memcached":
766            if opts.replica_count is not None:
767                _exitIfErrors(["--bucket-replica cannot be specified for a memcached bucket"])
768            if opts.conflict_resolution is not None:
769                _exitIfErrors(["--conflict-resolution cannot be specified for a memcached bucket"])
770            if opts.replica_indexes is not None:
771                _exitIfErrors(["--enable-index-replica cannot be specified for a memcached bucket"])
772            if opts.priority is not None:
773                _exitIfErrors(["--bucket-priority cannot be specified for a memcached bucket"])
774            if opts.eviction_policy is not None:
775                _exitIfErrors(["--bucket-eviction-policy cannot be specified for a memcached bucket"])
776            if opts.max_ttl is not None:
777                _exitIfErrors(["--max-ttl cannot be specified for a memcached bucket"])
778            if opts.compression_mode is not None:
779                _exitIfErrors(["--compression-mode cannot be specified for a memcached bucket"])
780        elif opts.type == "ephemeral":
781            if opts.eviction_policy in ["valueOnly", "fullEviction"]:
782                _exitIfErrors(["--bucket-eviction-policy must either be noEviction or nruEviction"])
783        elif opts.type == "couchbase":
784            if opts.eviction_policy in ["noEviction", "nruEviction"]:
785                _exitIfErrors(["--bucket-eviction-policy must either be valueOnly or fullEviction"])
786
787        priority = None
788        if opts.priority is not None:
789            if opts.priority == BUCKET_PRIORITY_HIGH_STR:
790                priority = BUCKET_PRIORITY_HIGH_INT
791            elif opts.priority == BUCKET_PRIORITY_LOW_STR:
792                priority = BUCKET_PRIORITY_LOW_INT
793
794        conflict_resolution_type = None
795        if opts.conflict_resolution is not None:
796            if opts.conflict_resolution == "sequence":
797                conflict_resolution_type = "seqno"
798            elif opts.conflict_resolution == "timestamp":
799                conflict_resolution_type = "lww"
800
801        _, errors = rest.create_bucket(opts.bucket_name, opts.type, opts.memory_quota, opts.eviction_policy,
802                                       opts.replica_count, opts.replica_indexes, priority, conflict_resolution_type,
803                                       opts.enable_flush, opts.max_ttl, opts.compression_mode, opts.wait)
804        _exitIfErrors(errors)
805        _success("Bucket created")
806
807    @staticmethod
808    def get_man_page_name():
809        return "couchbase-cli-bucket-create" + ".1" if os.name != "nt" else ".html"
810
811    @staticmethod
812    def get_description():
813        return "Add a new bucket to the cluster"
814
815
816class BucketDelete(Subcommand):
817    """The bucket delete subcommand"""
818
819    def __init__(self):
820        super(BucketDelete, self).__init__()
821        self.parser.prog = "couchbase-cli bucket-delete"
822        group = self.parser.add_argument_group("Bucket delete options")
823        group.add_argument("--bucket", dest="bucket_name", metavar="<name>", required=True,
824                           help="The name of bucket to delete")
825
826    def execute(self, opts):
827        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
828                              opts.cacert, opts.debug)
829        check_cluster_initialized(rest)
830
831        _, errors = rest.get_bucket(opts.bucket_name)
832        _exitIfErrors(errors)
833
834        _, errors = rest.delete_bucket(opts.bucket_name)
835        _exitIfErrors(errors)
836
837        _success("Bucket deleted")
838
839    @staticmethod
840    def get_man_page_name():
841        return "couchbase-cli-bucket-delete" + ".1" if os.name != "nt" else ".html"
842
843    @staticmethod
844    def get_description():
845        return "Delete an existing bucket"
846
847
848class BucketEdit(Subcommand):
849    """The bucket edit subcommand"""
850
851    def __init__(self):
852        super(BucketEdit, self).__init__()
853        self.parser.prog = "couchbase-cli bucket-edit"
854        group = self.parser.add_argument_group("Bucket edit options")
855        group.add_argument("--bucket", dest="bucket_name", metavar="<name>", required=True,
856                           help="The name of bucket to create")
857        group.add_argument("--bucket-ramsize", dest="memory_quota", metavar="<quota>",
858                           type=(int), help="The amount of memory to allocate the bucket")
859        group.add_argument("--bucket-replica", dest="replica_count", metavar="<num>",
860                           choices=["0", "1", "2", "3"],
861                           help="The replica count for the bucket")
862        group.add_argument("--bucket-priority", dest="priority", metavar="<priority>",
863                           choices=["low", "high"], help="The bucket disk io priority (low or high)")
864        group.add_argument("--bucket-eviction-policy", dest="eviction_policy", metavar="<policy>",
865                           choices=["valueOnly", "fullEviction"],
866                           help="The bucket eviction policy (valueOnly or fullEviction)")
867        group.add_argument("--max-ttl", dest="max_ttl", default=None, type=(int), metavar="<seconds>",
868                           help="Set the maximum TTL the bucket will accept")
869        group.add_argument("--compression-mode", dest="compression_mode",
870                           choices=["off", "passive", "active"], metavar="<mode>",
871                           help="Set the compression mode of the bucket")
872        group.add_argument("--enable-flush", dest="enable_flush", metavar="<0|1>",
873                           choices=["0", "1"], help="Enable bucket flush on this bucket (0 or 1)")
874        group.add_argument("--remove-bucket-port", dest="remove_port", metavar="<0|1>",
875                           choices=["0", "1"], help="Removes the bucket-port setting")
876
877    def execute(self, opts):
878        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
879                              opts.cacert, opts.debug)
880        check_cluster_initialized(rest)
881
882        enterprise, errors = rest.is_enterprise()
883        _exitIfErrors(errors)
884
885        if opts.max_ttl and not enterprise:
886            _exitIfErrors(["Maximum TTL can only be configured on enterprise edition"])
887
888        if opts.compression_mode and not enterprise:
889            _exitIfErrors(["Compression mode can only be configured on enterprise edition"])
890
891        bucket, errors = rest.get_bucket(opts.bucket_name)
892        _exitIfErrors(errors)
893
894
895        if "bucketType" in bucket and bucket["bucketType"] == "memcached":
896            if opts.memory_quota is not None:
897                _exitIfErrors(["--bucket-ramsize cannot be specified for a memcached bucket"])
898            if opts.replica_count is not None:
899                _exitIfErrors(["--bucket-replica cannot be specified for a memcached bucket"])
900            if opts.priority is not None:
901                _exitIfErrors(["--bucket-priority cannot be specified for a memcached bucket"])
902            if opts.eviction_policy is not None:
903                _exitIfErrors(["--bucket-eviction-policy cannot be specified for a memcached bucket"])
904            if opts.max_ttl is not None:
905                _exitIfErrors(["--max-ttl cannot be specified for a memcached bucket"])
906            if opts.compression_mode is not None:
907                _exitIfErrors(["--compression-mode cannot be specified for a memcached bucket"])
908
909        priority = None
910        if opts.priority is not None:
911            if opts.priority == BUCKET_PRIORITY_HIGH_STR:
912                priority = BUCKET_PRIORITY_HIGH_INT
913            elif opts.priority == BUCKET_PRIORITY_LOW_STR:
914                priority = BUCKET_PRIORITY_LOW_INT
915
916
917        if opts.remove_port:
918            if opts.remove_port == '1':
919                opts.remove_port = True
920            else:
921                opts.remove_port = False
922
923        _, errors = rest.edit_bucket(opts.bucket_name, opts.memory_quota, opts.eviction_policy, opts.replica_count,
924                                     priority, opts.enable_flush, opts.max_ttl, opts.compression_mode, opts.remove_port)
925        _exitIfErrors(errors)
926
927        _success("Bucket edited")
928
929    @staticmethod
930    def get_man_page_name():
931        return "couchbase-cli-bucket-edit" + ".1" if os.name != "nt" else ".html"
932
933    @staticmethod
934    def get_description():
935        return "Modify settings for an existing bucket"
936
937
938class BucketFlush(Subcommand):
939    """The bucket edit subcommand"""
940
941    def __init__(self):
942        super(BucketFlush, self).__init__()
943        self.parser.prog = "couchbase-cli bucket-flush"
944        group = self.parser.add_argument_group("Bucket flush options")
945        group.add_argument("--bucket", dest="bucket_name", metavar="<name>", required=True,
946                           help="The name of bucket to delete")
947        group.add_argument("--force", dest="force", action="store_true",
948                           help="Execute the command without asking to confirm")
949
950    def execute(self, opts):
951        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
952                              opts.cacert, opts.debug)
953        check_cluster_initialized(rest)
954
955        _, errors = rest.get_bucket(opts.bucket_name)
956        _exitIfErrors(errors)
957
958        if not opts.force:
959            question = "Running this command will totally PURGE database data from disk. " + \
960                       "Do you really want to do it? (Yes/No)"
961            confirm = raw_input(question)
962            if confirm not in ('y', 'Y', 'yes', 'Yes'):
963                return
964
965        _, errors = rest.flush_bucket(opts.bucket_name)
966        _exitIfErrors(errors)
967
968        _success("Bucket flushed")
969
970    @staticmethod
971    def get_man_page_name():
972        return "couchbase-cli-bucket-flush" + ".1" if os.name != "nt" else ".html"
973
974    @staticmethod
975    def get_description():
976        return "Flush all data from disk for a given bucket"
977
978
979class BucketList(Subcommand):
980    """The bucket list subcommand"""
981
982    def __init__(self):
983        super(BucketList, self).__init__()
984        self.parser.prog = "couchbase-cli bucket-list"
985
986    def execute(self, opts):
987        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
988                              opts.cacert, opts.debug)
989        check_cluster_initialized(rest)
990
991        result, errors = rest.list_buckets(extended=True)
992        _exitIfErrors(errors)
993
994        if opts.output == 'json':
995            print result
996        else:
997            for bucket in result:
998                print '%s' % bucket['name']
999                print ' bucketType: %s' % bucket['bucketType']
1000                print ' numReplicas: %s' % bucket['replicaNumber']
1001                print ' ramQuota: %s' % bucket['quota']['ram']
1002                print ' ramUsed: %s' % bucket['basicStats']['memUsed']
1003
1004    @staticmethod
1005    def get_man_page_name():
1006        return "couchbase-cli-bucket-list" + ".1" if os.name != "nt" else ".html"
1007
1008    @staticmethod
1009    def get_description():
1010        return "List all buckets in a cluster"
1011
1012
1013class CollectLogsStart(Subcommand):
1014    """The collect-logs-start subcommand"""
1015
1016    def __init__(self):
1017        super(CollectLogsStart, self).__init__()
1018        self.parser.prog = "couchbase-cli collect-logs-start"
1019        group = self.parser.add_argument_group("Collect logs start options")
1020        group.add_argument("--all-nodes", dest="all_nodes", action="store_true",
1021                           default=False, help="Collect logs for all nodes")
1022        group.add_argument("--nodes", dest="nodes", metavar="<node_list>",
1023                           help="A comma separated list of nodes to collect logs from")
1024        group.add_argument("--redaction-level", dest="redaction_level", metavar="<none|partial>",
1025                           choices=["none", "partial"], help="Level of log redaction to apply")
1026        group.add_argument("--salt", dest="salt", metavar="<string>",
1027                           help="The salt to use to redact the log")
1028        group.add_argument("--output-directory", dest="output_dir", metavar="<directory>",
1029                           help="Output directory to place the generated logs file")
1030        group.add_argument("--temporary-directory", dest="tmp_dir", metavar="<directory>",
1031                           help="Temporary directory to use when generating the logs")
1032        group.add_argument("--upload", dest="upload", action="store_true",
1033                           default=False, help="Logs should be uploaded for Couchbase support")
1034        group.add_argument("--upload-host", dest="upload_host", metavar="<host>",
1035                           help="The host to upload logs to")
1036        group.add_argument("--upload-proxy", dest="upload_proxy", metavar="<proxy>",
1037                           help="The proxy to used to upload the logs via")
1038        group.add_argument("--customer", dest="upload_customer", metavar="<name>",
1039                           help="The name of the customer uploading logs")
1040        group.add_argument("--ticket", dest="upload_ticket", metavar="<num>",
1041                           help="The ticket number the logs correspond to")
1042
1043    def execute(self, opts):
1044        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1045                              opts.cacert, opts.debug)
1046        check_cluster_initialized(rest)
1047
1048        if not opts.nodes and not opts.all_nodes:
1049            _exitIfErrors(["Must specify either --all-nodes or --nodes"])
1050
1051        if opts.nodes and opts.all_nodes:
1052            _exitIfErrors(["Cannot specify both --all-nodes and --nodes"])
1053
1054        if opts.salt and opts.redaction_level != "partial":
1055            _exitIfErrors(["--redaction-level has to be set to 'partial' when --salt is specified"])
1056
1057        servers = opts.nodes
1058        if opts.all_nodes:
1059            servers = "*"
1060
1061        if opts.upload:
1062            if not opts.upload_host:
1063                _exitIfErrors(["--upload-host is required when --upload is specified"])
1064            if not opts.upload_customer:
1065                _exitIfErrors(["--upload-customer is required when --upload is specified"])
1066        else:
1067            if opts.upload_host:
1068                _warning("--upload-host has no effect with specifying --upload")
1069            if opts.upload_customer:
1070                _warning("--upload-customer has no effect with specifying --upload")
1071            if opts.upload_ticket:
1072                _warning("--upload_ticket has no effect with specifying --upload")
1073            if opts.upload_proxy:
1074                _warning("--upload_proxy has no effect with specifying --upload")
1075
1076        _, errors = rest.collect_logs_start(servers, opts.redaction_level, opts.salt, opts.output_dir, opts.tmp_dir,
1077                                            opts.upload, opts.upload_host, opts.upload_proxy, opts.upload_customer,
1078                                            opts.upload_ticket)
1079        _exitIfErrors(errors)
1080        _success("Log collection started")
1081
1082    @staticmethod
1083    def get_man_page_name():
1084        return "couchbase-cli-collect-logs-start" + ".1" if os.name != "nt" else ".html"
1085
1086    @staticmethod
1087    def get_description():
1088        return "Start cluster log collection"
1089
1090
1091class CollectLogsStatus(Subcommand):
1092    """The collect-logs-status subcommand"""
1093
1094    def __init__(self):
1095        super(CollectLogsStatus, self).__init__()
1096        self.parser.prog = "couchbase-cli collect-logs-status"
1097
1098    def execute(self, opts):
1099        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1100                              opts.cacert, opts.debug)
1101        check_cluster_initialized(rest)
1102
1103        tasks, errors = rest.get_tasks()
1104        _exitIfErrors(errors)
1105
1106        found = False
1107        for task in tasks:
1108            if isinstance(task, dict) and 'type' in task and task['type'] == 'clusterLogsCollection':
1109                found = True
1110                self._print_task(task)
1111
1112        if not found:
1113            print "No log collection tasks were found"
1114
1115    def _print_task(self, task):
1116        print "Status: %s" % task['status']
1117        if 'perNode' in task:
1118            print "Details:"
1119            for node, node_status in task["perNode"].iteritems():
1120                print '\tNode:', node
1121                print '\tStatus:', node_status['status']
1122                for field in ["path", "statusCode", "url", "uploadStatusCode", "uploadOutput"]:
1123                    if field in node_status:
1124                        print '\t', field, ":", node_status[field]
1125            print
1126
1127    @staticmethod
1128    def get_man_page_name():
1129        return "couchbase-cli-collect-logs-status" + ".1" if os.name != "nt" else ".html"
1130
1131    @staticmethod
1132    def get_description():
1133        return "View the status of cluster log collection"
1134
1135
1136class CollectLogsStop(Subcommand):
1137    """The collect-logs-stop subcommand"""
1138
1139    def __init__(self):
1140        super(CollectLogsStop, self).__init__()
1141        self.parser.prog = "couchbase-cli collect-logs-stop"
1142
1143    def execute(self, opts):
1144        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1145                              opts.cacert, opts.debug)
1146        check_cluster_initialized(rest)
1147
1148        _, errors = rest.collect_logs_stop()
1149        _exitIfErrors(errors)
1150
1151        _success("Log collection stopped")
1152
1153    @staticmethod
1154    def get_man_page_name():
1155        return "couchbase-cli-collect-logs-stop" + ".1" if os.name != "nt" else ".html"
1156
1157    @staticmethod
1158    def get_description():
1159        return "Stop cluster log collection"
1160
1161
1162class Failover(Subcommand):
1163    """The failover subcommand"""
1164
1165    def __init__(self):
1166        super(Failover, self).__init__()
1167        self.parser.prog = "couchbase-cli failover"
1168        group = self.parser.add_argument_group("Failover options")
1169        group.add_argument("--server-failover", dest="servers_to_failover", metavar="<server_list>",
1170                           required=True, help="A list of servers to fail over")
1171        group.add_argument("--force", dest="force", action="store_true",
1172                           help="Hard failover the server")
1173        group.add_argument("--no-progress-bar", dest="no_bar", action="store_true",
1174                           default=False, help="Disables the progress bar")
1175        group.add_argument("--no-wait", dest="wait", action="store_false",
1176                           default=True, help="Don't wait for rebalance completion")
1177
1178    def execute(self, opts):
1179        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1180                              opts.cacert, opts.debug)
1181        check_cluster_initialized(rest)
1182
1183        opts.servers_to_failover = apply_default_port(opts.servers_to_failover)
1184        if not opts.force and len(opts.servers_to_failover) != 1:
1185            _exitIfErrors(["Only one node at a time can be gracefully failed over"])
1186
1187        _, errors = rest.failover(opts.servers_to_failover, opts.force)
1188        _exitIfErrors(errors)
1189
1190        if not opts.force:
1191            _, errors = rest.rebalance([])
1192            _exitIfErrors(errors)
1193
1194            time.sleep(1)
1195
1196            if opts.wait:
1197                bar = TopologyProgressBar(rest, 'Gracefully failing over', opts.no_bar)
1198                errors = bar.show()
1199                _exitIfErrors(errors)
1200                _success("Server failed over")
1201            else:
1202                _success("Server failed over started")
1203
1204        else:
1205            _success("Server failed over")
1206
1207    @staticmethod
1208    def get_man_page_name():
1209        return "couchbase-cli-failover" + ".1" if os.name != "nt" else ".html"
1210
1211    @staticmethod
1212    def get_description():
1213        return "Failover one or more servers"
1214
1215
1216class GroupManage(Subcommand):
1217    """The group manage subcommand"""
1218
1219    def __init__(self):
1220        super(GroupManage, self).__init__()
1221        self.parser.prog = "couchbase-cli group-manage"
1222        group = self.parser.add_argument_group("Group manage options")
1223        group.add_argument("--create", dest="create", action="store_true",
1224                           default=None, help="Create a new server group")
1225        group.add_argument("--delete", dest="delete", action="store_true",
1226                           default=None, help="Delete a server group")
1227        group.add_argument("--list", dest="list", action="store_true",
1228                           default=None, help="List all server groups")
1229        group.add_argument("--rename", dest="rename", help="Rename a server group")
1230        group.add_argument("--group-name", dest="name", metavar="<name>",
1231                           help="The name of the server group")
1232        group.add_argument("--move-servers", dest="move_servers", metavar="<server_list>",
1233                           help="A list of servers to move between groups")
1234        group.add_argument("--from-group", dest="from_group", metavar="<group>",
1235                           help="The group to move servers from")
1236        group.add_argument("--to-group", dest="to_group", metavar="<group>",
1237                           help="The group to move servers to")
1238
1239    def execute(self, opts):
1240        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1241                              opts.cacert, opts.debug)
1242        check_cluster_initialized(rest)
1243
1244        cmds = [opts.create, opts.delete, opts.list, opts.rename, opts.move_servers]
1245        print cmds
1246        if sum(cmd is not None for cmd in cmds) == 0:
1247            _exitIfErrors(["Must specify one of the following: --create, " +
1248                           "--delete, --list, --move-servers, or --rename"])
1249        elif sum(cmd is not None for cmd in cmds) != 1:
1250            _exitIfErrors(["Only one of the following may be specified: --create" +
1251                           ", --delete, --list, --move-servers, or --rename"])
1252
1253        if opts.create:
1254            self._create(rest, opts)
1255        elif opts.delete:
1256            self._delete(rest, opts)
1257        elif opts.list:
1258            self._list(rest, opts)
1259        elif opts.rename:
1260            self._rename(rest, opts)
1261        elif opts.move_servers is not None:
1262            self._move(rest, opts)
1263
1264    def _create(self, rest, opts):
1265        if opts.name is None:
1266            _exitIfErrors(["--group-name is required with --create flag"])
1267        _, errors = rest.create_server_group(opts.name)
1268        _exitIfErrors(errors)
1269        _success("Server group created")
1270
1271    def _delete(self, rest, opts):
1272        if opts.name is None:
1273            _exitIfErrors(["--group-name is required with --delete flag"])
1274        _, errors = rest.delete_server_group(opts.name)
1275        _exitIfErrors(errors)
1276        _success("Server group deleted")
1277
1278    def _list(self, rest, opts):
1279        groups, errors = rest.get_server_groups()
1280        _exitIfErrors(errors)
1281
1282        found = False
1283        for group in groups["groups"]:
1284            if opts.name is None or opts.name == group['name']:
1285                found = True
1286                print '%s' % group['name']
1287                for node in group['nodes']:
1288                    print ' server: %s' % node["hostname"]
1289        if not found and opts.name:
1290            _exitIfErrors(["Invalid group name: %s" % opts.name])
1291
1292    def _move(self, rest, opts):
1293        if opts.from_group is None:
1294            _exitIfErrors(["--from-group is required with --move-servers"])
1295        if opts.to_group is None:
1296            _exitIfErrors(["--to-group is required with --move-servers"])
1297
1298        servers = apply_default_port(opts.move_servers)
1299        _, errors = rest.move_servers_between_groups(servers, opts.from_group, opts.to_group)
1300        _exitIfErrors(errors)
1301        _success("Servers moved between groups")
1302
1303    def _rename(self, rest, opts):
1304        if opts.name is None:
1305            _exitIfErrors(["--group-name is required with --rename option"])
1306        _, errors = rest.rename_server_group(opts.rename, opts.name)
1307        _exitIfErrors(errors)
1308        _success("Server group renamed")
1309
1310    @staticmethod
1311    def get_man_page_name():
1312        return "couchbase-cli-group-manage" + ".1" if os.name != "nt" else ".html"
1313
1314    @staticmethod
1315    def get_description():
1316        return "Manage server groups"
1317
1318
1319class HostList(Subcommand):
1320    """The host list subcommand"""
1321
1322    def __init__(self):
1323        super(HostList, self).__init__()
1324        self.parser.prog = "couchbase-cli host-list"
1325
1326    def execute(self, opts):
1327        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1328                              opts.cacert, opts.debug)
1329        result, errors = rest.pools('default')
1330        _exitIfErrors(errors)
1331
1332        for node in result['nodes']:
1333            print node['hostname']
1334
1335    @staticmethod
1336    def get_man_page_name():
1337        return "couchbase-cli-host-list" + ".1" if os.name != "nt" else ".html"
1338
1339    @staticmethod
1340    def get_description():
1341        return "List all hosts in a cluster"
1342
1343
1344class MasterPassword(LocalSubcommand):
1345    """The master password subcommand"""
1346
1347    def __init__(self):
1348        super(MasterPassword, self).__init__()
1349        self.parser.prog = "couchbase-cli master-password"
1350        group = self.parser.add_argument_group("Master password options")
1351        group.add_argument("--send-password", dest="send_password", metavar="<password>",
1352                           required=False, action=CBNonEchoedAction, envvar=None,
1353                           prompt_text="Enter master password:",
1354                           help="Sends the master password to start the server")
1355
1356    def execute(self, opts):
1357        if opts.send_password is not None:
1358            path = [CB_BIN_PATH, os.environ['PATH']]
1359            if os.name == 'posix':
1360                os.environ['PATH'] = ':'.join(path)
1361            else:
1362                os.environ['PATH'] = ';'.join(path)
1363
1364            cookiefile = os.path.join(opts.config_path, "couchbase-server.babysitter.cookie")
1365            cookie = _exit_on_file_read_failure(cookiefile, "The node is down").rstrip()
1366
1367            nodefile = os.path.join(opts.config_path, "couchbase-server.babysitter.node")
1368            node = _exit_on_file_read_failure(nodefile).rstrip()
1369
1370            self.prompt_for_master_pwd(node, cookie, opts.send_password)
1371        else :
1372            _exitIfErrors(["No parameters set"])
1373
1374    def prompt_for_master_pwd(self, node, cookie, password):
1375        if password == '':
1376            password = getpass.getpass("\nEnter master password:")
1377        password = "\"" + password.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
1378
1379        randChars = ''.join(random.choice(string.ascii_letters) for i in xrange(20))
1380        name = 'cb-%s@127.0.0.1' % randChars
1381
1382        instr = "Res = rpc:call('" + node + "', encryption_service, set_password, [" \
1383                + password + "]), io:format(\"~p~n\", [Res])."
1384        args = ["-noinput", "-name", name, "-setcookie", cookie, "-eval", \
1385                instr, "-run", "init", "stop"]
1386
1387        res, error = self.run_process("erl", args)
1388        res = res.strip(' \t\n\r')
1389
1390        if res == "ok":
1391            print "SUCCESS: Password accepted. Node started booting."
1392        elif res == "retry":
1393            print "Incorrect password."
1394            self.prompt_for_master_pwd(node, cookie, '')
1395        elif res == "{error,not_allowed}":
1396            _exitIfErrors(["Password was already supplied"])
1397        elif res == "{badrpc,nodedown}":
1398            _exitIfErrors(["The node is down"])
1399        else:
1400            _exitIfErrors(["Incorrect password. Node shuts down."])
1401
1402    def run_process(self, name, args):
1403        try:
1404            if os.name == "nt":
1405                name = name + ".exe"
1406
1407            args.insert(0, name)
1408            p = subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
1409            output = p.stdout.read()
1410            error = p.stderr.read()
1411            p.wait()
1412            rc = p.returncode
1413            return output, error
1414        except OSError:
1415            _exitIfErrors(["Could not locate the %s executable" % name])
1416
1417    @staticmethod
1418    def get_man_page_name():
1419        return "couchbase-cli-master-password" + ".1" if os.name != "nt" else ".html"
1420
1421    @staticmethod
1422    def get_description():
1423        return "Unlocking the master password"
1424
1425
1426class NodeInit(Subcommand):
1427    """The node initialization subcommand"""
1428
1429    def __init__(self):
1430        super(NodeInit, self).__init__()
1431        self.parser.prog = "couchbase-cli node-init"
1432        group = self.parser.add_argument_group("Node initialization options")
1433        group.add_argument("--node-init-data-path", dest="data_path", metavar="<path>",
1434                           help="The path to store database files")
1435        group.add_argument("--node-init-index-path", dest="index_path", metavar="<path>",
1436                           help="The path to store index files")
1437        group.add_argument("--node-init-analytics-path", dest="analytics_path", metavar="<path>", action="append",
1438                           help="The path to store analytics files (supply one parameter for each path desired)")
1439        group.add_argument("--node-init-java-home", dest="java_home", metavar="<path>",
1440                           help="The path of the Java Runtime Environment (JRE) to use on this server")
1441        group.add_argument("--node-init-hostname", dest="hostname", metavar="<hostname>",
1442                           help="Sets the hostname for this server")
1443
1444    def execute(self, opts):
1445        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1446                              opts.cacert, opts.debug)
1447        # Cluster does not need to be initialized for this command
1448
1449        if opts.data_path is None and opts.index_path is None and opts.analytics_path is None \
1450            and opts.java_home is None and opts.hostname is None:
1451            _exitIfErrors(["No node initialization parameters specified"])
1452
1453        if opts.data_path or opts.index_path or opts.analytics_path or opts.java_home is not None:
1454            _, errors = rest.set_data_paths(opts.data_path, opts.index_path, opts.analytics_path, opts.java_home)
1455            _exitIfErrors(errors)
1456
1457        if opts.hostname:
1458            _, errors = rest.set_hostname(opts.hostname)
1459            _exitIfErrors(errors)
1460
1461        _success("Node initialized")
1462
1463    @staticmethod
1464    def get_man_page_name():
1465        return "couchbase-cli-node-init" + ".1" if os.name != "nt" else ".html"
1466
1467    @staticmethod
1468    def get_description():
1469        return "Set node specific settings"
1470
1471
1472class Rebalance(Subcommand):
1473    """The rebalance subcommand"""
1474
1475    def __init__(self):
1476        super(Rebalance, self).__init__()
1477        self.parser.prog = "couchbase-cli rebalance"
1478        group = self.parser.add_argument_group("Rebalance options")
1479        group.add_argument("--server-remove", dest="server_remove", metavar="<server_list>",
1480                           help="A list of servers to remove from the cluster")
1481        group.add_argument("--no-progress-bar", dest="no_bar", action="store_true",
1482                           default=False, help="Disables the progress bar")
1483        group.add_argument("--no-wait", dest="wait", action="store_false",
1484                           default=True, help="Don't wait for rebalance completion")
1485
1486    def execute(self, opts):
1487        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1488                              opts.cacert, opts.debug)
1489        check_cluster_initialized(rest)
1490
1491        eject_nodes = []
1492        if opts.server_remove:
1493            eject_nodes = apply_default_port(opts.server_remove)
1494
1495        _, errors = rest.rebalance(eject_nodes)
1496        _exitIfErrors(errors)
1497
1498        time.sleep(1)
1499
1500        if opts.wait:
1501            bar = TopologyProgressBar(rest, 'Rebalancing', opts.no_bar)
1502            errors = bar.show()
1503            _exitIfErrors(errors)
1504            _success("Rebalance complete")
1505        else:
1506            _success("Rebalance started")
1507
1508    @staticmethod
1509    def get_man_page_name():
1510        return "couchbase-cli-rebalance" + ".1" if os.name != "nt" else ".html"
1511
1512    @staticmethod
1513    def get_description():
1514        return "Start a cluster rebalancing"
1515
1516
1517class RebalanceStatus(Subcommand):
1518    """The rebalance status subcommand"""
1519
1520    def __init__(self):
1521        super(RebalanceStatus, self).__init__()
1522        self.parser.prog = "couchbase-cli rebalance-status"
1523
1524    def execute(self, opts):
1525        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1526                              opts.cacert, opts.debug)
1527        check_cluster_initialized(rest)
1528        status, errors = rest.rebalance_status()
1529        _exitIfErrors(errors)
1530
1531        print json.dumps(status, indent=2)
1532
1533    @staticmethod
1534    def get_man_page_name():
1535        return "couchbase-cli-rebalance-status" + ".1" if os.name != "nt" else ".html"
1536
1537    @staticmethod
1538    def get_description():
1539        return "Show rebalance status"
1540
1541
1542class RebalanceStop(Subcommand):
1543    """The rebalance stop subcommand"""
1544
1545    def __init__(self):
1546        super(RebalanceStop, self).__init__()
1547        self.parser.prog = "couchbase-cli rebalance-stop"
1548
1549    def execute(self, opts):
1550        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1551                              opts.cacert, opts.debug)
1552        check_cluster_initialized(rest)
1553        _, errors = rest.stop_rebalance()
1554        _exitIfErrors(errors)
1555
1556        _success("Rebalance stopped")
1557
1558    @staticmethod
1559    def get_man_page_name():
1560        return "couchbase-cli-rebalance-stop" + ".1" if os.name != "nt" else ".html"
1561
1562    @staticmethod
1563    def get_description():
1564        return "Stop a rebalance"
1565
1566
1567class Recovery(Subcommand):
1568    """The recovery command"""
1569
1570    def __init__(self):
1571        super(Recovery, self).__init__()
1572        self.parser.prog = "couchbase-cli recovery"
1573        group = self.parser.add_argument_group("Recovery options")
1574        group.add_argument("--server-recovery", dest="servers", metavar="<server_list>",
1575                           required=True, help="The list of servers to recover")
1576        group.add_argument("--recovery-type", dest="recovery_type", metavar="type",
1577                           choices=["delta", "full"], default="delta",
1578                           help="The recovery type (delta or full)")
1579
1580    def execute(self, opts):
1581        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1582                              opts.cacert, opts.debug)
1583        check_cluster_initialized(rest)
1584
1585        servers = apply_default_port(opts.servers)
1586        for server in servers:
1587            _, errors = rest.recovery(server, opts.recovery_type)
1588            _exitIfErrors(errors)
1589
1590        _success("Servers recovered")
1591
1592    @staticmethod
1593    def get_man_page_name():
1594        return "couchbase-cli-recovery" + ".1" if os.name != "nt" else ".html"
1595
1596    @staticmethod
1597    def get_description():
1598        return "Recover one or more servers"
1599
1600
1601class ResetAdminPassword(LocalSubcommand):
1602    """The reset admin password command"""
1603
1604    def __init__(self):
1605        super(ResetAdminPassword, self).__init__()
1606        self.parser.prog = "couchbase-cli reset-admin-password"
1607        group = self.parser.add_argument_group("Reset password options")
1608        group.add_argument("--new-password", dest="new_password", metavar="<password>",
1609                           required=False, action=CBNonEchoedAction, envvar=None,
1610                           prompt_text="Enter new administrator password:",
1611                           confirm_text="Confirm new administrator password:",
1612                           help="The new administrator password")
1613        group.add_argument("--regenerate", dest="regenerate", action="store_true",
1614                           help="Generates a random administrator password")
1615        group.add_argument("-P", "--port", metavar="<port>", default="8091",
1616                           help="The REST API port, defaults to 8091")
1617
1618    def execute(self, opts):
1619        token = _exit_on_file_read_failure(os.path.join(opts.config_path, "localtoken")).rstrip()
1620        rest = ClusterManager("http://127.0.0.1:" + opts.port, "@localtoken", token)
1621        check_cluster_initialized(rest)
1622
1623        if opts.new_password is not None and opts.regenerate == True:
1624            _exitIfErrors(["Cannot specify both --new-password and --regenerate at the same time"])
1625        elif opts.new_password is not None:
1626            _, errors = rest.set_admin_password(opts.new_password)
1627            _exitIfErrors(errors)
1628            _success("Administrator password changed")
1629        elif opts.regenerate:
1630            result, errors = rest.regenerate_admin_password()
1631            _exitIfErrors(errors)
1632            print result["password"]
1633        else:
1634            _exitIfErrors(["No parameters specified"])
1635
1636    @staticmethod
1637    def get_man_page_name():
1638        return "couchbase-cli-reset-admin-password" + ".1" if os.name != "nt" else ".html"
1639
1640    @staticmethod
1641    def get_description():
1642        return "Resets the administrator password"
1643
1644
1645class ServerAdd(Subcommand):
1646    """The server add command"""
1647
1648    def __init__(self):
1649        super(ServerAdd, self).__init__()
1650        self.parser.prog = "couchbase-cli server-add"
1651        group = self.parser.add_argument_group("Server add options")
1652        group.add_argument("--server-add", dest="servers", metavar="<server_list>", required=True,
1653                           help="The list of servers to add")
1654        group.add_argument("--server-add-username", dest="server_username", metavar="<username>",
1655                           required=True, help="The username for the server to add")
1656        group.add_argument("--server-add-password", dest="server_password", metavar="<password>",
1657                           required=True, help="The password for the server to add")
1658        group.add_argument("--group-name", dest="group_name", metavar="<name>",
1659                           help="The server group to add this server into")
1660        group.add_argument("--services", dest="services", default="data", metavar="<services>",
1661                           help="The services this server will run")
1662        group.add_argument("--index-storage-setting", dest="index_storage_mode", metavar="<mode>",
1663                           choices=["default", "memopt"], help="The index storage mode")
1664
1665    def execute(self, opts):
1666        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1667                              opts.cacert, opts.debug)
1668        check_cluster_initialized(rest)
1669
1670        enterprise, errors = rest.is_enterprise()
1671        _exitIfErrors(errors)
1672
1673        if not enterprise and opts.index_storage_mode == 'memopt':
1674            _exitIfErrors(["memopt option for --index-storage-setting can only be configured on enterprise edition"])
1675
1676        opts.services, errors = process_services(opts.services, enterprise)
1677        _exitIfErrors(errors)
1678
1679        settings, errors = rest.index_settings()
1680        _exitIfErrors(errors)
1681
1682        if opts.index_storage_mode is None and settings['storageMode'] == "" and "index" in opts.services:
1683            opts.index_storage_mode = "default"
1684
1685        # For supporting the default index backend changing from forestdb to plasma in Couchbase 5.0
1686        default = "plasma"
1687        if opts.index_storage_mode == "default" and settings['storageMode'] == "forestdb" or not enterprise:
1688            default = "forestdb"
1689
1690        if opts.index_storage_mode:
1691            param = index_storage_mode_to_param(opts.index_storage_mode, default)
1692            _, errors = rest.set_index_settings(param, None, None, None, None, None)
1693            _exitIfErrors(errors)
1694
1695        servers = apply_default_port(opts.servers)
1696        for server in servers:
1697            _, errors = rest.add_server(server, opts.group_name, opts.server_username,
1698                                        opts.server_password, opts.services)
1699            _exitIfErrors(errors)
1700
1701        _success("Server added")
1702
1703    @staticmethod
1704    def get_man_page_name():
1705        return "couchbase-cli-server-add" + ".1" if os.name != "nt" else ".html"
1706
1707    @staticmethod
1708    def get_description():
1709        return "Add servers to the cluster"
1710
1711
1712class ServerEshell(Subcommand):
1713    """The server eshell subcommand"""
1714
1715    def __init__(self):
1716        super(ServerEshell, self).__init__()
1717        self.parser.prog = "couchbase-cli server-eshell"
1718        group = self.parser.add_argument_group("Server eshell options")
1719        group.add_argument("--vm", dest="vm", default="ns_server", metavar="<name>",
1720                           help="The vm to connect to")
1721        group.add_argument("--erl-path", dest="erl_path", metavar="<path>", default=CB_BIN_PATH,
1722                           help="Override the path to the erl executable")
1723
1724    def execute(self, opts):
1725        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1726                              opts.cacert, opts.debug)
1727        # Cluster does not need to be initialized for this command
1728
1729        result, errors = rest.node_info()
1730        _exitIfErrors(errors)
1731
1732        node = result['otpNode']
1733        cookie = result['otpCookie']
1734
1735        if opts.vm != 'ns_server':
1736            cookie, errors = rest.get_babysitter_cookie()
1737            _exitIfErrors(errors)
1738
1739            [short, _] = node.split('@')
1740
1741            if opts.vm == 'babysitter':
1742                node = 'babysitter_of_%s@127.0.0.1' % short
1743            elif opts.vm == 'couchdb':
1744                node = 'couchdb_%s@127.0.0.1' % short
1745            else:
1746                _exitIfErrors(["Unknown vm type `%s`" % opts.vm])
1747
1748        rand_chars = ''.join(random.choice(string.ascii_letters) for i in xrange(20))
1749        name = 'ctl-%s@127.0.0.1' % rand_chars
1750
1751        cb_erl = os.path.join(opts.erl_path, 'erl')
1752        if os.path.isfile(cb_erl):
1753            path = cb_erl
1754        else:
1755            _warning("Cannot locate Couchbase erlang. Attempting to use non-Couchbase erlang")
1756            path = 'erl'
1757
1758        try:
1759            subprocess.call([path, '-name', name, '-setcookie', cookie, '-hidden', '-remsh', node])
1760        except OSError:
1761            _exitIfErrors(["Unable to find the erl executable"])
1762
1763    @staticmethod
1764    def get_man_page_name():
1765        return "couchbase-cli-server-eshell" + ".1" if os.name != "nt" else ".html"
1766
1767    @staticmethod
1768    def get_description():
1769        return "Opens a shell to the Couchbase cluster manager"
1770
1771    @staticmethod
1772    def is_hidden():
1773        # Internal command not recommended for production use
1774        return True
1775
1776
1777class ServerInfo(Subcommand):
1778    """The server info subcommand"""
1779
1780    def __init__(self):
1781        super(ServerInfo, self).__init__()
1782        self.parser.prog = "couchbase-cli server-info"
1783
1784    def execute(self, opts):
1785        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1786                              opts.cacert, opts.debug)
1787        # Cluster does not need to be initialized for this command
1788
1789        result, errors = rest.node_info()
1790        _exitIfErrors(errors)
1791
1792        print json.dumps(result, sort_keys=True, indent=2)
1793
1794    @staticmethod
1795    def get_man_page_name():
1796        return "couchbase-cli-server-info" + ".1" if os.name != "nt" else ".html"
1797
1798    @staticmethod
1799    def get_description():
1800        return "Show details of a node in the cluster"
1801
1802
1803class ServerList(Subcommand):
1804    """The server list subcommand"""
1805
1806    def __init__(self):
1807        super(ServerList, self).__init__()
1808        self.parser.prog = "couchbase-cli server-list"
1809
1810    def execute(self, opts):
1811        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1812                              opts.cacert, opts.debug)
1813        result, errors = rest.pools('default')
1814        _exitIfErrors(errors)
1815
1816        for node in result['nodes']:
1817            if node.get('otpNode') is None:
1818                raise Exception("could not access node")
1819
1820            print node['otpNode'], node['hostname'], node['status'], node['clusterMembership']
1821
1822    @staticmethod
1823    def get_man_page_name():
1824        return "couchbase-cli-server-list" + ".1" if os.name != "nt" else ".html"
1825
1826    @staticmethod
1827    def get_description():
1828        return "List all nodes in a cluster"
1829
1830
1831class ServerReadd(Subcommand):
1832    """The server readd subcommand (Deprecated)"""
1833
1834    def __init__(self):
1835        super(ServerReadd, self).__init__()
1836        self.parser.prog = "couchbase-cli server-readd"
1837        group = self.parser.add_argument_group("Server re-add options")
1838        group.add_argument("--server-add", dest="servers", metavar="<server_list>", required=True,
1839                           help="The list of servers to recover")
1840        # The parameters are unused, but kept for backwards compatibility
1841        group.add_argument("--server-username", dest="server_username", metavar="<username>",
1842                           help="The admin username for the server")
1843        group.add_argument("--server-password", dest="server_password", metavar="<password>",
1844                           help="The admin password for the server")
1845        group.add_argument("--group-name", dest="name", metavar="<name>",
1846                           help="The name of the server group")
1847
1848    def execute(self, opts):
1849        _deprecated("Please use the recovery command instead")
1850        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1851                              opts.cacert, opts.debug)
1852        check_cluster_initialized(rest)
1853
1854        servers = apply_default_port(opts.servers)
1855        for server in servers:
1856            _, errors = rest.readd_server(server)
1857            _exitIfErrors(errors)
1858
1859        _success("Servers recovered")
1860
1861    @staticmethod
1862    def get_man_page_name():
1863        return "couchbase-cli-server-readd" + ".1" if os.name != "nt" else ".html"
1864
1865    @staticmethod
1866    def get_description():
1867        return "Add failed server back to the cluster"
1868
1869    @staticmethod
1870    def is_hidden():
1871        # Deprecated command in 4.6, hidden in 5.0, pending removal
1872        return True
1873
1874
1875class SettingAlert(Subcommand):
1876    """The setting alert subcommand"""
1877
1878    def __init__(self):
1879        super(SettingAlert, self).__init__()
1880        self.parser.prog = "couchbase-cli setting-alert"
1881        group = self.parser.add_argument_group("Alert settings")
1882        group.add_argument("--enable-email-alert", dest="enabled", metavar="<1|0>", required=True,
1883                           choices=["0", "1"], help="Enable/disable email alerts")
1884        group.add_argument("--email-recipients", dest="email_recipients", metavar="<email_list>",
1885                           help="A comma separated list of email addresses")
1886        group.add_argument("--email-sender", dest="email_sender", metavar="<email_addr>",
1887                           help="The sender email address")
1888        group.add_argument("--email-user", dest="email_username", metavar="<username>",
1889                           default="", help="The email server username")
1890        group.add_argument("--email-password", dest="email_password", metavar="<password>",
1891                           default="", help="The email server password")
1892        group.add_argument("--email-host", dest="email_host", metavar="<host>",
1893                           help="The email server host")
1894        group.add_argument("--email-port", dest="email_port", metavar="<port>",
1895                           help="The email server port")
1896        group.add_argument("--enable-email-encrypt", dest="email_encrypt", metavar="<1|0>",
1897                           choices=["0", "1"], help="Enable SSL encryption for emails")
1898        group.add_argument("--alert-auto-failover-node", dest="alert_af_node",
1899                           action="store_true", help="Alert when a node is auto-failed over")
1900        group.add_argument("--alert-auto-failover-max-reached", dest="alert_af_max_reached",
1901                           action="store_true",
1902                           help="Alert when the max number of auto-failover nodes was reached")
1903        group.add_argument("--alert-auto-failover-node-down", dest="alert_af_node_down",
1904                           action="store_true",
1905                           help="Alert when a node wasn't auto-failed over because other nodes " +
1906                           "were down")
1907        group.add_argument("--alert-auto-failover-cluster-small", dest="alert_af_small",
1908                           action="store_true",
1909                           help="Alert when a node wasn't auto-failed over because cluster was" +
1910                           " too small")
1911        group.add_argument("--alert-auto-failover-disable", dest="alert_af_disable",
1912                           action="store_true",
1913                           help="Alert when a node wasn't auto-failed over because auto-failover" +
1914                           " is disabled")
1915        group.add_argument("--alert-ip-changed", dest="alert_ip_changed", action="store_true",
1916                           help="Alert when a nodes IP address changed")
1917        group.add_argument("--alert-disk-space", dest="alert_disk_space", action="store_true",
1918                           help="Alert when disk usage on a node reaches 90%%")
1919        group.add_argument("--alert-meta-overhead", dest="alert_meta_overhead", action="store_true",
1920                           help="Alert when metadata overhead is more than 50%%")
1921        group.add_argument("--alert-meta-oom", dest="alert_meta_oom", action="store_true",
1922                           help="Alert when all bucket memory is used for metadata")
1923        group.add_argument("--alert-write-failed", dest="alert_write_failed", action="store_true",
1924                           help="Alert when writing data to disk has failed")
1925        group.add_argument("--alert-audit-msg-dropped", dest="alert_audit_dropped",
1926                           action="store_true", help="Alert when writing event to audit log failed")
1927        group.add_argument("--alert-indexer-max-ram", dest="alert_indexer_max_ram",
1928                           action="store_true", help="Alert when indexer is using all of its allocated memory")
1929        group.add_argument("--alert-timestamp-drift-exceeded", dest="alert_cas_drift",
1930                           action="store_true", help="Alert when clocks on two servers are more than five seconds apart")
1931        group.add_argument("--alert-communication-issue", dest="alert_communication_issue",
1932                           action="store_true", help="Alert when nodes are experiencing communication issues")
1933
1934    def execute(self, opts):
1935        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1936                              opts.cacert, opts.debug)
1937        check_cluster_initialized(rest)
1938
1939        if opts.enabled == "1":
1940            if opts.email_recipients is None:
1941                _exitIfErrors(["--email-recipient must be set when email alerts are enabled"])
1942            if opts.email_sender is None:
1943                _exitIfErrors(["--email-sender must be set when email alerts are enabled"])
1944            if opts.email_host is None:
1945                _exitIfErrors(["--email-host must be set when email alerts are enabled"])
1946            if opts.email_port is None:
1947                _exitIfErrors(["--email-port must be set when email alerts are enabled"])
1948
1949        alerts = list()
1950        if opts.alert_af_node:
1951            alerts.append('auto_failover_node')
1952        if opts.alert_af_max_reached:
1953            alerts.append('auto_failover_maximum_reached')
1954        if opts.alert_af_node_down:
1955            alerts.append('auto_failover_other_nodes_down')
1956        if opts.alert_af_small:
1957            alerts.append('auto_failover_cluster_too_small')
1958        if opts.alert_af_disable:
1959            alerts.append('auto_failover_disabled')
1960        if opts.alert_ip_changed:
1961            alerts.append('ip')
1962        if opts.alert_disk_space:
1963            alerts.append('disk')
1964        if opts.alert_meta_overhead:
1965            alerts.append('overhead')
1966        if opts.alert_meta_oom:
1967            alerts.append('ep_oom_errors')
1968        if opts.alert_write_failed:
1969            alerts.append('ep_item_commit_failed')
1970        if opts.alert_audit_dropped:
1971            alerts.append('audit_dropped_events')
1972        if opts.alert_indexer_max_ram:
1973            alerts.append('indexer_ram_max_usage')
1974        if opts.alert_cas_drift:
1975            alerts.append('ep_clock_cas_drift_threshold_exceeded')
1976        if opts.alert_communication_issue:
1977            alerts.append('communication_issue')
1978
1979        enabled = "true"
1980        if opts.enabled == "0":
1981            enabled = "false"
1982
1983        email_encrypt = "false"
1984        if opts.email_encrypt == "1":
1985            email_encrypt = "true"
1986
1987        _, errors = rest.set_alert_settings(enabled, opts.email_recipients,
1988                                            opts.email_sender, opts.email_username,
1989                                            opts.email_password, opts.email_host,
1990                                            opts.email_port, email_encrypt,
1991                                            ",".join(alerts))
1992        _exitIfErrors(errors)
1993
1994        _success("Email alert settings modified")
1995
1996    @staticmethod
1997    def get_man_page_name():
1998        return "couchbase-cli-setting-alert" + ".1" if os.name != "nt" else ".html"
1999
2000    @staticmethod
2001    def get_description():
2002        return "Modify email alert settings"
2003
2004
2005class SettingAudit(Subcommand):
2006    """The settings audit subcommand"""
2007
2008    def __init__(self):
2009        super(SettingAudit, self).__init__()
2010        self.parser.prog = "couchbase-cli setting-audit"
2011        group = self.parser.add_argument_group("Audit settings")
2012        group.add_argument("--audit-enabled", dest="enabled", metavar="<1|0>", choices=["0", "1"],
2013                           help="Enable/disable auditing")
2014        group.add_argument("--audit-log-path", dest="log_path", metavar="<path>",
2015                           help="The audit log path")
2016        group.add_argument("--audit-log-rotate-interval", dest="rotate_interval", type=(int),
2017                           metavar="<seconds>", help="The audit log rotate interval")
2018        group.add_argument("--audit-log-rotate-size", dest="rotate_size", type=(int),
2019                           metavar="<bytes>", help="The audit log rotate size")
2020
2021    def execute(self, opts):
2022        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2023                              opts.cacert, opts.debug)
2024        check_cluster_initialized(rest)
2025
2026        if not (opts.enabled or opts.log_path or opts.rotate_interval or opts.rotate_size):
2027            _exitIfErrors(["No settings specified to be changed"])
2028
2029        if opts.enabled == "1":
2030            opts.enabled = "true"
2031        elif opts.enabled == "0":
2032            opts.enabled = "false"
2033
2034        _, errors = rest.set_audit_settings(opts.enabled, opts.log_path,
2035                                            opts.rotate_interval, opts.rotate_size)
2036        _exitIfErrors(errors)
2037
2038        _success("Audit settings modified")
2039
2040    @staticmethod
2041    def get_man_page_name():
2042        return "couchbase-cli-setting-audit" + ".1" if os.name != "nt" else ".html"
2043
2044    @staticmethod
2045    def get_description():
2046        return "Modify audit settings"
2047
2048
2049class SettingAutofailover(Subcommand):
2050    """The settings auto-failover subcommand"""
2051
2052    def __init__(self):
2053        super(SettingAutofailover, self).__init__()
2054        self.parser.prog = "couchbase-cli setting-autofailover"
2055        group = self.parser.add_argument_group("Auto-failover settings")
2056        group.add_argument("--enable-auto-failover", dest="enabled", metavar="<1|0>",
2057                           choices=["0", "1"], help="Enable/disable auto-failover")
2058        group.add_argument("--auto-failover-timeout", dest="timeout", metavar="<seconds>",
2059                           type=(int), help="The auto-failover timeout")
2060        group.add_argument("--enable-failover-of-server-groups", dest="enableFailoverOfServerGroups", metavar="<1|0>",
2061                           choices=["0", "1"], help="Enable/disable auto-failover of server Groups")
2062        group.add_argument("--max-failovers ", dest="maxFailovers", metavar="<1|2|3>", choices=["1", "2", "3"],
2063                           help="Maximum number of times an auto-failover event can happen")
2064        group.add_argument("--enable-failover-on-data-disk-issues", dest="enableFailoverOnDataDiskIssues",
2065                           metavar="<1|0>", choices=["0", "1"],
2066                           help="Enable/disable auto-failover when the Data Service reports disk issues")
2067        group.add_argument("--failover-data-disk-period", dest="failoverOnDataDiskPeriod",
2068                           metavar="<seconds>", type=(int),
2069                           help="The amount of time the Data Serivce disk failures has to be happening for to trigger"
2070                                " an auto-failover")
2071
2072    def execute(self, opts):
2073        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2074                              opts.cacert, opts.debug)
2075        check_cluster_initialized(rest)
2076
2077        if opts.enabled == "1":
2078            opts.enabled = "true"
2079        elif opts.enabled == "0":
2080            opts.enabled = "false"
2081
2082        if opts.enableFailoverOnDataDiskIssues == "1":
2083            opts.enableFailoverOnDataDiskIssues = "true"
2084        elif opts.enableFailoverOnDataDiskIssues == "0":
2085            opts.enableFailoverOnDataDiskIssues = "false"
2086
2087        if opts.enableFailoverOfServerGroups == "1":
2088            opts.enableFailoverOfServerGroups = "true"
2089        elif opts.enableFailoverOfServerGroups == "0":
2090            opts.enableFailoverOfServerGroups = "false"
2091
2092        enterprise, errors = rest.is_enterprise()
2093        _exitIfErrors(errors)
2094
2095        if not enterprise:
2096            if opts.enableFailoverOfServerGroups:
2097                _exitIfErrors(["--enable-failover-of-server-groups can only be configured on enterprise edition"])
2098            if opts.enableFailoverOnDataDiskIssues or opts.failoverOnDataDiskPeriod:
2099                _exitIfErrors(["Auto failover on Data Service disk issues can only be configured on enterprise edition"])
2100            if opts.maxFailovers:
2101                _exitIfErrors(["--max-count can only be configured on enterprise edition"])
2102
2103        if not any([opts.enabled, opts.timeout, opts.enableFailoverOnDataDiskIssues, opts.failoverOnDataDiskPeriod,
2104                    opts.enableFailoverOfServerGroups, opts.maxFailovers]):
2105            _exitIfErrors(["No settings specified to be changed"])
2106
2107        if ((opts.enableFailoverOnDataDiskIssues is None or opts.enableFailoverOnDataDiskIssues == "false")
2108            and opts.failoverOnDataDiskPeriod):
2109            _exitIfErrors(["--enable-failover-on-data-disk-issues must be set to 1 when auto-failover Data"
2110                           " Service disk period has been set"])
2111
2112        if opts.enableFailoverOnDataDiskIssues and opts.failoverOnDataDiskPeriod is None:
2113            _exitIfErrors(["--failover-data-disk-period must be set when auto-failover on Data Service disk"
2114                           " is enabled"])
2115
2116        if opts.enabled == "false" or opts.enabled is None:
2117            if opts.enableFailoverOnDataDiskIssues or opts.failoverOnDataDiskPeriod:
2118                _exitIfErrors(["--enable-auto-failover must be set to 1 when auto-failover on Data Service disk issues"
2119                               " settings are being configured"])
2120            if opts.enableFailoverOfServerGroups:
2121                _exitIfErrors(["--enable-auto-failover must be set to 1 when enabling auto-failover of Server Groups"])
2122            if opts.timeout:
2123                _warning("Timeout specified will not take affect because auto-failover is being disabled")
2124
2125        _, errors = rest.set_autofailover_settings(opts.enabled, opts.timeout, opts.enableFailoverOfServerGroups,
2126                                                   opts.maxFailovers, opts.enableFailoverOnDataDiskIssues,
2127                                                   opts.failoverOnDataDiskPeriod)
2128        _exitIfErrors(errors)
2129
2130        _success("Auto-failover settings modified")
2131
2132    @staticmethod
2133    def get_man_page_name():
2134        return "couchbase-cli-setting-autofailover" + ".1" if os.name != "nt" else ".html"
2135
2136    @staticmethod
2137    def get_description():
2138        return "Modify auto failover settings"
2139
2140
2141class SettingAutoreprovision(Subcommand):
2142    """The settings auto-reprovision subcommand"""
2143
2144    def __init__(self):
2145        super(SettingAutoreprovision, self).__init__()
2146        self.parser.prog = "couchbase-cli setting-autoreprovision"
2147        group = self.parser.add_argument_group("Auto-reprovision settings")
2148        group.add_argument("--enabled", dest="enabled", metavar="<1|0>", required=True,
2149                           choices=["0", "1"], help="Enable/disable auto-reprovision")
2150        group.add_argument("--max-nodes", dest="max_nodes", metavar="<num>", type=(int),
2151                           help="The numbers of server that can be auto-reprovisioned before a rebalance")
2152
2153    def execute(self, opts):
2154        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2155                              opts.cacert, opts.debug)
2156        check_cluster_initialized(rest)
2157
2158        if opts.enabled == "1":
2159            opts.enabled = "true"
2160        elif opts.enabled == "0":
2161            opts.enabled = "false"
2162
2163        if opts.enabled and opts.max_nodes is None:
2164            _exitIfErrors(["--max-nodes must be specified if auto-reprovision is enabled"])
2165
2166        if not (opts.enabled or opts.max_nodes):
2167            _exitIfErrors(["No settings specified to be changed"])
2168
2169        if (opts.enabled is None or opts.enabled == "false") and opts.max_nodes:
2170            _warning("--max-servers will not take affect because auto-reprovision is being disabled")
2171
2172        _, errors = rest.set_autoreprovision_settings(opts.enabled, opts.max_nodes)
2173        _exitIfErrors(errors)
2174
2175        _success("Auto-reprovision settings modified")
2176
2177    @staticmethod
2178    def get_man_page_name():
2179        return "couchbase-cli-setting-autoreprovision" + ".1" if os.name != "nt" else ".html"
2180
2181    @staticmethod
2182    def get_description():
2183        return "Modify auto-reprovision settings"
2184
2185
2186class SettingCluster(Subcommand):
2187    """The settings cluster subcommand"""
2188
2189    def __init__(self):
2190        super(SettingCluster, self).__init__()
2191        self.parser.prog = "couchbase-cli setting-cluster"
2192        group = self.parser.add_argument_group("Cluster settings")
2193        group.add_argument("--cluster-username", dest="new_username", metavar="<username>",
2194                           help="The cluster administrator username")
2195        group.add_argument("--cluster-password", dest="new_password", metavar="<password>",
2196                           help="Only compact the data files")
2197        group.add_argument("--cluster-port", dest="port", type=(int), metavar="<port>",
2198                           help="The cluster administration console port")
2199        group.add_argument("--cluster-ramsize", dest="data_mem_quota", metavar="<quota>",
2200                           type=(int), help="The data service memory quota in megabytes")
2201        group.add_argument("--cluster-index-ramsize", dest="index_mem_quota", metavar="<quota>",
2202                           type=(int), help="The index service memory quota in megabytes")
2203        group.add_argument("--cluster-fts-ramsize", dest="fts_mem_quota", metavar="<quota>",
2204                           type=(int), help="The full-text service memory quota in megabytes")
2205        group.add_argument("--cluster-eventing-ramsize", dest="eventing_mem_quota", metavar="<quota>",
2206                           type=(int), help="The Eventing service memory quota in megabytes")
2207        group.add_argument("--cluster-analytics-ramsize", dest="cbas_mem_quota", metavar="<quota>",
2208                           type=(int), help="The analytics service memory quota in megabytes")
2209        group.add_argument("--cluster-name", dest="name", metavar="<name>", help="The cluster name")
2210
2211    def execute(self, opts):
2212        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2213                              opts.cacert, opts.debug)
2214        check_cluster_initialized(rest)
2215
2216        if opts.data_mem_quota or opts.index_mem_quota or opts.fts_mem_quota or opts.cbas_mem_quota \
2217                or opts.eventing_mem_quota or opts.name:
2218            _, errors = rest.set_pools_default(opts.data_mem_quota, opts.index_mem_quota, opts.fts_mem_quota,
2219                                               opts.cbas_mem_quota, opts.eventing_mem_quota, opts.name)
2220            _exitIfErrors(errors)
2221
2222        if opts.new_username or opts.new_password or opts.port:
2223            username = opts.username
2224            if opts.new_username:
2225                username = opts.new_username
2226
2227            password = opts.password
2228            if opts.new_password:
2229                password = opts.new_password
2230
2231            _, errors = rest.set_admin_credentials(username, password, opts.port)
2232            _exitIfErrors(errors)
2233
2234        _success("Cluster settings modified")
2235
2236    @staticmethod
2237    def get_man_page_name():
2238        return "couchbase-cli-setting-cluster" + ".1" if os.name != "nt" else ".html"
2239
2240    @staticmethod
2241    def get_description():
2242        return "Modify cluster settings"
2243
2244
2245class ClusterEdit(SettingCluster):
2246    """The cluster edit subcommand (Deprecated)"""
2247
2248    def __init__(self):
2249        super(ClusterEdit, self).__init__()
2250        self.parser.prog = "couchbase-cli cluster-edit"
2251
2252    def execute(self, opts):
2253        _deprecated("Please use the setting-cluster command instead")
2254        super(ClusterEdit, self).execute(opts)
2255
2256    @staticmethod
2257    def get_man_page_name():
2258        return "couchbase-cli-cluster-edit" + ".1" if os.name != "nt" else ".html"
2259
2260    @staticmethod
2261    def is_hidden():
2262        # Deprecated command in 4.6, hidden in 5.0, pending removal
2263        return True
2264
2265
2266class SettingCompaction(Subcommand):
2267    """The setting compaction subcommand"""
2268
2269    def __init__(self):
2270        super(SettingCompaction, self).__init__()
2271        self.parser.prog = "couchbase-cli setting-compaction"
2272        group = self.parser.add_argument_group("Compaction settings")
2273        group.add_argument("--compaction-db-percentage", dest="db_perc", metavar="<perc>",
2274                           type=(int),
2275                           help="Compacts the db once the fragmentation reaches this percentage")
2276        group.add_argument("--compaction-db-size", dest="db_size", metavar="<megabytes>",
2277                           type=(int),
2278                           help="Compacts db once the fragmentation reaches this size (MB)")
2279        group.add_argument("--compaction-view-percentage", dest="view_perc", metavar="<perc>",
2280                           type=(int),
2281                           help="Compacts the view once the fragmentation reaches this percentage")
2282        group.add_argument("--compaction-view-size", dest="view_size", metavar="<megabytes>",
2283                           type=(int),
2284                           help="Compacts view once the fragmentation reaches this size (MB)")
2285        group.add_argument("--compaction-period-from", dest="from_period", metavar="<HH:MM>",
2286                           help="Only run compaction after this time")
2287        group.add_argument("--compaction-period-to", dest="to_period", metavar="<HH:MM>",
2288                           help="Only run compaction before this time")
2289        group.add_argument("--enable-compaction-abort", dest="enable_abort", metavar="<1|0>",
2290                           choices=["0", "1"], help="Allow compactions to be aborted")
2291        group.add_argument("--enable-compaction-parallel", dest="enable_parallel", metavar="<1|0>",
2292                           choices=["0", "1"], help="Allow parallel compactions")
2293        group.add_argument("--metadata-purge-interval", dest="purge_interval", metavar="<float>",
2294                           type=(float), help="The metadata purge interval")
2295        group.add_argument("--gsi-compaction-mode", dest="gsi_mode",
2296                          choices=["append", "circular"],
2297                          help="Sets the gsi compaction mode (append or circular)")
2298        group.add_argument("--compaction-gsi-percentage", dest="gsi_perc", type=(int), metavar="<perc>",
2299                          help="Starts compaction once gsi file fragmentation has reached this percentage (Append mode only)")
2300        group.add_argument("--compaction-gsi-interval", dest="gsi_interval", metavar="<days>",
2301                          help="A comma separated list of days compaction can run (Circular mode only)")
2302        group.add_argument("--compaction-gsi-period-from", dest="gsi_from_period", metavar="<HH:MM>",
2303                          help="Allow gsi compaction to run after this time (Circular mode only)")
2304        group.add_argument("--compaction-gsi-period-to", dest="gsi_to_period", metavar="<HH:MM>",
2305                          help="Allow gsi compaction to run before this time (Circular mode only)")
2306        group.add_argument("--enable-gsi-compaction-abort", dest="enable_gsi_abort", metavar="<1|0>",
2307                          choices=["0", "1"],
2308                          help="Abort gsi compaction if when run outside of the accepted interaval (Circular mode only)")
2309
2310    def execute(self, opts):
2311        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2312                              opts.cacert, opts.debug)
2313        check_cluster_initialized(rest)
2314
2315        if opts.db_perc is not None and (opts.db_perc < 2 or opts.db_perc > 100):
2316            _exitIfErrors(["--compaction-db-percentage must be between 2 and 100"])
2317
2318        if opts.view_perc is not None and (opts.view_perc < 2 or opts.view_perc > 100):
2319            _exitIfErrors(["--compaction-view-percentage must be between 2 and 100"])
2320
2321        if opts.db_size is not None:
2322            if int(opts.db_size) < 1:
2323                _exitIfErrors(["--compaction-db-size must be between greater than 1 or infinity"])
2324            opts.db_size = int(opts.db_size) * 1024**2
2325
2326        if opts.view_size is not None:
2327            if int(opts.view_size) < 1:
2328                _exitIfErrors(["--compaction-view-size must be between greater than 1 or infinity"])
2329            opts.view_size = int(opts.view_size) * 1024**2
2330
2331        if opts.from_period and not (opts.to_period and opts.enable_abort):
2332            errors = []
2333            if opts.to_period is None:
2334                errors.append("--compaction-period-to is required when using --compaction-period-from")
2335            if opts.enable_abort is None:
2336                errors.append("--enable-compaction-abort is required when using --compaction-period-from")
2337            _exitIfErrors(errors)
2338
2339        if opts.to_period and not (opts.from_period and opts.enable_abort):
2340            errors = []
2341            if opts.from_period is None:
2342                errors.append("--compaction-period-from is required when using --compaction-period-to")
2343            if opts.enable_abort is None:
2344                errors.append("--enable-compaction-abort is required when using --compaction-period-to")
2345            _exitIfErrors(errors)
2346
2347        if opts.enable_abort and not (opts.from_period and opts.to_period):
2348            errors = []
2349            if opts.from_period is None:
2350                errors.append("--compaction-period-from is required when using --enable-compaction-abort")
2351            if opts.to_period is None:
2352                errors.append("--compaction-period-to is required when using --enable-compaction-abort")
2353            _exitIfErrors(errors)
2354
2355        from_hour, from_min = self._handle_timevalue(opts.from_period,
2356                                                     "--compaction-period-from")
2357        to_hour, to_min = self._handle_timevalue(opts.to_period, "--compaction-period-to")
2358
2359        if opts.enable_abort == "1":
2360            opts.enable_abort = "true"
2361        elif opts.enable_abort == "0":
2362            opts.enable_abort = "false"
2363
2364        if opts.enable_parallel == "1":
2365            opts.enable_parallel = "true"
2366        else:
2367            opts.enable_parallel = "false"
2368
2369        if opts.purge_interval is not None and (opts.purge_interval < 0.04 or opts.purge_interval > 60.0):\
2370            _exitIfErrors(["--metadata-purge-interval must be between 0.04 and 60.0"])
2371
2372        g_from_hour = None
2373        g_from_min = None
2374        g_to_hour = None
2375        g_to_min = None
2376        if opts.gsi_mode == "append":
2377            opts.gsi_mode = "full"
2378            if opts.gsi_perc is None:
2379                _exitIfErrors(["--compaction-gsi-percentage must be specified when" +
2380                               " --gsi-compaction-mode is set to append"])
2381        elif opts.gsi_mode == "circular":
2382            if opts.gsi_from_period is not None and opts.gsi_to_period is None:
2383                _exitIfErrors(["--compaction-gsi-period-to is required with --compaction-gsi-period-from"])
2384            if opts.gsi_to_period is not None and opts.gsi_from_period is None:
2385                _exitIfErrors(["--compaction-gsi-period-from is required with --compaction-gsi-period-to"])
2386
2387            g_from_hour, g_from_min = self._handle_timevalue(opts.gsi_from_period,
2388                                                             "--compaction-gsi-period-from")
2389            g_to_hour, g_to_min = self._handle_timevalue(opts.gsi_to_period,
2390                                                            "--compaction-gsi-period-to")
2391
2392            if opts.enable_gsi_abort == "1":
2393                opts.enable_gsi_abort = "true"
2394            else:
2395                opts.enable_gsi_abort = "false"
2396
2397        _, errors = rest.set_compaction_settings(opts.db_perc, opts.db_size, opts.view_perc,
2398                                                 opts.view_size, from_hour, from_min, to_hour,
2399                                                 to_min, opts.enable_abort, opts.enable_parallel,
2400                                                 opts.purge_interval, opts.gsi_mode,
2401                                                 opts.gsi_perc, opts.gsi_interval, g_from_hour,
2402                                                 g_from_min, g_to_hour, g_to_min,
2403                                                 opts.enable_gsi_abort)
2404        _exitIfErrors(errors)
2405
2406        _success("Compaction settings modified")
2407
2408    def _handle_timevalue(self, opt_value, opt_name):
2409        hour = None
2410        minute = None
2411        if opt_value:
2412            if opt_value.find(':') == -1:
2413                _exitIfErrors(["Invalid value for %s, must be in form XX:XX" % opt_name])
2414            hour, minute = opt_value.split(':', 1)
2415            try:
2416                hour = int(hour)
2417            except ValueError:
2418                _exitIfErrors(["Invalid hour value for %s, must be an integer" % opt_name])
2419            if hour not in range(24):
2420                _exitIfErrors(["Invalid hour value for %s, must be 0-23" % opt_name])
2421
2422            try:
2423                minute = int(minute)
2424            except ValueError:
2425                _exitIfErrors(["Invalid minute value for %s, must be an integer" % opt_name])
2426            if minute not in range(60):
2427                _exitIfErrors(["Invalid minute value for %s, must be 0-59" % opt_name])
2428        return hour, minute
2429
2430    @staticmethod
2431    def get_man_page_name():
2432        return "couchbase-cli-setting-compaction" + ".1" if os.name != "nt" else ".html"
2433
2434    @staticmethod
2435    def get_description():
2436        return "Modify auto-compaction settings"
2437
2438
2439class SettingIndex(Subcommand):
2440    """The setting index subcommand"""
2441
2442    def __init__(self):
2443        super(SettingIndex, self).__init__()
2444        self.parser.prog = "couchbase-cli setting-index"
2445        group = self.parser.add_argument_group("Index settings")
2446        group.add_argument("--index-max-rollback-points", dest="max_rollback", metavar="<num>",
2447                           type=(int), help="Max rollback points")
2448        group.add_argument("--index-stable-snapshot-interval", dest="stable_snap", type=(int),
2449                           metavar="<seconds>", help="Stable snapshot interval in seconds")
2450        group.add_argument("--index-memory-snapshot-interval", dest="mem_snap", metavar="<ms>",
2451                           type=(int), help="Stable snapshot interval in milliseconds")
2452        group.add_argument("--index-storage-setting", dest="storage_mode", metavar="<mode>",
2453                           choices=["default", "memopt"], help="The index storage backend")
2454        group.add_argument("--index-threads", dest="threads", metavar="<num>",
2455                           type=(int), help="The number of indexer threads")
2456        group.add_argument("--index-log-level", dest="log_level", metavar="<level>",
2457                           choices=["debug", "silent", "fatal", "error", "warn", "info", "verbose",
2458                                    "timing", "trace"],
2459                           help="The indexer log level")
2460
2461    def execute(self, opts):
2462        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2463                              opts.cacert, opts.debug)
2464        check_cluster_initialized(rest)
2465
2466        enterprise, errors = rest.is_enterprise()
2467        _exitIfErrors(errors)
2468
2469        if opts.max_rollback is None and opts.stable_snap is None \
2470            and opts.mem_snap is None and opts.storage_mode is None \
2471            and opts.threads is None and opts.log_level is None:
2472            _exitIfErrors(["No settings specified to be changed"])
2473
2474        settings, errors = rest.index_settings()
2475        _exitIfErrors(errors)
2476
2477        # For supporting the default index backend changing from forestdb to plasma in Couchbase 5.0
2478        default = "plasma"
2479        if opts.storage_mode == "default" and settings['storageMode'] == "forestdb" or not enterprise:
2480            default = "forestdb"
2481
2482        opts.storage_mode = index_storage_mode_to_param(opts.storage_mode, default)
2483        _, errors = rest.set_index_settings(opts.storage_mode, opts.max_rollback,
2484                                            opts.stable_snap, opts.mem_snap,
2485                                            opts.threads, opts.log_level)
2486        _exitIfErrors(errors)
2487
2488        _success("Indexer settings modified")
2489
2490    @staticmethod
2491    def get_man_page_name():
2492        return "couchbase-cli-setting-index" + ".1" if os.name != "nt" else ".html"
2493
2494    @staticmethod
2495    def get_description():
2496        return "Modify index settings"
2497
2498
2499class SettingLdap(Subcommand):
2500    """The setting ldap subcommand"""
2501
2502    def __init__(self):
2503        super(SettingLdap, self).__init__()
2504        self.parser.prog = "couchbase-cli setting-ldap"
2505        group = self.parser.add_argument_group("LDAP settings")
2506        group.add_argument("--ldap-enabled", dest="enabled", metavar="<1|0>", required=True,
2507                           choices=["0", "1"], help="Enable/disable LDAP")
2508        group.add_argument("--ldap-admins", dest="admins", metavar="<user_list>",
2509                           help="A comma separated list of full admins")
2510        group.add_argument("--ldap-roadmins", dest="roadmins", metavar="<user_list>",
2511                           help="A comma separated list of read only admins")
2512        group.add_argument("--ldap-default", dest="default", default="none",
2513                           choices=["admins", "roadmins", "none"], metavar="<default>",
2514                           help="Enable/disable LDAP")
2515
2516    def execute(self, opts):
2517        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2518                              opts.cacert, opts.debug)
2519        check_cluster_initialized(rest)
2520
2521        admins = ""
2522        if opts.admins:
2523            admins = opts.admins.replace(",", "\n")
2524
2525        ro_admins = ""
2526        if opts.roadmins:
2527            ro_admins = opts.roadmins.replace(",", "\n")
2528
2529        errors = None
2530        if opts.enabled == '1':
2531            if opts.default == 'admins':
2532                if ro_admins:
2533                    _warning("--ldap-ro-admins option ignored since default is read only admins")
2534                _, errors = rest.ldap_settings('true', ro_admins, None)
2535            elif opts.default == 'roadmins':
2536                if admins:
2537                    _warning("--ldap-admins option ignored since default is admins")
2538                _, errors = rest.ldap_settings('true', None, admins)
2539            else:
2540                _, errors = rest.ldap_settings('true', ro_admins, admins)
2541        else:
2542            if admins:
2543                _warning("--ldap-admins option ignored since ldap is being disabled")
2544            if ro_admins:
2545                _warning("--ldap-roadmins option ignored since ldap is being disabled")
2546            _, errors = rest.ldap_settings('false', "", "")
2547
2548        _exitIfErrors(errors)
2549
2550        _success("LDAP settings modified")
2551
2552    @staticmethod
2553    def get_man_page_name():
2554        return "couchbase-cli-setting-ldap" + ".1" if os.name != "nt" else ".html"
2555
2556    @staticmethod
2557    def get_description():
2558        return "Modify LDAP settings"
2559
2560
2561class SettingNotification(Subcommand):
2562    """The settings notification subcommand"""
2563
2564    def __init__(self):
2565        super(SettingNotification, self).__init__()
2566        self.parser.prog = "couchbase-cli setting-notification"
2567        group = self.parser.add_argument_group("Notification Settings")
2568        group.add_argument("--enable-notifications", dest="enabled", metavar="<1|0>", required=True,
2569                           choices=["0", "1"], help="Enables/disable notifications")
2570
2571    def execute(self, opts):
2572        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2573                              opts.cacert, opts.debug)
2574
2575        enabled = None
2576        if opts.enabled == "1":
2577            enabled = True
2578        elif opts.enabled == "0":
2579            enabled = False
2580
2581        _, errors = rest.enable_notifications(enabled)
2582        _exitIfErrors(errors)
2583
2584        _success("Notification settings updated")
2585
2586    @staticmethod
2587    def get_man_page_name():
2588        return "couchbase-cli-setting-notification" + ".1" if os.name != "nt" else ".html"
2589
2590    @staticmethod
2591    def get_description():
2592        return "Modify email notification settings"
2593
2594
2595class SettingPasswordPolicy(Subcommand):
2596    """The settings password policy subcommand"""
2597
2598    def __init__(self):
2599        super(SettingPasswordPolicy, self).__init__()
2600        self.parser.prog = "couchbase-cli setting-password-policy"
2601        group = self.parser.add_argument_group("Password Policy Settings")
2602        group.add_argument("--get", dest="get", action="store_true", default=False,
2603                           help="Get the current password policy")
2604        group.add_argument("--set", dest="set", action="store_true", default=False,
2605                           help="Set a new password policy")
2606        group.add_argument("--min-length", dest="min_length", type=(int), default=False,
2607                           metavar="<num>",
2608                           help="Specifies the minimum password length for new passwords")
2609        group.add_argument("--uppercase", dest="upper_case", action="store_true", default=False,
2610                           help="Specifies new passwords must contain an upper case character")
2611        group.add_argument("--lowercase", dest="lower_case", action="store_true", default=False,
2612                           help="Specifies new passwords must contain a lower case character")
2613        group.add_argument("--digit", dest="digit", action="store_true", default=False,
2614                           help="Specifies new passwords must at least one digit")
2615        group.add_argument("--special-char", dest="special_char", action="store_true", default=False,
2616                           help="Specifies new passwords must at least one special character")
2617
2618
2619    def execute(self, opts):
2620        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2621                              opts.cacert, opts.debug)
2622
2623
2624        actions = sum([opts.get, opts.set])
2625        if actions == 0:
2626            _exitIfErrors(["Must specify either --get or --set"])
2627        elif actions > 1:
2628            _exitIfErrors(["The --get and --set flags may not be specified at " +
2629                           "the same time"])
2630        elif opts.get:
2631            self._get(rest, opts)
2632        elif opts.set:
2633            self._set(rest, opts)
2634
2635    def _get(self, rest, opts):
2636        policy, errors = rest.get_password_policy()
2637        _exitIfErrors(errors)
2638        print json.dumps(policy, sort_keys=True, indent=2)
2639
2640    def _set(self, rest, opts):
2641        _, errors = rest.set_password_policy(opts.min_length, opts.upper_case, opts.lower_case,
2642                                             opts.digit, opts.special_char)
2643        _exitIfErrors(errors)
2644        _success("Password policy updated")
2645
2646    @staticmethod
2647    def get_man_page_name():
2648        return "couchbase-cli-setting-password-policy" + ".1" if os.name != "nt" else ".html"
2649
2650    @staticmethod
2651    def get_description():
2652        return "Modify the password policy"
2653
2654
2655class SettingSecurity(Subcommand):
2656    """The settings security subcommand"""
2657
2658    def __init__(self):
2659        super(SettingSecurity, self).__init__()
2660        self.parser.prog = "couchbase-cli setting-security"
2661        group = self.parser.add_argument_group("Cluster Security Settings")
2662        group.add_argument("--disable-http-ui", dest="disable_http_ui", metavar="<0|1>", choices=['0', '1'],
2663                           default=False, help="Disables access to the UI over HTTP (0 or 1)")
2664
2665
2666    def execute(self, opts):
2667        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2668                              opts.cacert, opts.debug)
2669
2670        errors = None
2671        if opts.disable_http_ui == '1':
2672            _, errors = rest.set_security_settings(True)
2673        else:
2674            _, errors = rest.set_security_settings(False)
2675        _exitIfErrors(errors)
2676        _success("Security policy updated")
2677
2678    @staticmethod
2679    def get_man_page_name():
2680        return "couchbase-cli-security-policy" + ".1" if os.name != "nt" else ".html"
2681
2682    @staticmethod
2683    def get_description():
2684        return "Modify security policies"
2685
2686
2687class SettingXdcr(Subcommand):
2688    """The setting xdcr subcommand"""
2689
2690    def __init__(self):
2691        super(SettingXdcr, self).__init__()
2692        self.parser.prog = "couchbase-cli setting-xdcr"
2693        group = self.parser.add_argument_group("XDCR Settings")
2694        group.add_argument("--checkpoint-interval", dest="chk_int", type=(int), metavar="<num>",
2695                           help="Intervals between checkpoints in seconds (60 to 14400)")
2696        group.add_argument("--worker-batch-size", dest="worker_batch_size", metavar="<num>",
2697                           type=(int), help="Doc batch size (500 to 10000)")
2698        group.add_argument("--doc-batch-size", dest="doc_batch_size", type=(int), metavar="<KB>",
2699                           help="Document batching size in KB (10 to 100000)")
2700        group.add_argument("--failure-restart-interval", dest="fail_interval", metavar="<seconds>",
2701                           type=(int),
2702                           help="Interval for restarting failed xdcr in seconds (1 to 300)")
2703        group.add_argument("--optimistic-replication-threshold", dest="rep_thresh", type=(int),
2704                           metavar="<bytes>",
2705                           help="Document body size threshold (bytes) to trigger optimistic " +
2706                           "replication")
2707        group.add_argument("--source-nozzle-per-node", dest="src_nozzles", metavar="<num>",
2708                           type=(int),
2709                           help="The number of source nozzles per source node (1 to 10)")
2710        group.add_argument("--target-nozzle-per-node", dest="dst_nozzles", metavar="<num>",
2711                           type=(int),
2712                           help="The number of outgoing nozzles per target node (1 to 10)")
2713        group.add_argument("--bandwidth-usage-limit", dest="usage_limit", type=(int),
2714                           metavar="<num>", help="The bandwidth usage limit in MB/Sec")
2715        group.add_argument("--enable-compression", dest="compression", metavar="<1|0>", choices=["1", "0"],
2716                           help="Enable/disable compression")
2717        group.add_argument("--log-level", dest="log_level", metavar="<level>",
2718                           choices=["Error", "Info", "Debug", "Trace"],
2719                           help="The XDCR log level")
2720        group.add_argument("--stats-interval", dest="stats_interval", metavar="<ms>",
2721                           help="The interval for statistics updates (in milliseconds)")
2722
2723    def execute(self, opts):
2724        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2725                              opts.cacert, opts.debug)
2726
2727        check_cluster_initialized(rest)
2728        enterprise, errors = rest.is_enterprise()
2729
2730        _exitIfErrors(errors)
2731        if not enterprise and opts.compression:
2732            _exitIfErrors(["--enable-compression can only be configured on enterprise edition"])
2733
2734        if opts.compression == "0":
2735            opts.compression = "None"
2736        elif opts.compression =="1":
2737            opts.compression = "Auto"
2738
2739        _, errors = rest.xdcr_global_settings(opts.chk_int, opts.worker_batch_size,
2740                                              opts.doc_batch_size, opts.fail_interval,
2741                                              opts.rep_thresh, opts.src_nozzles,
2742                                              opts.dst_nozzles, opts.usage_limit,
2743                                              opts.compression, opts.log_level,
2744                                              opts.stats_interval)
2745        _exitIfErrors(errors)
2746
2747        _success("Global XDCR settings updated")
2748
2749    @staticmethod
2750    def get_man_page_name():
2751        return "couchbase-cli-setting-xdcr" + ".1" if os.name != "nt" else ".html"
2752
2753    @staticmethod
2754    def get_description():
2755        return "Modify XDCR related settings"
2756
2757class SettingMasterPassword(Subcommand):
2758    """The setting master password subcommand"""
2759
2760    def __init__(self):
2761        super(SettingMasterPassword, self).__init__()
2762        self.parser.prog = "couchbase-cli setting-master-password"
2763        group = self.parser.add_argument_group("Master password options")
2764        group.add_argument("--new-password", dest="new_password", metavar="<password>",
2765                           required=False, action=CBNonEchoedAction, envvar=None,
2766                           prompt_text="Enter new master password:",
2767                           confirm_text="Confirm new master password:",
2768                           help="Sets a new master password")
2769        group.add_argument("--rotate-data-key", dest="rotate_data_key", action="store_true",
2770                           help="Rotates the master password data key")
2771
2772    def execute(self, opts):
2773        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2774                              opts.cacert, opts.debug)
2775
2776        if opts.new_password is not None:
2777            _, errors = rest.set_master_pwd(opts.new_password)
2778            _exitIfErrors(errors)
2779            _success("New master password set")
2780        elif opts.rotate_data_key == True:
2781            _, errors = rest.rotate_master_pwd()
2782            _exitIfErrors(errors)
2783            _success("Data key rotated")
2784        else:
2785            _exitIfErrors(["No parameters set"])
2786
2787    @staticmethod
2788    def get_man_page_name():
2789        return "couchbase-cli-setting-master-password" + ".1" if os.name != "nt" else ".html"
2790
2791    @staticmethod
2792    def get_description():
2793        return "Changing the settings of the master password"
2794
2795
2796class SslManage(Subcommand):
2797    """The user manage subcommand"""
2798
2799    def __init__(self):
2800        super(SslManage, self).__init__()
2801        self.parser.prog = "couchbase-cli ssl-manage"
2802        group = self.parser.add_argument_group("SSL manage options")
2803        group.add_argument("--cluster-cert-info", dest="cluster_cert", action="store_true",
2804                           default=False, help="Gets the cluster certificate")
2805        group.add_argument("--node-cert-info", dest="node_cert", action="store_true",
2806                           default=False, help="Gets the node certificate")
2807        group.add_argument("--regenerate-cert", dest="regenerate", metavar="<path>",
2808                           help="Regenerate the cluster certificate and save it to a file")
2809        group.add_argument("--set-node-certificate", dest="set_cert", action="store_true",
2810                           default=False, help="Sets the node certificate")
2811        group.add_argument("--upload-cluster-ca", dest="upload_cert", metavar="<path>",
2812                           help="Upload a new cluster certificate")
2813        group.add_argument("--set-client-auth", dest="client_auth_path", metavar="<path>",
2814                           help="A path to a file containing the client auth configuration")
2815        group.add_argument("--client-auth", dest="show_client_auth", action="store_true",
2816                           help="Show ssl client certificate authentication value")
2817        group.add_argument("--extended", dest="extended", action="store_true",
2818                           default=False, help="Print extended certificate information")
2819
2820    def execute(self, opts):
2821        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2822                              opts.cacert, opts.debug)
2823        check_cluster_initialized(rest)
2824
2825        if opts.regenerate is not None:
2826            try:
2827                open(opts.regenerate, 'a').close()
2828            except IOError:
2829                _exitIfErrors(["Unable to create file at `%s`" % opts.regenerate])
2830            certificate, errors = rest.