Add ansible library for creating the k8n env. 99/74899/1
authorPetr Ospalý <p.ospaly@partner.samsung.com>
Wed, 19 Dec 2018 12:54:09 +0000 (13:54 +0100)
committerPetr Ospalý <p.ospaly@partner.samsung.com>
Wed, 19 Dec 2018 12:56:09 +0000 (13:56 +0100)
Issue-ID: OOM-1551
Change-Id: I485f58e560086345bd27a4a7f94d2651a0cd357e
Signed-off-by: Petr Ospalý <p.ospaly@partner.samsung.com>
ansible/library/rancher_k8s_environment.py [new file with mode: 0644]

diff --git a/ansible/library/rancher_k8s_environment.py b/ansible/library/rancher_k8s_environment.py
new file mode 100644 (file)
index 0000000..d3d8ac0
--- /dev/null
@@ -0,0 +1,341 @@
+#!/usr/bin/python
+
+DOCUMENTATION='''
+---
+module: rancher_k8s_environment
+description:
+  - This module will create or delete Kubernetes environment.
+  - It will also delete other environments when variables are set accordingly.
+notes:
+  - It identifies environment only by name. Expect problems with same named environments.
+  - All hosts running Kubernetes cluster should have same OS otherwise there
+    is possibility of misbehavement.
+options:
+  server:
+    required: true
+    description:
+      - Url of rancher server i.e. "http://10.0.0.1:8080".
+  name:
+    required: true
+    descritpion:
+      - Name of the environment to create/remove.
+  descr:
+    description:
+      - Description of environment to create.
+  state:
+    description:
+      - If "present" environment will be created or setup depending if it exists.
+        With multiple environments with same name expect error.
+        If "absent" environment will be removed. If multiple environments have same
+        name all will be deleted.
+    default: present
+    choices: [present, absent]
+  delete_not_k8s:
+    description:
+      - Indicates if environments with different orchestration than Kubernetes should
+        be deleted.
+    type: bool
+    default: yes
+  delete_other_k8s:
+    description:
+      - Indicates if environments with different name than specified should
+        be deleted.
+    type: bool
+    default: no
+  force:
+    description:
+      - Indicates if environment should be deleted and recreated.
+    type: bool
+    default: yes
+  host_os:
+    required: true
+    description:
+      - OS (family from ansible_os_family variable) of the hosts running cluster. If
+        "RedHat" then datavolume fix will be applied.
+        Fix described here:
+          https://github.com/rancher/rancher/issues/10015
+'''
+
+import json
+import time
+
+import requests
+from ansible.module_utils.basic import AnsibleModule
+
+
+
+def get_existing_environments(rancher_address):
+    req = requests.get('{}/v2-beta/projects'.format(rancher_address))
+    envs = req.json()['data']
+    return envs
+
+
+def not_k8s_ids(environments):
+    envs = filter(lambda x: x['orchestration'] != 'kubernetes', environments)
+    return [env['id'] for env in envs]
+
+
+def other_k8s_ids(environments, name):
+    envs = filter(lambda x: x['orchestration'] == 'kubernetes' and x['name'] != name,
+                  environments)
+    return [env['id'] for env in envs]
+
+
+def env_ids_by_name(environments, name):
+    envs = filter(lambda x: x['name'] == name, environments)
+    return [env['id'] for env in envs]
+
+
+def env_info_by_id(environments, env_id):
+    env = filter(lambda x: x['id'] == env_id, environments)
+    return [{'id': x['id'], 'name': x['name']} for x in env][0]
+
+
+def delete_multiple_environments(rancher_address, env_ids):
+    deleted = []
+    for env_id in env_ids:
+        deleted.append(delete_environment(rancher_address, env_id))
+    return deleted
+
+
+def delete_environment(rancher_address, env_id):
+    req = requests.delete('{}/v2-beta/projects/{}'.format(rancher_address, env_id))
+    deleted = req.json()['data'][0]
+    return {'id': deleted['id'],
+            'name': deleted['name'],
+            'orchestration': deleted['orchestration']}
+
+
+def create_k8s_environment(rancher_address, name, descr):
+    k8s_template_id = None
+    for _ in range(10):
+        k8s_template = requests.get(
+            '{}/v2-beta/projecttemplates?name=Kubernetes'.format(rancher_address)).json()
+        if k8s_template['data']:
+            k8s_template_id = k8s_template['data'][0]['id']
+            break
+        time.sleep(3)
+    if k8s_template_id is None:
+        raise ValueError('Template for kubernetes not found.')
+    body = {
+        'name': name,
+        'description': descr,
+        'projectTemplateId': k8s_template_id,
+        'allowSystemRole': False,
+        'members': [],
+        'virtualMachine': False,
+        'servicesPortRange': None,
+        'projectLinks': []
+    }
+
+    body_json = json.dumps(body)
+    req = requests.post('{}/v2-beta/projects'.format(rancher_address), data=body_json)
+    created = req.json()
+    return {'id': created['id'], 'name': created['name']}
+
+
+def get_kubelet_service(rancher_address, env_id):
+    for _ in range(10):
+        response = requests.get(
+            '{}/v2-beta/projects/{}/services/?name=kubelet'.format(rancher_address,
+                                                                   env_id))
+
+        if response.status_code >= 400:
+            # too early or too late for obtaining data
+            # small delay will improve our chances to collect it
+            time.sleep(1)
+            continue
+
+        content = response.json()
+
+        if content['data']:
+            return content['data'][0]
+
+        # this is unfortunate, response from service api received but data
+        # not available, lets try again
+        time.sleep(5)
+
+    return None
+
+
+def fix_datavolume_rhel(rancher_address, env_id):
+    kubelet_svc = get_kubelet_service(rancher_address, env_id)
+    if kubelet_svc:
+        try:
+            data_volume_index = kubelet_svc['launchConfig']['dataVolumes'].index(
+                '/sys:/sys:ro,rprivate')
+        except ValueError:
+            return 'Already changed'
+        kubelet_svc['launchConfig']['dataVolumes'][
+            data_volume_index] = '/sys/fs/cgroup:/sys/fs/cgroup:ro,rprivate'
+        body = {
+            'inServiceStrategy': {
+                'batchSize': 1,
+                'intervalMillis': 2000,
+                'startFirst': False,
+                'launchConfig': kubelet_svc['launchConfig'],
+                'secondaryLaunchConfigs': []
+            }
+        }
+        body_json = json.dumps(body)
+        requests.post(
+            '{}/v2-beta/projects/{}/services/{}?action=upgrade'.format(rancher_address,
+                                                                       env_id,
+                                                                       kubelet_svc[
+                                                                           'id']),
+            data=body_json)
+        for _ in range(10):
+            req_svc = requests.get(
+                '{}/v2-beta/projects/{}/services/{}'.format(rancher_address, env_id,
+                                                            kubelet_svc['id']))
+            req_svc_content = req_svc.json()
+            if 'finishupgrade' in req_svc_content['actions']:
+                req_finish = requests.post(
+                    req_svc_content['actions']['finishupgrade'])
+                return {
+                    'dataVolumes': req_finish.json()['upgrade']['inServiceStrategy'][
+                        'launchConfig']['dataVolumes']}
+            time.sleep(5)
+    else:
+        raise ValueError('Could not get kubelet service')
+
+
+def create_registration_tokens(rancher_address, env_id):
+    body = {'name': str(env_id)}
+    body_json = json.dumps(body)
+    response = requests.post(
+        '{}/v2-beta/projects/{}/registrationtokens'.format(rancher_address, env_id,
+                                                           data=body_json))
+    for _ in range(10):
+        tokens = requests.get(response.json()['links']['self'])
+        tokens_content = tokens.json()
+        if tokens_content['image'] is not None and tokens_content[
+                'registrationUrl'] is not None:
+            return {'image': tokens_content['image'],
+                    'reg_url': tokens_content['registrationUrl']}
+        time.sleep(3)
+    return None
+
+
+def get_registration_tokens(rancher_address, env_id):
+    reg_tokens = requests.get(
+        '{}/v2-beta/projects/{}/registrationtokens'.format(rancher_address, env_id))
+    reg_tokens_content = reg_tokens.json()
+    tokens = reg_tokens_content['data']
+    if not tokens:
+        return None
+    return {'image': tokens[0]['image'], 'reg_url': tokens[0]['registrationUrl']}
+
+
+def create_apikey(rancher_address, env_id):
+    body = {
+        'name': 'kubectl_env_{}'.format(env_id),
+        'description': "Provides access to kubectl"
+    }
+    body_json = json.dumps(body)
+    apikey_req = requests.post(
+        '{}/v2-beta/apikey'.format(rancher_address, env_id, data=body_json))
+    apikey_content = apikey_req.json()
+    return {'public': apikey_content['publicValue'],
+            'private': apikey_content['secretValue']}
+
+
+def run_module():
+    module = AnsibleModule(
+        argument_spec=dict(
+            server=dict(type='str', required=True),
+            name=dict(type='str', required=True),
+            descr=dict(type='str'),
+            state=dict(type='str', choices=['present', 'absent'], default='present'),
+            delete_other_k8s=dict(type='bool', default=False),
+            delete_not_k8s=dict(type='bool', default=True),
+            force=dict(type='bool', default=True),
+            host_os=dict(type='str', required=True)
+        )
+    )
+
+    params = module.params
+    rancher_address = params['server']
+    name = params['name']
+    descr = params['descr']
+    delete_not_k8s = params['delete_not_k8s']
+    delete_other_k8s = params['delete_other_k8s']
+    force = params['force']
+    host_os = params['host_os']
+    state = params['state']
+
+    existing_envs = get_existing_environments(rancher_address)
+    same_name_ids = env_ids_by_name(existing_envs, name)
+
+    to_delete_ids = []
+    changes = {}
+
+    if delete_other_k8s:
+        to_delete_ids += other_k8s_ids(existing_envs, name)
+
+    if delete_not_k8s:
+        to_delete_ids += not_k8s_ids(existing_envs)
+    if force or state == 'absent':
+        to_delete_ids += same_name_ids
+
+    deleted = delete_multiple_environments(rancher_address, to_delete_ids)
+
+    if deleted:
+        changes['deleted'] = deleted
+        if state == 'absent':
+            module.exit_json(changed=True, deleted=changes['deleted'])
+    else:
+        if state == 'absent':
+            module.exit_json(changed=False)
+
+    if len(same_name_ids) > 1 and not force:
+        module.fail_json(msg='Multiple environments with same name. '
+                             'Use "force: yes" to delete '
+                             'all environments with same name.')
+
+    if same_name_ids and not force:
+        changes['environment'] = env_info_by_id(existing_envs, same_name_ids[0])
+        if host_os == 'RedHat':
+            try:
+                rhel_fix = fix_datavolume_rhel(rancher_address, same_name_ids[0])
+                changes['rhel_fix'] = rhel_fix
+            except ValueError as err:
+                module.fail_json(
+                    msg='Error: {} Try to recreate k8s environment.'.format(err))
+
+        reg_tokens = get_registration_tokens(rancher_address, same_name_ids[0])
+        if not reg_tokens:
+            reg_tokens = create_registration_tokens(rancher_address, same_name_ids[0])
+        changes['registration_tokens'] = reg_tokens
+
+        apikey = create_apikey(rancher_address, same_name_ids[0])
+        changes['apikey'] = apikey
+        module.exit_json(changed=True, data=changes,
+                         msg='New environment was not created. Only set up was done')
+    try:
+        new_env = create_k8s_environment(rancher_address, name, descr)
+    except ValueError as err:
+        module.fail_json(msg='Error: {} Try to recreate k8s environment.'.format(err))
+
+    if host_os == 'RedHat':
+        try:
+            rhel_fix = fix_datavolume_rhel(rancher_address, new_env['id'])
+            changes['rhel_fix'] = rhel_fix
+        except ValueError as err:
+            module.fail_json(msg='Error: {} Try to recreate k8s environment.'.format(
+                err))
+
+    reg_tokens = create_registration_tokens(rancher_address, new_env['id'])
+
+    apikey = create_apikey(rancher_address, new_env['id'])
+
+    changes['environment'] = new_env
+    changes['registration_tokens'] = reg_tokens
+    changes['apikey'] = apikey
+
+    module.exit_json(changed=True, data=changes)
+
+
+if __name__ == '__main__':
+    run_module()
+