[ANSIBLE 3.2.0] Upgrade 'nginx' role's tasks to ansible 3.2.0
[oom/offline-installer.git] / ansible / library / rancher1_api.py
1 #!/usr/bin/python
2
3 from ansible.module_utils.basic import AnsibleModule
4
5 import requests
6 import json
7 import functools
8 import time
9
10 DOCUMENTATION = """
11 ---
12 module: rancher1_api
13 short_description: Client library for rancher API
14 description:
15   - This module modifies a rancher 1.6 using it's API (v1).
16   - It supports some rancher features by the virtue of a 'mode'.
17   - 'modes' hide from you some necessary cruft and expose you to the only
18     important and interestig variables wich must be set. The mode mechanism
19     makes this module more easy to use and you don't have to create an
20     unnecessary boilerplate for the API.
21   - Only a few modes are/will be implemented so far - as they are/will be
22     needed. In the future the 'raw' mode can be added to enable you to craft
23     your own API requests, but that would be on the same level of a user
24     experience as running curl commands, and because the rancher 1.6 is already
25     obsoleted by the project, it would be a wasted effort.
26 options:
27   rancher:
28     description:
29       - The domain name or the IP address and the port of the rancher
30         where API is exposed.
31       - For example: http://10.0.0.1:8080
32     required: true
33     aliases:
34       - server
35       - rancher_server
36       - rancher_api
37       - api
38   account_key:
39     description:
40       - The public and secret part of the API key-pair separated by colon.
41       - You can find all your keys in web UI.
42       - For example:
43         B1716C4133D3825051CB:3P2eb3QhokFKYUiXRNZLxvGNSRYgh6LHjuMicCHQ
44     required: false
45   mode:
46     description:
47       - A recognized mode how to deal with some concrete configuration task
48         in rancher API to ease the usage.
49       - The implemented modes so far are:
50         'settings':
51             Many options under <api_server>/v1/settings API url and some can
52             be seen also under advanced options in the web UI.
53         'access_control':
54             It setups user and password for the account (defaults to 'admin')
55             and it enables the local authentication - so the web UI and API
56             will require username/password (UI) or apikey (API).
57     required: true
58     aliases:
59       - rancher_mode
60       - api_mode
61     choices:
62       - settings
63       - access_control
64   data:
65     description:
66       - Dictionary with key/value pairs. The actual names and meaning of pairs
67         depends on the used mode.
68       - settings mode:
69         option - Option/path in JSON API (url).
70         value - A new value to replace the current one.
71       - access_control mode:
72         account_id - The unique ID of the account - the default rancher admin
73         has '1a1'. Better way would be to just create arbitrary username and
74         set credentials for that, but due to time constraints, the route with
75         an ID is simpler. The designated '1a1' could be hardcoded and hidden
76         but if the user will want to use some other account (there are many),
77         then it can be just changed to some other ID.
78         password - A new password in a plaintext.
79     required: true
80   timeout:
81     description:
82       - How long in seconds to wait for a response before raising error
83     required: false
84     default: 10.0
85 """
86
87 default_timeout = 10.0
88
89
90 class ModeError(Exception):
91     pass
92
93
94 def _decorate_rancher_api_request(request_method):
95
96     @functools.wraps(request_method)
97     def wrap_request(*args, **kwargs):
98
99         response = request_method(*args, **kwargs)
100         authorized = True
101
102         if response.status_code == 401:
103             authorized = False
104         elif response.status_code != requests.codes.ok:
105             response.raise_for_status()
106
107         try:
108             json_data = response.json()
109         except Exception:
110             json_data = None
111
112         return json_data, authorized
113
114     return wrap_request
115
116
117 @_decorate_rancher_api_request
118 def get_rancher_api_value(url, headers=None, timeout=default_timeout,
119                           username=None, password=None):
120
121     if username and password:
122         return requests.get(url, headers=headers,
123                             timeout=timeout,
124                             allow_redirects=False,
125                             auth=(username, password))
126     else:
127         return requests.get(url, headers=headers,
128                             timeout=timeout,
129                             allow_redirects=False)
130
131
132 @_decorate_rancher_api_request
133 def set_rancher_api_value(url, payload, headers=None, timeout=default_timeout,
134                           username=None, password=None, method='PUT'):
135
136     if method == 'PUT':
137         request_set_method = requests.put
138     elif method == 'POST':
139         request_set_method = requests.post
140     else:
141         raise ModeError('ERROR: Wrong request method: %s' % str(method))
142
143     if username and password:
144         return request_set_method(url, headers=headers,
145                                   timeout=timeout,
146                                   allow_redirects=False,
147                                   data=json.dumps(payload),
148                                   auth=(username, password))
149     else:
150         return request_set_method(url, headers=headers,
151                                   timeout=timeout,
152                                   allow_redirects=False,
153                                   data=json.dumps(payload))
154
155
156 def create_rancher_api_url(server, mode, option):
157     request_url = server.strip('/') + '/v1/'
158
159     if mode == 'raw':
160         request_url += option.strip('/')
161     elif mode == 'settings':
162         request_url += 'settings/' + option.strip('/')
163     elif mode == 'access_control':
164         request_url += option.strip('/')
165
166     return request_url
167
168
169 def get_keypair(keypair):
170     if keypair:
171         keypair = keypair.split(':')
172         if len(keypair) == 2:
173             return keypair[0], keypair[1]
174
175     return None, None
176
177
178 def mode_access_control(api_url, data=None, headers=None,
179                         timeout=default_timeout, access_key=None,
180                         secret_key=None, dry_run=False):
181
182     # returns true if local auth was enabled or false if passwd changed
183     def is_admin_enabled(json_data, password):
184         try:
185             if json_data['type'] == "localAuthConfig" and \
186                     json_data['accessMode'] == "unrestricted" and \
187                     json_data['username'] == "admin" and \
188                     json_data['password'] == password and \
189                     json_data['enabled']:
190                 return True
191         except Exception:
192             pass
193
194         try:
195             if json_data['type'] == "error" and \
196                     json_data['code'] == "IncorrectPassword":
197                 return False
198         except Exception:
199             pass
200
201         # this should never happen
202         raise ModeError('ERROR: Unknown status of the local authentication')
203
204     def create_localauth_payload(password):
205         payload = {
206             "enabled": True,
207             "accessMode": "unrestricted",
208             "username": "admin",
209             "password": password
210         }
211
212         return payload
213
214     def get_admin_password_id():
215         # assemble request URL
216         request_url = api_url + 'accounts/' + data['account_id'].strip('/') \
217             + '/credentials'
218
219         # API get current value
220         try:
221             json_response, authorized = \
222                 get_rancher_api_value(request_url,
223                                       username=access_key,
224                                       password=secret_key,
225                                       headers=headers,
226                                       timeout=timeout)
227         except requests.HTTPError as e:
228             raise ModeError(str(e))
229         except requests.Timeout as e:
230             raise ModeError(str(e))
231
232         if not authorized or not json_response:
233             raise ModeError('ERROR: BAD RESPONSE (GET) - no json value in '
234                             + 'the response')
235
236         try:
237             for item in json_response['data']:
238                 if item['type'] == 'password' and \
239                         item['accountId'] == data['account_id']:
240                     return item['id']
241         except Exception:
242             pass
243
244         return None
245
246     def remove_password(password_id, action):
247         if action == 'deactivate':
248             action_status = 'deactivating'
249         elif action == 'remove':
250             action_status = 'removing'
251
252         request_url = api_url + 'passwords/' + password_id + \
253             '/?action=' + action
254
255         try:
256             json_response, authorized = \
257                 set_rancher_api_value(request_url,
258                                       {},
259                                       username=access_key,
260                                       password=secret_key,
261                                       headers=headers,
262                                       method='POST',
263                                       timeout=timeout)
264         except requests.HTTPError as e:
265             raise ModeError(str(e))
266         except requests.Timeout as e:
267             raise ModeError(str(e))
268
269         if not authorized or not json_response:
270             raise ModeError('ERROR: BAD RESPONSE (POST) - no json value in '
271                             + 'the response')
272
273         if json_response['state'] != action_status:
274             raise ModeError("ERROR: Failed to '%s' the password: %s" %
275                             (action, password_id))
276
277     # check if data contains all required fields
278     try:
279         if not isinstance(data['account_id'], str) or data['account_id'] == '':
280             raise ModeError("ERROR: 'account_id' must contain an id of the "
281                             + "affected account")
282     except KeyError:
283         raise ModeError("ERROR: Mode 'access_control' requires the field: "
284                         + "'account_id': %s" % str(data))
285     try:
286         if not isinstance(data['password'], str) or data['password'] == '':
287             raise ModeError("ERROR: 'password' must contain some password")
288     except KeyError:
289         raise ModeError("ERROR: Mode 'access_control' requires the field: "
290                         + "'password': %s" % str(data))
291
292     # assemble request URL
293     request_url = api_url + 'localauthconfigs'
294
295     # API get current value
296     try:
297         json_response, authorized = \
298             get_rancher_api_value(request_url,
299                                   username=access_key,
300                                   password=secret_key,
301                                   headers=headers,
302                                   timeout=timeout)
303     except requests.HTTPError as e:
304         raise ModeError(str(e))
305     except requests.Timeout as e:
306         raise ModeError(str(e))
307
308     if not authorized or not json_response:
309         raise ModeError('ERROR: BAD RESPONSE (GET) - no json value in the '
310                         + 'response')
311
312     # we will check if local auth is enabled or not
313     enabled = False
314     try:
315         for item in json_response['data']:
316             if item['type'] == 'localAuthConfig' and \
317                     item['accessMode'] == 'unrestricted' and \
318                     item['enabled']:
319                 enabled = True
320                 break
321     except Exception:
322         enabled = False
323
324     if dry_run:
325         # we will not set anything and only signal potential change
326         if enabled:
327             changed = False
328         else:
329             changed = True
330     else:
331         # we will try to enable again with the same password
332         localauth_payload = create_localauth_payload(data['password'])
333         try:
334             json_response, authorized = \
335                 set_rancher_api_value(request_url,
336                                       localauth_payload,
337                                       username=access_key,
338                                       password=secret_key,
339                                       headers=headers,
340                                       method='POST',
341                                       timeout=timeout)
342         except requests.HTTPError as e:
343             raise ModeError(str(e))
344         except requests.Timeout as e:
345             raise ModeError(str(e))
346
347         # here we ignore authorized status - we will try to reset password
348         if not json_response:
349             raise ModeError('ERROR: BAD RESPONSE (POST) - no json value in '
350                             + 'the response')
351
352         # we check if the admin was already set or not...
353         if enabled and is_admin_enabled(json_response, data['password']):
354             # it was enabled before and password is the same - no change
355             changed = False
356         elif is_admin_enabled(json_response, data['password']):
357             # we enabled it for the first time
358             changed = True
359         # ...and reset password if needed (unauthorized access)
360         else:
361             # local auth is enabled but the password differs
362             # we must reset the admin's password
363             password_id = get_admin_password_id()
364
365             if password_id is None:
366                 raise ModeError("ERROR: admin's password is set, but we "
367                                 + "cannot identify it")
368
369             # One of the way to reset the password is to remove it first
370             # TODO - refactor this
371             remove_password(password_id, 'deactivate')
372             time.sleep(2)
373             remove_password(password_id, 'remove')
374             time.sleep(1)
375
376             try:
377                 json_response, authorized = \
378                     set_rancher_api_value(request_url,
379                                           localauth_payload,
380                                           username=access_key,
381                                           password=secret_key,
382                                           headers=headers,
383                                           method='POST',
384                                           timeout=timeout)
385             except requests.HTTPError as e:
386                 raise ModeError(str(e))
387             except requests.Timeout as e:
388                 raise ModeError(str(e))
389
390             # finally we signal the change
391             changed = True
392
393     if changed:
394         msg = "Local authentication is enabled, admin has assigned password"
395     else:
396         msg = "Local authentication was already enabled, admin's password " \
397             + "is unchanged"
398
399     return changed, msg
400
401
402 def mode_settings(api_url, data=None, headers=None, timeout=default_timeout,
403                   access_key=None, secret_key=None, dry_run=False):
404
405     def is_valid_rancher_api_option(json_data):
406
407         try:
408             api_activeValue = json_data['activeValue']
409             api_source = json_data['source']
410         except Exception:
411             return False
412
413         if api_activeValue is None and api_source is None:
414             return False
415
416         return True
417
418     def create_rancher_api_payload(json_data, new_value):
419
420         payload = {}
421         differs = False
422
423         try:
424             api_id = json_data['id']
425             api_activeValue = json_data['activeValue']
426             api_name = json_data['name']
427             api_source = json_data['source']
428         except Exception:
429             raise ValueError
430
431         payload.update({"activeValue": api_activeValue,
432                         "id": api_id,
433                         "name": api_name,
434                         "source": api_source,
435                         "value": new_value})
436
437         if api_activeValue != new_value:
438             differs = True
439
440         return differs, payload
441
442     # check if data contains all required fields
443     try:
444         if not isinstance(data['option'], str) or data['option'] == '':
445             raise ModeError("ERROR: 'option' must contain a name of the "
446                             + "option")
447     except KeyError:
448         raise ModeError("ERROR: Mode 'settings' requires the field: 'option': "
449                         + "%s" % str(data))
450     try:
451         if not isinstance(data['value'], str) or data['value'] == '':
452             raise ModeError("ERROR: 'value' must contain a value")
453     except KeyError:
454         raise ModeError("ERROR: Mode 'settings' requires the field: 'value': "
455                         + "%s" % str(data))
456
457     # assemble request URL
458     request_url = api_url + 'settings/' + data['option'].strip('/')
459
460     # API get current value
461     try:
462         json_response, authorized = \
463             get_rancher_api_value(request_url,
464                                   username=access_key,
465                                   password=secret_key,
466                                   headers=headers,
467                                   timeout=timeout)
468     except requests.HTTPError as e:
469         raise ModeError(str(e))
470     except requests.Timeout as e:
471         raise ModeError(str(e))
472
473     if not authorized or not json_response:
474         raise ModeError('ERROR: BAD RESPONSE (GET) - no json value in the '
475                         + 'response')
476
477     if is_valid_rancher_api_option(json_response):
478         valid = True
479         try:
480             differs, payload = create_rancher_api_payload(json_response,
481                                                           data['value'])
482         except ValueError:
483             raise ModeError('ERROR: INVALID JSON - missing json values in '
484                             + 'the response')
485     else:
486         valid = False
487
488     if valid and differs and dry_run:
489         # ansible dry-run mode
490         changed = True
491     elif valid and differs:
492         # API set new value
493         try:
494             json_response, authorized = \
495                 set_rancher_api_value(request_url,
496                                       payload,
497                                       username=access_key,
498                                       password=secret_key,
499                                       headers=headers,
500                                       timeout=timeout)
501         except requests.HTTPError as e:
502             raise ModeError(str(e))
503         except requests.Timeout as e:
504             raise ModeError(str(e))
505
506         if not authorized or not json_response:
507             raise ModeError('ERROR: BAD RESPONSE (PUT) - no json value in '
508                             + 'the response')
509         else:
510             changed = True
511     else:
512         changed = False
513
514     if changed:
515         msg = "Option '%s' is now set to the new value: %s" \
516             % (data['option'], data['value'])
517     else:
518         msg = "Option '%s' is unchanged." % (data['option'])
519
520     return changed, msg
521
522
523 def mode_handler(server, rancher_mode, data=None, timeout=default_timeout,
524                  account_key=None, dry_run=False):
525
526     changed = False
527     msg = 'UNKNOWN: UNAPPLICABLE MODE'
528
529     # check API key-pair
530     if account_key:
531         access_key, secret_key = get_keypair(account_key)
532         if not (access_key and secret_key):
533             raise ModeError('ERROR: INVALID API KEY-PAIR')
534
535     # all requests share these headers
536     http_headers = {'Content-Type': 'application/json',
537                     'Accept': 'application/json'}
538
539     # assemble API url
540     api_url = server.strip('/') + '/v1/'
541
542     if rancher_mode == 'settings':
543         changed, msg = mode_settings(api_url, data=data,
544                                      headers=http_headers,
545                                      timeout=timeout,
546                                      access_key=access_key,
547                                      secret_key=secret_key,
548                                      dry_run=dry_run)
549     elif rancher_mode == 'access_control':
550         changed, msg = mode_access_control(api_url, data=data,
551                                            headers=http_headers,
552                                            timeout=timeout,
553                                            access_key=access_key,
554                                            secret_key=secret_key,
555                                            dry_run=dry_run)
556
557     return changed, msg
558
559
560 def main():
561     module = AnsibleModule(
562         argument_spec=dict(
563             rancher=dict(type='str', required=True,
564                          aliases=['server',
565                                   'rancher_api',
566                                   'rancher_server',
567                                   'api']),
568             account_key=dict(type='str', required=False),
569             mode=dict(required=True,
570                       choices=['settings', 'access_control'],
571                       aliases=['api_mode']),
572             data=dict(type='dict', required=True),
573             timeout=dict(type='float', default=default_timeout),
574         ),
575         supports_check_mode=True
576     )
577
578     rancher_server = module.params['rancher']
579     rancher_account_key = module.params['account_key']
580     rancher_mode = module.params['mode']
581     rancher_data = module.params['data']
582     rancher_timeout = module.params['timeout']
583
584     try:
585         changed, msg = mode_handler(rancher_server,
586                                     rancher_mode,
587                                     data=rancher_data,
588                                     account_key=rancher_account_key,
589                                     timeout=rancher_timeout,
590                                     dry_run=module.check_mode)
591     except ModeError as e:
592         module.fail_json(msg=str(e))
593
594     module.exit_json(changed=changed, msg=msg)
595
596
597 if __name__ == '__main__':
598     main()