xref: /4.6.0/couchbase-cli/cluster_manager.py (revision d08d5a08)
1"""Management API's for Couchbase Cluster"""
2
3import requests
4import csv
5import StringIO
6
7N1QL_SERVICE = 'n1ql'
8INDEX_SERVICE = 'index'
9MGMT_SERVICE = 'mgmt'
10FTS_SERVICE = 'fts'
11
12DEFAULT_REQUEST_TIMEOUT = 60
13
14# Remove this once we can verify SSL certificates
15requests.packages.urllib3.disable_warnings()
16
17def request(f):
18    def g(*args, **kwargs):
19        cm = args[0]
20        url = args[1]
21        try:
22            return f(*args, **kwargs)
23        except requests.exceptions.ConnectionError, e:
24            return None, ['Unable to connect to host at %s' % cm.hostname]
25        except requests.exceptions.ReadTimeout, e:
26            return None, ['Request to host `%s` timed out after %d seconds' % (url, cm.timeout)]
27    return g
28
29
30class ServiceNotAvailableException(Exception):
31    """An exception raised when a service does not exist in the target cluster"""
32
33    def __init__(self, service):
34        Exception.__init__(self, "Service %s not available in target cluster" % service)
35
36class ClusterManager(object):
37    """A set of REST API's for managing a Couchbase cluster"""
38
39    def __init__(self, host, port, username, password, ssl=False, timeout=DEFAULT_REQUEST_TIMEOUT):
40        if ssl:
41            self.hostname = 'https://%s:%s' % (host, str(port))
42        else:
43            self.hostname = 'http://%s:%s' % (host, str(port))
44
45        self.username = username
46        self.password = password
47        self.timeout = timeout
48        self.ssl = ssl
49
50    def n1ql_query(self, stmt, args=None):
51        """Sends a N1QL query
52
53        Sends a N1QL query and returns the result of the query. Raises a
54        ServiceNotAvailable exception if the target cluster is no running the n1ql
55        service."""
56
57        hosts, errors = self.get_hostnames_for_service(N1QL_SERVICE)
58        if errors:
59            return None, errors
60
61        if not hosts:
62            raise ServiceNotAvailableException(N1QL_SERVICE)
63
64        url = hosts[0] + '/query/service'
65        body = {'statement': str(stmt)}
66
67        if args:
68            body['args'] = str(args)
69
70        result, errors = self._post_form_encoded(url, body)
71        if errors:
72            return None, errors
73
74        return result, None
75
76    def get_hostnames_for_service(self, service_name):
77        """ Gets all hostnames that run a service
78
79        Gets all hostnames for specified service and returns a list of strings
80        in the form "http://hostname:port". If the ClusterManager is configured
81        to use SSL/TLS then "https://" is prefixed to each name instead of
82        "http://"."""
83        url = self.hostname + '/pools/default/nodeServices'
84        data, errors = self._get(url)
85        if errors:
86            return None, errors
87
88        hosts = []
89        for node in data['nodesExt']:
90            node_host = '127.0.0.1'
91            if 'hostname' in node:
92                node_host = node['hostname']
93
94            http_prefix = 'http://'
95            fts_port_name = 'fts'
96            n1ql_port_name = 'n1ql'
97            mgmt_port_name = 'mgmt'
98            index_port_name = 'indexHttp'
99
100            if self.ssl:
101                http_prefix = 'https://'
102                n1ql_port_name = 'n1qlSSL'
103                mgmt_port_name = 'mgmtSSL'
104                # The is no ssl port for the index or fts services
105
106            if service_name == MGMT_SERVICE and mgmt_port_name in node['services']:
107                hosts.append(http_prefix + node_host + ':' + str(node['services'][mgmt_port_name]))
108
109            if service_name == N1QL_SERVICE and n1ql_port_name in node['services']:
110                hosts.append(http_prefix + node_host + ':' + str(node['services'][n1ql_port_name]))
111
112            if service_name == INDEX_SERVICE and index_port_name in node['services']:
113                hosts.append(http_prefix + node_host + ':' + str(node['services'][index_port_name]))
114
115            if service_name == FTS_SERVICE and fts_port_name in node['services']:
116                hosts.append(http_prefix + node_host + ':' + str(node['services'][fts_port_name]))
117
118        return hosts, None
119
120    def pools(self):
121        """ Retrieves information about Couchbase management pools
122
123        Returns Couchbase pools data"""
124        url = self.hostname + '/pools'
125        return self._get(url)
126
127    def set_admin_password(self, password):
128        url = self.hostname + '/controller/resetAdminPassword'
129        params = { "password": password }
130
131        return self._post_form_encoded(url, params)
132
133    def regenerate_admin_password(self):
134        url = self.hostname + '/controller/resetAdminPassword?generate=1'
135
136        return self._post_form_encoded(url, None)
137
138    def get_server_groups(self):
139        url = self.hostname + '/pools/default/serverGroups'
140        return self._get(url)
141
142    def get_server_group(self, groupName):
143        groups, errors = self.get_server_groups()
144        if errors:
145            return None, error
146
147        if not groups or not groups["groups"] or groups["groups"] == 0:
148            return None, ["No server groups found"]
149
150        if groupName:
151            for group in groups["groups"]:
152                if group["name"] == groupName:
153                    return group, None
154            return None, ["Group `%s` not found" % groupName]
155        else:
156            return groups["groups"][0], None
157
158    def add_server(self, add_server, groupName, username, password, services):
159        group, errors = self.get_server_group(groupName)
160        if errors:
161            return None, errors
162
163        url = self.hostname + group["addNodeURI"]
164        params = { "hostname": add_server,
165                   "user": username,
166                   "password": password,
167                   "services": services }
168
169        return self._post_form_encoded(url, params)
170
171    def create_bucket(self, bucket, ramQuotaMB, authType, saslPassword,
172                      replicaNumber, proxyPort, bucketType):
173        url = self.hostname + '/pools/default/buckets'
174
175        params = dict()
176        if authType == 'none':
177            params = { "name": bucket,
178                       "ramQuotaMB": ramQuotaMB,
179                       "authType": authType,
180                       "replicaNumber": replicaNumber,
181                       "proxyPort": proxyPort,
182                       "bucketType": bucketType }
183
184        elif authType == 'sasl':
185            params = { "name": bucket,
186                       "ramQuotaMB": ramQuotaMB,
187                       "authType": authType,
188                       "replicaNumber": replicaNumber,
189                       "proxyPort": 0,
190                       "bucketType": bucketType }
191
192        return self._post_form_encoded(url, params)
193
194    def list_buckets(self):
195        url = self.hostname + '/pools/default/buckets'
196        result, errors = self._get(url)
197        if errors:
198            return None, errors
199
200        names = list()
201        for bucket in result:
202            names.append(bucket["name"])
203
204        return names, None
205
206    def set_index_settings(self, storageMode):
207        """ Sets global index settings"""
208        params = dict()
209        params["storageMode"] = storageMode
210
211        url = self.hostname + '/settings/indexes'
212        return self._post_form_encoded(url, params)
213
214    def index_settings(self):
215        """ Retrieves the index settings
216
217            Returns a map of all global index settings"""
218        url = self.hostname + '/settings/indexes'
219        return self._get(url)
220
221    def rotate_master_pwd(self):
222        url = self.hostname + '/node/controller/rotateDataKey'
223        return self._post_form_encoded(url, None)
224
225    def set_master_pwd(self, password):
226        url = self.hostname + '/node/controller/changeMasterPassword'
227        params = { "newPassword": password }
228        return self._post_form_encoded(url, params)
229
230    def setRoles(self,userList,roleList,userNameList):
231        # we take a comma-delimited list of roles that needs to go into a dictionary
232        paramDict = {"roles" : roleList}
233        userIds = []
234        userNames = []
235        userF = StringIO.StringIO(userList)
236        for idList in csv.reader(userF, delimiter=','):
237            userIds.extend(idList)
238
239        # did they specify user names?
240        if userNameList != None:
241            userNameF = StringIO.StringIO(userNameList)
242            for nameList in csv.reader(userNameF, delimiter=','):
243                userNames.extend(nameList)
244            if len(userNames) != len(userIds):
245                return None, ["Error: specified %d user ids and %d user names, must have the same number of each." %  (len(userIds),len(userNames))]
246
247        # did they specify user names?
248        # but we need a separate REST call for each user in the comma-delimited user list
249        for index in range(len(userIds)):
250            user = userIds[index]
251            paramDict["id"] = user
252            if len(userNames) > 0:
253                paramDict["name"] = userNames[index]
254            url = self.hostname + '/settings/rbac/users/' + user
255            data, errors = self._put(url,paramDict)
256            if errors:
257                return data, errors
258
259        return data, errors
260
261    def deleteRoles(self,userList):
262        # need a separate REST call for each user in the comma-delimited user list
263        userF = StringIO.StringIO(userList)
264        reader = csv.reader(userF, delimiter=',')
265        for users in reader:
266            for user in users:
267                url = self.hostname + '/settings/rbac/users/' + user
268                data, errors = self._delete(url)
269                if errors:
270                    return data, errors
271
272        return data, errors
273
274    def getRoles(self):
275        url = self.hostname + '/settings/rbac/users'
276        data, errors = self._get(url)
277
278        return data, errors
279
280    def myRoles(self):
281        url = self.hostname + '/whoami'
282        data, errors = self._get(url)
283
284        return data, errors
285
286    def retrieve_cluster_certificate(self, extended=False):
287        """ Retrieves the current cluster certificate
288
289        Gets the current cluster certificate. If extended is set tot True then
290        we return the extended certificate which contains the certificate type,
291        certicicate key, expiration, subject, and warnings."""
292        url = self.hostname + '/pools/default/certificate'
293        if extended:
294            url += '?extended=true'
295        return self._get(url)
296
297    def regenerate_cluster_certificate(self):
298        """ Regenerates the cluster certificate
299
300        Regenerates the cluster certificate and returns the new certificate."""
301        url = self.hostname + '/controller/regenerateCertificate'
302        return self._post_form_encoded(url, None)
303
304    def upload_cluster_certificate(self, certificate):
305        """ Uploads a new cluster certificate"""
306        url = self.hostname + '/controller/uploadClusterCA'
307        return self._post_form_encoded(url, certificate)
308
309    def retrieve_node_certificate(self, node):
310        """ Retrieves the current node certificate
311
312        Returns the current node certificate"""
313        url = self.hostname + '/pools/default/certificate/node/' + node
314        return self._get(url)
315
316    def set_node_certificate(self):
317        """Activates the current node certificate
318
319        Grabs chain.pem and pkey.pem from the <data folder>/inbox/ directory and
320        applies them to the node. chain.pem contains the chain encoded certificates
321        starting from the node certificat and ending with the last intermediate
322        certificate before cluster CA. pkey.pem contains the pem encoded private
323        key for node certifiactes. Both files should exist on the server before
324        this API is called."""
325        url = self.hostname + '/node/controller/reloadCertificate'
326        return self._post_form_encoded(url, None)
327
328    # Low level methods for basic HTML operations
329
330    @request
331    def _get(self, url):
332        response = requests.get(url, auth=(self.username, self.password), verify=False,
333                                timeout=self.timeout)
334        return _handle_response(response)
335
336    @request
337    def _post_form_encoded(self, url, params):
338        response = requests.post(url, auth=(self.username, self.password), data=params,
339                                 verify=False, timeout=self.timeout)
340        return _handle_response(response)
341
342    @request
343    def _put(self, url, params):
344        response = requests.put(url, params, auth=(self.username, self.password),
345                                verify=False, timeout=self.timeout)
346        return _handle_response(response)
347
348    @request
349    def _delete(self, url):
350        response = requests.delete(url, auth=(self.username, self.password),
351                                   verify=False, timeout=self.timeout)
352        return _handle_response(response)
353
354
355def _handle_response(response):
356    if response.status_code in [200, 202]:
357        if 'Content-Type' not in response.headers:
358            return "", None
359        if 'application/json' in response.headers['Content-Type']:
360            return response.json(), None
361        else:
362            return response.text, None
363    elif response.status_code in [400, 404]:
364        if 'application/json' in response.headers['Content-Type']:
365            errors = response.json()
366            if isinstance(errors, list):
367                return None, errors
368        return None, [response.text]
369    elif response.status_code == 401:
370        return None, ['ERROR: unable to access the REST API - please check your username' +
371                      '(-u) and password (-p)']
372    elif response.status_code == 500:
373        return None, ['ERROR: Internal server error, please retry your request']
374    else:
375        return None, ['Error: Recieved unexpected status %d' % response.status_code]
376