xref: /4.6.0/couchbase-cli/node.py (revision 78921a5e)
1"""
2  Implementation for rebalance, add, remove, stop rebalance.
3"""
4
5import platform
6import cluster_manager
7import getpass
8import random
9import subprocess
10import time
11import os
12import sys
13import util_cli as util
14import socket
15import re
16import urlparse
17import json
18
19from usage import command_error
20from restclient import *
21from listservers import *
22from _csv import reader
23
24try:
25    import pump_bfd2
26    IS_ENTERPRISE= True
27except ImportError:
28    IS_ENTERPRISE = False
29
30MAX_LEN_PASSWORD = 24
31
32# the rest commands and associated URIs for various node operations
33
34rest_cmds = {
35    'rebalance'         :'/controller/rebalance',
36    'rebalance-stop'    :'/controller/stopRebalance',
37    'rebalance-status'  :'/pools/default/rebalanceProgress',
38    'server-add'        :'/controller/addNode',
39    'server-readd'      :'/controller/reAddNode',
40    'failover'          :'/controller/failOver',
41    'recovery'          :'/controller/setRecoveryType',
42    'cluster-init'      :'/settings/web',
43    'cluster-edit'      :'/settings/web',
44    'node-init'         :'/nodes/self/controller/settings',
45    'setting-cluster'   :'/pools/default',
46    'setting-compaction'    :'/controller/setAutoCompaction',
47    'setting-notification'  :'/settings/stats',
48    'setting-autofailover'  :'/settings/autoFailover',
49    'setting-alert'         :'/settings/alerts',
50    'setting-audit'         :'/settings/audit',
51    'setting-ldap'          :'/settings/saslauthdAuth',
52    'user-manage'           :'/settings/readOnlyUser',
53    'setting-index'         :'/settings/indexes',
54    'group-manage'          :'/pools/default/serverGroups',
55    'ssl-manage'            :'/pools/default/certificate',
56    'collect-logs-start'  : '/controller/startLogsCollection',
57    'collect-logs-stop'   : '/controller/cancelLogsCollection',
58    'collect-logs-status' : '/pools/default/tasks',
59    'admin-role-manage'   : '/settings/rbac/users',
60    'master-password'     : '/node/controller/changeMasterPassword'
61}
62
63server_no_remove = [
64    'rebalance-stop',
65    'rebalance-status',
66    'server-add',
67    'server-readd',
68    'failover',
69    'recovery',
70]
71server_no_add = [
72    'rebalance-stop',
73    'rebalance-status',
74    'failover',
75    'recovery',
76]
77
78# Map of operations and the HTTP methods used against the REST interface
79
80methods = {
81    'rebalance'         :'POST',
82    'rebalance-stop'    :'POST',
83    'rebalance-status'  :'GET',
84    'eject-server'      :'POST',
85    'server-add'        :'POST',
86    'server-readd'      :'POST',
87    'failover'          :'POST',
88    'recovery'          :'POST',
89    'cluster-init'      :'POST',
90    'cluster-edit'      :'POST',
91    'node-init'         :'POST',
92    'setting-cluster'   :'POST',
93    'setting-compaction'    :'POST',
94    'setting-notification'  :'POST',
95    'setting-autofailover'  :'POST',
96    'setting-alert'         :'POST',
97    'setting-audit'         :'POST',
98    'setting-ldap'          :'POST',
99    'setting-index'         :'POST',
100    'user-manage'           :'POST',
101    'group-manage'          :'POST',
102    'ssl-manage'            :'GET',
103    'collect-logs-start'  : 'POST',
104    'collect-logs-stop'   : 'POST',
105    'collect-logs-status' : 'GET',
106    'admin-role-manage'   : 'PUT',
107    'master-password'     : 'POST',
108}
109
110bool_to_str = lambda value: str(bool(int(value))).lower()
111
112# Map of HTTP success code, success message and error message for
113# handling HTTP response properly
114
115class Node:
116    SEP = ","
117    def __init__(self):
118        self.rest_cmd = rest_cmds['rebalance-status']
119        self.method = 'GET'
120        self.debug = False
121        self.server = ''
122        self.port = ''
123        self.user = ''
124        self.password = ''
125        self.ssl = False
126
127        self.ro_username = ''
128        self.ro_password = ''
129        self.params = {}
130        self.output = 'standard'
131        self.password_new = None
132        self.username_new = None
133        self.sa_username = None
134        self.sa_password = None
135        self.port_new = None
136        self.per_node_quota = None
137        self.cluster_index_ramsize = None
138        self.cluster_fts_ramsize = None
139        self.index_storage_setting = None
140        self.cluster_name = None
141        self.data_path = None
142        self.index_path = None
143        self.hostname = None
144        self.enable_auto_failover = None
145        self.enable_notification = None
146        self.autofailover_timeout = None
147        self.enable_email_alert = None
148
149        #compaction related settings
150        self.compaction_db_percentage = None
151        self.compaction_db_size = None
152        self.compaction_view_percentage = None
153        self.compaction_view_size = None
154        self.compaction_period_from = None
155        self.compaction_period_to = None
156        self.enable_compaction_abort = None
157        self.enable_compaction_parallel = None
158        self.purge_interval = None
159        self.gsi_compact_mode = None
160        self.gsi_compact_perc = None
161        self.gsi_compact_interval = None
162        self.gsi_compact_period_from = None
163        self.gsi_compact_period_to = None
164        self.gsi_compact_abort = None
165
166        #alert settings
167        self.email_recipient = None
168        self.email_sender = None
169        self.email_user = None
170        self.email_password = None
171        self.email_host = None
172        self.email_port = None
173        self.email_enable_encrypt = None
174        self.autofailover_node = None
175        self.autofailover_max_reached = None
176        self.autofailover_node_down = None
177        self.autofailover_cluster_small = None
178        self.autofailover_disabled = None
179        self.alert_ip_changed = None
180        self.alert_disk_space = None
181        self.alert_meta_overhead = None
182        self.alert_meta_oom = None
183        self.alert_write_failed = None
184        self.alert_audit_dropped = None
185
186        #group management
187        self.group_name = None
188        self.server_list = []
189        self.from_group = None
190        self.to_group = None
191        self.group_rename = None
192
193        #SSL certificate management
194        self.certificate_file = None
195        self.extended = False
196        self.cmd = None
197
198        self.hard_failover = None
199        self.recovery_type = None
200        self.recovery_buckets = None
201
202        # Collect logs
203        self.nodes = None
204        self.all_nodes = None
205        self.upload = False
206        self.upload_host = None
207        self.customer = None
208        self.ticket = ""
209
210        #auditing
211        self.audit_enabled = None
212        self.audit_log_path = None
213        self.audit_log_rotate_interval = None
214
215        #ldap
216        self.ldap_enabled = None
217        self.ldap_admins = ''
218        self.ldap_roadmins = ''
219        self.ldap_default = "none"
220
221        #index
222        self.max_rollback_points = None
223        self.stable_snapshot_interval = None
224        self.memory_snapshot_interval = None
225        self.index_threads = None
226        self.services = None
227        self.log_level = None
228
229        #set-roles / delete-roles
230        self.roles = None
231        self.my_roles = False
232        self.get_roles = None
233        self.set_users = None
234        self.set_names = None
235        self.delete_users = None
236
237        # master password
238        self.new_password_specified = False
239        self.new_password = ''
240        self.rotate_data_key = False
241        self.send_password_specified = False
242        self.send_password = ''
243        self.config_path = None
244
245    def runCmd(self, cmd, server, port,
246               user, password, ssl, opts):
247        self.rest_cmd = rest_cmds[cmd]
248        self.method = methods[cmd]
249        self.server = server
250        self.port = int(port)
251        self.user = user
252        self.password = password
253        self.ssl = ssl
254
255        servers = self.processOpts(cmd, opts)
256        if self.debug:
257            print "INFO: servers %s" % servers
258
259        if cmd == 'server-add' and not servers['add']:
260            command_error("please list one or more --server-add=HOST[:PORT],"
261                  " or use -h for more help.")
262
263        if cmd == 'server-readd' and not servers['add']:
264            command_error("please list one or more --server-add=HOST[:PORT],"
265                  " or use -h for more help.")
266
267        if cmd in ('server-add', 'rebalance'):
268            if len(servers['add']) > 0:
269                if cmd == 'rebalance':
270                    print "DEPRECATED: Adding server from the rebalance command is " + \
271                          "deprecated and will be removed in future release, use " + \
272                          "the server-add command to add servers instead."
273                self.groupAddServers()
274            if cmd == 'rebalance':
275                self.rebalance(servers)
276
277        elif cmd == 'server-readd':
278            self.reAddServers(servers)
279
280        elif cmd == 'rebalance-status':
281            output_result = self.rebalanceStatus()
282            print output_result
283
284        elif cmd == 'rebalance-stop':
285            output_result = self.rebalanceStop()
286            print output_result
287
288        elif cmd == 'failover':
289            if len(servers['failover']) <= 0:
290                command_error("please list one or more --server-failover=HOST[:PORT];"
291                      " or use -h for more help.")
292            if len(servers['failover'].keys()[0].split(',')) > 1:
293                print "DEPRECATED: Failing over more than one server at a time is deprecated" + \
294                    "and will not be allowed in future release."
295
296            self.failover(servers)
297
298        elif cmd == 'recovery':
299            if len(servers['recovery']) <= 0:
300                command_error("please list one or more --server-recovery=HOST[:PORT];"
301                      " or use -h for more help.")
302            self.recovery(servers)
303
304        elif cmd in ('cluster-init', 'cluster-edit'):
305            self.clusterInit(cmd)
306
307        elif cmd == 'master-password':
308            self.masterPassword()
309
310        elif cmd == 'node-init':
311            self.nodeInit()
312
313        elif cmd == 'setting-cluster':
314            self.clusterSetting()
315
316        elif cmd == 'setting-compaction':
317            self.compaction()
318
319        elif cmd == 'setting-notification':
320            self.notification()
321
322        elif cmd == 'setting-alert':
323            self.alert()
324
325        elif cmd == 'setting-autofailover':
326            self.autofailover()
327
328        elif cmd == 'setting-audit':
329            self.audit()
330
331        elif cmd == 'setting-ldap':
332            self.ldap()
333
334        elif cmd == 'setting-index':
335            self.index()
336
337        elif cmd == 'user-manage':
338            self.userManage()
339
340        elif cmd == 'group-manage':
341            self.groupManage()
342
343        elif cmd == 'ssl-manage':
344            self.retrieveCert()
345
346        elif cmd == 'collect-logs-start':
347            self.collectLogsStart(servers)
348
349        elif cmd == 'collect-logs-stop':
350            self.collectLogsStop()
351
352        elif cmd == 'collect-logs-status':
353            self.collectLogsStatus()
354
355        elif cmd == 'admin-role-manage':
356            self.alterRoles()
357
358    def masterPassword(self):
359        cm = cluster_manager.ClusterManager(self.server, self.port, self.user,
360                                                self.password, self.ssl)
361        if self.new_password_specified:
362            newPassword = self.new_password
363            if self.new_password == '':
364                newPassword = getpass.getpass("\nEnter new master password:")
365            _, errors = cm.set_master_pwd(newPassword)
366            _exitIfErrors(errors)
367            print "SUCCESS: New master password set"
368        elif self.rotate_data_key:
369            _, errors = cm.rotate_master_pwd()
370            _exitIfErrors(errors)
371            print "SUCCESS: Data key rotated"
372        elif self.send_password_specified:
373            mydir = os.path.join(os.path.dirname(sys.argv[0]), "..", "..", "bin")
374            path = [mydir, os.environ['PATH']]
375            if os.name == 'posix':
376                os.environ['PATH'] = ':'.join(path)
377            else:
378                os.environ['PATH'] = ';'.join(path)
379
380            if self.config_path == None:
381                self.config_path = os.path.abspath(os.path.join(mydir, "..", "var", "lib", "couchbase"))
382
383            cookiefile = self.find_master_pwd_path("couchbase-server.cookie", self.config_path)
384            if cookiefile == None:
385                _exitIfErrors(["File couchbase-server.cookie is not found under the specified root"])
386
387            cookie = _exitOnFileReadFailure(cookiefile).rstrip()
388
389            node = "babysitter_of_ns_1@127.0.0.1"
390            nodefile = self.find_master_pwd_path("couchbase-server.babysitter.node", self.config_path)
391            if nodefile != None:
392                node = _exitOnFileReadFailure(nodefile).rstrip()
393
394            self.prompt_for_master_pwd(node, cookie)
395        else :
396            _exitIfErrors(["ERROR: no parameters set"])
397
398    def run_process(self, name, args):
399        try:
400            if platform.system() == 'Windows':
401                name = name + ".exe"
402
403            args.insert(0, name)
404            p = subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
405            output = p.stdout.read()
406            error = p.stderr.read()
407            p.wait()
408            rc = p.returncode
409            return output, error
410        except OSError:
411            _exitIfErrors(["Could not locate the %s executable" % name])
412
413    def find_master_pwd_path(self, filename, user_path):
414        variants = [os.path.abspath(os.path.join(user_path, filename)),
415                    "/opt/couchbase/var/lib/couchbase/" + filename,
416                    os.path.expanduser("~/Library/Application Support/Couchbase/var/lib/couchbase/" + filename)]
417        for path in variants:
418            if os.path.isfile(path):
419                return path
420
421        return None
422
423    def prompt_for_master_pwd(self, node, cookie):
424        password = self.send_password
425        if password == '':
426            password = getpass.getpass("\nEnter master password:")
427        password = "\"" + password.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
428
429        randChars = ''.join(random.choice(string.ascii_letters) for i in xrange(20))
430        name = 'cb-%s@127.0.0.1' % randChars
431
432        instr = "Res = rpc:call('" + node + "', encryption_service, set_password, [" \
433                + password + "]), io:format(\"~p~n\", [Res])."
434        args = ["-noinput", "-name", name, "-setcookie", cookie, "-eval", \
435                instr, "-run", "init", "stop"]
436
437        res, error = self.run_process("erl", args)
438        res = res.strip(' \t\n\r')
439
440        if res == "ok":
441            print "SUCCESS: Password accepted. Node started booting."
442        elif res == "retry":
443            if self.send_password != '':
444                _exitIfErrors(["Incorrect password, please retry"])
445            print "Incorrect password."
446            self.prompt_for_master_pwd(node, cookie)
447        elif res == "{badrpc,nodedown}":
448            _exitIfErrors(["Either the node is down or password was already supplied"])
449        else:
450            _exitIfErrors(["Incorrect password. Node shuts down."])
451
452    def clusterInit(self, cmd):
453        # We need to ensure that creating the REST username/password is the
454        # last REST API that is called because once that API succeeds the
455        # cluster is initialized and cluster-init cannot be run again.
456
457        cm = cluster_manager.ClusterManager(self.server, self.port, self.user,
458                                            self.password, self.ssl)
459
460        if cmd == 'cluster-init':
461            data, errors = cm.pools()
462            _exitIfErrors(errors)
463            if data['pools'] and len(data['pools']) > 0:
464                print "Error: cluster is already initialized, use cluster-edit to change settings"
465                return
466
467        err, services = self.process_services(False)
468        if err:
469            print err
470            return
471
472        allowDefault = 'index' in services.split(',')
473        param = self.index_storage_to_param(self.index_storage_setting, allowDefault)
474
475        #set memory quota
476        if cmd == 'cluster-init':
477            if 'kv' in services.split(',') and not self.per_node_quota:
478                print "ERROR: option cluster-ramsize is not specified"
479                return
480            elif 'index' in services.split(','):
481                if not self.cluster_index_ramsize:
482                    print "ERROR: option cluster-index-ramsize is not specified"
483                    return
484                if param is None:
485                    print "ERROR: invalid index storage setting `%s`. Must be [default, memopt]" \
486                        % self.index_storage_setting
487                    return
488            elif 'fts' in services.split(',') and not self.cluster_fts_ramsize:
489                print "ERROR: option fts-index-ramsize is not specified"
490                return
491
492        if param is not None:
493            _, errors = cm.set_index_settings(param)
494            _exitIfErrors(errors)
495
496        opts = {
497            "error_msg": "unable to set memory quota",
498            "success_msg": "set memory quota successfully"
499        }
500        rest = util.restclient_factory(self.server,
501                                       self.port,
502                                       {'debug':self.debug},
503                                       self.ssl)
504        if self.per_node_quota:
505            rest.setParam('memoryQuota', self.per_node_quota)
506        if self.cluster_index_ramsize:
507            rest.setParam('indexMemoryQuota', self.cluster_index_ramsize)
508        if self.cluster_fts_ramsize:
509            rest.setParam('ftsMemoryQuota', self.cluster_fts_ramsize)
510        if rest.params:
511            output_result = rest.restCmd(self.method,
512                                         '/pools/default',
513                                         self.user,
514                                         self.password,
515                                         opts)
516
517        #setup services
518        if cmd == "cluster-init":
519            opts = {
520                "error_msg": "unable to setup services",
521                "success_msg": "setup service successfully"
522            }
523            rest = util.restclient_factory(self.server,
524                                           self.port,
525                                           {'debug':self.debug},
526                                           self.ssl)
527            rest.setParam('services', services)
528            output_result = rest.restCmd(self.method,
529                                         '/node/controller/setupServices',
530                                         self.user,
531                                         self.password,
532                                         opts)
533
534        # setup REST credentials/REST port
535        if cmd == 'cluster-init' or self.username_new or self.password_new or self.port_new:
536            self.enable_notification = "true"
537            self.notification(False)
538            rest = util.restclient_factory(self.server,
539                                         self.port,
540                                         {'debug':self.debug},
541                                         self.ssl)
542            if self.port_new:
543                rest.setParam('port', self.port_new)
544            else:
545                rest.setParam('port', 'SAME')
546            rest.setParam('initStatus', 'done')
547            if self.username_new:
548                rest.setParam('username', self.username_new)
549            else:
550                rest.setParam('username', self.user)
551            if self.password_new:
552                rest.setParam('password', self.password_new)
553            else:
554                rest.setParam('password', self.password)
555
556            if not (rest.getParam('username') and rest.getParam('password')):
557                print "ERROR: Both username and password are required."
558                return
559
560            if len(rest.getParam('password')) > MAX_LEN_PASSWORD:
561                print "ERROR: Password length %s exceeds maximum number of characters allowed, which is %s" \
562                      % (len(rest.getParam('password')), MAX_LEN_PASSWORD)
563                return
564
565            opts = {
566                "error_msg": "unable to init/modify %s" % self.server,
567                "success_msg": "init/edit %s" % self.server
568            }
569
570            output_result = rest.restCmd(self.method,
571                                         self.rest_cmd,
572                                         self.user,
573                                         self.password,
574                                         opts)
575        print output_result
576
577    def index_storage_to_param(self, value, allowDefault):
578        if (not value and allowDefault) or value == "default":
579            return "forestdb"
580        if value == "memopt":
581            return "memory_optimized"
582        return None
583
584    def process_services(self, data_required):
585        if not self.services:
586            self.services = "data"
587        sep = Node.SEP
588        if self.services.find(sep) < 0:
589            #backward compatible when using ";" as separator
590            sep = ";"
591        svc_list = list(set([w.strip() for w in self.services.split(sep)]))
592        svc_candidate = ["data", "index", "query", "fts"]
593        for svc in svc_list:
594            if svc not in svc_candidate:
595                return "ERROR: invalid service: %s" % svc, None
596        if data_required and "data" not in svc_list:
597            svc_list.append("data")
598        if not IS_ENTERPRISE:
599            if len(svc_list) != len(svc_candidate):
600                if len(svc_list) != 1 or "data" not in svc_list:
601                    return "ERROR: Community Edition requires that all nodes provision all services or data service only", None
602
603        services = ",".join(svc_list)
604        for old, new in [[";", ","], ["data", "kv"], ["query", "n1ql"]]:
605            services = services.replace(old, new)
606        return None, services
607
608    def nodeInit(self):
609        rest = util.restclient_factory(self.server,
610                                     self.port,
611                                     {'debug':self.debug},
612                                     self.ssl)
613        if self.data_path:
614            rest.setParam('path', self.data_path)
615
616        if self.index_path:
617            rest.setParam('index_path', self.index_path)
618
619        opts = {
620            "error_msg": "unable to init %s" % self.server,
621            "success_msg": "init %s" % self.server
622        }
623
624        output_result = rest.restCmd(self.method,
625                                     self.rest_cmd,
626                                     self.user,
627                                     self.password,
628                                     opts)
629        if self.hostname:
630            rest = util.restclient_factory(self.server,
631                                         self.port,
632                                         {'debug':self.debug},
633                                         self.ssl)
634            if self.hostname:
635                rest.setParam('hostname', self.hostname)
636
637            opts = {
638                "error_msg": "unable to set hostname for %s" % self.server,
639                "success_msg": "set hostname for %s" % self.server
640            }
641
642            output_result = rest.restCmd('POST',
643                                         '/node/controller/rename',
644                                         self.user,
645                                         self.password,
646                                         opts)
647        print output_result
648
649    def compaction(self):
650        rest = util.restclient_factory(self.server,
651                                     self.port,
652                                     {'debug':self.debug},
653                                     self.ssl)
654
655        if self.compaction_db_percentage:
656            rest.setParam('databaseFragmentationThreshold[percentage]', self.compaction_db_percentage)
657        if self.compaction_db_size:
658            self.compaction_db_size = int(self.compaction_db_size) * 1024**2
659            rest.setParam('databaseFragmentationThreshold[size]', self.compaction_db_size)
660        if self.compaction_view_percentage:
661            rest.setParam('viewFragmentationThreshold[percentage]', self.compaction_view_percentage)
662        if self.compaction_view_size:
663            self.compaction_view_size = int(self.compaction_view_size) * 1024**2
664            rest.setParam('viewFragmentationThreshold[size]', self.compaction_view_size)
665        if self.compaction_period_from:
666            hour, minute = self.compaction_period_from.split(':')
667            if (int(hour) not in range(24)) or (int(minute) not in range(60)):
668                print "ERROR: invalid hour or minute value for compaction period"
669                return
670            else:
671                rest.setParam('allowedTimePeriod[fromHour]', int(hour))
672                rest.setParam('allowedTimePeriod[fromMinute]', int(minute))
673        if self.compaction_period_to:
674            hour, minute = self.compaction_period_to.split(':')
675            if (int(hour) not in range(24)) or (int(minute) not in range(60)):
676                print "ERROR: invalid hour or minute value for compaction"
677                return
678            else:
679                rest.setParam('allowedTimePeriod[toHour]', hour)
680                rest.setParam('allowedTimePeriod[toMinute]', minute)
681        if self.enable_compaction_abort:
682            rest.setParam('allowedTimePeriod[abortOutside]', self.enable_compaction_abort)
683        if self.enable_compaction_parallel:
684            rest.setParam('parallelDBAndViewCompaction', self.enable_compaction_parallel)
685        else:
686            self.enable_compaction_parallel = bool_to_str(0)
687            rest.setParam('parallelDBAndViewCompaction', self.enable_compaction_parallel)
688
689        if self.compaction_period_from or self.compaction_period_to or self.enable_compaction_abort:
690            if not (self.compaction_period_from and self.compaction_period_to and \
691                    self.enable_compaction_abort):
692                print "ERROR: compaction-period-from, compaction-period-to and enable-compaction-abort have to be specified at the same time"
693                return
694        if self.purge_interval:
695            rest.setParam('purgeInterval', self.purge_interval)
696
697        if self.gsi_compact_mode is not None and self.gsi_compact_mode not in ["append", "circular"]:
698            _exitIfErrors(["ERROR: --gsi-compaction-mode must be \"append\" or \"circular\""])
699
700        if self.gsi_compact_mode == "append":
701            rest.setParam('indexCompactionMode', "full")
702            if self.gsi_compact_perc is None:
703                _exitIfErrors(["ERROR: --compaction-gsi-percentage must be specified when --gsi-compaction-mode is append"])
704
705            if self.gsi_compact_perc is not None:
706                rest.setParam('indexFragmentationThreshold[percentage]', self.gsi_compact_perc)
707        elif self.gsi_compact_mode == "circular":
708            rest.setParam('indexCompactionMode', "circular")
709
710            if self.gsi_compact_interval is None:
711                self.gsi_compact_interval = ""
712            rest.setParam('indexCircularCompaction[daysOfWeek]', self.gsi_compact_interval)
713
714            if self.gsi_compact_period_from is not None:
715                hour, minute = self.gsi_compact_period_from.split(':')
716                if (int(hour) not in range(24)) or (int(minute) not in range(60)):
717                    _exitIfErrors(["ERROR: invalid hour or minute value for gsi compaction from period"])
718                else:
719                    rest.setParam('indexCircularCompaction[interval][fromHour]', int(hour))
720                    rest.setParam('indexCircularCompaction[interval][fromMinute]', int(minute))
721            if self.gsi_compact_period_to is not None:
722                hour, minute = self.gsi_compact_period_to.split(':')
723                if (int(hour) not in range(24)) or (int(minute) not in range(60)):
724                    _exitIfErrors(["ERROR: invalid hour or minute value for gsi compaction to period"])
725                else:
726                    rest.setParam('indexCircularCompaction[interval][toHour]', hour)
727                    rest.setParam('indexCircularCompaction[interval][toMinute]', minute)
728
729            if self.enable_compaction_abort is not None:
730                rest.setParam('indexCircularCompaction[interval][abortOutside]', self.gsi_compact_abort)
731            else:
732                rest.setParam('indexCircularCompaction[interval][abortOutside]', "false")
733
734        opts = {
735            "error_msg": "unable to set compaction settings",
736            "success_msg": "set compaction settings"
737        }
738        output_result = rest.restCmd(self.method,
739                                     self.rest_cmd,
740                                     self.user,
741                                     self.password,
742                                     opts)
743        print output_result
744
745    def clusterSetting(self):
746        rest = util.restclient_factory(self.server,
747                                     self.port,
748                                     {'debug':self.debug},
749                                     self.ssl)
750        if self.per_node_quota:
751            rest.setParam('memoryQuota', self.per_node_quota)
752        if self.cluster_name is not None:
753            rest.setParam('clusterName', self.cluster_name)
754        if self.cluster_index_ramsize:
755            rest.setParam('indexMemoryQuota', self.cluster_index_ramsize)
756        if self.cluster_fts_ramsize:
757            rest.setParam('ftsMemoryQuota', self.cluster_fts_ramsize)
758        opts = {
759            "error_msg": "unable to set cluster configurations",
760            "success_msg": "set cluster settings"
761        }
762        if rest.params:
763            output_result = rest.restCmd(self.method,
764                                     self.rest_cmd,
765                                     self.user,
766                                     self.password,
767                                     opts)
768            print output_result
769        else:
770            print "Error: No parameters specified"
771
772    def notification(self, print_status=True):
773        rest = util.restclient_factory(self.server,
774                                     self.port,
775                                     {'debug':self.debug},
776                                     self.ssl)
777        if self.enable_notification:
778            rest.setParam('sendStats', self.enable_notification)
779
780        opts = {
781            "error_msg": "unable to set notification settings",
782            "success_msg": "set notification settings"
783        }
784        output_result = rest.restCmd(self.method,
785                                     '/settings/stats',
786                                     self.user,
787                                     self.password,
788                                     opts)
789
790        if print_status:
791            print output_result
792
793    def alert(self):
794        rest = util.restclient_factory(self.server,
795                                     self.port,
796                                     {'debug':self.debug},
797                                     self.ssl)
798        alert_opts = ''
799        if self.enable_email_alert:
800            rest.setParam('enabled', self.enable_email_alert)
801        if self.email_recipient:
802            rest.setParam('recipients', self.email_recipient)
803        if self.email_sender:
804            rest.setParam('sender', self.email_sender)
805        if self.email_user:
806            rest.setParam('emailUser', self.email_user)
807        if self.email_password:
808            rest.setParam('emailPass', self.email_password)
809        if self.email_host:
810            rest.setParam('emailHost', self.email_host)
811        if self.email_port:
812            rest.setParam('emailPort', self.email_port)
813        if self.email_enable_encrypt:
814            rest.setParam('emailEncrypt', self.email_enable_encrypt)
815        if self.autofailover_node:
816            alert_opts = alert_opts + 'auto_failover_node,'
817        if self.autofailover_max_reached:
818            alert_opts = alert_opts + 'auto_failover_maximum_reached,'
819        if self.autofailover_node_down:
820            alert_opts = alert_opts + 'auto_failover_other_nodes_down,'
821        if self.autofailover_cluster_small:
822            alert_opts = alert_opts + 'auto_failover_cluster_too_small,'
823        if self.autofailover_disabled:
824            alert_opts = alert_opts + 'auto_failover_disabled,'
825        if self.alert_ip_changed:
826            alert_opts = alert_opts + 'ip,'
827        if self.alert_disk_space:
828            alert_opts = alert_opts + 'disk,'
829        if self.alert_meta_overhead:
830            alert_opts = alert_opts + 'overhead,'
831        if self.alert_meta_oom:
832            alert_opts = alert_opts + 'ep_oom_errors,'
833        if self.alert_write_failed:
834            alert_opts = alert_opts + 'ep_item_commit_failed,'
835        if self.alert_audit_dropped:
836            alert_opts = alert_opts + 'audit_dropped_events,'
837
838        if alert_opts:
839            # remove last separator
840            alert_opts = alert_opts[:-1]
841            rest.setParam('alerts', alert_opts)
842
843        opts = {
844            "error_msg": "unable to set alert settings",
845            "success_msg": "set alert settings"
846        }
847        output_result = rest.restCmd(self.method,
848                                     self.rest_cmd,
849                                     self.user,
850                                     self.password,
851                                     opts)
852        print output_result
853
854    def autofailover(self):
855        rest = util.restclient_factory(self.server,
856                                     self.port,
857                                     {'debug':self.debug},
858                                     self.ssl)
859        if self.autofailover_timeout:
860            if int(self.autofailover_timeout) < 30:
861                print "ERROR: Timeout value must be larger than 30 second."
862                return
863            else:
864                rest.setParam('timeout', self.autofailover_timeout)
865
866        if self.enable_auto_failover:
867            rest.setParam('enabled', self.enable_auto_failover)
868
869        opts = {
870            "error_msg": "unable to set auto failover settings",
871            "success_msg": "set auto failover settings"
872        }
873        output_result = rest.restCmd(self.method,
874                                     self.rest_cmd,
875                                     self.user,
876                                     self.password,
877                                     opts)
878        print output_result
879
880    def audit(self):
881        rest = util.restclient_factory(self.server,
882                                     self.port,
883                                     {'debug':self.debug},
884                                     self.ssl)
885        if self.audit_enabled:
886            rest.setParam('auditdEnabled', self.audit_enabled)
887
888        if self.audit_log_path:
889            rest.setParam('logPath', self.audit_log_path)
890        elif self.audit_enabled == "true":
891             rest.setParam('logPath', "/opt/couchbase/var/lib/couchbase/logs")
892        if self.audit_log_rotate_interval:
893            rest.setParam('rotateInterval', int(self.audit_log_rotate_interval)*60)
894        elif self.audit_enabled == "true":
895            rest.setParam('rotateInterval', 86400)
896
897        opts = {
898            "error_msg": "unable to set audit settings",
899            "success_msg": "set audit settings"
900        }
901        output_result = rest.restCmd(self.method,
902                                     self.rest_cmd,
903                                     self.user,
904                                     self.password,
905                                     opts)
906        print output_result
907
908    def ldap(self):
909        print "DEPRECATED: Adding ldap users from setting-ldap is deprecated and will not " + \
910              "be allowed in future releases. Please use admin-role-manage instead. " + \
911              "setting-ldap should only be used to enable and disable ldap on a cluster."
912        rest = util.restclient_factory(self.server,
913                                     self.port,
914                                     {'debug':self.debug},
915                                     self.ssl)
916        if self.ldap_enabled == 'true':
917            rest.setParam('enabled', 'true')
918            if self.ldap_default == 'admins':
919                rest.setParam('roAdmins', self.ldap_roadmins.replace(Node.SEP, "\n"))
920            elif self.ldap_default == 'roadmins':
921                rest.setParam('admins', self.ldap_admins.replace(Node.SEP, "\n"))
922            else:
923                rest.setParam('admins', self.ldap_admins.replace(Node.SEP,"\n"))
924                rest.setParam('roAdmins', self.ldap_roadmins.replace(Node.SEP, "\n"))
925        else:
926            rest.setParam('enabled', 'false')
927
928        opts = {
929            "error_msg": "unable to set LDAP auth settings",
930            "success_msg": "set LDAP auth settings"
931        }
932        output_result = rest.restCmd(self.method,
933                                     self.rest_cmd,
934                                     self.user,
935                                     self.password,
936                                     opts)
937        print output_result
938
939    # Role-Based Access Control
940    def alterRoles(self):
941        cm = cluster_manager.ClusterManager(self.server, self.port, self.user,
942                                            self.password, self.ssl)
943
944        # need to check arguments
945        if (self.my_roles == None and self.get_roles == None and \
946            self.set_users == None and self.delete_users == None):
947            print "ERROR: You must specify either '--my-roles', '--get-roles', " \
948                "'--set-users', or '--delete-users'"
949            return
950
951        if self.my_roles and (self.get_roles or self.set_users or self.roles or self.delete_users):
952            print "ERROR: The 'my-roles' option may not be used with any other option."
953            return
954
955        if self.get_roles and (self.my_roles or self.set_users or self.roles or self.delete_users):
956            print "ERROR: The 'get-roles' option may not be used with any other option."
957            return
958
959        if (self.set_users and self.roles == None) or (self.set_users == None and self.roles):
960            print "ERROR: You must specify lists of both users and roles for those users.\n  --set-users=[comma delimited user list] --roles=[comma-delimited list of one or more from admin, ro_admin, cluster_admin, replication_admin, bucket_admin[bucket name or '*'], views_admin[bucket name or '*']"
961            return
962
963        # my_roles
964        if self.my_roles:
965            data, errors = cm.myRoles()
966            if errors == None:
967                print "SUCCESS: my roles:"
968
969        # get_roles
970        elif self.get_roles:
971            data, errors = cm.getRoles()
972            if errors == None:
973                print "SUCCESS: user/role list:"
974
975        # set_users
976        elif self.set_users:
977            data, errors = cm.setRoles(self.set_users,self.roles,self.set_names)
978            if errors == None:
979                print "SUCCESS: set roles for ",self.set_users,". New user/role list:"
980
981        # delete_users
982        else:
983            data, errors = cm.deleteRoles(self.delete_users)
984            if errors == None:
985                print "SUCCESS: removed users ", self.delete_users, ". New user/role list:"
986
987        _exitIfErrors(errors)
988        print json.dumps(data,indent=2)
989
990    def index(self):
991        rest = util.restclient_factory(self.server,
992                                     self.port,
993                                     {'debug':self.debug},
994                                     self.ssl)
995        if self.max_rollback_points:
996            rest.setParam("maxRollbackPoints", self.max_rollback_points)
997        if self.stable_snapshot_interval:
998            rest.setParam("stableSnapshotInterval", self.stable_snapshot_interval)
999        if self.memory_snapshot_interval:
1000            rest.setParam("memorySnapshotInterval", self.memory_snapshot_interval)
1001        if self.index_threads:
1002            rest.setParam("indexerThreads", self.index_threads)
1003        if self.log_level:
1004            rest.setParam("logLevel", self.log_level)
1005
1006        opts = {
1007            "error_msg": "unable to set index settings",
1008            "success_msg": "set index settings"
1009        }
1010        output_result = rest.restCmd(self.method,
1011                                     self.rest_cmd,
1012                                     self.user,
1013                                     self.password,
1014                                     opts)
1015        print output_result
1016
1017    def processOpts(self, cmd, opts):
1018        """ Set standard opts.
1019            note: use of a server key keeps optional
1020            args aligned with server.
1021            """
1022        servers = {
1023            'add': {},
1024            'remove': {},
1025            'failover': {},
1026            'recovery': {},
1027            'log': {},
1028        }
1029
1030        # don't allow options that don't correspond to given commands
1031
1032        for o, a in opts:
1033            command_error_msg = "option '%s' is not used with command '%s'" % (o, cmd)
1034
1035            if o in ( "-r", "--server-remove"):
1036                if cmd in server_no_remove:
1037                    command_error(command_error_msg)
1038            elif o in ( "-a", "--server-add",
1039                        "--server-add-username",
1040                        "--server-add-password"):
1041                if cmd in server_no_add:
1042                    command_error(command_error_msg)
1043
1044        server = None
1045        for o, a in opts:
1046            if o in ("-a", "--server-add"):
1047                if a == "self":
1048                    a = socket.gethostbyname(socket.getfqdn())
1049                server = "%s:%d" % util.hostport(a)
1050                servers['add'][server] = { 'user':'', 'password':''}
1051                self.server_list.append(server)
1052            elif o == "--server-add-username":
1053                if server:
1054                    servers['add'][server]['user'] = a
1055                self.sa_username = a
1056            elif o == "--server-add-password":
1057                if server:
1058                    servers['add'][server]['password'] = a
1059                self.sa_password = a
1060            elif o in ( "-r", "--server-remove"):
1061                server = "%s:%d" % util.hostport(a)
1062                servers['remove'][server] = True
1063                server = None
1064            elif o in ( "--server-failover"):
1065                server = "%s:%d" % util.hostport(a)
1066                servers['failover'][server] = True
1067                server = None
1068            elif o in ( "--server-recovery"):
1069                server = "%s:%d" % util.hostport(a)
1070                servers['recovery'][server] = True
1071                server = None
1072            elif o == "--nodes":
1073                for server in self.normalize_servers(a):
1074                    servers['log'][server] = True
1075            elif o in ('-o', '--output'):
1076                if a == 'json':
1077                    self.output = a
1078                server = None
1079            elif o in ('-d', '--debug'):
1080                self.debug = True
1081                server = None
1082            elif o in ('--cluster-init-password', '--cluster-password'):
1083                self.password_new = a
1084            elif o in ('--cluster-init-username', '--cluster-username'):
1085                self.username_new = a
1086            elif o in ('--cluster-init-port', '--cluster-port'):
1087                self.port_new = a
1088            elif o in ('--cluster-init-ramsize', '--cluster-ramsize'):
1089                self.per_node_quota = a
1090            elif o == '--cluster-index-ramsize':
1091                self.cluster_index_ramsize = a
1092            elif o == '--cluster-fts-ramsize':
1093                self.cluster_fts_ramsize = a
1094            elif o == '--index-storage-setting':
1095                self.index_storage_setting = a
1096            elif o == '--cluster-name':
1097                self.cluster_name = a
1098            elif o == '--enable-auto-failover':
1099                self.enable_auto_failover = bool_to_str(a)
1100            elif o == '--enable-notification':
1101                self.enable_notification = bool_to_str(a)
1102            elif o == '--auto-failover-timeout':
1103                self.autofailover_timeout = a
1104            elif o == '--compaction-db-percentage':
1105                self.compaction_db_percentage = a
1106            elif o == '--compaction-db-size':
1107                self.compaction_db_size = a
1108            elif o == '--compaction-view-percentage':
1109                self.compaction_view_percentage = a
1110            elif o == '--compaction-view-size':
1111                self.compaction_view_size = a
1112            elif o == '--compaction-period-from':
1113                self.compaction_period_from = a
1114            elif o == '--compaction-period-to':
1115                self.compaction_period_to = a
1116            elif o == '--enable-compaction-abort':
1117                self.enable_compaction_abort = bool_to_str(a)
1118            elif o == '--enable-compaction-parallel':
1119                self.enable_compaction_parallel = bool_to_str(a)
1120            elif o == '--gsi-compaction-mode':
1121                self.gsi_compact_mode = a
1122            elif o == '--compaction-gsi-percentage':
1123                self.gsi_compact_perc = a
1124            elif o == '--compaction-gsi-interval':
1125                self.gsi_compact_interval = a
1126            elif o == '--compaction-gsi-period-from':
1127                self.gsi_compact_period_from = a
1128            elif o == '--compaction-gsi-period-to':
1129                self.gsi_compact_period_to = a
1130            elif o == '--enable-gsi-compaction-abort':
1131                self.gsi_compact_abort = bool_to_str(a)
1132            elif o == '--enable-email-alert':
1133                self.enable_email_alert = bool_to_str(a)
1134            elif o == '--new-password':
1135                self.new_password_specified = True
1136                self.new_password = a
1137            elif o == '--rotate-data-key':
1138                self.rotate_data_key = True
1139            elif o == '--send-password':
1140                self.send_password_specified = True
1141                self.send_password = a
1142            elif o == '--config-path':
1143                self.config_path = a
1144            elif o == '--node-init-data-path':
1145                self.data_path = a
1146            elif o == '--node-init-index-path':
1147                self.index_path = a
1148            elif o == '--node-init-hostname':
1149                self.hostname = a
1150            elif o == '--email-recipients':
1151                self.email_recipient = a
1152            elif o == '--email-sender':
1153                self.email_sender = a
1154            elif o == '--email-user':
1155                self.email_user = a
1156            elif o == '--email-password':
1157                self.email_password = a
1158            elif o == '--email-host':
1159                self.email_host = a
1160            elif o == '--email-port':
1161                self.email_port = a
1162            elif o == '--enable-email-encrypt':
1163                self.email_enable_encrypt = bool_to_str(a)
1164            elif o == '--alert-auto-failover-node':
1165                self.autofailover_node = True
1166            elif o == '--alert-auto-failover-max-reached':
1167                self.autofailover_max_reached = True
1168            elif o == '--alert-auto-failover-node-down':
1169                self.autofailover_node_down = True
1170            elif o == '--alert-auto-failover-cluster-small':
1171                self.autofailover_cluster_small = True
1172            elif o == '--alert-auto-failover-disabled':
1173                self.autofailover_disabled = True
1174            elif o == '--alert-ip-changed':
1175                self.alert_ip_changed = True
1176            elif o == '--alert-disk-space':
1177                self.alert_disk_space = True
1178            elif o == '--alert-meta-overhead':
1179                self.alert_meta_overhead = True
1180            elif o == '--alert-meta-oom':
1181                self.alert_meta_oom = True
1182            elif o == '--alert-write-failed':
1183                self.alert_write_failed = True
1184            elif o == '--alert-audit-msg-dropped':
1185                self.alert_audit_dropped = True
1186            elif o == '--create':
1187                self.cmd = 'create'
1188            elif o == '--list':
1189                self.cmd = 'list'
1190            elif o == '--delete':
1191                self.cmd = 'delete'
1192            elif o == '--set':
1193                self.cmd = 'set'
1194            elif o == '--ro-username':
1195                self.ro_username = a
1196            elif o == '--ro-password':
1197                self.ro_password = a
1198            elif o == '--metadata-purge-interval':
1199                self.purge_interval = a
1200            elif o == '--group-name':
1201                self.group_name = a
1202            elif o == '--add-servers':
1203                self.server_list = self.normalize_servers(a)
1204                self.cmd = 'add-servers'
1205            elif o == '--remove-servers':
1206                self.server_list = self.normalize_servers(a)
1207                self.cmd = 'remove-servers'
1208            elif o == '--move-servers':
1209                self.server_list = self.normalize_servers(a)
1210                self.cmd = 'move-servers'
1211            elif o == '--from-group':
1212                self.from_group = a
1213            elif o == '--to-group':
1214                self.to_group = a
1215            elif o == '--rename':
1216                self.group_rename = a
1217                self.cmd = 'rename'
1218            elif o == '--retrieve-cert':
1219                self.cmd = 'retrieve'
1220                self.certificate_file = a
1221            elif o == '--regenerate-cert':
1222                self.cmd = 'regenerate'
1223                self.certificate_file = a
1224            elif o == '--cluster-cert-info':
1225                self.cmd = 'cluster-cert-info'
1226            elif o == '--extended':
1227                self.extended = True
1228            elif o == '--node-cert-info':
1229                self.cmd = 'node-cert-info'
1230            elif o == '--upload-cluster-ca':
1231                self.cmd = 'upload-cluster-ca'
1232                self.certificate_file = a
1233            elif o == '--set-node-certificate':
1234                self.cmd = 'set-node-certificate'
1235            elif o == '--force':
1236                self.hard_failover = True
1237            elif o == '--recovery-type':
1238                self.recovery_type = a
1239            elif o == '--recovery-buckets':
1240                self.recovery_buckets = a
1241            elif o == '--nodes':
1242                self.nodes = a
1243            elif o == '--all-nodes':
1244                self.all_nodes = True
1245            elif o == '--upload':
1246                self.upload = True
1247            elif o == '--upload-host':
1248                self.upload_host = a
1249            elif o == '--customer':
1250                self.customer = a
1251            elif o == '--ticket':
1252                self.ticket = a
1253            elif o == '--services':
1254                self.services = a
1255            elif o == '--audit-log-rotate-interval':
1256                self.audit_log_rotate_interval = a
1257            elif o == '--audit-log-path':
1258                self.audit_log_path = a
1259            elif o == '--audit-enabled':
1260                self.audit_enabled = bool_to_str(a)
1261            elif o == '--ldap-enabled':
1262                self.ldap_enabled = bool_to_str(a)
1263            elif o == '--ldap-admins':
1264                self.ldap_admins = a
1265            elif o == '--ldap-roadmins':
1266                self.ldap_roadmins = a
1267            elif o == '--ldap-default':
1268                self.ldap_default = a
1269            elif o == '--index-max-rollback-points':
1270                self.max_rollback_points = a
1271            elif o == '--index-stable-snapshot-interval':
1272                self.stable_snapshot_interval = a
1273            elif o == '--index-memory-snapshot-interval':
1274                self.memory_snapshot_interval = a
1275            elif o == '--index-threads':
1276                self.index_threads = a
1277            elif o == '--index-log-level':
1278                self.log_level = a
1279
1280            elif o == '--roles':
1281                self.roles = a
1282            elif o == '--my-roles':
1283                self.my_roles = True
1284            elif o == '--get-roles':
1285                self.get_roles = True
1286            elif o == '--set-users':
1287                self.set_users = a
1288            elif o == '--set-names':
1289                self.set_names = a
1290            elif o == '--delete-users':
1291                self.delete_users = a
1292
1293        return servers
1294
1295    def normalize_servers(self, server_list):
1296        slist = []
1297        sep = Node.SEP
1298        if server_list.find(sep) < 0:
1299            #backward compatible with ";" as separator
1300            sep = ";"
1301        for server in server_list.split(sep):
1302            hostport = "%s:%d" % util.hostport(server)
1303            slist.append(hostport)
1304        return slist
1305
1306    def reAddServers(self, servers):
1307        print "DEPRECATED: The server-readd command is deprecated and will be " + \
1308              "removed in a future release. Please use recovery instead."
1309        known_otps, eject_otps, failover_otps, readd_otps, _ = \
1310            self.getNodeOtps(to_readd=servers['add'])
1311
1312        for readd_otp in readd_otps:
1313            rest = util.restclient_factory(self.server,
1314                                         self.port,
1315                                         {'debug':self.debug},
1316                                         self.ssl)
1317            rest.setParam('otpNode', readd_otp)
1318
1319            opts = {
1320                'error_msg': "unable to re-add %s" % readd_otp,
1321                'success_msg': "re-add %s" % readd_otp
1322            }
1323            output_result = rest.restCmd('POST',
1324                                         rest_cmds['server-readd'],
1325                                         self.user,
1326                                         self.password,
1327                                         opts)
1328            print output_result
1329
1330    def getNodeOtps(self, to_eject=[], to_failover=[], to_readd=[]):
1331        """ Convert known nodes into otp node id's.
1332            """
1333        listservers = ListServers()
1334        known_nodes_list = listservers.getNodes(
1335                                listservers.getData(self.server,
1336                                                    self.port,
1337                                                    self.user,
1338                                                    self.password))
1339        known_otps = []
1340        eject_otps = []
1341        failover_otps = []
1342        readd_otps = []
1343        hostnames = []
1344
1345        for node in known_nodes_list:
1346            if node.get('otpNode') is None:
1347                raise Exception("could not access node")
1348            known_otps.append(node['otpNode'])
1349            hostnames.append(node['hostname'])
1350            if node['hostname'] in to_eject:
1351                eject_otps.append(node['otpNode'])
1352            if node['hostname'] in to_failover:
1353                if node['clusterMembership'] != 'active':
1354                    raise Exception('node %s is not active' % node['hostname'])
1355                else:
1356                    failover_otps.append((node['otpNode'], node['status']))
1357            _, host = node['otpNode'].split('@')
1358            hostport = "%s:%d" % util.hostport(host)
1359            if node['hostname'] in to_readd or hostport in to_readd:
1360                readd_otps.append(node['otpNode'])
1361
1362        return (known_otps, eject_otps, failover_otps, readd_otps, hostnames)
1363
1364    def recovery(self, servers):
1365        known_otps, eject_otps, failover_otps, readd_otps, _ = \
1366            self.getNodeOtps(to_readd=servers['recovery'])
1367        for readd_otp in readd_otps:
1368            rest = util.restclient_factory(self.server,
1369                                         self.port,
1370                                         {'debug':self.debug},
1371                                         self.ssl)
1372            opts = {
1373                'error_msg': "unable to setRecoveryType for node %s" % readd_otp,
1374                'success_msg': "setRecoveryType for node %s" % readd_otp
1375            }
1376            rest.setParam('otpNode', readd_otp)
1377            if self.recovery_type:
1378                rest.setParam('recoveryType', self.recovery_type)
1379            else:
1380                rest.setParam('recoveryType', 'delta')
1381            output_result = rest.restCmd('POST',
1382                                         '/controller/setRecoveryType',
1383                                         self.user,
1384                                         self.password,
1385                                         opts)
1386            print output_result
1387
1388    def rebalance(self, servers):
1389        known_otps, eject_otps, failover_otps, readd_otps, _ = \
1390            self.getNodeOtps(to_eject=servers['remove'])
1391        rest = util.restclient_factory(self.server,
1392                                     self.port,
1393                                     {'debug':self.debug},
1394                                     self.ssl)
1395        rest.setParam('knownNodes', ','.join(known_otps))
1396        rest.setParam('ejectedNodes', ','.join(eject_otps))
1397        if self.recovery_buckets:
1398            rest.setParam('requireDeltaRecoveryBuckets', self.recovery_buckets)
1399        opts = {
1400            'success_msg': 'rebalanced cluster',
1401            'error_msg': 'unable to rebalance cluster'
1402        }
1403        output_result = rest.restCmd('POST',
1404                                     rest_cmds['rebalance'],
1405                                     self.user,
1406                                     self.password,
1407                                     opts)
1408        if self.debug:
1409            print "INFO: rebalance started: %s" % output_result
1410
1411        sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
1412
1413        print "INFO: rebalancing",
1414
1415        status, error = self.rebalanceStatus(prefix='\n')
1416        while status in['running', 'unknown']:
1417            print ".",
1418            time.sleep(0.5)
1419            try:
1420                status, error = self.rebalanceStatus(prefix='\n')
1421            except socket.error:
1422                time.sleep(2)
1423                status, error = self.rebalanceStatus(prefix='\n')
1424
1425        if error:
1426            print '\n' + error
1427            sys.exit(1)
1428        else:
1429            print '\n' + output_result
1430
1431    def rebalanceStatus(self, prefix=''):
1432        rest = util.restclient_factory(self.server,
1433                                     self.port,
1434                                     {'debug':self.debug},
1435                                     self.ssl)
1436
1437        opts = {
1438            'error_msg': "unable to obtain rebalance status",
1439            'success_msg': "retrieve replication status successfully"
1440        }
1441        output_result = rest.restCmd('GET',
1442                                     '/pools/default/tasks',
1443                                     self.user,
1444                                     self.password,
1445                                     opts)
1446        tasks = rest.getJson(output_result)
1447        for task in tasks:
1448            error_message = None
1449            if "errorMessage" in task:
1450                error_message = task['errorMessage']
1451
1452            if task["type"] == "rebalance":
1453                if task["status"] == "running":
1454                    return task["status"], error_message
1455                if task["status"] == "notRunning":
1456                    if task.has_key("statusIsStale"):
1457                        if task["statusIsStale"] or task["statusIsStale"] == "true":
1458                            return "unknown", error_message
1459
1460                return task["status"], error_message
1461
1462        return "unknown", error_message
1463
1464    def rebalanceStop(self):
1465        rest = util.restclient_factory(self.server,
1466                                     self.port,
1467                                     {'debug':self.debug},
1468                                     self.ssl)
1469
1470        opts = {
1471            'success_msg': 'rebalance cluster stopped',
1472            'error_msg': 'unable to stop rebalance'
1473        }
1474        output_result = rest.restCmd('POST',
1475                                     rest_cmds['rebalance-stop'],
1476                                     self.user,
1477                                     self.password,
1478                                     opts)
1479        return output_result
1480
1481
1482    def failover(self, servers):
1483        known_otps, eject_otps, failover_otps, readd_otps, _ = \
1484            self.getNodeOtps(to_failover=servers['failover'])
1485
1486        if len(failover_otps) <= 0:
1487            command_error("specified servers are not part of the cluster: %s" %
1488                  servers['failover'].keys())
1489
1490        for failover_otp, node_status in failover_otps:
1491            rest = util.restclient_factory(self.server,
1492                                         self.port,
1493                                         {'debug':self.debug},
1494                                         self.ssl)
1495            opts = {
1496                'error_msg': "unable to failover %s" % failover_otp,
1497                'success_msg': "failover %s" % failover_otp
1498            }
1499            rest.setParam('otpNode', failover_otp)
1500            if self.hard_failover or node_status != 'healthy':
1501                output_result = rest.restCmd('POST',
1502                                             rest_cmds['failover'],
1503                                             self.user,
1504                                             self.password,
1505                                             opts)
1506                print output_result
1507            else:
1508                output_result = rest.restCmd('POST',
1509                                             '/controller/startGracefulFailover',
1510                                             self.user,
1511                                             self.password,
1512                                             opts)
1513                if self.debug:
1514                    print "INFO: rebalance started: %s" % output_result
1515
1516                sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
1517
1518                print "INFO: graceful failover",
1519
1520                status, error = self.rebalanceStatus(prefix='\n')
1521                while status == 'running':
1522                    print ".",
1523                    time.sleep(0.5)
1524                    try:
1525                        status, error = self.rebalanceStatus(prefix='\n')
1526                    except socket.error:
1527                        time.sleep(2)
1528                        status, error = self.rebalanceStatus(prefix='\n')
1529
1530                if error:
1531                    print '\n' + error
1532                else:
1533                    print '\n' + output_result
1534
1535    def userManage(self):
1536        if self.cmd == 'list':
1537            self.roUserList()
1538        elif self.cmd == 'delete':
1539            self.roUserDelete()
1540        elif self.cmd == 'set':
1541            self.roUserSet()
1542
1543    def roUserList(self):
1544        rest = util.restclient_factory(self.server,
1545                                     self.port,
1546                                     {'debug':self.debug},
1547                                     self.ssl)
1548        opts = { 'error_msg':'not any read only user defined'}
1549        try:
1550            output_result = rest.restCmd('GET',
1551                                         '/settings/readOnlyAdminName',
1552                                         self.user,
1553                                         self.password,
1554                                         opts)
1555            json = rest.getJson(output_result)
1556            print json
1557        except:
1558            pass
1559
1560    def roUserDelete(self):
1561        rest = util.restclient_factory(self.server,
1562                                     self.port,
1563                                     {'debug':self.debug},
1564                                     self.ssl)
1565
1566        opts = {
1567            'success_msg': 'readOnly user deleted',
1568            'error_msg': 'unable to delete readOnly user'
1569        }
1570        output_result = rest.restCmd('DELETE',
1571                                     "/settings/readOnlyUser",
1572                                     self.user,
1573                                     self.password,
1574                                     opts)
1575        print output_result
1576
1577    def roUserSet(self):
1578        rest = util.restclient_factory(self.server,
1579                                     self.port,
1580                                     {'debug':self.debug},
1581                                     self.ssl)
1582        try:
1583            output_result = rest.restCmd('GET',
1584                                         '/settings/readOnlyAdminName',
1585                                         self.user,
1586                                         self.password)
1587            json = rest.getJson(output_result)
1588            print "ERROR: readonly user %s exist already. Delete it before creating a new one" % json
1589            return
1590        except:
1591            pass
1592
1593        rest = util.restclient_factory(self.server,
1594                                     self.port,
1595                                     {'debug':self.debug},
1596                                     self.ssl)
1597        if self.ro_username:
1598            rest.setParam('username', self.ro_username)
1599        if self.ro_password:
1600            rest.setParam('password', self.ro_password)
1601        opts = {
1602            'success_msg': 'readOnly user created',
1603            'error_msg': 'fail to create readOnly user'
1604        }
1605        output_result = rest.restCmd('POST',
1606                                     "/settings/readOnlyUser",
1607                                     self.user,
1608                                     self.password,
1609                                     opts)
1610        print output_result
1611
1612    def groupManage(self):
1613        if self.cmd == 'move-servers':
1614            self.groupMoveServer()
1615        elif self.cmd == 'list':
1616             self.groupList()
1617        else:
1618            if self.group_name is None:
1619                command_error("please specify --group-name for the operation")
1620            elif self.cmd == 'delete':
1621                self.groupDelete()
1622            elif self.cmd == 'create':
1623                self.groupCreate()
1624            elif self.cmd == 'add-servers':
1625                print "DEPRECATED: Adding server from group-manage is deprecated, use server-add instead"
1626                self.groupAddServers()
1627            elif self.cmd == 'rename':
1628                self.groupRename()
1629            else:
1630                print "Unknown group command:%s" % self.cmd
1631
1632    def getGroupUri(self, groupName):
1633        rest = util.restclient_factory(self.server,
1634                                     self.port,
1635                                     {'debug':self.debug},
1636                                     self.ssl)
1637        output_result = rest.restCmd('GET',
1638                                     '/pools/default/serverGroups',
1639                                     self.user,
1640                                     self.password)
1641        groups = rest.getJson(output_result)
1642        for group in groups["groups"]:
1643            if groupName == group["name"]:
1644                return group["uri"]
1645        return None
1646
1647    def getServerGroups(self):
1648        rest = util.restclient_factory(self.server,
1649                                     self.port,
1650                                     {'debug':self.debug},
1651                                     self.ssl)
1652        output_result = rest.restCmd('GET',
1653                                     '/pools/default/serverGroups',
1654                                     self.user,
1655                                     self.password)
1656        return rest.getJson(output_result)
1657
1658    def groupList(self):
1659        rest = util.restclient_factory(self.server,
1660                                     self.port,
1661                                     {'debug':self.debug},
1662                                     self.ssl)
1663        output_result = rest.restCmd('GET',
1664                                     '/pools/default/serverGroups',
1665                                     self.user,
1666                                     self.password)
1667        groups = rest.getJson(output_result)
1668        found = False
1669        for group in groups["groups"]:
1670            if self.group_name is None or self.group_name == group['name']:
1671                found = True
1672                print '%s' % group['name']
1673                for node in group['nodes']:
1674                    print ' server: %s' % node["hostname"]
1675        if not found and self.group_name:
1676            print "Invalid group name: %s" % self.group_name
1677
1678    def groupCreate(self):
1679        rest = util.restclient_factory(self.server,
1680                                     self.port,
1681                                     {'debug':self.debug},
1682                                     self.ssl)
1683        rest.setParam('name', self.group_name)
1684        opts = {
1685            'error_msg': "unable to create group %s" % self.group_name,
1686            'success_msg': "group created %s" % self.group_name
1687        }
1688        output_result = rest.restCmd('POST',
1689                                     '/pools/default/serverGroups',
1690                                     self.user,
1691                                     self.password,
1692                                     opts)
1693        print output_result
1694
1695    def groupRename(self):
1696        uri = self.getGroupUri(self.group_name)
1697        if uri is None:
1698            command_error("invalid group name:%s" % self.group_name)
1699        if self.group_rename is None:
1700            command_error("invalid group name:%s" % self.group_name)
1701
1702        rest = util.restclient_factory(self.server,
1703                                     self.port,
1704                                     {'debug':self.debug},
1705                                     self.ssl)
1706        rest.setParam('name', self.group_rename)
1707        opts = {
1708            'error_msg': "unable to rename group %s" % self.group_name,
1709            'success_msg': "group renamed %s" % self.group_name
1710        }
1711        output_result = rest.restCmd('PUT',
1712                                     uri,
1713                                     self.user,
1714                                     self.password,
1715                                     opts)
1716        print output_result
1717
1718    def groupDelete(self):
1719        uri = self.getGroupUri(self.group_name)
1720        if uri is None:
1721            command_error("invalid group name:%s" % self.group_name)
1722
1723        rest = util.restclient_factory(self.server,
1724                                     self.port,
1725                                     {'debug':self.debug},
1726                                     self.ssl)
1727        rest.setParam('name', self.group_name)
1728        opts = {
1729            'error_msg': "unable to delete group %s" % self.group_name,
1730            'success_msg': "group deleted %s" % self.group_name
1731        }
1732        output_result = rest.restCmd('DELETE',
1733                                     uri,
1734                                     self.user,
1735                                     self.password,
1736                                     opts)
1737        print output_result
1738
1739    def groupAddServers(self):
1740        # If this is the first index node added then we need to make sure to
1741        # set the index storage setting.
1742        indexStorageParam = self.index_storage_to_param(self.index_storage_setting, True)
1743        if not indexStorageParam:
1744            print "ERROR: invalid index storage setting `%s`. Must be [default, memopt]" \
1745                % self.index_storage_setting
1746            return
1747
1748        cm = cluster_manager.ClusterManager(self.server, self.port, self.user,
1749                                            self.password, self.ssl)
1750
1751        settings, errors = cm.index_settings()
1752        _exitIfErrors(errors)
1753
1754        if not settings:
1755            print "Error: unable to infer the current index storage mode"
1756            return
1757
1758        if settings['storageMode'] == "":
1759            _, errors = cm.set_index_settings(indexStorageParam)
1760            if errors:
1761                _exitIfErrors(errors)
1762        elif settings['storageMode'] != self.index_storage_setting and \
1763             self.index_storage_setting:
1764            print "Error: Cannot change index storage mode from `%s` to `%s`" % \
1765                (settings['storageMode'], self.index_storage_setting)
1766            return
1767
1768        err, services = self.process_services(False)
1769        if err:
1770            print err
1771            return
1772        for server in self.server_list:
1773            _, errors = cm.add_server(server, self.group_name, self.sa_username,
1774                                           self.sa_password, services)
1775            if errors:
1776                _exitIfErrors(errors, "Error: Failed to add server %s: " % server)
1777
1778            if self.group_name:
1779                print "Server %s added to group %s" % (server, self.group_name)
1780            else:
1781                print "Server %s added" % server
1782
1783    def groupMoveServer(self):
1784        groups = self.getServerGroups()
1785        node_info = {}
1786        for group in groups["groups"]:
1787            if self.from_group == group['name']:
1788                for server in self.server_list:
1789                    for node in group["nodes"]:
1790                        if server == node["hostname"]:
1791                            node_info[server] = node
1792                            group["nodes"].remove(node)
1793        if not node_info:
1794            print "No servers removed from group '%s'" % self.from_group
1795            return
1796
1797        for group in groups["groups"]:
1798            if self.to_group == group['name']:
1799                for server in self.server_list:
1800                    found = False
1801                    for node in group["nodes"]:
1802                        if server == node["hostname"]:
1803                            found = True
1804                            break
1805                    if not found:
1806                        group["nodes"].append(node_info[server])
1807
1808        payload = json.dumps(groups)
1809        rest = util.restclient_factory(self.server,
1810                                     self.port,
1811                                     {'debug':self.debug},
1812                                     self.ssl)
1813        rest.setPayload(payload)
1814
1815        opts = {
1816            'error_msg': "unable to move servers from group '%s' to group '%s'" % (self.from_group, self.to_group),
1817            'success_msg': "move servers from group '%s' to group '%s'" % (self.from_group, self.to_group)
1818        }
1819        output_result = rest.restCmd('PUT',
1820                                     groups["uri"],
1821                                     self.user,
1822                                     self.password,
1823                                     opts)
1824        print output_result
1825
1826    def retrieveCert(self):
1827        if self.cmd in ['retrieve', 'regenerate', 'upload-cluster-ca'] and self.certificate_file is None:
1828            command_error("please specify certificate file name for the operation")
1829
1830        cm = cluster_manager.ClusterManager(self.server, self.port, self.user, self.password, self.ssl)
1831
1832        if self.cmd == 'retrieve':
1833            print "Warning --retrieve-cert is deprecated, use --cluster-cert-info"
1834            certificate, errors = cm.retrieve_cluster_certificate()
1835            _exitIfErrors(errors)
1836            _exitOnFileWriteFailure(self.certificate_file, certificate)
1837            print "SUCCESS: %s certificate to '%s'" % (self.cmd, self.certificate_file)
1838        elif self.cmd  == 'regenerate':
1839            certificate, errors = cm.regenerate_cluster_certificate()
1840            _exitIfErrors(errors)
1841            _exitOnFileWriteFailure(self.certificate_file, certificate)
1842            print "SUCCESS: %s certificate to '%s'" % (self.cmd, self.certificate_file)
1843        elif self.cmd == 'cluster-cert-info':
1844            certificate, errors = cm.retrieve_cluster_certificate(self.extended)
1845            _exitIfErrors(errors)
1846            if isinstance(certificate, dict):
1847                print json.dumps(certificate, sort_keys=True, indent=2)
1848            else:
1849                print certificate
1850        elif self.cmd == 'node-cert-info':
1851            certificate, errors = cm.retrieve_node_certificate('%s:%d' % (self.server, self.port))
1852            _exitIfErrors(errors)
1853            print json.dumps(certificate, sort_keys=True, indent=2)
1854        elif self.cmd == 'upload-cluster-ca':
1855            certificate = _exitOnFileReadFailure(self.certificate_file)
1856            _, errors = cm.upload_cluster_certificate(certificate)
1857            _exitIfErrors(errors)
1858            print "SUCCESS: uploaded cluster certificate to %s:%d" % (self.server, self.port)
1859        elif self.cmd == 'set-node-certificate':
1860            _, errors = cm.set_node_certificate()
1861            _exitIfErrors(errors)
1862            print "SUCCESS: node certificate set"
1863        else:
1864            print "ERROR: unknown request:", self.cmd
1865
1866    def collectLogsStart(self, servers):
1867        """Starts a cluster-wide log collection task"""
1868        if (servers['log'] is None) and (self.all_nodes is not True):
1869            command_error("please specify a list of nodes to collect logs from, " +
1870                  " or 'all-nodes'")
1871
1872        rest = util.restclient_factory(self.server, self.port,
1873                                     {'debug': self.debug}, self.ssl)
1874        if self.all_nodes:
1875            rest.setParam("nodes", "*")
1876        else:
1877            known_otps, eject_otps, failover_otps, readd_otps, hostnames = \
1878                self.getNodeOtps(to_readd=servers['log'])
1879            if not len(readd_otps):
1880                msg = ",".join(hostnames)
1881                command_error("invalid node name specified for collecting logs, available nodes are:\n"+msg)
1882
1883            nodelist = ",".join(readd_otps)
1884            rest.setParam("nodes", nodelist)
1885            print "NODES:", nodelist
1886
1887        if self.upload:
1888            if self.upload_host is None:
1889                command_error("please specify an upload-host when using --upload")
1890
1891            rest.setParam("uploadHost", self.upload_host)
1892
1893            if not self.customer:
1894                command_error("please specify a value for --customer when using" +
1895                      " --upload")
1896
1897            rest.setParam("customer", self.customer)
1898            rest.setParam("ticket", self.ticket)
1899
1900        opts = {
1901            'error_msg': "unable to start log collection:",
1902            'success_msg': "Log collection started"
1903        }
1904
1905        output_result = rest.restCmd(self.method, self.rest_cmd, self.user,
1906                                     self.password, opts)
1907        print output_result
1908
1909    def collectLogsStop(self):
1910        """Stops a cluster-wide log collection task"""
1911        rest = util.restclient_factory(self.server, self.port,
1912                                     {'debug': self.debug}, self.ssl)
1913
1914        opts = {
1915            'success_msg': 'collect logs successfully stopped',
1916            'error_msg': 'unable to stop collect logs'
1917        }
1918        output_result = rest.restCmd(self.method, self.rest_cmd, self.user,
1919                                     self.password, opts)
1920        print output_result
1921
1922    def collectLogsStatus(self):
1923        """Shows the current status of log collection task"""
1924        rest = util.restclient_factory(self.server, self.port,
1925                                     {'debug': self.debug}, self.ssl)
1926
1927        opts = {
1928            'error_msg': 'unable to obtain collect logs status'
1929        }
1930        output_result = rest.restCmd(self.method, self.rest_cmd, self.user,
1931                                     self.password, opts)
1932
1933        output_json = rest.getJson(output_result)
1934
1935        for e in output_json:
1936            if ((type(e) == type(dict()) and ('type' in e) and
1937                (e['type'] == 'clusterLogsCollection'))):
1938                print "Status:   %s" % e['status']
1939                if 'perNode' in e:
1940                    print "Details:"
1941                    for node, ns in e["perNode"].iteritems():
1942                        print '\tNode:', node
1943                        print '\tStatus:', ns['status']
1944                        for f in ["path", "statusCode", "url", "uploadStatusCode", "uploadOutput"]:
1945                            if f in ns:
1946                                print '\t', f, ":", ns[f]
1947                        print
1948                return
1949
1950    def getCommandSummary(self, cmd):
1951        """Return one-line summary info for each supported command"""
1952        command_summary = {
1953            "server-list" :"list all servers in a cluster",
1954            "server-info" :"show details on one server",
1955            "server-add" :"add one or more servers to the cluster",
1956            "server-readd" :"readd a server that was failed over",
1957            "group-manage" :"manage server groups",
1958            "rebalance" :"start a cluster rebalancing",
1959            "rebalance-stop" :"stop current cluster rebalancing",
1960            "rebalance-status" :"show status of current cluster rebalancing",
1961            "failover" :"failover one or more servers",
1962            "recovery" :"recover one or more servers",
1963            "setting-cluster" : "set cluster settings",
1964            "setting-compaction" : "set auto compaction settings",
1965            "setting-notification" : "set notification settings",
1966            "setting-alert" : "set email alert settings",
1967            "setting-autofailover" : "set auto failover settings",
1968            "collect-logs-start" : "start a cluster-wide log collection",
1969            "collect-logs-stop" : "stop a cluster-wide log collection",
1970            "collect-logs-status" : "show the status of cluster-wide log collection",
1971            "cluster-init" : "set the username,password and port of the cluster",
1972            "cluster-edit" : "modify cluster settings",
1973            "node-init" : "set node specific parameters",
1974            "ssl-manage" : "manage cluster certificate",
1975            "user-manage" : "manage read only user",
1976            "setting-index" : "set index settings",
1977            "setting-ldap" : "set ldap settings",
1978            "setting-audit" : "set audit settings",
1979            "admin-role-manage" : "set access-control roles for users"
1980        }
1981        if cmd in command_summary:
1982            return command_summary[cmd]
1983        else:
1984            return None
1985
1986    def getCommandHelp(self, cmd):
1987        """ Obtain detailed parameter help for Node commands
1988        Returns a list of pairs (arg1, arg1-information) or None if there's
1989        no help or cmd is unknown.
1990        """
1991
1992        # Some common flags for server- commands
1993        server_common = [("--server-add=HOST[:PORT]", "server to be added,"),
1994                         ("--server-add-username=USERNAME",
1995                          "admin username for the server to be added"),
1996                         ("--server-add-password=PASSWORD",
1997                          "admin password for the server to be added"),
1998                         ("--group-name=GROUPNAME", "group that server belongs")]
1999
2000        services = [("--services=data,index,query,fts",
2001                     "services that server runs")]
2002
2003        if cmd == "server-add" or cmd == "rebalance":
2004            return [("--index-storage-setting=SETTING", "index storage type [default, memopt]")] \
2005            + server_common + services
2006        elif cmd == "server-readd":
2007            return server_common
2008        elif cmd == "group-manage":
2009            return [
2010            ("--group-name=GROUPNAME", "group name"),
2011            ("--create", "create a new group"),
2012            ("--delete", "delete an empty group"),
2013            ("--list", "show group/server relationship map"),
2014            ("--rename=NEWGROUPNAME", "rename group to new name"),
2015            ("--add-servers=HOST[:PORT],HOST[:PORT]",
2016             "add a list of servers to group"),
2017            ("--move-servers=HOST[:PORT],HOST[:PORT]",
2018             "move a list of servers from group"),
2019            ("--from-group=GROUPNAME", "group name to move servers from"),
2020            ("--to-group=GROUPNAME", "group name to move servers into"),
2021            ("--index-storage-setting=SETTING", "index storage type [default, memopt]")] + services
2022        elif cmd == "cluster-init" or cmd == "cluster-edit":
2023            return [
2024            ("--cluster-username=USER", "new admin username"),
2025            ("--cluster-password=PASSWORD", "new admin password"),
2026            ("--cluster-port=PORT", "new cluster REST/http port"),
2027            ("--cluster-ramsize=RAMSIZEMB", "per node data service ram quota in MB"),
2028            ("--cluster-index-ramsize=RAMSIZEMB", "per node index service ram quota in MB"),
2029            ("--cluster-fts-ramsize=RAMSIZEMB", "per node fts service ram quota in MB"),
2030            ("--index-storage-setting=SETTING", "index storage type [default, memopt]")] + services
2031        elif cmd == "node-init":
2032            return [
2033            ("--node-init-data-path=PATH", "data path for database files"),
2034            ("--node-init-index-path=PATH", "index path for view data"),
2035            ("--node-init-hostname=HOSTNAME", "the hostname of this node")]
2036        elif cmd == "failover":
2037            return [
2038            ("--server-failover=HOST[:PORT]", "server to failover"),
2039            ("--force", "failover node from cluster right away")]
2040        elif cmd == "recovery":
2041            return [
2042            ("--server-recovery=HOST[:PORT]", "server to recover"),
2043            ("--recovery-type=TYPE[delta|full]",
2044             "type of recovery to be performed for a node")]
2045        elif cmd == "user-manage":
2046            return [
2047            ("--set", "create/modify a read only user"),
2048            ("--list", "list any read only user"),
2049            ("--delete", "delete read only user"),
2050            ("--ro-username=USERNAME", "readonly user name"),
2051            ("--ro-password=PASSWORD", "readonly user password")]
2052        elif cmd == "setting-compaction":
2053            return [
2054            ("", ""),
2055            ("Data/View compaction settings:", ""),
2056            ("  --compaction-db-percentage=PERC",
2057            "Starts compaction once data file fragmentation has reached this percentage"),
2058            ("  --compaction-db-size=SIZE",
2059             "Starts compaction once data file fragmentation has reached this size"),
2060            ("  --compaction-view-percentage=PERC",
2061            "Starts compaction once view file fragmentation has reached this percentage"),
2062            ("  --compaction-view-size=SIZE",
2063             "Starts compaction once view file fragmentation has reached this size"),
2064            ("  --compaction-period-from=HH:MM", "Allow compaction to run after this time"),
2065            ("  --compaction-period-to=HH:MM", "Allow compaction to run before this time"),
2066            ("  --enable-compaction-abort=[0|1]",
2067            "Abort compaction if when run outside of the accepted interval"),
2068            ("  --enable-compaction-parallel=[0|1]", "Allow view/data file compaction at the same time"),
2069            ("", ""),
2070            ("GSI index compaction settings:", ""),
2071            ("  --gsi-compaction-mode", "Sets the gsi compaction mode [append|circular]"),
2072            ("  --compaction-gsi-percentage=PERC",
2073            "Starts compaction once gsi file fragmentation has reached this percentage (Append mode only)"),
2074            ("  --compaction-gsi-interval",
2075            "A comma separated list of days compaction can run (Circular mode only)"),
2076            ("  --compaction-gsi-period-from=HH:MM",
2077            "Allow gsi compaction to run after this time (Circular mode only)"),
2078            ("  --compaction-gsi-period-to=HH:MM",
2079            "Allow gsi compaction to run before this time (Circular mode only)"),
2080            ("  --enable-gsi-compaction-abort=[0|1]",
2081            "Abort gsi compaction if when run outside of the accepted interaval (Circular mode only)")]
2082        elif cmd == "setting-alert":
2083            return [
2084            ("--enable-email-alert=[0|1]", "allow email alert"),
2085            ("--email-recipients=RECIPIENT",
2086             "email recipients, separate addresses with , or ;"),
2087            ("--email-sender=SENDER", "sender email address"),
2088            ("--email-user=USER", "email server username"),
2089            ("--email-password=PWD", "email server password"),
2090            ("--email-host=HOST", "email server host"),
2091            ("--email-port=PORT", "email server port"),
2092            ("--enable-email-encrypt=[0|1]", "email encrypt"),
2093            ("--alert-auto-failover-node", "node was auto failover"),
2094            ("--alert-auto-failover-max-reached",
2095             "maximum number of auto failover nodes was reached"),
2096            ("--alert-auto-failover-node-down",
2097             "node wasn't auto failover as other nodes are down at the same time"),
2098            ("--alert-auto-failover-cluster-small",
2099             "node wasn't auto fail over as cluster was too small"),
2100            ("--alert-auto-failover-disabled",
2101             "node was not auto-failed-over as auto-failover for one or more services running on the node is disabled"),
2102            ("--alert-ip-changed", "node ip address has changed unexpectedly"),
2103            ("--alert-disk-space",
2104             "disk space used for persistent storgage has reached at least 90% capacity"),
2105            ("--alert-meta-overhead",
2106             "metadata overhead is more than 50%"),
2107            ("--alert-meta-oom",
2108             "bucket memory on a node is entirely used for metadata"),
2109            ("--alert-write-failed",
2110             "writing data to disk for a specific bucket has failed"),
2111            ("--alert-audit-msg-dropped", "writing event to audit log has failed")]
2112        elif cmd == "setting-cluster":
2113            return [("--cluster-name=[CLUSTERNAME]", "cluster name"),
2114                    ("--cluster-ramsize=[RAMSIZEMB]", "per node data service ram quota in MB"),
2115                    ("--cluster-index-ramsize=[RAMSIZEMB]","per node index service ram quota in MB"),
2116                    ("--cluster-fts-ramsize=RAMSIZEMB", "per node fts service ram quota in MB")]
2117        elif cmd == "setting-notification":
2118            return [("--enable-notification=[0|1]", "allow notification")]
2119        elif cmd == "setting-autofailover":
2120            return [("--enable-auto-failover=[0|1]", "allow auto failover"),
2121                    ("--auto-failover-timeout=TIMEOUT (>=30)",
2122                     "specify timeout that expires to trigger auto failover")]
2123        elif cmd == "ssl-manage":
2124            return [("--cluster-cert-info", "prints cluster certificate info"),
2125                    ("--node-cert-info", "prints node certificate info"),
2126                    ("--retrieve-cert=CERTIFICATE",
2127                     "retrieve cluster certificate AND save to a pem file"),
2128                    ("--regenerate-cert=CERTIFICATE",
2129                     "regenerate cluster certificate AND save to a pem file"),
2130                    ("--set-node-certificate", "sets the node certificate"),
2131                    ("--upload-cluster-ca", "uploads a new cluster certificate")]
2132        elif cmd == "setting-audit":
2133            return [
2134            ("--audit-log-rotate-interval=[MINUTES]", "log rotation interval"),
2135            ("--audit-log-path=[PATH]", "target log directory"),
2136            ("--audit-enabled=[0|1]", "enable auditing or not")]
2137        elif cmd == "setting-ldap":
2138            return [
2139            ("--ldap-admins=", "full admins, separated by comma"),
2140            ("--ldap-roadmins=", "read only admins, separated by comma"),
2141            ("--ldap-enabled=[0|1]", "using LDAP protocol for authentication"),
2142            ("--ldap-default=[admins|roadmins|none]", "set default ldap accounts")]
2143        elif cmd == "setting-index":
2144            return [
2145            ("--index-max-rollback-points=[5]", "max rollback points"),
2146            ("--index-stable-snapshot-interval=SECONDS", "stable snapshot interval"),
2147            ("--index-memory-snapshot-interval=SECONDS", "in memory snapshot interval"),
2148            ("--index-threads=[4]", "indexer threads"),
2149            ("--index-log-level=[debug|silent|fatal|error|warn|info|verbose|timing|trace]", "indexer log level")]
2150        elif cmd == "collect-logs-start":
2151            return [
2152            ("--all-nodes", "Collect logs from all accessible cluster nodes"),
2153            ("--nodes=HOST[:PORT],HOST[:PORT]",
2154             "Collect logs from the specified subset of cluster nodes"),
2155            ("--upload", "Upload collects logs to specified host"),
2156            ("--upload-host=HOST",
2157             "Host to upload logs to (Manditory when --upload specified)"),
2158            ("--customer=CUSTOMER",
2159             "Customer name to use when uploading logs (Mandatory when --upload specified)"),
2160            ("--ticket=TICKET_NUMBER",
2161             "Ticket number to associate the uploaded logs with")]
2162        elif cmd == "admin-role-manage":
2163            return [
2164            ("--my-roles", "Return a list of roles for the current user."),
2165            ("--get-roles", "Return list of users and roles."),
2166            ("--set-users", "A comma-delimited list of user ids to set acess-control roles for"),
2167            ("--set-names", "A optional quoted, comma-delimited list names, one for each specified user id"),
2168            ("--roles", "A comma-delimited list of roles to set for users, one or more from admin, ro_admin, cluster_admin, replication_admin, bucket_admin[bucket name or '*'], views_admin[bucket name or '*']"),
2169            ("--delete-users", "A comma-delimited list of users to remove from access control")
2170            ]
2171        elif cmd == "master-password":
2172            return [
2173            ("--new-password", "Prompts user for a new master password on this node."),
2174            ("--rotate-data-key", "Rotates the master password data key."),
2175            ("--send-password", "Prompts for the master password to start the server."),
2176            ]
2177        else:
2178            return None
2179
2180    def getCommandExampleHelp(self, cmd):
2181        """ Obtain detailed example help for command
2182        Returns a list of command examples to illustrate how to use command
2183        or None if there's no example help or cmd is unknown.
2184        """
2185
2186        if cmd == "cluster-init":
2187            return [("Set data service ram quota and index ram quota",
2188"""
2189    couchbase-cli cluster-init -c 192.168.0.1:8091 \\
2190       --cluster-username=Administrator \\
2191       --cluster-password=password \\
2192       --cluster-port=8080 \\
2193       --services=data,index \\
2194       --cluster-ramsize=300 \\
2195       --cluster-index-ramsize=256\\
2196       --index-storage-setting=memopt""")]
2197        elif cmd == "cluster-edit":
2198            return [("Change the cluster username, password, port and data service ram quota",
2199"""
2200    couchbase-cli cluster-edit -c 192.168.0.1:8091 \\
2201       --cluster-username=Administrator1 \\
2202       --cluster-password=password1 \\
2203       --cluster-port=8080 \\
2204       --cluster-ramsize=300 \\
2205       -u Administrator -p password""")]
2206        elif cmd == "node-init":
2207            return [("Set data path and hostname for an unprovisioned cluster",
2208"""
2209    couchbse-cli node-init -c 192.168.0.1:8091 \\
2210       --node-init-data-path=/tmp/data \\
2211       --node-init-index-path=/tmp/index \\
2212       --node-init-hostname=myhostname \\
2213       -u Administrator -p password"""),
2214                    ("Change the data path",
2215"""
2216     couchbase-cli node-init -c 192.168.0.1:8091 \\
2217       --node-init-data-path=/tmp \\
2218       -u Administrator -p password""")]
2219        elif cmd == "server-add":
2220            return [("Add a node to a cluster, but do not rebalance",
2221"""
2222    couchbase-cli server-add -c 192.168.0.1:8091 \\
2223       --server-add=192.168.0.2:8091 \\
2224       --server-add-username=Administrator1 \\
2225       --server-add-password=password1 \\
2226
2227       --group-name=group1 \\
2228       --index-storage-setting=memopt \\
2229       -u Administrator -p password"""),
2230                    ("Add a node to a cluster, but do not rebalance",
2231"""
2232    couchbase-cli server-add -c 192.168.0.1:8091 \\
2233       --server-add=192.168.0.2:8091 \\
2234       --server-add-username=Administrator1 \\
2235       --server-add-password=password1 \\
2236       --group-name=group1 \\
2237       -u Administrator -p password""")]
2238        elif cmd == "rebalance":
2239            return [("Add a node to a cluster and rebalance",
2240"""
2241    couchbase-cli rebalance -c 192.168.0.1:8091 \\
2242       --server-add=192.168.0.2:8091 \\
2243       --server-add-username=Administrator1 \\
2244       --server-add-password=password1 \\
2245       --group-name=group1 \\
2246       -u Administrator -p password"""),
2247                    ("Add a node to a cluster and rebalance",
2248"""
2249    couchbase-cli rebalance -c 192.168.0.1:8091 \\
2250       --server-add=192.168.0.2:8091 \\
2251       --server-add-username=Administrator1 \\
2252       --server-add-password=password1 \\
2253       --group-name=group1 \\
2254       -u Administrator -p password"""),
2255                    ("Remove a node from a cluster and rebalance",
2256"""
2257    couchbase-cli rebalance -c 192.168.0.1:8091 \\
2258       --server-remove=192.168.0.2:8091 \\
2259       -u Administrator -p password"""),
2260                    ("Remove and add nodes from/to a cluster and rebalance",
2261"""
2262    couchbase-cli rebalance -c 192.168.0.1:8091 \\
2263      --server-remove=192.168.0.2 \\
2264      --server-add=192.168.0.4 \\
2265      --server-add-username=Administrator1 \\
2266      --server-add-password=password1 \\
2267      --group-name=group1 \\
2268      -u Administrator -p password""")
2269       ]
2270        elif cmd == "rebalance-stop":
2271            return [("Stop the current rebalancing",
2272"""
2273    couchbase-cli rebalance-stop -c 192.168.0.1:8091 \\
2274       -u Administrator -p password""")]
2275        elif cmd == "recovery":
2276            return [("Set recovery type to a server",
2277"""
2278    couchbase-cli recovery -c 192.168.0.1:8091 \\
2279       --server-recovery=192.168.0.2 \\
2280       --recovery-type=full \\
2281       -u Administrator -p password""")]
2282        elif cmd == "failover":
2283            return [("Set a failover, readd, recovery and rebalance sequence operations",
2284"""
2285    couchbase-cli failover -c 192.168.0.1:8091 \\
2286       --server-failover=192.168.0.2 \\
2287       -u Administrator -p password
2288
2289    couchbase-cli server-readd -c 192.168.0.1:8091 \\
2290       --server-add=192.168.0.2 \\
2291       -u Administrator -p password
2292
2293    couchbase-cli recovery -c 192.168.0.1:8091 \\
2294       --server-recovery=192.168.0.2 \\
2295       --recovery-type=delta \\
2296       -u Administrator -p password
2297
2298    couchbase-cli rebalance -c 192.168.0.1:8091 \\
2299       --recovery-buckets="default,bucket1" \\
2300       -u Administrator -p password""")]
2301        elif cmd == "user-manage":
2302            return [("List read only user in a cluster",
2303"""
2304    couchbase-cli user-manage --list -c 192.168.0.1:8091 \\
2305           -u Administrator -p password"""),
2306                ("Delete a read only user in a cluster",
2307"""
2308    couchbase-cli user-manage -c 192.168.0.1:8091 \\
2309        --delete --ro-username=readonlyuser \\
2310        -u Administrator -p password"""),
2311                ("Create/modify a read only user in a cluster",
2312"""
2313    couchbase-cli user-manage -c 192.168.0.1:8091 \\
2314        --set --ro-username=readonlyuser --ro-password=readonlypassword \\
2315        -u Administrator -p password""")]
2316        elif cmd == "group-manage":
2317            return [("Create a new group",
2318"""
2319    couchbase-cli group-manage -c 192.168.0.1:8091 \\
2320        --create --group-name=group1 -u Administrator -p password"""),
2321                ("Delete an empty group",
2322"""
2323    couchbase-cli group-manage -c 192.168.0.1:8091 \\
2324        --delete --group-name=group1 -u Administrator -p password"""),
2325                ("Rename an existed group",
2326"""
2327    couchbase-cli group-manage -c 192.168.0.1:8091 \\
2328        --rename=newgroup --group-name=group1 -u Administrator -p password"""),
2329                ("Show group/server map",
2330"""
2331    couchbase-cli group-manage -c 192.168.0.1:8091 \\
2332        --list -u Administrator -p password"""),
2333                ("Add a server to a group",
2334"""
2335    couchbase-cli group-manage -c 192.168.0.1:8091 \\
2336        --add-servers=10.1.1.1:8091,10.1.1.2:8091 \\
2337        --group-name=group1 \\
2338        --server-add-username=Administrator1 \\
2339        --server-add-password=password1 \\
2340        --services=data,index,query,fts \\
2341        -u Administrator -p password"""),
2342                ("Move list of servers from group1 to group2",
2343"""
2344    couchbase-cli group-manage -c 192.168.0.1:8091 \\
2345        --move-servers=10.1.1.1:8091,10.1.1.2:8091 \\
2346        --from-group=group1 \\
2347        --to-group=group2 \\
2348        -u Administrator -p password""")]
2349        elif cmd == "ssl-manage":
2350            return [("Download a cluster certificate",
2351"""
2352    couchbase-cli ssl-manage -c 192.168.0.1:8091 \\
2353        --retrieve-cert=/tmp/test.pem \\
2354        -u Administrator -p password
2355
2356    couchbase-cli ssl-manage -c 192.168.0.1:8091 \\
2357        --cluster-cert-info \\
2358        -u Administrator -p password"""),
2359                ("Regenerate AND download a cluster certificate",
2360"""
2361    couchbase-cli ssl-manage -c 192.168.0.1:8091 \\
2362        --regenerate-cert=/tmp/test.pem \\
2363        -u Administrator -p password"""),
2364                ("Download the extended cluster certificate",
2365"""
2366    couchbase-cli ssl-manage -c 192.168.0.1:8091 \\
2367        --cluster-cert-info --extended \\
2368        -u Administrator -p password"""),
2369                ("Download the current node certificate",
2370"""
2371    couchbase-cli ssl-manage -c 192.168.0.1:8091 \\
2372        --node-cert-info \\
2373        -u Administrator -p password"""),
2374                ("Upload a new cluster certificate",
2375"""
2376    couchbase-cli ssl-manage -c 192.168.0.1:8091 \\
2377        --upload-cluster-ca=/tmp/test.pem \\
2378        -u Administrator -p password"""),
2379                ("Set the new node certificate",
2380"""
2381    couchbase-cli ssl-manage -c 192.168.0.1:8091 \\
2382        --set-node-certificate \\
2383        -u Administrator -p password""")]
2384        elif cmd == "collect-logs-start":
2385            return [("Start cluster-wide log collection for whole cluster",
2386"""
2387    couchbase-cli collect-logs-start -c 192.168.0.1:8091 \\
2388        -u Administrator -p password \\
2389        --all-nodes --upload --upload-host=host.upload.com \\
2390        --customer="example inc" --ticket=12345"""),
2391                ("Start cluster-wide log collection for selected nodes",
2392"""
2393    couchbase-cli collect-logs-start -c 192.168.0.1:8091 \\
2394        -u Administrator -p password \\
2395        --nodes=10.1.2.3:8091,10.1.2.4 --upload --upload-host=host.upload.com \\
2396        --customer="example inc" --ticket=12345""")]
2397        elif cmd == "collect-logs-stop":
2398            return [("Stop cluster-wide log collection",
2399"""
2400    couchbase-cli collect-logs-stop -c 192.168.0.1:8091 \\
2401        -u Administrator -p password""")]
2402        elif cmd == "collect-logs-status":
2403            return [("Show status of cluster-wide log collection",
2404"""
2405    couchbase-cli collect-logs-status -c 192.168.0.1:8091 \\
2406        -u Administrator -p password""")]
2407        elif cmd == "setting-ldap":
2408            return [("Enable LDAP with None default",
2409"""
2410    couchbase-cli setting-ldap -c 192.168.0.1:8091 \\
2411        --ldap-enabled=1 --ldap-admins=u1,u2 --ldap-roadmins=u3,u3,u5 \\
2412        -u Administrator -p password"""),
2413                ("Enable LDAP with full admin default",
2414"""
2415    couchbase-cli setting-ldap -c 192.168.0.1:8091 \\
2416        --ldap-enabled=1 --ldap-default=admins --ldap-roadmins=u3,u3,u5 \\
2417        -u Administrator -p password"""),
2418                ("Enable LDAP with read only default",
2419"""
2420    couchbase-cli setting-ldap -c 192.168.0.1:8091 \\
2421        --ldap-enabled=1 --ldap-default=roadmins --ldap-admins=u1,u2 \\
2422        -u Administrator -p password"""),
2423                ("Disable LDAP",
2424"""
2425    couchbase-cli setting-ldap -c 192.168.0.1:8091 \\
2426        --ldap-enabled=0  -u Administrator -p password""")]
2427        elif cmd == "setting-audit":
2428            return [("Enable audit",
2429"""
2430    couchbase-cli setting-audit -c 192.168.0.1:8091 \\
2431        --audit-enabled=1 --audit-log-rotate-interval=900 \\
2432        --audit-log-path="/opt/couchbase/var/lib/couchbase/logs"
2433        -u Administrator -p password"""),
2434                ("Disable audit",
2435"""
2436    couchbase-cli setting-audit -c 192.168.0.1:8091 \\
2437        --audit-enabled=0 -u Administrator -p password""")]
2438        elif cmd == "setting-index":
2439            return [("Indexer setting",
2440"""
2441    couchbase-cli setting-index  -c 192.168.0.1:8091 \\
2442        --index-max-rollback-points=5 \\
2443        --index-stable-snapshot-interval=5000 \\
2444        --index-memory-snapshot-interval=200 \\
2445        --index-threads=5 \\
2446        --index-log-level=debug \\
2447        -u Administrator -p password""")]
2448
2449        elif cmd == "admin-role-manage":
2450            return [("Show the current users roles.",
2451"""
2452    couchbase-cli admin-role-manage -c 192.168.0.1:8091 --my-roles
2453            """),
2454            ("Get a list of all users and roles",
2455"""
2456    couchbase-cli admin-role-manage -c 192.168.0.1:8091 --get-roles
2457            """),
2458                    ("Make bob and mary cluster_admins, and bucket admins for the default bucket",
2459"""
2460    couchbase-cli admin-role-manage -c 192.168.0.1:8091 \\
2461        --set-users=bob,mary --set-names="Bob Smith,Mary Jones" --roles=cluster_admin,bucket_admin[default]
2462            """),
2463                    ("Make jen bucket admins for all buckets",
2464"""
2465    couchbase-cli admin-role-manage -c 192.168.0.1:8091 \\
2466        --set-users=jen --roles=bucket_admin[*]
2467            """),
2468            ("Remove all roles for bob",
2469"""
2470    couchbase-cli admin-role-manage -c 192.168.0.1:8091 --delete-users=bob
2471            """)
2472            ]
2473        elif cmd == "master-password":
2474            return [("Change the master password",
2475"""
2476    couchbase-cli master-password -c 192.168.0.1:8091 -u Administrator \\
2477        -p password --new-password
2478            """),
2479            ("Rotate the master password data key",
2480"""
2481    couchbase-cli master-password -c 192.168.0.1:8091 -u Administrator \\
2482        -p password --rotate-data-key
2483            """),
2484            ("Send the master password to the server",
2485"""
2486    couchbase-cli master-password -c 192.168.0.1:8091 --send-password
2487            """),
2488            ]
2489
2490        else:
2491            return None
2492
2493def _exitIfErrors(errors, prefix=""):
2494    if errors:
2495        for error in errors:
2496            print prefix + error
2497        sys.exit(1)
2498
2499def _exitOnFileWriteFailure(fname, bytes):
2500    try:
2501        fp = open(fname, 'w')
2502        fp.write(bytes)
2503        fp.close()
2504    except IOError, error:
2505        print "ERROR:", error
2506        sys.exit(1)
2507
2508def _exitOnFileReadFailure(fname):
2509    try:
2510        fp = open(fname, 'r')
2511        bytes = fp.read()
2512        fp.close()
2513        return bytes
2514    except IOError, error:
2515        print "ERROR:", error
2516        sys.exit(1)
2517