xref: /6.0.3/couchbase-cli/cbmgr.py (revision e794237c)
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", "analytivs"]:
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-hostname", dest="hostname", metavar="<hostname>",
1440                           help="Sets the hostname for this server")
1441
1442    def execute(self, opts):
1443        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1444                              opts.cacert, opts.debug)
1445        # Cluster does not need to be initialized for this command
1446
1447        if opts.data_path is None and opts.index_path is None and opts.analytics_path is None and opts.hostname is None:
1448            _exitIfErrors(["No node initialization parameters specified"])
1449
1450        if opts.data_path or opts.index_path or opts.analytics_path:
1451            _, errors = rest.set_data_paths(opts.data_path, opts.index_path, opts.analytics_path)
1452            _exitIfErrors(errors)
1453
1454        if opts.hostname:
1455            _, errors = rest.set_hostname(opts.hostname)
1456            _exitIfErrors(errors)
1457
1458        _success("Node initialized")
1459
1460    @staticmethod
1461    def get_man_page_name():
1462        return "couchbase-cli-node-init" + ".1" if os.name != "nt" else ".html"
1463
1464    @staticmethod
1465    def get_description():
1466        return "Set node specific settings"
1467
1468
1469class Rebalance(Subcommand):
1470    """The rebalance subcommand"""
1471
1472    def __init__(self):
1473        super(Rebalance, self).__init__()
1474        self.parser.prog = "couchbase-cli rebalance"
1475        group = self.parser.add_argument_group("Rebalance options")
1476        group.add_argument("--server-remove", dest="server_remove", metavar="<server_list>",
1477                           help="A list of servers to remove from the cluster")
1478        group.add_argument("--no-progress-bar", dest="no_bar", action="store_true",
1479                           default=False, help="Disables the progress bar")
1480        group.add_argument("--no-wait", dest="wait", action="store_false",
1481                           default=True, help="Don't wait for rebalance completion")
1482
1483    def execute(self, opts):
1484        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1485                              opts.cacert, opts.debug)
1486        check_cluster_initialized(rest)
1487
1488        eject_nodes = []
1489        if opts.server_remove:
1490            eject_nodes = apply_default_port(opts.server_remove)
1491
1492        _, errors = rest.rebalance(eject_nodes)
1493        _exitIfErrors(errors)
1494
1495        time.sleep(1)
1496
1497        if opts.wait:
1498            bar = TopologyProgressBar(rest, 'Rebalancing', opts.no_bar)
1499            errors = bar.show()
1500            _exitIfErrors(errors)
1501            _success("Rebalance complete")
1502        else:
1503            _success("Rebalance started")
1504
1505    @staticmethod
1506    def get_man_page_name():
1507        return "couchbase-cli-rebalance" + ".1" if os.name != "nt" else ".html"
1508
1509    @staticmethod
1510    def get_description():
1511        return "Start a cluster rebalancing"
1512
1513
1514class RebalanceStatus(Subcommand):
1515    """The rebalance status subcommand"""
1516
1517    def __init__(self):
1518        super(RebalanceStatus, self).__init__()
1519        self.parser.prog = "couchbase-cli rebalance-status"
1520
1521    def execute(self, opts):
1522        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1523                              opts.cacert, opts.debug)
1524        check_cluster_initialized(rest)
1525        status, errors = rest.rebalance_status()
1526        _exitIfErrors(errors)
1527
1528        print json.dumps(status, indent=2)
1529
1530    @staticmethod
1531    def get_man_page_name():
1532        return "couchbase-cli-rebalance-status" + ".1" if os.name != "nt" else ".html"
1533
1534    @staticmethod
1535    def get_description():
1536        return "Show rebalance status"
1537
1538
1539class RebalanceStop(Subcommand):
1540    """The rebalance stop subcommand"""
1541
1542    def __init__(self):
1543        super(RebalanceStop, self).__init__()
1544        self.parser.prog = "couchbase-cli rebalance-stop"
1545
1546    def execute(self, opts):
1547        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1548                              opts.cacert, opts.debug)
1549        check_cluster_initialized(rest)
1550        _, errors = rest.stop_rebalance()
1551        _exitIfErrors(errors)
1552
1553        _success("Rebalance stopped")
1554
1555    @staticmethod
1556    def get_man_page_name():
1557        return "couchbase-cli-rebalance-stop" + ".1" if os.name != "nt" else ".html"
1558
1559    @staticmethod
1560    def get_description():
1561        return "Stop a rebalance"
1562
1563
1564class Recovery(Subcommand):
1565    """The recovery command"""
1566
1567    def __init__(self):
1568        super(Recovery, self).__init__()
1569        self.parser.prog = "couchbase-cli recovery"
1570        group = self.parser.add_argument_group("Recovery options")
1571        group.add_argument("--server-recovery", dest="servers", metavar="<server_list>",
1572                           required=True, help="The list of servers to recover")
1573        group.add_argument("--recovery-type", dest="recovery_type", metavar="type",
1574                           choices=["delta", "full"], default="delta",
1575                           help="The recovery type (delta or full)")
1576
1577    def execute(self, opts):
1578        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1579                              opts.cacert, opts.debug)
1580        check_cluster_initialized(rest)
1581
1582        servers = apply_default_port(opts.servers)
1583        for server in servers:
1584            _, errors = rest.recovery(server, opts.recovery_type)
1585            _exitIfErrors(errors)
1586
1587        _success("Servers recovered")
1588
1589    @staticmethod
1590    def get_man_page_name():
1591        return "couchbase-cli-recovery" + ".1" if os.name != "nt" else ".html"
1592
1593    @staticmethod
1594    def get_description():
1595        return "Recover one or more servers"
1596
1597
1598class ResetAdminPassword(LocalSubcommand):
1599    """The reset admin password command"""
1600
1601    def __init__(self):
1602        super(ResetAdminPassword, self).__init__()
1603        self.parser.prog = "couchbase-cli reset-admin-password"
1604        group = self.parser.add_argument_group("Reset password options")
1605        group.add_argument("--new-password", dest="new_password", metavar="<password>",
1606                           required=False, action=CBNonEchoedAction, envvar=None,
1607                           prompt_text="Enter new administrator password:",
1608                           confirm_text="Confirm new administrator password:",
1609                           help="The new administrator password")
1610        group.add_argument("--regenerate", dest="regenerate", action="store_true",
1611                           help="Generates a random administrator password")
1612        group.add_argument("-P", "--port", metavar="<port>", default="8091",
1613                           help="The REST API port, defaults to 8091")
1614
1615    def execute(self, opts):
1616        token = _exit_on_file_read_failure(os.path.join(opts.config_path, "localtoken")).rstrip()
1617        rest = ClusterManager("http://127.0.0.1:" + opts.port, "@localtoken", token)
1618        check_cluster_initialized(rest)
1619
1620        if opts.new_password is not None and opts.regenerate == True:
1621            _exitIfErrors(["Cannot specify both --new-password and --regenerate at the same time"])
1622        elif opts.new_password is not None:
1623            _, errors = rest.set_admin_password(opts.new_password)
1624            _exitIfErrors(errors)
1625            _success("Administrator password changed")
1626        elif opts.regenerate:
1627            result, errors = rest.regenerate_admin_password()
1628            _exitIfErrors(errors)
1629            print result["password"]
1630        else:
1631            _exitIfErrors(["No parameters specified"])
1632
1633    @staticmethod
1634    def get_man_page_name():
1635        return "couchbase-cli-reset-admin-password" + ".1" if os.name != "nt" else ".html"
1636
1637    @staticmethod
1638    def get_description():
1639        return "Resets the administrator password"
1640
1641
1642class ServerAdd(Subcommand):
1643    """The server add command"""
1644
1645    def __init__(self):
1646        super(ServerAdd, self).__init__()
1647        self.parser.prog = "couchbase-cli server-add"
1648        group = self.parser.add_argument_group("Server add options")
1649        group.add_argument("--server-add", dest="servers", metavar="<server_list>", required=True,
1650                           help="The list of servers to add")
1651        group.add_argument("--server-add-username", dest="server_username", metavar="<username>",
1652                           required=True, help="The username for the server to add")
1653        group.add_argument("--server-add-password", dest="server_password", metavar="<password>",
1654                           required=True, help="The password for the server to add")
1655        group.add_argument("--group-name", dest="group_name", metavar="<name>",
1656                           help="The server group to add this server into")
1657        group.add_argument("--services", dest="services", default="data", metavar="<services>",
1658                           help="The services this server will run")
1659        group.add_argument("--index-storage-setting", dest="storage_mode", metavar="<mode>",
1660                           choices=["default", "memopt"], help="The index storage mode")
1661
1662    def execute(self, opts):
1663        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1664                              opts.cacert, opts.debug)
1665        check_cluster_initialized(rest)
1666
1667        enterprise, errors = rest.is_enterprise()
1668        _exitIfErrors(errors)
1669
1670        if not enterprise and opts.index_storage_mode == 'memopt':
1671            _exitIfErrors(["memopt option for --index-storage-setting can only be configured on enterprise edition"])
1672
1673        opts.services, errors = process_services(opts.services, enterprise)
1674        _exitIfErrors(errors)
1675
1676        settings, errors = rest.index_settings()
1677        _exitIfErrors(errors)
1678
1679        if opts.storage_mode is None and settings['storageMode'] == "" and "index" in opts.services:
1680            opts.storage_mode = "default"
1681
1682        # For supporting the default index backend changing from forestdb to plasma in Couchbase 5.0
1683        default = "plasma"
1684        if opts.storage_mode == "default" and settings['storageMode'] == "forestdb" or not enterprise:
1685            default = "forestdb"
1686
1687        if opts.storage_mode:
1688            param = index_storage_mode_to_param(opts.storage_mode, default)
1689            _, errors = rest.set_index_settings(param, None, None, None, None, None)
1690            _exitIfErrors(errors)
1691
1692        servers = apply_default_port(opts.servers)
1693        for server in servers:
1694            _, errors = rest.add_server(server, opts.group_name, opts.server_username,
1695                                        opts.server_password, opts.services)
1696            _exitIfErrors(errors)
1697
1698        _success("Server added")
1699
1700    @staticmethod
1701    def get_man_page_name():
1702        return "couchbase-cli-server-add" + ".1" if os.name != "nt" else ".html"
1703
1704    @staticmethod
1705    def get_description():
1706        return "Add servers to the cluster"
1707
1708
1709class ServerEshell(Subcommand):
1710    """The server eshell subcommand"""
1711
1712    def __init__(self):
1713        super(ServerEshell, self).__init__()
1714        self.parser.prog = "couchbase-cli server-eshell"
1715        group = self.parser.add_argument_group("Server eshell options")
1716        group.add_argument("--vm", dest="vm", default="ns_server", metavar="<name>",
1717                           help="The vm to connect to")
1718        group.add_argument("--erl-path", dest="erl_path", metavar="<path>", default=CB_BIN_PATH,
1719                           help="Override the path to the erl executable")
1720
1721    def execute(self, opts):
1722        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1723                              opts.cacert, opts.debug)
1724        # Cluster does not need to be initialized for this command
1725
1726        result, errors = rest.node_info()
1727        _exitIfErrors(errors)
1728
1729        node = result['otpNode']
1730        cookie = result['otpCookie']
1731
1732        if opts.vm != 'ns_server':
1733            cookie, errors = rest.get_babysitter_cookie()
1734            _exitIfErrors(errors)
1735
1736            [short, _] = node.split('@')
1737
1738            if opts.vm == 'babysitter':
1739                node = 'babysitter_of_%s@127.0.0.1' % short
1740            elif opts.vm == 'couchdb':
1741                node = 'couchdb_%s@127.0.0.1' % short
1742            else:
1743                _exitIfErrors(["Unknown vm type `%s`" % opts.vm])
1744
1745        rand_chars = ''.join(random.choice(string.ascii_letters) for i in xrange(20))
1746        name = 'ctl-%s@127.0.0.1' % rand_chars
1747
1748        cb_erl = os.path.join(opts.erl_path, 'erl')
1749        if os.path.isfile(cb_erl):
1750            path = cb_erl
1751        else:
1752            _warning("Cannot locate Couchbase erlang. Attempting to use non-Couchbase erlang")
1753            path = 'erl'
1754
1755        try:
1756            subprocess.call([path, '-name', name, '-setcookie', cookie, '-hidden', '-remsh', node])
1757        except OSError:
1758            _exitIfErrors(["Unable to find the erl executable"])
1759
1760    @staticmethod
1761    def get_man_page_name():
1762        return "couchbase-cli-server-eshell" + ".1" if os.name != "nt" else ".html"
1763
1764    @staticmethod
1765    def get_description():
1766        return "Opens a shell to the Couchbase cluster manager"
1767
1768    @staticmethod
1769    def is_hidden():
1770        # Internal command not recommended for production use
1771        return True
1772
1773
1774class ServerInfo(Subcommand):
1775    """The server info subcommand"""
1776
1777    def __init__(self):
1778        super(ServerInfo, self).__init__()
1779        self.parser.prog = "couchbase-cli server-info"
1780
1781    def execute(self, opts):
1782        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1783                              opts.cacert, opts.debug)
1784        # Cluster does not need to be initialized for this command
1785
1786        result, errors = rest.node_info()
1787        _exitIfErrors(errors)
1788
1789        print json.dumps(result, sort_keys=True, indent=2)
1790
1791    @staticmethod
1792    def get_man_page_name():
1793        return "couchbase-cli-server-info" + ".1" if os.name != "nt" else ".html"
1794
1795    @staticmethod
1796    def get_description():
1797        return "Show details of a node in the cluster"
1798
1799
1800class ServerList(Subcommand):
1801    """The server list subcommand"""
1802
1803    def __init__(self):
1804        super(ServerList, self).__init__()
1805        self.parser.prog = "couchbase-cli server-list"
1806
1807    def execute(self, opts):
1808        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1809                              opts.cacert, opts.debug)
1810        result, errors = rest.pools('default')
1811        _exitIfErrors(errors)
1812
1813        for node in result['nodes']:
1814            if node.get('otpNode') is None:
1815                raise Exception("could not access node")
1816
1817            print node['otpNode'], node['hostname'], node['status'], node['clusterMembership']
1818
1819    @staticmethod
1820    def get_man_page_name():
1821        return "couchbase-cli-server-list" + ".1" if os.name != "nt" else ".html"
1822
1823    @staticmethod
1824    def get_description():
1825        return "List all nodes in a cluster"
1826
1827
1828class ServerReadd(Subcommand):
1829    """The server readd subcommand (Deprecated)"""
1830
1831    def __init__(self):
1832        super(ServerReadd, self).__init__()
1833        self.parser.prog = "couchbase-cli server-readd"
1834        group = self.parser.add_argument_group("Server re-add options")
1835        group.add_argument("--server-add", dest="servers", metavar="<server_list>", required=True,
1836                           help="The list of servers to recover")
1837        # The parameters are unused, but kept for backwards compatibility
1838        group.add_argument("--server-username", dest="server_username", metavar="<username>",
1839                           help="The admin username for the server")
1840        group.add_argument("--server-password", dest="server_password", metavar="<password>",
1841                           help="The admin password for the server")
1842        group.add_argument("--group-name", dest="name", metavar="<name>",
1843                           help="The name of the server group")
1844
1845    def execute(self, opts):
1846        _deprecated("Please use the recovery command instead")
1847        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1848                              opts.cacert, opts.debug)
1849        check_cluster_initialized(rest)
1850
1851        servers = apply_default_port(opts.servers)
1852        for server in servers:
1853            _, errors = rest.readd_server(server)
1854            _exitIfErrors(errors)
1855
1856        _success("Servers recovered")
1857
1858    @staticmethod
1859    def get_man_page_name():
1860        return "couchbase-cli-server-readd" + ".1" if os.name != "nt" else ".html"
1861
1862    @staticmethod
1863    def get_description():
1864        return "Add failed server back to the cluster"
1865
1866    @staticmethod
1867    def is_hidden():
1868        # Deprecated command in 4.6, hidden in 5.0, pending removal
1869        return True
1870
1871
1872class SettingAlert(Subcommand):
1873    """The setting alert subcommand"""
1874
1875    def __init__(self):
1876        super(SettingAlert, self).__init__()
1877        self.parser.prog = "couchbase-cli setting-alert"
1878        group = self.parser.add_argument_group("Alert settings")
1879        group.add_argument("--enable-email-alert", dest="enabled", metavar="<1|0>", required=True,
1880                           choices=["0", "1"], help="Enable/disable email alerts")
1881        group.add_argument("--email-recipients", dest="email_recipients", metavar="<email_list>",
1882                           help="A comma separated list of email addresses")
1883        group.add_argument("--email-sender", dest="email_sender", metavar="<email_addr>",
1884                           help="The sender email address")
1885        group.add_argument("--email-user", dest="email_username", metavar="<username>",
1886                           default="", help="The email server username")
1887        group.add_argument("--email-password", dest="email_password", metavar="<password>",
1888                           default="", help="The email server password")
1889        group.add_argument("--email-host", dest="email_host", metavar="<host>",
1890                           help="The email server host")
1891        group.add_argument("--email-port", dest="email_port", metavar="<port>",
1892                           help="The email server port")
1893        group.add_argument("--enable-email-encrypt", dest="email_encrypt", metavar="<1|0>",
1894                           choices=["0", "1"], help="Enable SSL encryption for emails")
1895        group.add_argument("--alert-auto-failover-node", dest="alert_af_node",
1896                           action="store_true", help="Alert when a node is auto-failed over")
1897        group.add_argument("--alert-auto-failover-max-reached", dest="alert_af_max_reached",
1898                           action="store_true",
1899                           help="Alert when the max number of auto-failover nodes was reached")
1900        group.add_argument("--alert-auto-failover-node-down", dest="alert_af_node_down",
1901                           action="store_true",
1902                           help="Alert when a node wasn't auto-failed over because other nodes " +
1903                           "were down")
1904        group.add_argument("--alert-auto-failover-cluster-small", dest="alert_af_small",
1905                           action="store_true",
1906                           help="Alert when a node wasn't auto-failed over because cluster was" +
1907                           " too small")
1908        group.add_argument("--alert-auto-failover-disable", dest="alert_af_disable",
1909                           action="store_true",
1910                           help="Alert when a node wasn't auto-failed over because auto-failover" +
1911                           " is disabled")
1912        group.add_argument("--alert-ip-changed", dest="alert_ip_changed", action="store_true",
1913                           help="Alert when a nodes IP address changed")
1914        group.add_argument("--alert-disk-space", dest="alert_disk_space", action="store_true",
1915                           help="Alert when disk usage on a node reaches 90%%")
1916        group.add_argument("--alert-meta-overhead", dest="alert_meta_overhead", action="store_true",
1917                           help="Alert when metadata overhead is more than 50%%")
1918        group.add_argument("--alert-meta-oom", dest="alert_meta_oom", action="store_true",
1919                           help="Alert when all bucket memory is used for metadata")
1920        group.add_argument("--alert-write-failed", dest="alert_write_failed", action="store_true",
1921                           help="Alert when writing data to disk has failed")
1922        group.add_argument("--alert-audit-msg-dropped", dest="alert_audit_dropped",
1923                           action="store_true", help="Alert when writing event to audit log failed")
1924        group.add_argument("--alert-indexer-max-ram", dest="alert_indexer_max_ram",
1925                           action="store_true", help="Alert when indexer is using all of its allocated memory")
1926        group.add_argument("--alert-timestamp-drift-exceeded", dest="alert_cas_drift",
1927                           action="store_true", help="Alert when clocks on two servers are more than five seconds apart")
1928        group.add_argument("--alert-communication-issue", dest="alert_communication_issue",
1929                           action="store_true", help="Alert when nodes are experiencing communication issues")
1930
1931    def execute(self, opts):
1932        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
1933                              opts.cacert, opts.debug)
1934        check_cluster_initialized(rest)
1935
1936        if opts.enabled == "1":
1937            if opts.email_recipients is None:
1938                _exitIfErrors(["--email-recipient must be set when email alerts are enabled"])
1939            if opts.email_sender is None:
1940                _exitIfErrors(["--email-sender must be set when email alerts are enabled"])
1941            if opts.email_host is None:
1942                _exitIfErrors(["--email-host must be set when email alerts are enabled"])
1943            if opts.email_port is None:
1944                _exitIfErrors(["--email-port must be set when email alerts are enabled"])
1945
1946        alerts = list()
1947        if opts.alert_af_node:
1948            alerts.append('auto_failover_node')
1949        if opts.alert_af_max_reached:
1950            alerts.append('auto_failover_maximum_reached')
1951        if opts.alert_af_node_down:
1952            alerts.append('auto_failover_other_nodes_down')
1953        if opts.alert_af_small:
1954            alerts.append('auto_failover_cluster_too_small')
1955        if opts.alert_af_disable:
1956            alerts.append('auto_failover_disabled')
1957        if opts.alert_ip_changed:
1958            alerts.append('ip')
1959        if opts.alert_disk_space:
1960            alerts.append('disk')
1961        if opts.alert_meta_overhead:
1962            alerts.append('overhead')
1963        if opts.alert_meta_oom:
1964            alerts.append('ep_oom_errors')
1965        if opts.alert_write_failed:
1966            alerts.append('ep_item_commit_failed')
1967        if opts.alert_audit_dropped:
1968            alerts.append('audit_dropped_events')
1969        if opts.alert_indexer_max_ram:
1970            alerts.append('indexer_ram_max_usage')
1971        if opts.alert_cas_drift:
1972            alerts.append('ep_clock_cas_drift_threshold_exceeded')
1973        if opts.alert_communication_issue:
1974            alerts.append('communication_issue')
1975
1976        enabled = "true"
1977        if opts.enabled == "0":
1978            enabled = "false"
1979
1980        email_encrypt = "false"
1981        if opts.email_encrypt == "1":
1982            email_encrypt = "true"
1983
1984        _, errors = rest.set_alert_settings(enabled, opts.email_recipients,
1985                                            opts.email_sender, opts.email_username,
1986                                            opts.email_password, opts.email_host,
1987                                            opts.email_port, email_encrypt,
1988                                            ",".join(alerts))
1989        _exitIfErrors(errors)
1990
1991        _success("Email alert settings modified")
1992
1993    @staticmethod
1994    def get_man_page_name():
1995        return "couchbase-cli-setting-alert" + ".1" if os.name != "nt" else ".html"
1996
1997    @staticmethod
1998    def get_description():
1999        return "Modify email alert settings"
2000
2001
2002class SettingAudit(Subcommand):
2003    """The settings audit subcommand"""
2004
2005    def __init__(self):
2006        super(SettingAudit, self).__init__()
2007        self.parser.prog = "couchbase-cli setting-audit"
2008        group = self.parser.add_argument_group("Audit settings")
2009        group.add_argument("--audit-enabled", dest="enabled", metavar="<1|0>", choices=["0", "1"],
2010                           help="Enable/disable auditing")
2011        group.add_argument("--audit-log-path", dest="log_path", metavar="<path>",
2012                           help="The audit log path")
2013        group.add_argument("--audit-log-rotate-interval", dest="rotate_interval", type=(int),
2014                           metavar="<seconds>", help="The audit log rotate interval")
2015        group.add_argument("--audit-log-rotate-size", dest="rotate_size", type=(int),
2016                           metavar="<bytes>", help="The audit log rotate size")
2017
2018    def execute(self, opts):
2019        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2020                              opts.cacert, opts.debug)
2021        check_cluster_initialized(rest)
2022
2023        if not (opts.enabled or opts.log_path or opts.rotate_interval or opts.rotate_size):
2024            _exitIfErrors(["No settings specified to be changed"])
2025
2026        if opts.enabled == "1":
2027            opts.enabled = "true"
2028        elif opts.enabled == "0":
2029            opts.enabled = "false"
2030
2031        _, errors = rest.set_audit_settings(opts.enabled, opts.log_path,
2032                                            opts.rotate_interval, opts.rotate_size)
2033        _exitIfErrors(errors)
2034
2035        _success("Audit settings modified")
2036
2037    @staticmethod
2038    def get_man_page_name():
2039        return "couchbase-cli-setting-audit" + ".1" if os.name != "nt" else ".html"
2040
2041    @staticmethod
2042    def get_description():
2043        return "Modify audit settings"
2044
2045
2046class SettingAutofailover(Subcommand):
2047    """The settings auto-failover subcommand"""
2048
2049    def __init__(self):
2050        super(SettingAutofailover, self).__init__()
2051        self.parser.prog = "couchbase-cli setting-autofailover"
2052        group = self.parser.add_argument_group("Auto-failover settings")
2053        group.add_argument("--enable-auto-failover", dest="enabled", metavar="<1|0>",
2054                           choices=["0", "1"], help="Enable/disable auto-failover")
2055        group.add_argument("--auto-failover-timeout", dest="timeout", metavar="<seconds>",
2056                           type=(int), help="The auto-failover timeout")
2057        group.add_argument("--enable-failover-of-server-groups", dest="enableFailoverOfServerGroups", metavar="<1|0>",
2058                           choices=["0", "1"], help="Enable/disable auto-failover of server Groups")
2059        group.add_argument("--max-failovers ", dest="maxFailovers", metavar="<1|2|3>", choices=["1", "2", "3"],
2060                           help="Maximum number of times an auto-failover event can happen")
2061        group.add_argument("--enable-failover-on-data-disk-issues", dest="enableFailoverOnDataDiskIssues",
2062                           metavar="<1|0>", choices=["0", "1"],
2063                           help="Enable/disable auto-failover when the Data Service reports disk issues")
2064        group.add_argument("--failover-data-disk-period", dest="failoverOnDataDiskPeriod",
2065                           metavar="<seconds>", type=(int),
2066                           help="The amount of time the Data Serivce disk failures has to be happening for to trigger"
2067                                " an auto-failover")
2068
2069    def execute(self, opts):
2070        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2071                              opts.cacert, opts.debug)
2072        check_cluster_initialized(rest)
2073
2074        if opts.enabled == "1":
2075            opts.enabled = "true"
2076        elif opts.enabled == "0":
2077            opts.enabled = "false"
2078
2079        if opts.enableFailoverOnDataDiskIssues == "1":
2080            opts.enableFailoverOnDataDiskIssues = "true"
2081        elif opts.enableFailoverOnDataDiskIssues == "0":
2082            opts.enableFailoverOnDataDiskIssues = "false"
2083
2084        if opts.enableFailoverOfServerGroups == "1":
2085            opts.enableFailoverOfServerGroups = "true"
2086        elif opts.enableFailoverOfServerGroups == "0":
2087            opts.enableFailoverOfServerGroups = "false"
2088
2089        enterprise, errors = rest.is_enterprise()
2090        _exitIfErrors(errors)
2091
2092        if not enterprise:
2093            if opts.enableFailoverOfServerGroups:
2094                _exitIfErrors(["--enable-failover-of-server-groups can only be configured on enterprise edition"])
2095            if opts.enableFailoverOnDataDiskIssues or opts.failoverOnDataDiskPeriod:
2096                _exitIfErrors(["Auto failover on Data Service disk issues can only be configured on enterprise edition"])
2097            if opts.maxFailovers:
2098                _exitIfErrors(["--max-count can only be configured on enterprise edition"])
2099
2100        if not any([opts.enabled, opts.timeout, opts.enableFailoverOnDataDiskIssues, opts.failoverOnDataDiskPeriod,
2101                    opts.enableFailoverOfServerGroups, opts.maxFailovers]):
2102            _exitIfErrors(["No settings specified to be changed"])
2103
2104        if ((opts.enableFailoverOnDataDiskIssues is None or opts.enableFailoverOnDataDiskIssues == "false")
2105            and opts.failoverOnDataDiskPeriod):
2106            _exitIfErrors(["--enable-failover-on-data-disk-issues must be set to 1 when auto-failover Data"
2107                           " Service disk period has been set"])
2108
2109        if opts.enableFailoverOnDataDiskIssues and opts.failoverOnDataDiskPeriod is None:
2110            _exitIfErrors(["--failover-data-disk-period must be set when auto-failover on Data Service disk"
2111                           " is enabled"])
2112
2113        if opts.enabled == "false" or opts.enabled is None:
2114            if opts.enableFailoverOnDataDiskIssues or opts.failoverOnDataDiskPeriod:
2115                _exitIfErrors(["--enable-auto-failover must be set to 1 when auto-failover on Data Service disk issues"
2116                               " settings are being configured"])
2117            if opts.enableFailoverOfServerGroups:
2118                _exitIfErrors(["--enable-auto-failover must be set to 1 when enabling auto-failover of Server Groups"])
2119            if opts.timeout:
2120                _warning("Timeout specified will not take affect because auto-failover is being disabled")
2121
2122        _, errors = rest.set_autofailover_settings(opts.enabled, opts.timeout, opts.enableFailoverOfServerGroups,
2123                                                   opts.maxFailovers, opts.enableFailoverOnDataDiskIssues,
2124                                                   opts.failoverOnDataDiskPeriod)
2125        _exitIfErrors(errors)
2126
2127        _success("Auto-failover settings modified")
2128
2129    @staticmethod
2130    def get_man_page_name():
2131        return "couchbase-cli-setting-autofailover" + ".1" if os.name != "nt" else ".html"
2132
2133    @staticmethod
2134    def get_description():
2135        return "Modify auto failover settings"
2136
2137
2138class SettingAutoreprovision(Subcommand):
2139    """The settings auto-reprovision subcommand"""
2140
2141    def __init__(self):
2142        super(SettingAutoreprovision, self).__init__()
2143        self.parser.prog = "couchbase-cli setting-autoreprovision"
2144        group = self.parser.add_argument_group("Auto-reprovision settings")
2145        group.add_argument("--enabled", dest="enabled", metavar="<1|0>", required=True,
2146                           choices=["0", "1"], help="Enable/disable auto-reprovision")
2147        group.add_argument("--max-nodes", dest="max_nodes", metavar="<num>", type=(int),
2148                           help="The numbers of server that can be auto-reprovisioned before a rebalance")
2149
2150    def execute(self, opts):
2151        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2152                              opts.cacert, opts.debug)
2153        check_cluster_initialized(rest)
2154
2155        if opts.enabled == "1":
2156            opts.enabled = "true"
2157        elif opts.enabled == "0":
2158            opts.enabled = "false"
2159
2160        if opts.enabled and opts.max_nodes is None:
2161            _exitIfErrors(["--max-nodes must be specified if auto-reprovision is enabled"])
2162
2163        if not (opts.enabled or opts.max_nodes):
2164            _exitIfErrors(["No settings specified to be changed"])
2165
2166        if (opts.enabled is None or opts.enabled == "false") and opts.max_nodes:
2167            _warning("--max-servers will not take affect because auto-reprovision is being disabled")
2168
2169        _, errors = rest.set_autoreprovision_settings(opts.enabled, opts.max_nodes)
2170        _exitIfErrors(errors)
2171
2172        _success("Auto-reprovision settings modified")
2173
2174    @staticmethod
2175    def get_man_page_name():
2176        return "couchbase-cli-setting-autoreprovision" + ".1" if os.name != "nt" else ".html"
2177
2178    @staticmethod
2179    def get_description():
2180        return "Modify auto-reprovision settings"
2181
2182
2183class SettingCluster(Subcommand):
2184    """The settings cluster subcommand"""
2185
2186    def __init__(self):
2187        super(SettingCluster, self).__init__()
2188        self.parser.prog = "couchbase-cli setting-cluster"
2189        group = self.parser.add_argument_group("Cluster settings")
2190        group.add_argument("--cluster-username", dest="new_username", metavar="<username>",
2191                           help="The cluster administrator username")
2192        group.add_argument("--cluster-password", dest="new_password", metavar="<password>",
2193                           help="Only compact the data files")
2194        group.add_argument("--cluster-port", dest="port", type=(int), metavar="<port>",
2195                           help="The cluster administration console port")
2196        group.add_argument("--cluster-ramsize", dest="data_mem_quota", metavar="<quota>",
2197                           type=(int), help="The data service memory quota in megabytes")
2198        group.add_argument("--cluster-index-ramsize", dest="index_mem_quota", metavar="<quota>",
2199                           type=(int), help="The index service memory quota in megabytes")
2200        group.add_argument("--cluster-fts-ramsize", dest="fts_mem_quota", metavar="<quota>",
2201                           type=(int), help="The full-text service memory quota in megabytes")
2202        group.add_argument("--cluster-eventing-ramsize", dest="eventing_mem_quota", metavar="<quota>",
2203                           type=(int), help="The Eventing service memory quota in megabytes")
2204        group.add_argument("--cluster-analytics-ramsize", dest="cbas_mem_quota", metavar="<quota>",
2205                           type=(int), help="The analytics service memory quota in megabytes")
2206        group.add_argument("--cluster-name", dest="name", metavar="<name>", help="The cluster name")
2207
2208    def execute(self, opts):
2209        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2210                              opts.cacert, opts.debug)
2211        check_cluster_initialized(rest)
2212
2213        if opts.data_mem_quota or opts.index_mem_quota or opts.fts_mem_quota or opts.cbas_mem_quota \
2214                or opts.eventing_mem_quota or opts.name:
2215            _, errors = rest.set_pools_default(opts.data_mem_quota, opts.index_mem_quota, opts.fts_mem_quota,
2216                                               opts.cbas_mem_quota, opts.eventing_mem_quota, opts.name)
2217            _exitIfErrors(errors)
2218
2219        if opts.new_username or opts.new_password or opts.port:
2220            username = opts.username
2221            if opts.new_username:
2222                username = opts.new_username
2223
2224            password = opts.password
2225            if opts.new_password:
2226                password = opts.new_password
2227
2228            _, errors = rest.set_admin_credentials(username, password, opts.port)
2229            _exitIfErrors(errors)
2230
2231        _success("Cluster settings modified")
2232
2233    @staticmethod
2234    def get_man_page_name():
2235        return "couchbase-cli-setting-cluster" + ".1" if os.name != "nt" else ".html"
2236
2237    @staticmethod
2238    def get_description():
2239        return "Modify cluster settings"
2240
2241
2242class ClusterEdit(SettingCluster):
2243    """The cluster edit subcommand (Deprecated)"""
2244
2245    def __init__(self):
2246        super(ClusterEdit, self).__init__()
2247        self.parser.prog = "couchbase-cli cluster-edit"
2248
2249    def execute(self, opts):
2250        _deprecated("Please use the setting-cluster command instead")
2251        super(ClusterEdit, self).execute(opts)
2252
2253    @staticmethod
2254    def get_man_page_name():
2255        return "couchbase-cli-cluster-edit" + ".1" if os.name != "nt" else ".html"
2256
2257    @staticmethod
2258    def is_hidden():
2259        # Deprecated command in 4.6, hidden in 5.0, pending removal
2260        return True
2261
2262
2263class SettingCompaction(Subcommand):
2264    """The setting compaction subcommand"""
2265
2266    def __init__(self):
2267        super(SettingCompaction, self).__init__()
2268        self.parser.prog = "couchbase-cli setting-compaction"
2269        group = self.parser.add_argument_group("Compaction settings")
2270        group.add_argument("--compaction-db-percentage", dest="db_perc", metavar="<perc>",
2271                           type=(int),
2272                           help="Compacts the db once the fragmentation reaches this percentage")
2273        group.add_argument("--compaction-db-size", dest="db_size", metavar="<megabytes>",
2274                           type=(int),
2275                           help="Compacts db once the fragmentation reaches this size (MB)")
2276        group.add_argument("--compaction-view-percentage", dest="view_perc", metavar="<perc>",
2277                           type=(int),
2278                           help="Compacts the view once the fragmentation reaches this percentage")
2279        group.add_argument("--compaction-view-size", dest="view_size", metavar="<megabytes>",
2280                           type=(int),
2281                           help="Compacts view once the fragmentation reaches this size (MB)")
2282        group.add_argument("--compaction-period-from", dest="from_period", metavar="<HH:MM>",
2283                           help="Only run compaction after this time")
2284        group.add_argument("--compaction-period-to", dest="to_period", metavar="<HH:MM>",
2285                           help="Only run compaction before this time")
2286        group.add_argument("--enable-compaction-abort", dest="enable_abort", metavar="<1|0>",
2287                           choices=["0", "1"], help="Allow compactions to be aborted")
2288        group.add_argument("--enable-compaction-parallel", dest="enable_parallel", metavar="<1|0>",
2289                           choices=["0", "1"], help="Allow parallel compactions")
2290        group.add_argument("--metadata-purge-interval", dest="purge_interval", metavar="<float>",
2291                           type=(float), help="The metadata purge interval")
2292        group.add_argument("--gsi-compaction-mode", dest="gsi_mode",
2293                          choices=["append", "circular"],
2294                          help="Sets the gsi compaction mode (append or circular)")
2295        group.add_argument("--compaction-gsi-percentage", dest="gsi_perc", type=(int), metavar="<perc>",
2296                          help="Starts compaction once gsi file fragmentation has reached this percentage (Append mode only)")
2297        group.add_argument("--compaction-gsi-interval", dest="gsi_interval", metavar="<days>",
2298                          help="A comma separated list of days compaction can run (Circular mode only)")
2299        group.add_argument("--compaction-gsi-period-from", dest="gsi_from_period", metavar="<HH:MM>",
2300                          help="Allow gsi compaction to run after this time (Circular mode only)")
2301        group.add_argument("--compaction-gsi-period-to", dest="gsi_to_period", metavar="<HH:MM>",
2302                          help="Allow gsi compaction to run before this time (Circular mode only)")
2303        group.add_argument("--enable-gsi-compaction-abort", dest="enable_gsi_abort", metavar="<1|0>",
2304                          choices=["0", "1"],
2305                          help="Abort gsi compaction if when run outside of the accepted interaval (Circular mode only)")
2306
2307    def execute(self, opts):
2308        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2309                              opts.cacert, opts.debug)
2310        check_cluster_initialized(rest)
2311
2312        if opts.db_perc is not None and (opts.db_perc < 2 or opts.db_perc > 100):
2313            _exitIfErrors(["--compaction-db-percentage must be between 2 and 100"])
2314
2315        if opts.view_perc is not None and (opts.view_perc < 2 or opts.view_perc > 100):
2316            _exitIfErrors(["--compaction-view-percentage must be between 2 and 100"])
2317
2318        if opts.db_size is not None:
2319            if int(opts.db_size) < 1:
2320                _exitIfErrors(["--compaction-db-size must be between greater than 1 or infinity"])
2321            opts.db_size = int(opts.db_size) * 1024**2
2322
2323        if opts.view_size is not None:
2324            if int(opts.view_size) < 1:
2325                _exitIfErrors(["--compaction-view-size must be between greater than 1 or infinity"])
2326            opts.view_size = int(opts.view_size) * 1024**2
2327
2328        if opts.from_period and not (opts.to_period and opts.enable_abort):
2329            errors = []
2330            if opts.to_period is None:
2331                errors.append("--compaction-period-to is required when using --compaction-period-from")
2332            if opts.enable_abort is None:
2333                errors.append("--enable-compaction-abort is required when using --compaction-period-from")
2334            _exitIfErrors(errors)
2335
2336        if opts.to_period and not (opts.from_period and opts.enable_abort):
2337            errors = []
2338            if opts.from_period is None:
2339                errors.append("--compaction-period-from is required when using --compaction-period-to")
2340            if opts.enable_abort is None:
2341                errors.append("--enable-compaction-abort is required when using --compaction-period-to")
2342            _exitIfErrors(errors)
2343
2344        if opts.enable_abort and not (opts.from_period and opts.to_period):
2345            errors = []
2346            if opts.from_period is None:
2347                errors.append("--compaction-period-from is required when using --enable-compaction-abort")
2348            if opts.to_period is None:
2349                errors.append("--compaction-period-to is required when using --enable-compaction-abort")
2350            _exitIfErrors(errors)
2351
2352        from_hour, from_min = self._handle_timevalue(opts.from_period,
2353                                                     "--compaction-period-from")
2354        to_hour, to_min = self._handle_timevalue(opts.to_period, "--compaction-period-to")
2355
2356        if opts.enable_abort == "1":
2357            opts.enable_abort = "true"
2358        elif opts.enable_abort == "0":
2359            opts.enable_abort = "false"
2360
2361        if opts.enable_parallel == "1":
2362            opts.enable_parallel = "true"
2363        else:
2364            opts.enable_parallel = "false"
2365
2366        if opts.purge_interval is not None and (opts.purge_interval < 0.04 or opts.purge_interval > 60.0):\
2367            _exitIfErrors(["--metadata-purge-interval must be between 0.04 and 60.0"])
2368
2369        g_from_hour = None
2370        g_from_min = None
2371        g_to_hour = None
2372        g_to_min = None
2373        if opts.gsi_mode == "append":
2374            opts.gsi_mode = "full"
2375            if opts.gsi_perc is None:
2376                _exitIfErrors(["--compaction-gsi-percentage must be specified when" +
2377                               " --gsi-compaction-mode is set to append"])
2378        elif opts.gsi_mode == "circular":
2379            if opts.gsi_from_period is not None and opts.gsi_to_period is None:
2380                _exitIfErrors(["--compaction-gsi-period-to is required with --compaction-gsi-period-from"])
2381            if opts.gsi_to_period is not None and opts.gsi_from_period is None:
2382                _exitIfErrors(["--compaction-gsi-period-from is required with --compaction-gsi-period-to"])
2383
2384            g_from_hour, g_from_min = self._handle_timevalue(opts.gsi_from_period,
2385                                                             "--compaction-gsi-period-from")
2386            g_to_hour, g_to_min = self._handle_timevalue(opts.gsi_to_period,
2387                                                            "--compaction-gsi-period-to")
2388
2389            if opts.enable_gsi_abort == "1":
2390                opts.enable_gsi_abort = "true"
2391            else:
2392                opts.enable_gsi_abort = "false"
2393
2394        _, errors = rest.set_compaction_settings(opts.db_perc, opts.db_size, opts.view_perc,
2395                                                 opts.view_size, from_hour, from_min, to_hour,
2396                                                 to_min, opts.enable_abort, opts.enable_parallel,
2397                                                 opts.purge_interval, opts.gsi_mode,
2398                                                 opts.gsi_perc, opts.gsi_interval, g_from_hour,
2399                                                 g_from_min, g_to_hour, g_to_min,
2400                                                 opts.enable_gsi_abort)
2401        _exitIfErrors(errors)
2402
2403        _success("Compaction settings modified")
2404
2405    def _handle_timevalue(self, opt_value, opt_name):
2406        hour = None
2407        minute = None
2408        if opt_value:
2409            if opt_value.find(':') == -1:
2410                _exitIfErrors(["Invalid value for %s, must be in form XX:XX" % opt_name])
2411            hour, minute = opt_value.split(':', 1)
2412            try:
2413                hour = int(hour)
2414            except ValueError:
2415                _exitIfErrors(["Invalid hour value for %s, must be an integer" % opt_name])
2416            if hour not in range(24):
2417                _exitIfErrors(["Invalid hour value for %s, must be 0-23" % opt_name])
2418
2419            try:
2420                minute = int(minute)
2421            except ValueError:
2422                _exitIfErrors(["Invalid minute value for %s, must be an integer" % opt_name])
2423            if minute not in range(60):
2424                _exitIfErrors(["Invalid minute value for %s, must be 0-59" % opt_name])
2425        return hour, minute
2426
2427    @staticmethod
2428    def get_man_page_name():
2429        return "couchbase-cli-setting-compaction" + ".1" if os.name != "nt" else ".html"
2430
2431    @staticmethod
2432    def get_description():
2433        return "Modify auto-compaction settings"
2434
2435
2436class SettingIndex(Subcommand):
2437    """The setting index subcommand"""
2438
2439    def __init__(self):
2440        super(SettingIndex, self).__init__()
2441        self.parser.prog = "couchbase-cli setting-index"
2442        group = self.parser.add_argument_group("Index settings")
2443        group.add_argument("--index-max-rollback-points", dest="max_rollback", metavar="<num>",
2444                           type=(int), help="Max rollback points")
2445        group.add_argument("--index-stable-snapshot-interval", dest="stable_snap", type=(int),
2446                           metavar="<seconds>", help="Stable snapshot interval in seconds")
2447        group.add_argument("--index-memory-snapshot-interval", dest="mem_snap", metavar="<ms>",
2448                           type=(int), help="Stable snapshot interval in milliseconds")
2449        group.add_argument("--index-storage-setting", dest="storage_mode", metavar="<mode>",
2450                           choices=["default", "memopt"], help="The index storage backend")
2451        group.add_argument("--index-threads", dest="threads", metavar="<num>",
2452                           type=(int), help="The number of indexer threads")
2453        group.add_argument("--index-log-level", dest="log_level", metavar="<level>",
2454                           choices=["debug", "silent", "fatal", "error", "warn", "info", "verbose",
2455                                    "timing", "trace"],
2456                           help="The indexer log level")
2457
2458    def execute(self, opts):
2459        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2460                              opts.cacert, opts.debug)
2461        check_cluster_initialized(rest)
2462
2463        enterprise, errors = rest.is_enterprise()
2464        _exitIfErrors(errors)
2465
2466        if opts.max_rollback is None and opts.stable_snap is None \
2467            and opts.mem_snap is None and opts.storage_mode is None \
2468            and opts.threads is None and opts.log_level is None:
2469            _exitIfErrors(["No settings specified to be changed"])
2470
2471        settings, errors = rest.index_settings()
2472        _exitIfErrors(errors)
2473
2474        # For supporting the default index backend changing from forestdb to plasma in Couchbase 5.0
2475        default = "plasma"
2476        if opts.storage_mode == "default" and settings['storageMode'] == "forestdb" or not enterprise:
2477            default = "forestdb"
2478
2479        opts.storage_mode = index_storage_mode_to_param(opts.storage_mode, default)
2480        _, errors = rest.set_index_settings(opts.storage_mode, opts.max_rollback,
2481                                            opts.stable_snap, opts.mem_snap,
2482                                            opts.threads, opts.log_level)
2483        _exitIfErrors(errors)
2484
2485        _success("Indexer settings modified")
2486
2487    @staticmethod
2488    def get_man_page_name():
2489        return "couchbase-cli-setting-index" + ".1" if os.name != "nt" else ".html"
2490
2491    @staticmethod
2492    def get_description():
2493        return "Modify index settings"
2494
2495
2496class SettingLdap(Subcommand):
2497    """The setting ldap subcommand"""
2498
2499    def __init__(self):
2500        super(SettingLdap, self).__init__()
2501        self.parser.prog = "couchbase-cli setting-ldap"
2502        group = self.parser.add_argument_group("LDAP settings")
2503        group.add_argument("--ldap-enabled", dest="enabled", metavar="<1|0>", required=True,
2504                           choices=["0", "1"], help="Enable/disable LDAP")
2505        group.add_argument("--ldap-admins", dest="admins", metavar="<user_list>",
2506                           help="A comma separated list of full admins")
2507        group.add_argument("--ldap-roadmins", dest="roadmins", metavar="<user_list>",
2508                           help="A comma separated list of read only admins")
2509        group.add_argument("--ldap-default", dest="default", default="none",
2510                           choices=["admins", "roadmins", "none"], metavar="<default>",
2511                           help="Enable/disable LDAP")
2512
2513    def execute(self, opts):
2514        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2515                              opts.cacert, opts.debug)
2516        check_cluster_initialized(rest)
2517
2518        admins = ""
2519        if opts.admins:
2520            admins = opts.admins.replace(",", "\n")
2521
2522        ro_admins = ""
2523        if opts.roadmins:
2524            ro_admins = opts.roadmins.replace(",", "\n")
2525
2526        errors = None
2527        if opts.enabled == '1':
2528            if opts.default == 'admins':
2529                if ro_admins:
2530                    _warning("--ldap-ro-admins option ignored since default is read only admins")
2531                _, errors = rest.ldap_settings('true', ro_admins, None)
2532            elif opts.default == 'roadmins':
2533                if admins:
2534                    _warning("--ldap-admins option ignored since default is admins")
2535                _, errors = rest.ldap_settings('true', None, admins)
2536            else:
2537                _, errors = rest.ldap_settings('true', ro_admins, admins)
2538        else:
2539            if admins:
2540                _warning("--ldap-admins option ignored since ldap is being disabled")
2541            if ro_admins:
2542                _warning("--ldap-roadmins option ignored since ldap is being disabled")
2543            _, errors = rest.ldap_settings('false', "", "")
2544
2545        _exitIfErrors(errors)
2546
2547        _success("LDAP settings modified")
2548
2549    @staticmethod
2550    def get_man_page_name():
2551        return "couchbase-cli-setting-ldap" + ".1" if os.name != "nt" else ".html"
2552
2553    @staticmethod
2554    def get_description():
2555        return "Modify LDAP settings"
2556
2557
2558class SettingNotification(Subcommand):
2559    """The settings notification subcommand"""
2560
2561    def __init__(self):
2562        super(SettingNotification, self).__init__()
2563        self.parser.prog = "couchbase-cli setting-notification"
2564        group = self.parser.add_argument_group("Notification Settings")
2565        group.add_argument("--enable-notifications", dest="enabled", metavar="<1|0>", required=True,
2566                           choices=["0", "1"], help="Enables/disable notifications")
2567
2568    def execute(self, opts):
2569        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2570                              opts.cacert, opts.debug)
2571
2572        enabled = None
2573        if opts.enabled == "1":
2574            enabled = True
2575        elif opts.enabled == "0":
2576            enabled = False
2577
2578        _, errors = rest.enable_notifications(enabled)
2579        _exitIfErrors(errors)
2580
2581        _success("Notification settings updated")
2582
2583    @staticmethod
2584    def get_man_page_name():
2585        return "couchbase-cli-setting-notification" + ".1" if os.name != "nt" else ".html"
2586
2587    @staticmethod
2588    def get_description():
2589        return "Modify email notification settings"
2590
2591
2592class SettingPasswordPolicy(Subcommand):
2593    """The settings password policy subcommand"""
2594
2595    def __init__(self):
2596        super(SettingPasswordPolicy, self).__init__()
2597        self.parser.prog = "couchbase-cli setting-password-policy"
2598        group = self.parser.add_argument_group("Password Policy Settings")
2599        group.add_argument("--get", dest="get", action="store_true", default=False,
2600                           help="Get the current password policy")
2601        group.add_argument("--set", dest="set", action="store_true", default=False,
2602                           help="Set a new password policy")
2603        group.add_argument("--min-length", dest="min_length", type=(int), default=False,
2604                           metavar="<num>",
2605                           help="Specifies the minimum password length for new passwords")
2606        group.add_argument("--uppercase", dest="upper_case", action="store_true", default=False,
2607                           help="Specifies new passwords must contain an upper case character")
2608        group.add_argument("--lowercase", dest="lower_case", action="store_true", default=False,
2609                           help="Specifies new passwords must contain a lower case character")
2610        group.add_argument("--digit", dest="digit", action="store_true", default=False,
2611                           help="Specifies new passwords must at least one digit")
2612        group.add_argument("--special-char", dest="special_char", action="store_true", default=False,
2613                           help="Specifies new passwords must at least one special character")
2614
2615
2616    def execute(self, opts):
2617        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2618                              opts.cacert, opts.debug)
2619
2620
2621        actions = sum([opts.get, opts.set])
2622        if actions == 0:
2623            _exitIfErrors(["Must specify either --get or --set"])
2624        elif actions > 1:
2625            _exitIfErrors(["The --get and --set flags may not be specified at " +
2626                           "the same time"])
2627        elif opts.get:
2628            self._get(rest, opts)
2629        elif opts.set:
2630            self._set(rest, opts)
2631
2632    def _get(self, rest, opts):
2633        policy, errors = rest.get_password_policy()
2634        _exitIfErrors(errors)
2635        print json.dumps(policy, sort_keys=True, indent=2)
2636
2637    def _set(self, rest, opts):
2638        _, errors = rest.set_password_policy(opts.min_length, opts.upper_case, opts.lower_case,
2639                                             opts.digit, opts.special_char)
2640        _exitIfErrors(errors)
2641        _success("Password policy updated")
2642
2643    @staticmethod
2644    def get_man_page_name():
2645        return "couchbase-cli-setting-password-policy" + ".1" if os.name != "nt" else ".html"
2646
2647    @staticmethod
2648    def get_description():
2649        return "Modify the password policy"
2650
2651
2652class SettingSecurity(Subcommand):
2653    """The settings security subcommand"""
2654
2655    def __init__(self):
2656        super(SettingSecurity, self).__init__()
2657        self.parser.prog = "couchbase-cli setting-security"
2658        group = self.parser.add_argument_group("Cluster Security Settings")
2659        group.add_argument("--disable-http-ui", dest="disable_http_ui", metavar="<0|1>", choices=['0', '1'],
2660                           default=False, help="Disables access to the UI over HTTP (0 or 1)")
2661
2662
2663    def execute(self, opts):
2664        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2665                              opts.cacert, opts.debug)
2666
2667        errors = None
2668        if opts.disable_http_ui == '1':
2669            _, errors = rest.set_security_settings(True)
2670        else:
2671            _, errors = rest.set_security_settings(False)
2672        _exitIfErrors(errors)
2673        _success("Security policy updated")
2674
2675    @staticmethod
2676    def get_man_page_name():
2677        return "couchbase-cli-security-policy" + ".1" if os.name != "nt" else ".html"
2678
2679    @staticmethod
2680    def get_description():
2681        return "Modify security policies"
2682
2683
2684class SettingXdcr(Subcommand):
2685    """The setting xdcr subcommand"""
2686
2687    def __init__(self):
2688        super(SettingXdcr, self).__init__()
2689        self.parser.prog = "couchbase-cli setting-xdcr"
2690        group = self.parser.add_argument_group("XDCR Settings")
2691        group.add_argument("--checkpoint-interval", dest="chk_int", type=(int), metavar="<num>",
2692                           help="Intervals between checkpoints in seconds (60 to 14400)")
2693        group.add_argument("--worker-batch-size", dest="worker_batch_size", metavar="<num>",
2694                           type=(int), help="Doc batch size (500 to 10000)")
2695        group.add_argument("--doc-batch-size", dest="doc_batch_size", type=(int), metavar="<KB>",
2696                           help="Document batching size in KB (10 to 100000)")
2697        group.add_argument("--failure-restart-interval", dest="fail_interval", metavar="<seconds>",
2698                           type=(int),
2699                           help="Interval for restarting failed xdcr in seconds (1 to 300)")
2700        group.add_argument("--optimistic-replication-threshold", dest="rep_thresh", type=(int),
2701                           metavar="<bytes>",
2702                           help="Document body size threshold (bytes) to trigger optimistic " +
2703                           "replication")
2704        group.add_argument("--source-nozzle-per-node", dest="src_nozzles", metavar="<num>",
2705                           type=(int),
2706                           help="The number of source nozzles per source node (1 to 10)")
2707        group.add_argument("--target-nozzle-per-node", dest="dst_nozzles", metavar="<num>",
2708                           type=(int),
2709                           help="The number of outgoing nozzles per target node (1 to 10)")
2710        group.add_argument("--bandwidth-usage-limit", dest="usage_limit", type=(int),
2711                           metavar="<num>", help="The bandwidth usage limit in MB/Sec")
2712        group.add_argument("--enable-compression", dest="compression", metavar="<1|0>", choices=["1", "0"],
2713                           help="Enable/disable compression")
2714        group.add_argument("--log-level", dest="log_level", metavar="<level>",
2715                           choices=["Error", "Info", "Debug", "Trace"],
2716                           help="The XDCR log level")
2717        group.add_argument("--stats-interval", dest="stats_interval", metavar="<ms>",
2718                           help="The interval for statistics updates (in milliseconds)")
2719
2720    def execute(self, opts):
2721        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2722                              opts.cacert, opts.debug)
2723
2724        check_cluster_initialized(rest)
2725        enterprise, errors = rest.is_enterprise()
2726
2727        _exitIfErrors(errors)
2728        if not enterprise and opts.compression:
2729            _exitIfErrors(["--enable-compression can only be configured on enterprise edition"])
2730
2731        if opts.compression == "0":
2732            opts.compression = "None"
2733        elif opts.compression =="1":
2734            opts.compression = "Auto"
2735
2736        _, errors = rest.xdcr_global_settings(opts.chk_int, opts.worker_batch_size,
2737                                              opts.doc_batch_size, opts.fail_interval,
2738                                              opts.rep_thresh, opts.src_nozzles,
2739                                              opts.dst_nozzles, opts.usage_limit,
2740                                              opts.compression, opts.log_level,
2741                                              opts.stats_interval)
2742        _exitIfErrors(errors)
2743
2744        _success("Global XDCR settings updated")
2745
2746    @staticmethod
2747    def get_man_page_name():
2748        return "couchbase-cli-setting-xdcr" + ".1" if os.name != "nt" else ".html"
2749
2750    @staticmethod
2751    def get_description():
2752        return "Modify XDCR related settings"
2753
2754class SettingMasterPassword(Subcommand):
2755    """The setting master password subcommand"""
2756
2757    def __init__(self):
2758        super(SettingMasterPassword, self).__init__()
2759        self.parser.prog = "couchbase-cli setting-master-password"
2760        group = self.parser.add_argument_group("Master password options")
2761        group.add_argument("--new-password", dest="new_password", metavar="<password>",
2762                           required=False, action=CBNonEchoedAction, envvar=None,
2763                           prompt_text="Enter new master password:",
2764                           confirm_text="Confirm new master password:",
2765                           help="Sets a new master password")
2766        group.add_argument("--rotate-data-key", dest="rotate_data_key", action="store_true",
2767                           help="Rotates the master password data key")
2768
2769    def execute(self, opts):
2770        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2771                              opts.cacert, opts.debug)
2772
2773        if opts.new_password is not None:
2774            _, errors = rest.set_master_pwd(opts.new_password)
2775            _exitIfErrors(errors)
2776            _success("New master password set")
2777        elif opts.rotate_data_key == True:
2778            _, errors = rest.rotate_master_pwd()
2779            _exitIfErrors(errors)
2780            _success("Data key rotated")
2781        else:
2782            _exitIfErrors(["No parameters set"])
2783
2784    @staticmethod
2785    def get_man_page_name():
2786        return "couchbase-cli-setting-master-password" + ".1" if os.name != "nt" else ".html"
2787
2788    @staticmethod
2789    def get_description():
2790        return "Changing the settings of the master password"
2791
2792
2793class SslManage(Subcommand):
2794    """The user manage subcommand"""
2795
2796    def __init__(self):
2797        super(SslManage, self).__init__()
2798        self.parser.prog = "couchbase-cli ssl-manage"
2799        group = self.parser.add_argument_group("SSL manage options")
2800        group.add_argument("--cluster-cert-info", dest="cluster_cert", action="store_true",
2801                           default=False, help="Gets the cluster certificate")
2802        group.add_argument("--node-cert-info", dest="node_cert", action="store_true",
2803                           default=False, help="Gets the node certificate")
2804        group.add_argument("--regenerate-cert", dest="regenerate", metavar="<path>",
2805                           help="Regenerate the cluster certificate and save it to a file")
2806        group.add_argument("--set-node-certificate", dest="set_cert", action="store_true",
2807                           default=False, help="Sets the node certificate")
2808        group.add_argument("--upload-cluster-ca", dest="upload_cert", metavar="<path>",
2809                           help="Upload a new cluster certificate")
2810        group.add_argument("--set-client-auth", dest="client_auth_path", metavar="<path>",
2811                           help="A path to a file containing the client auth configuration")
2812        group.add_argument("--client-auth", dest="show_client_auth", action="store_true",
2813                           help="Show ssl client certificate authentication value")
2814        group.add_argument("--extended", dest="extended", action="store_true",
2815                           default=False, help="Print extended certificate information")
2816
2817    def execute(self, opts):
2818        rest = ClusterManager(opts.cluster, opts.username, opts.password, opts.ssl, opts.ssl_verify,
2819                              opts.cacert, opts.debug)
2820        check_cluster_initialized(rest)
2821
2822        if opts.regenerate is not None:
2823            try:
2824                open(opts.regenerate, 'a').close()
2825            except IOError:
2826                _exitIfErrors(["Unable to create file at `%s`" % opts.regenerate])
2827            certificate, errors = rest.regenerate_cluster_certificate()
2828            _exitIfErrors(errors)
2829            _exit_on_file_write_failure(opts.regenerate, certificate)
2830            _success("Certificate regenerate and copied to '%s'" % (opts.regenerate))
2831        elif opts.cluster_cert:
2832