Petr OspalĂ˝ | a0eff59 | 2018-12-19 13:54:09 +0100 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | DOCUMENTATION=''' |
| 4 | --- |
| 5 | module: rancher_k8s_environment |
| 6 | description: |
| 7 | - This module will create or delete Kubernetes environment. |
| 8 | - It will also delete other environments when variables are set accordingly. |
| 9 | notes: |
| 10 | - It identifies environment only by name. Expect problems with same named environments. |
| 11 | - All hosts running Kubernetes cluster should have same OS otherwise there |
| 12 | is possibility of misbehavement. |
| 13 | options: |
| 14 | server: |
| 15 | required: true |
| 16 | description: |
| 17 | - Url of rancher server i.e. "http://10.0.0.1:8080". |
| 18 | name: |
| 19 | required: true |
| 20 | descritpion: |
| 21 | - Name of the environment to create/remove. |
| 22 | descr: |
| 23 | description: |
| 24 | - Description of environment to create. |
| 25 | state: |
| 26 | description: |
| 27 | - If "present" environment will be created or setup depending if it exists. |
| 28 | With multiple environments with same name expect error. |
| 29 | If "absent" environment will be removed. If multiple environments have same |
| 30 | name all will be deleted. |
| 31 | default: present |
| 32 | choices: [present, absent] |
| 33 | delete_not_k8s: |
| 34 | description: |
| 35 | - Indicates if environments with different orchestration than Kubernetes should |
| 36 | be deleted. |
| 37 | type: bool |
| 38 | default: yes |
| 39 | delete_other_k8s: |
| 40 | description: |
| 41 | - Indicates if environments with different name than specified should |
| 42 | be deleted. |
| 43 | type: bool |
| 44 | default: no |
| 45 | force: |
| 46 | description: |
| 47 | - Indicates if environment should be deleted and recreated. |
| 48 | type: bool |
| 49 | default: yes |
| 50 | host_os: |
| 51 | required: true |
| 52 | description: |
| 53 | - OS (family from ansible_os_family variable) of the hosts running cluster. If |
| 54 | "RedHat" then datavolume fix will be applied. |
| 55 | Fix described here: |
| 56 | https://github.com/rancher/rancher/issues/10015 |
| 57 | ''' |
| 58 | |
| 59 | import json |
| 60 | import time |
| 61 | |
| 62 | import requests |
| 63 | from ansible.module_utils.basic import AnsibleModule |
| 64 | |
| 65 | |
| 66 | |
| 67 | def get_existing_environments(rancher_address): |
| 68 | req = requests.get('{}/v2-beta/projects'.format(rancher_address)) |
| 69 | envs = req.json()['data'] |
| 70 | return envs |
| 71 | |
| 72 | |
| 73 | def not_k8s_ids(environments): |
| 74 | envs = filter(lambda x: x['orchestration'] != 'kubernetes', environments) |
| 75 | return [env['id'] for env in envs] |
| 76 | |
| 77 | |
| 78 | def other_k8s_ids(environments, name): |
| 79 | envs = filter(lambda x: x['orchestration'] == 'kubernetes' and x['name'] != name, |
| 80 | environments) |
| 81 | return [env['id'] for env in envs] |
| 82 | |
| 83 | |
| 84 | def env_ids_by_name(environments, name): |
| 85 | envs = filter(lambda x: x['name'] == name, environments) |
| 86 | return [env['id'] for env in envs] |
| 87 | |
| 88 | |
| 89 | def env_info_by_id(environments, env_id): |
| 90 | env = filter(lambda x: x['id'] == env_id, environments) |
| 91 | return [{'id': x['id'], 'name': x['name']} for x in env][0] |
| 92 | |
| 93 | |
| 94 | def delete_multiple_environments(rancher_address, env_ids): |
| 95 | deleted = [] |
| 96 | for env_id in env_ids: |
| 97 | deleted.append(delete_environment(rancher_address, env_id)) |
| 98 | return deleted |
| 99 | |
| 100 | |
| 101 | def delete_environment(rancher_address, env_id): |
| 102 | req = requests.delete('{}/v2-beta/projects/{}'.format(rancher_address, env_id)) |
| 103 | deleted = req.json()['data'][0] |
| 104 | return {'id': deleted['id'], |
| 105 | 'name': deleted['name'], |
| 106 | 'orchestration': deleted['orchestration']} |
| 107 | |
| 108 | |
| 109 | def create_k8s_environment(rancher_address, name, descr): |
| 110 | k8s_template_id = None |
| 111 | for _ in range(10): |
| 112 | k8s_template = requests.get( |
| 113 | '{}/v2-beta/projecttemplates?name=Kubernetes'.format(rancher_address)).json() |
| 114 | if k8s_template['data']: |
| 115 | k8s_template_id = k8s_template['data'][0]['id'] |
| 116 | break |
| 117 | time.sleep(3) |
| 118 | if k8s_template_id is None: |
| 119 | raise ValueError('Template for kubernetes not found.') |
| 120 | body = { |
| 121 | 'name': name, |
| 122 | 'description': descr, |
| 123 | 'projectTemplateId': k8s_template_id, |
| 124 | 'allowSystemRole': False, |
| 125 | 'members': [], |
| 126 | 'virtualMachine': False, |
| 127 | 'servicesPortRange': None, |
| 128 | 'projectLinks': [] |
| 129 | } |
| 130 | |
| 131 | body_json = json.dumps(body) |
| 132 | req = requests.post('{}/v2-beta/projects'.format(rancher_address), data=body_json) |
| 133 | created = req.json() |
| 134 | return {'id': created['id'], 'name': created['name']} |
| 135 | |
| 136 | |
| 137 | def get_kubelet_service(rancher_address, env_id): |
| 138 | for _ in range(10): |
| 139 | response = requests.get( |
| 140 | '{}/v2-beta/projects/{}/services/?name=kubelet'.format(rancher_address, |
| 141 | env_id)) |
| 142 | |
| 143 | if response.status_code >= 400: |
| 144 | # too early or too late for obtaining data |
| 145 | # small delay will improve our chances to collect it |
| 146 | time.sleep(1) |
| 147 | continue |
| 148 | |
| 149 | content = response.json() |
| 150 | |
| 151 | if content['data']: |
| 152 | return content['data'][0] |
| 153 | |
| 154 | # this is unfortunate, response from service api received but data |
| 155 | # not available, lets try again |
| 156 | time.sleep(5) |
| 157 | |
| 158 | return None |
| 159 | |
| 160 | |
| 161 | def fix_datavolume_rhel(rancher_address, env_id): |
| 162 | kubelet_svc = get_kubelet_service(rancher_address, env_id) |
| 163 | if kubelet_svc: |
| 164 | try: |
| 165 | data_volume_index = kubelet_svc['launchConfig']['dataVolumes'].index( |
| 166 | '/sys:/sys:ro,rprivate') |
| 167 | except ValueError: |
| 168 | return 'Already changed' |
| 169 | kubelet_svc['launchConfig']['dataVolumes'][ |
| 170 | data_volume_index] = '/sys/fs/cgroup:/sys/fs/cgroup:ro,rprivate' |
| 171 | body = { |
| 172 | 'inServiceStrategy': { |
| 173 | 'batchSize': 1, |
| 174 | 'intervalMillis': 2000, |
| 175 | 'startFirst': False, |
| 176 | 'launchConfig': kubelet_svc['launchConfig'], |
| 177 | 'secondaryLaunchConfigs': [] |
| 178 | } |
| 179 | } |
| 180 | body_json = json.dumps(body) |
| 181 | requests.post( |
| 182 | '{}/v2-beta/projects/{}/services/{}?action=upgrade'.format(rancher_address, |
| 183 | env_id, |
| 184 | kubelet_svc[ |
| 185 | 'id']), |
| 186 | data=body_json) |
| 187 | for _ in range(10): |
| 188 | req_svc = requests.get( |
| 189 | '{}/v2-beta/projects/{}/services/{}'.format(rancher_address, env_id, |
| 190 | kubelet_svc['id'])) |
| 191 | req_svc_content = req_svc.json() |
| 192 | if 'finishupgrade' in req_svc_content['actions']: |
| 193 | req_finish = requests.post( |
| 194 | req_svc_content['actions']['finishupgrade']) |
| 195 | return { |
| 196 | 'dataVolumes': req_finish.json()['upgrade']['inServiceStrategy'][ |
| 197 | 'launchConfig']['dataVolumes']} |
| 198 | time.sleep(5) |
| 199 | else: |
| 200 | raise ValueError('Could not get kubelet service') |
| 201 | |
| 202 | |
| 203 | def create_registration_tokens(rancher_address, env_id): |
| 204 | body = {'name': str(env_id)} |
| 205 | body_json = json.dumps(body) |
| 206 | response = requests.post( |
| 207 | '{}/v2-beta/projects/{}/registrationtokens'.format(rancher_address, env_id, |
| 208 | data=body_json)) |
| 209 | for _ in range(10): |
| 210 | tokens = requests.get(response.json()['links']['self']) |
| 211 | tokens_content = tokens.json() |
| 212 | if tokens_content['image'] is not None and tokens_content[ |
| 213 | 'registrationUrl'] is not None: |
| 214 | return {'image': tokens_content['image'], |
| 215 | 'reg_url': tokens_content['registrationUrl']} |
| 216 | time.sleep(3) |
| 217 | return None |
| 218 | |
| 219 | |
| 220 | def get_registration_tokens(rancher_address, env_id): |
| 221 | reg_tokens = requests.get( |
| 222 | '{}/v2-beta/projects/{}/registrationtokens'.format(rancher_address, env_id)) |
| 223 | reg_tokens_content = reg_tokens.json() |
| 224 | tokens = reg_tokens_content['data'] |
| 225 | if not tokens: |
| 226 | return None |
| 227 | return {'image': tokens[0]['image'], 'reg_url': tokens[0]['registrationUrl']} |
| 228 | |
| 229 | |
| 230 | def create_apikey(rancher_address, env_id): |
| 231 | body = { |
| 232 | 'name': 'kubectl_env_{}'.format(env_id), |
| 233 | 'description': "Provides access to kubectl" |
| 234 | } |
| 235 | body_json = json.dumps(body) |
| 236 | apikey_req = requests.post( |
| 237 | '{}/v2-beta/apikey'.format(rancher_address, env_id, data=body_json)) |
| 238 | apikey_content = apikey_req.json() |
| 239 | return {'public': apikey_content['publicValue'], |
| 240 | 'private': apikey_content['secretValue']} |
| 241 | |
| 242 | |
| 243 | def run_module(): |
| 244 | module = AnsibleModule( |
| 245 | argument_spec=dict( |
| 246 | server=dict(type='str', required=True), |
| 247 | name=dict(type='str', required=True), |
| 248 | descr=dict(type='str'), |
| 249 | state=dict(type='str', choices=['present', 'absent'], default='present'), |
| 250 | delete_other_k8s=dict(type='bool', default=False), |
| 251 | delete_not_k8s=dict(type='bool', default=True), |
| 252 | force=dict(type='bool', default=True), |
| 253 | host_os=dict(type='str', required=True) |
| 254 | ) |
| 255 | ) |
| 256 | |
| 257 | params = module.params |
| 258 | rancher_address = params['server'] |
| 259 | name = params['name'] |
| 260 | descr = params['descr'] |
| 261 | delete_not_k8s = params['delete_not_k8s'] |
| 262 | delete_other_k8s = params['delete_other_k8s'] |
| 263 | force = params['force'] |
| 264 | host_os = params['host_os'] |
| 265 | state = params['state'] |
| 266 | |
| 267 | existing_envs = get_existing_environments(rancher_address) |
| 268 | same_name_ids = env_ids_by_name(existing_envs, name) |
| 269 | |
| 270 | to_delete_ids = [] |
| 271 | changes = {} |
| 272 | |
| 273 | if delete_other_k8s: |
| 274 | to_delete_ids += other_k8s_ids(existing_envs, name) |
| 275 | |
| 276 | if delete_not_k8s: |
| 277 | to_delete_ids += not_k8s_ids(existing_envs) |
| 278 | if force or state == 'absent': |
| 279 | to_delete_ids += same_name_ids |
| 280 | |
| 281 | deleted = delete_multiple_environments(rancher_address, to_delete_ids) |
| 282 | |
| 283 | if deleted: |
| 284 | changes['deleted'] = deleted |
| 285 | if state == 'absent': |
| 286 | module.exit_json(changed=True, deleted=changes['deleted']) |
| 287 | else: |
| 288 | if state == 'absent': |
| 289 | module.exit_json(changed=False) |
| 290 | |
| 291 | if len(same_name_ids) > 1 and not force: |
| 292 | module.fail_json(msg='Multiple environments with same name. ' |
| 293 | 'Use "force: yes" to delete ' |
| 294 | 'all environments with same name.') |
| 295 | |
| 296 | if same_name_ids and not force: |
| 297 | changes['environment'] = env_info_by_id(existing_envs, same_name_ids[0]) |
| 298 | if host_os == 'RedHat': |
| 299 | try: |
| 300 | rhel_fix = fix_datavolume_rhel(rancher_address, same_name_ids[0]) |
| 301 | changes['rhel_fix'] = rhel_fix |
| 302 | except ValueError as err: |
| 303 | module.fail_json( |
| 304 | msg='Error: {} Try to recreate k8s environment.'.format(err)) |
| 305 | |
| 306 | reg_tokens = get_registration_tokens(rancher_address, same_name_ids[0]) |
| 307 | if not reg_tokens: |
| 308 | reg_tokens = create_registration_tokens(rancher_address, same_name_ids[0]) |
| 309 | changes['registration_tokens'] = reg_tokens |
| 310 | |
| 311 | apikey = create_apikey(rancher_address, same_name_ids[0]) |
| 312 | changes['apikey'] = apikey |
| 313 | module.exit_json(changed=True, data=changes, |
| 314 | msg='New environment was not created. Only set up was done') |
| 315 | try: |
| 316 | new_env = create_k8s_environment(rancher_address, name, descr) |
| 317 | except ValueError as err: |
| 318 | module.fail_json(msg='Error: {} Try to recreate k8s environment.'.format(err)) |
| 319 | |
| 320 | if host_os == 'RedHat': |
| 321 | try: |
| 322 | rhel_fix = fix_datavolume_rhel(rancher_address, new_env['id']) |
| 323 | changes['rhel_fix'] = rhel_fix |
| 324 | except ValueError as err: |
| 325 | module.fail_json(msg='Error: {} Try to recreate k8s environment.'.format( |
| 326 | err)) |
| 327 | |
| 328 | reg_tokens = create_registration_tokens(rancher_address, new_env['id']) |
| 329 | |
| 330 | apikey = create_apikey(rancher_address, new_env['id']) |
| 331 | |
| 332 | changes['environment'] = new_env |
| 333 | changes['registration_tokens'] = reg_tokens |
| 334 | changes['apikey'] = apikey |
| 335 | |
| 336 | module.exit_json(changed=True, data=changes) |
| 337 | |
| 338 | |
| 339 | if __name__ == '__main__': |
| 340 | run_module() |
| 341 | |