3 from ansible.module_utils.basic import AnsibleModule
13 short_description: Client library for rancher API
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.
29 - The domain name or the IP address and the port of the rancher
31 - For example: http://10.0.0.1:8080
40 - The public and secret part of the API key-pair separated by colon.
41 - You can find all your keys in web UI.
43 B1716C4133D3825051CB:3P2eb3QhokFKYUiXRNZLxvGNSRYgh6LHjuMicCHQ
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:
51 Many options under <api_server>/v1/settings API url and some can
52 be seen also under advanced options in the web UI.
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).
66 - Dictionary with key/value pairs. The actual names and meaning of pairs
67 depends on the used 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.
82 - How long in seconds to wait for a response before raising error
87 default_timeout = 10.0
90 class ModeError(Exception):
94 def _decorate_rancher_api_request(request_method):
96 @functools.wraps(request_method)
97 def wrap_request(*args, **kwargs):
99 response = request_method(*args, **kwargs)
102 if response.status_code == 401:
104 elif response.status_code != requests.codes.ok:
105 response.raise_for_status()
108 json_data = response.json()
112 return json_data, authorized
117 @_decorate_rancher_api_request
118 def get_rancher_api_value(url, headers=None, timeout=default_timeout,
119 username=None, password=None):
121 if username and password:
122 return requests.get(url, headers=headers,
124 allow_redirects=False,
125 auth=(username, password))
127 return requests.get(url, headers=headers,
129 allow_redirects=False)
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'):
137 request_set_method = requests.put
138 elif method == 'POST':
139 request_set_method = requests.post
141 raise ModeError('ERROR: Wrong request method: %s' % str(method))
143 if username and password:
144 return request_set_method(url, headers=headers,
146 allow_redirects=False,
147 data=json.dumps(payload),
148 auth=(username, password))
150 return request_set_method(url, headers=headers,
152 allow_redirects=False,
153 data=json.dumps(payload))
156 def create_rancher_api_url(server, mode, option):
157 request_url = server.strip('/') + '/v1/'
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('/')
169 def get_keypair(keypair):
171 keypair = keypair.split(':')
172 if len(keypair) == 2:
173 return keypair[0], keypair[1]
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):
182 # returns true if local auth was enabled or false if passwd changed
183 def is_admin_enabled(json_data, password):
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']:
195 if json_data['type'] == "error" and \
196 json_data['code'] == "IncorrectPassword":
201 # this should never happen
202 raise ModeError('ERROR: Unknown status of the local authentication')
204 def create_localauth_payload(password):
207 "accessMode": "unrestricted",
214 def get_admin_password_id():
215 # assemble request URL
216 request_url = api_url + 'accounts/' + data['account_id'].strip('/') \
219 # API get current value
221 json_response, authorized = \
222 get_rancher_api_value(request_url,
227 except requests.HTTPError as e:
228 raise ModeError(str(e))
229 except requests.Timeout as e:
230 raise ModeError(str(e))
232 if not authorized or not json_response:
233 raise ModeError('ERROR: BAD RESPONSE (GET) - no json value in '
237 for item in json_response['data']:
238 if item['type'] == 'password' and \
239 item['accountId'] == data['account_id']:
246 def remove_password(password_id, action):
247 if action == 'deactivate':
248 action_status = 'deactivating'
249 elif action == 'remove':
250 action_status = 'removing'
252 request_url = api_url + 'passwords/' + password_id + \
256 json_response, authorized = \
257 set_rancher_api_value(request_url,
264 except requests.HTTPError as e:
265 raise ModeError(str(e))
266 except requests.Timeout as e:
267 raise ModeError(str(e))
269 if not authorized or not json_response:
270 raise ModeError('ERROR: BAD RESPONSE (POST) - no json value in '
273 if json_response['state'] != action_status:
274 raise ModeError("ERROR: Failed to '%s' the password: %s" %
275 (action, password_id))
277 # check if data contains all required fields
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")
283 raise ModeError("ERROR: Mode 'access_control' requires the field: "
284 + "'account_id': %s" % str(data))
286 if not isinstance(data['password'], str) or data['password'] == '':
287 raise ModeError("ERROR: 'password' must contain some password")
289 raise ModeError("ERROR: Mode 'access_control' requires the field: "
290 + "'password': %s" % str(data))
292 # assemble request URL
293 request_url = api_url + 'localauthconfigs'
295 # API get current value
297 json_response, authorized = \
298 get_rancher_api_value(request_url,
303 except requests.HTTPError as e:
304 raise ModeError(str(e))
305 except requests.Timeout as e:
306 raise ModeError(str(e))
308 if not authorized or not json_response:
309 raise ModeError('ERROR: BAD RESPONSE (GET) - no json value in the '
312 # we will check if local auth is enabled or not
315 for item in json_response['data']:
316 if item['type'] == 'localAuthConfig' and \
317 item['accessMode'] == 'unrestricted' and \
325 # we will not set anything and only signal potential change
331 # we will try to enable again with the same password
332 localauth_payload = create_localauth_payload(data['password'])
334 json_response, authorized = \
335 set_rancher_api_value(request_url,
342 except requests.HTTPError as e:
343 raise ModeError(str(e))
344 except requests.Timeout as e:
345 raise ModeError(str(e))
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 '
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
356 elif is_admin_enabled(json_response, data['password']):
357 # we enabled it for the first time
359 # ...and reset password if needed (unauthorized access)
361 # local auth is enabled but the password differs
362 # we must reset the admin's password
363 password_id = get_admin_password_id()
365 if password_id is None:
366 raise ModeError("ERROR: admin's password is set, but we "
367 + "cannot identify it")
369 # One of the way to reset the password is to remove it first
370 # TODO - refactor this
371 remove_password(password_id, 'deactivate')
373 remove_password(password_id, 'remove')
377 json_response, authorized = \
378 set_rancher_api_value(request_url,
385 except requests.HTTPError as e:
386 raise ModeError(str(e))
387 except requests.Timeout as e:
388 raise ModeError(str(e))
390 # finally we signal the change
394 msg = "Local authentication is enabled, admin has assigned password"
396 msg = "Local authentication was already enabled, admin's password " \
402 def mode_settings(api_url, data=None, headers=None, timeout=default_timeout,
403 access_key=None, secret_key=None, dry_run=False):
405 def is_valid_rancher_api_option(json_data):
408 api_activeValue = json_data['activeValue']
409 api_source = json_data['source']
413 if api_activeValue is None and api_source is None:
418 def create_rancher_api_payload(json_data, new_value):
424 api_id = json_data['id']
425 api_activeValue = json_data['activeValue']
426 api_name = json_data['name']
427 api_source = json_data['source']
431 payload.update({"activeValue": api_activeValue,
434 "source": api_source,
437 if api_activeValue != new_value:
440 return differs, payload
442 # check if data contains all required fields
444 if not isinstance(data['option'], str) or data['option'] == '':
445 raise ModeError("ERROR: 'option' must contain a name of the "
448 raise ModeError("ERROR: Mode 'settings' requires the field: 'option': "
451 if not isinstance(data['value'], str) or data['value'] == '':
452 raise ModeError("ERROR: 'value' must contain a value")
454 raise ModeError("ERROR: Mode 'settings' requires the field: 'value': "
457 # assemble request URL
458 request_url = api_url + 'settings/' + data['option'].strip('/')
460 # API get current value
462 json_response, authorized = \
463 get_rancher_api_value(request_url,
468 except requests.HTTPError as e:
469 raise ModeError(str(e))
470 except requests.Timeout as e:
471 raise ModeError(str(e))
473 if not authorized or not json_response:
474 raise ModeError('ERROR: BAD RESPONSE (GET) - no json value in the '
477 if is_valid_rancher_api_option(json_response):
480 differs, payload = create_rancher_api_payload(json_response,
483 raise ModeError('ERROR: INVALID JSON - missing json values in '
488 if valid and differs and dry_run:
489 # ansible dry-run mode
491 elif valid and differs:
494 json_response, authorized = \
495 set_rancher_api_value(request_url,
501 except requests.HTTPError as e:
502 raise ModeError(str(e))
503 except requests.Timeout as e:
504 raise ModeError(str(e))
506 if not authorized or not json_response:
507 raise ModeError('ERROR: BAD RESPONSE (PUT) - no json value in '
515 msg = "Option '%s' is now set to the new value: %s" \
516 % (data['option'], data['value'])
518 msg = "Option '%s' is unchanged." % (data['option'])
523 def mode_handler(server, rancher_mode, data=None, timeout=default_timeout,
524 account_key=None, dry_run=False):
527 msg = 'UNKNOWN: UNAPPLICABLE MODE'
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')
535 # all requests share these headers
536 http_headers = {'Content-Type': 'application/json',
537 'Accept': 'application/json'}
540 api_url = server.strip('/') + '/v1/'
542 if rancher_mode == 'settings':
543 changed, msg = mode_settings(api_url, data=data,
544 headers=http_headers,
546 access_key=access_key,
547 secret_key=secret_key,
549 elif rancher_mode == 'access_control':
550 changed, msg = mode_access_control(api_url, data=data,
551 headers=http_headers,
553 access_key=access_key,
554 secret_key=secret_key,
561 module = AnsibleModule(
563 rancher=dict(type='str', required=True,
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),
575 supports_check_mode=True
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']
585 changed, msg = mode_handler(rancher_server,
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))
594 module.exit_json(changed=changed, msg=msg)
597 if __name__ == '__main__':