From 587f0d9c0b91b3b703dba3099cca5527a5210576 Mon Sep 17 00:00:00 2001 From: Aleksandr Malyavin Date: Mon, 4 Apr 2022 17:03:01 +0300 Subject: [PATCH] Update README, add kubernetes support --- README.md | 4 +- examples/main.yaml | 133 +++++++++ library/decort_k8s.py | 236 ++++++++++++++++ library/decort_vins.py | 76 +++--- module_utils/decort_utils.py | 507 ++++++++++++++++++++++++++--------- 5 files changed, 793 insertions(+), 163 deletions(-) create mode 100644 library/decort_k8s.py diff --git a/README.md b/README.md index b35609a..e7de5c5 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Note that this module may produce unreliable results when used with older DECORT Requirements: * Ansible 2.7 or higher -* Python 2.6 or higher -* PyJWT 1.7.1 Python module +* Python 3.7 or higher +* PyJWT 2.0.0 Python module or higher * requests Python module * netaddr Python module * DECORT cloud platform version 3.5.0 or higher diff --git a/examples/main.yaml b/examples/main.yaml index 6f20332..f31f4ad 100644 --- a/examples/main.yaml +++ b/examples/main.yaml @@ -188,3 +188,136 @@ var: my_pfw.facts delegate_to: localhost + - name: Create k8s cluster with params + decort_k8s: + authenticator: jwt + jwt: "{{ token.jwt }}" + controller_url: "{{ decort_ctrl }}" + k8s_name: "k8s_cluster_name" + wg_name: "k8s_wg_name" + k8ci_id: "{{ k8ci_id }}" + rg_id: "{{ my_rg.facts.id }}" + master_count: 1 + master_cpu: 2 + master_ram_mb: 2048 + master_disk_gb: 20 + worker_count: 3 + worker_cpu: 1 + worker_ram_mb: 1024 + worker_disk_gb: 20 + extnet_id: "{{ target_ext_net_id }}" + with_lb: True + state: present + register: k8s + delegate_to: localhost + + - name: print out the result + debug: + var: k8s + delegate_to: localhost + + - name: Disable k8s cluster + decort_k8s: + authenticator: jwt + jwt: "{{ token.jwt }}" + controller_url: "{{ decort_ctrl }}" + k8s_name: "k8s_cluster_name" + wg_name: "k8s_wg_name" + k8ci_id: "{{ k8ci_id }}" + rg_id: "{{ my_rg.facts.id }}" + state: disabled + register: k8s + delegate_to: localhost + + - name: print out the result + debug: + var: k8s + delegate_to: localhost + + - name: Delete in trash k8s cluster + decort_k8s: + authenticator: jwt + jwt: "{{ token.jwt }}" + controller_url: "{{ decort_ctrl }}" + k8s_name: "k8s_cluster_name" + wg_name: "k8s_wg_name" + k8ci_id: "{{ k8ci_id }}" + rg_id: "{{ my_rg.facts.id }}" + state: absent + permanent: False + register: k8s + delegate_to: localhost + + - name: print out the result + debug: + var: k8s + delegate_to: localhost + + - name: Restore from trash deleted k8s cluster + decort_k8s: + authenticator: jwt + jwt: "{{ token.jwt }}" + controller_url: "{{ decort_ctrl }}" + k8s_name: "k8s_cluster_name" + wg_name: "k8s_wg_name" + k8ci_id: "{{ k8ci_id }}" + rg_id: "{{ my_rg.facts.id }}" + state: enabled + register: k8s + delegate_to: localhost + + - name: print out the result + debug: + var: k8s + delegate_to: localhost + + - name: Enable k8s cluster + decort_k8s: + authenticator: jwt + jwt: "{{ token.jwt }}" + controller_url: "{{ decort_ctrl }}" + k8s_name: "k8s_cluster_name" + wg_name: "k8s_wg_name" + k8ci_id: "{{ k8ci_id }}" + rg_id: "{{ my_rg.facts.id }}" + state: enabled + register: k8s + delegate_to: localhost + + - name: Enable k8s cluster + decort_k8s: + authenticator: jwt + jwt: "{{ token.jwt }}" + controller_url: "{{ decort_ctrl }}" + k8s_name: "k8s_cluster_name" + wg_name: "k8s_wg_name" + k8ci_id: "{{ k8ci_id }}" + rg_id: "{{ my_rg.facts.id }}" + state: enabled + started: True + register: k8s + delegate_to: localhost + + - name: print out the result + debug: + var: k8s + delegate_to: localhost + + - name: Destroy k8s cluster + decort_k8s: + authenticator: jwt + jwt: "{{ token.jwt }}" + controller_url: "{{ decort_ctrl }}" + k8s_name: "k8s_cluster_name" + wg_name: "k8s_wg_name" + k8ci_id: "{{ k8ci_id }}" + rg_id: "{{ my_rg.facts.id }}" + state: absent + permanent: True + register: k8s + delegate_to: localhost + + - name: print out the result + debug: + var: k8s + delegate_to: localhost diff --git a/library/decort_k8s.py b/library/decort_k8s.py new file mode 100644 index 0000000..3e3053b --- /dev/null +++ b/library/decort_k8s.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# +# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible +# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC +# +# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt) +# + +# +# Author: Aleksandr Malyavin (aleksandr.malyavin@digitalenergy.online) +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback + +from ansible.module_utils.decort_utils import * + + +def decort_k8s_package_facts(arg_k8s_facts, arg_check_mode=False): + """Package a dictionary of k8s facts according to the decort_k8s module specification. This dictionary will + be returned to the upstream Ansible engine at the completion of the module run. + + @param arg_k8s_facts: dictionary with k8s facts as returned by API call to .../k8s/get + @param arg_check_mode: boolean that tells if this Ansible module is run in check mode + """ + + ret_dict = dict(id=0, + name="none", + state="CHECK_MODE", + ) + + if arg_check_mode: + # in check mode return immediately with the default values + return ret_dict + + if arg_k8s_facts is None: + # if void facts provided - change state value to ABSENT and return + ret_dict['state'] = "ABSENT" + return ret_dict + + ret_dict['id'] = arg_k8s_facts['id'] + ret_dict['name'] = arg_k8s_facts['name'] + ret_dict['techStatus'] = arg_k8s_facts['techStatus'] + ret_dict['state'] = arg_k8s_facts['status'] + + return ret_dict + +def decort_k8s_parameters(): + """Build and return a dictionary of parameters expected by decort_k8s module in a form accepted + by AnsibleModule utility class.""" + + return dict( + account_id=dict(type='int', required=False), + account_name=dict(type='str', required=False, default=''), + annotation=dict(type='str', required=False, default=''), + app_id=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_APP_ID'])), + app_secret=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_APP_SECRET']), + no_log=True), + authenticator=dict(type='str', + required=True, + choices=['legacy', 'oauth2', 'jwt']), + controller_url=dict(type='str', required=True), + # datacenter=dict(type='str', required=False, default=''), + jwt=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_JWT']), + no_log=True), + oauth2_url=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_OAUTH2_URL'])), + password=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_PASSWORD']), + no_log=True), + quotas=dict(type='dict', required=False), + state=dict(type='str', + default='present', + choices=['absent', 'disabled', 'enabled', 'present']), + permanent=dict(type='bool', default=False), + started=dict(type='bool', default=True), + user=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_USER'])), + k8s_name=dict(type='str', required=True), + rg_id=dict(type='int', required=True), + k8ci_id=dict(type='int', required=True), + wg_name=dict(type='str', required=True), + master_count=dict(type='int', default=1), + master_cpu=dict(type='int', default=2), + master_ram_mb=dict(type='int', default=2048), + master_disk_gb=dict(type='int', default=10), + worker_count=dict(type='int', default=1), + worker_cpu=dict(type='int', default=1), + worker_ram_mb=dict(type='int', default=1024), + worker_disk_gb=dict(type='int', default=0), + extnet_id=dict(type='int', default=0), + description=dict(type='str', default="Created by decort ansible module"), + with_lb=dict(type='bool', default=True), + verify_ssl=dict(type='bool', required=False, default=True), + workflow_callback=dict(type='str', required=False), + workflow_context=dict(type='str', required=False), + ) + + +def main(): + module_parameters = decort_k8s_parameters() + + amodule = AnsibleModule(argument_spec=module_parameters, + supports_check_mode=True, + mutually_exclusive=[ + ['oauth2', 'password'], + ['password', 'jwt'], + ['jwt', 'oauth2'], + ], + required_together=[ + ['app_id', 'app_secret'], + ['user', 'password'], + ], + ) + + decon = DecortController(amodule) + k8s_id, k8s_facts = decon.k8s_find(arg_k8s_name=amodule.params['k8s_name'], + arg_check_state=False) + k8s_should_exist = True + + if k8s_id: + if k8s_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING", "CREATING", + "RESTORING"] and amodule.params['state'] != "present": + decon.result['failed'] = True + decon.result['changed'] = False + decon.result['msg'] = ("No change can be done for existing k8s ID {} because of its current " + "status '{}'").format(k8s_id, k8s_facts['status']) + elif k8s_facts['status'] in ["DISABLED", "ENABLED", "CREATED", "DELETED"] and amodule.params['state'] == "absent": + if amodule.params['permanent'] is True: + decon.k8s_delete(k8s_id, True) + k8s_facts['status'] = 'DESTROYED' + k8s_should_exist = False + else: + decon.k8s_delete(k8s_id) + k8s_facts['status'] = 'DELETED' + k8s_should_exist = True + elif k8s_facts['status'] == "ENABLED" and amodule.params['started'] is True: + decon.k8s_state(k8s_facts, amodule.params['state'], amodule.params['started']) + elif k8s_facts['status'] == amodule.params['state'].upper(): + decon.k8s_state(k8s_facts, amodule.params['state']) + elif k8s_facts['status'] in ["ENABLED", "CREATED"] and amodule.params['state'] == "disabled": + decon.k8s_state(k8s_facts, 'disabled') + elif k8s_facts['status'] in ["DISABLED", "CREATED"]: + if amodule.params['state'] == 'enabled': + decon.k8s_state(k8s_facts, 'enabled', amodule.params['started']) + elif amodule.params['state'] == "disabled": + decon.k8s_state(k8s_facts, 'disabled') + k8s_should_exist = True + elif k8s_facts['status'] == "DELETED": + if amodule.params['state'] in ('enabled', 'present'): + decon.k8s_restore(k8s_id) + k8s_should_exist = True + elif amodule.params['state'] == 'disabled': + decon.result['failed'] = True + decon.result['changed'] = False + decon.result['msg'] = ("Invalid target state '{}' requested for k8s ID {} in the " + "current status '{}'").format(k8s_id, + amodule.params['state'], + k8s_facts['status']) + k8s_should_exist = False + elif k8s_facts['status'] == "DESTROYED": + if amodule.params['state'] in ('present', 'enabled'): + k8s_should_exist = True + elif amodule.params['state'] == 'absent': + # nop + decon.result['failed'] = False + decon.result['changed'] = False + decon.result['msg'] = ("No state change required for k8s ID {} because of its " + "current status '{}'").format(k8s_id, + k8s_facts['status']) + k8s_should_exist = False + elif amodule.params['state'] == 'disabled': + # error + decon.result['failed'] = True + decon.result['changed'] = False + decon.result['msg'] = ("Invalid target state '{}' requested for k8s ID {} in the " + "current status '{}'").format(k8s_id, + amodule.params['state'], + k8s_facts['status']) + else: + k8s_should_exist = False + if amodule.params['state'] == 'absent': + decon.result['failed'] = False + decon.result['changed'] = False + decon.result['msg'] = ("Nothing to do as target state 'absent' was requested for " + "non-existent k8s name '{}'").format(amodule.params['k8s_name']) + elif amodule.params['state'] in ('present', 'enabled'): + decon.check_amodule_argument('k8s_name') + k8s_id = decon.k8s_provision(amodule.params['k8s_name'], + amodule.params['wg_name'], + amodule.params['k8ci_id'], + amodule.params['rg_id'], + amodule.params['master_count'], + amodule.params['master_cpu'], + amodule.params['master_ram_mb'], + amodule.params['master_disk_gb'], + amodule.params['worker_count'], + amodule.params['worker_cpu'], + amodule.params['worker_ram_mb'], + amodule.params['worker_disk_gb'], + amodule.params['extnet_id'], + amodule.params['with_lb'], + amodule.params['description'], + ) + k8s_should_exist = True + elif amodule.params['state'] == 'disabled': + decon.result['failed'] = True + decon.result['changed'] = False + decon.result['msg'] = ("Invalid target state '{}' requested for non-existent " + "k8s name '{}' ").format(amodule.params['state'], + amodule.params['k8s_name']) + if decon.result['failed']: + amodule.fail_json(**decon.result) + else: + if k8s_should_exist: + if decon.result['changed']: + _, k8s_facts = decon.k8s_find(arg_k8s_id=k8s_id) + decon.result['facts'] = decort_k8s_package_facts(k8s_facts, amodule.check_mode) + amodule.exit_json(**decon.result) + +if __name__ == "__main__": + main() diff --git a/library/decort_vins.py b/library/decort_vins.py index 4a5936f..924eb4f 100644 --- a/library/decort_vins.py +++ b/library/decort_vins.py @@ -284,15 +284,15 @@ def decort_vins_package_facts(arg_vins_facts, arg_check_mode=False): else: ret_dict['ext_ip_addr'] = "" ret_dict['ext_net_id'] = -1 - + # arg_vins_facts['vnfs']['GW']['config'] # ext_ip_addr -> ext_net_ip # ??? -> ext_net_id # tech_status -> techStatus - return ret_dict + def decort_vins_parameters(): """Build and return a dictionary of parameters expected by decort_vins module in a form accepted by AnsibleModule utility class.""" @@ -342,6 +342,7 @@ def decort_vins_parameters(): workflow_context=dict(type='str', required=False), ) + # Workflow digest: # 1) authenticate to DECORT controller & validate authentication by issuing API call - done when creating DECORTController # 2) check if the ViNS with this id or name exists under specified account / resource group @@ -368,12 +369,12 @@ def main(): decon = DecortController(amodule) vins_id = 0 - vins_level = "" # "ID" if specified by ID, "RG" - at resource group, "ACC" - at account level - vins_facts = None # will hold ViNS facts + vins_level = "" # "ID" if specified by ID, "RG" - at resource group, "ACC" - at account level + vins_facts = None # will hold ViNS facts validated_rg_id = 0 - rg_facts = None # will hold RG facts + rg_facts = None # will hold RG facts validated_acc_id = 0 - acc_facts = None # will hold Account facts + acc_facts = None # will hold Account facts if amodule.params['vins_id']: # expect existing ViNS with the specified ID @@ -383,56 +384,56 @@ def main(): decon.result['failed'] = True decon.result['msg'] = "Specified ViNS ID {} not found.".format(amodule.params['vins_id']) decon.fail_json(**decon.result) - vins_level="ID" + vins_level = "ID" validated_acc_id = vins_facts['accountId'] validated_rg_id = vins_facts['rgId'] elif amodule.params['rg_id']: # expect ViNS @ RG level in the RG with specified ID - vins_level="RG" - # This call to rg_find will abort the module if no RG with such ID is present - validated_rg_id, rg_facts = decon.rg_find(0, # account ID set to 0 as we search for RG by RG ID - amodule.params['rg_id'], arg_rg_name="") + vins_level = "RG" + # This call to rg_find will abort the module if no RG with such ID is present + validated_rg_id, rg_facts = decon.rg_find(0, # account ID set to 0 as we search for RG by RG ID + amodule.params['rg_id'], arg_rg_name="") # This call to vins_find may return vins_id=0 if no ViNS found vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'], - account_id=0, - rg_id=amodule.params['rg_id'], - check_state=False) + account_id=0, + rg_id=amodule.params['rg_id'], + check_state=False) # TODO: add checks and setup ViNS presence flags accordingly pass elif amodule.params['account_id'] or amodule.params['account_name'] != "": - # Specified account must be present and accessible by the user, otherwise abort the module + # Specified account must be present and accessible by the user, otherwise abort the module validated_acc_id, acc_facts = decon.account_find(amodule.params['account_name'], amodule.params['account_id']) if not validated_acc_id: decon.result['failed'] = True decon.result['msg'] = ("Current user does not have access to the requested account " - "or non-existent account specified.") + "or non-existent account specified.") decon.fail_json(**decon.result) - if amodule.params['rg_name'] != "": # at this point we know that rg_id=0 + if amodule.params['rg_name'] != "": # at this point we know that rg_id=0 # expect ViNS @ RG level in the RG with specified name under specified account - # RG with the specified name must be present under the account, otherwise abort the module + # RG with the specified name must be present under the account, otherwise abort the module validated_rg_id, rg_facts = decon.rg_find(validated_acc_id, 0, amodule.params['rg_name']) - if (not validated_rg_id or - rg_facts['status'] in ["DESTROYING", "DESTROYED", "DELETING", "DELETED", "DISABLING", "ENABLING"]): + if (not validated_rg_id or + rg_facts['status'] in ["DESTROYING", "DESTROYED", "DELETING", "DELETED", "DISABLING", "ENABLING"]): decon.result['failed'] = True decon.result['msg'] = "RG name '{}' not found or has invalid state.".format(amodule.params['rg_name']) decon.fail_json(**decon.result) # This call to vins_find may return vins_id=0 if no ViNS with this name found under specified RG vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'], - account_id=0, # set to 0, as we are looking for ViNS under RG - rg_id=validated_rg_id, - check_state=False) + account_id=0, # set to 0, as we are looking for ViNS under RG + rg_id=validated_rg_id, + check_state=False) vins_level = "RG" # TODO: add checks and setup ViNS presence flags accordingly - else: # At this point we know for sure that rg_name="" and rg_id=0 + else: # At this point we know for sure that rg_name="" and rg_id=0 # So we expect ViNS @ account level # This call to vins_find may return vins_id=0 if no ViNS found vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'], - account_id=validated_acc_id, - rg_id=0, - check_state=False) + account_id=validated_acc_id, + rg_id=0, + check_state=False) vins_level = "ACC" # TODO: add checks and setup ViNS presence flags accordingly - else: + else: # this is "invalid arguments combination" sink # if we end up here, it means that module was invoked with vins_id=0 and rg_id=0 decon.result['failed'] = True @@ -453,12 +454,12 @@ def main(): # # When managing existing ViNS we need to account for both "static" and "transient" # status. Full range of ViNS statii is as follows: - # + # # "MODELED", "CREATED", "ENABLED", "ENABLING", "DISABLED", "DISABLING", "DELETED", "DELETING", "DESTROYED", "DESTROYING" - # + # vins_should_exist = False - + if vins_id: vins_should_exist = True if vins_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]: @@ -526,7 +527,7 @@ def main(): # annotation - take from module arguments vins_id = decon.vins_provision(vins_facts['name'], validated_acc_id, validated_rg_id, - amodule.params['ipcidr'], + amodule.params['ipcidr'], amodule.params['ext_net_id'], amodule.params['ext_ip_addr'], amodule.params['annotation']) vins_should_exist = True @@ -559,18 +560,17 @@ def main(): decon.check_amodule_argument('vins_name') # as we already have account ID and RG ID we can create ViNS and get vins_id on success vins_id = decon.vins_provision(amodule.params['vins_name'], - validated_acc_id, validated_rg_id, - amodule.params['ipcidr'], - amodule.params['ext_net_id'], amodule.params['ext_ip_addr'], - amodule.params['annotation']) - vins_should_exist = True + validated_acc_id, validated_rg_id, + amodule.params['ipcidr'], + amodule.params['ext_net_id'], amodule.params['ext_ip_addr'], + amodule.params['annotation']) + vins_should_exist = True elif amodule.params['state'] == 'disabled': decon.result['failed'] = True decon.result['changed'] = False decon.result['msg'] = ("Invalid target state '{}' requested for non-existent " "ViNS name '{}'").format(amodule.params['state'], amodule.params['vins_name']) - # # conditional switch end - complete module run # diff --git a/module_utils/decort_utils.py b/module_utils/decort_utils.py index c50ab96..380fcdf 100644 --- a/module_utils/decort_utils.py +++ b/module_utils/decort_utils.py @@ -40,6 +40,7 @@ import requests from ansible.module_utils.basic import AnsibleModule + # # TODO: the following functionality to be implemented and/or tested # 4) workflow callbacks @@ -150,7 +151,7 @@ class DecortController(object): if self.authenticator == "jwt": # validate supplied JWT on the DECORT controller self.validate_jwt() # this call will abort the script if validation fails - jwt_decoded = jwt.decode(self.jwt, verify=False) + jwt_decoded = jwt.decode(self.jwt, algorithms=["ES384"], options={"verify_signature": False}) self.decort_username = jwt_decoded['username'] + "@" + jwt_decoded['iss'] elif self.authenticator == "legacy": # obtain session id from the DECORT controller and thus validate the the legacy user @@ -161,7 +162,7 @@ class DecortController(object): # obtain JWT from Oauth2 provider and validate on the DECORT controller self.obtain_oauth2_jwt() self.validate_jwt() # this call will abort the script if validation fails - jwt_decoded = jwt.decode(self.jwt, verify=False) + jwt_decoded = jwt.decode(self.jwt, algorithms=["ES384"], options={"verify_signature": False}) self.decort_username = jwt_decoded['username'] + "@" + jwt_decoded['iss'] # self.run_phase = "Initializing DecortController instance complete." @@ -205,7 +206,7 @@ class DecortController(object): client_id=self.app_id, client_secret=self.app_secret, response_type="id_token", - validity=3600,) + validity=3600, ) # TODO: Need standard code snippet to handle server timeouts gracefully # Consider a few retries before giving up or use requests.Session & requests.HTTPAdapter # see https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request @@ -273,7 +274,7 @@ class DecortController(object): return False req_url = self.controller_url + "/restmachine/cloudapi/account/list" - req_header = dict(Authorization="bearer {}".format(arg_jwt),) + req_header = dict(Authorization="bearer {}".format(arg_jwt), ) try: api_resp = requests.post(req_url, headers=req_header, verify=self.verify_ssl) @@ -318,7 +319,7 @@ class DecortController(object): req_url = self.controller_url + "/restmachine/cloudapi/user/authenticate" req_data = dict(username=self.user, - password=self.password,) + password=self.password, ) try: api_resp = requests.post(req_url, data=req_data, verify=self.verify_ssl) @@ -388,7 +389,8 @@ class DecortController(object): return None # actually, this directive will never be executed as fail_json aborts the script except requests.exceptions.Timeout: self.result['failed'] = True - self.result['msg'] = "Timeout when trying to connect to '{}' when calling DECORT API.".format(api_resp.url) + self.result['msg'] = "Timeout when trying to connect to '{}' when calling DECORT API.".format( + api_resp.url) self.amodule.fail_json(**self.result) return None @@ -536,22 +538,22 @@ class DecortController(object): if disk['id'] not in new_data_disks: detach_list.append(disk['id']) - attach_list = [ did for did in new_data_disks if did not in current_list ] + attach_list = [did for did in new_data_disks if did not in current_list] for did in detach_list: - api_params = dict(computeId = comp_dict['id'], diskId=did) + api_params = dict(computeId=comp_dict['id'], diskId=did) self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/diskDetach", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['changed'] = True for did in attach_list: - api_params = dict(computeId = comp_dict['id'], diskId=did) + api_params = dict(computeId=comp_dict['id'], diskId=did) self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/diskAttach", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['changed'] = True self.result['failed'] = False - + return def compute_delete(self, comp_id, permanently=False): @@ -572,7 +574,7 @@ class DecortController(object): return api_params = dict(computeId=comp_id, - permanently=permanently,) + permanently=permanently, ) self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/delete", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -593,7 +595,7 @@ class DecortController(object): ret_comp_dict = None ret_rg_id = 0 - api_params = dict(computeId=comp_id,) + api_params = dict(computeId=comp_id, ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/get", api_params) if api_resp.status_code == 200: ret_comp_id = comp_id @@ -605,7 +607,6 @@ class DecortController(object): return ret_comp_id, ret_comp_dict, ret_rg_id - def compute_find(self, comp_id, comp_name="", rg_id=0, check_state=True): @@ -652,21 +653,22 @@ class DecortController(object): # Therefore, RG ID cannot be zero and compute name cannot be empty. if not rg_id and comp_name == "": self.result['failed'] = True - self.result['msg'] = "compute_find(): cannot find Compute by name when either name is empty or RG ID is zero." + self.result[ + 'msg'] = "compute_find(): cannot find Compute by name when either name is empty or RG ID is zero." self.amodule.fail_json(**self.result) # fail the module - exit - api_params = dict(includedeleted=True,) + api_params = dict(includedeleted=True, ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/list", api_params) if api_resp.status_code == 200: comp_list = json.loads(api_resp.content.decode('utf8')) else: self.result['failed'] = True self.result['msg'] = ("compute_find(): failed to get list Computes. HTTP code {}, " - "response {}.").format(api_resp.status_code, api_resp.reason) + "response {}.").format(api_resp.status_code, api_resp.reason) self.amodule.fail_json(**self.result) # fail the module - exit - + # if we have validated RG ID at this point, look up Compute by name in this RG # rg.vms list contains IDs of compute instances registered with this RG until compute is # destroyed. So we may see here computes in "active" and DELETED states. @@ -753,11 +755,11 @@ class DecortController(object): self.result['failed'] = False self.result['warning'] = ("compute_powerstate(): no power state change required for Compute ID {} from its " "current state '{}' to desired state '{}'.").format(comp_facts['id'], - comp_facts['techStatus'], - target_state) + comp_facts['techStatus'], + target_state) return - def kvmvm_provision(self, rg_id, + def kvmvm_provision(self, rg_id, comp_name, arch, cpu, ram, boot_disk, image_id, @@ -791,7 +793,7 @@ class DecortController(object): "was requested.").format(comp_name, rg_id) return 0 - api_url="" + api_url = "" if arch == "X86_64": api_url = "/restmachine/cloudapi/kvmx86/create" elif arch == "PPC64_LE": @@ -808,7 +810,7 @@ class DecortController(object): imageId=image_id, bootDisk=boot_disk, start=start_on_create, # start_machine parameter requires DECORT API ver 3.3.1 or higher - netType="NONE") # we create VM without any network connections + netType="NONE") # we create VM without any network connections if userdata: api_params['userdata'] = json.dumps(userdata) # we need to pass a string object as "userdata" @@ -864,7 +866,7 @@ class DecortController(object): for repair in new_networks: repair['id'] = int(repair['id']) - api_params = dict(accountId = comp_dict['accountId']) + api_params = dict(accountId=comp_dict['accountId']) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/search", api_params) vins_list = json.loads(api_resp.content.decode('utf8')) # @@ -879,7 +881,8 @@ class DecortController(object): # return api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/extnet/list", api_params) - extnet_list = json.loads(api_resp.content.decode('utf8')) # list of dicts: "name" holds "NET_ADDR/NETMASK", "id" is ID + extnet_list = json.loads( + api_resp.content.decode('utf8')) # list of dicts: "name" holds "NET_ADDR/NETMASK", "id" is ID # # Empty extnet_list does not constitute error condition, so we should not fail the module in # this case. Therefore the following code fragment is commented out. @@ -891,15 +894,15 @@ class DecortController(object): # return # Prepare the lists of network interfaces for the compute instance: - vins_iface_list = [] # will contain dict(id=, ipAddress=, mac=) for ifaces connected to ViNS(es) - enet_iface_list = [] # will contain dict(id=, ipAddress=, mac=) for ifaces connected to Ext net(s) + vins_iface_list = [] # will contain dict(id=, ipAddress=, mac=) for ifaces connected to ViNS(es) + enet_iface_list = [] # will contain dict(id=, ipAddress=, mac=) for ifaces connected to Ext net(s) for iface in comp_dict['interfaces']: if iface['connType'] == 'VXLAN': for vrunner in vins_list: if vrunner['vxlanId'] == iface['connId']: iface_data = dict(id=vrunner['id'], - ipAddress=iface['ipAddress'], - mac=iface['mac']) + ipAddress=iface['ipAddress'], + mac=iface['mac']) vins_iface_list.append(iface_data) elif iface['connType'] == 'VLAN': ip_addr = netaddr.IPAddress(iface['ipAddress']) @@ -910,8 +913,8 @@ class DecortController(object): ip_extnet = netaddr.IPNetwork(erunner['ipcidr']) if ip_addr.value >= ip_extnet.first and ip_addr.value <= ip_extnet.last: iface_data = dict(id=erunner['id'], - ipAddress=iface['ipAddress'], - mac=iface['mac']) + ipAddress=iface['ipAddress'], + mac=iface['mac']) enet_iface_list.append(iface_data) # If at this point compt_dict["interfaces"] lists some interfaces, but neither vins_iface_list @@ -921,55 +924,55 @@ class DecortController(object): if len(comp_dict['interfaces']) and (not len(vins_iface_list) and not len(enet_iface_list)): self.result['failed'] = True self.result['msg'] = ("compute_networks() no match between {} interface(s) of Compute ID {}" - "and available {} ViNS(es) or {} ExtNet(s).").format(len(comp_dict['interfaces']), - comp_dict['id'], - len(vins_list), - len(extnet_list)) + "and available {} ViNS(es) or {} ExtNet(s).").format(len(comp_dict['interfaces']), + comp_dict['id'], + len(vins_list), + len(extnet_list)) return - vins_id_list = [ rec['id'] for rec in vins_iface_list ] + vins_id_list = [rec['id'] for rec in vins_iface_list] - enet_id_list = [ rec['id'] for rec in enet_iface_list ] + enet_id_list = [rec['id'] for rec in enet_iface_list] # Build attach list by looking for ViNS/Ext net IDs that appear in new_networks, but do not appear in current lists - attach_list = [] # attach list holds both ViNS and Ext Net attachment specs, as API handles them the same way + attach_list = [] # attach list holds both ViNS and Ext Net attachment specs, as API handles them the same way for netrunner in new_networks: - if netrunner['type'] == 'VINS' and ( netrunner['id'] not in vins_id_list ): + if netrunner['type'] == 'VINS' and (netrunner['id'] not in vins_id_list): net2attach = dict(computeId=comp_dict['id'], - netType='VINS', - netId=netrunner['id'], - ipAddr=netrunner.get('ip_addr', "")) + netType='VINS', + netId=netrunner['id'], + ipAddr=netrunner.get('ip_addr', "")) attach_list.append(net2attach) - elif netrunner['type'] == 'EXTNET' and ( netrunner['id'] not in enet_id_list ): + elif netrunner['type'] == 'EXTNET' and (netrunner['id'] not in enet_id_list): net2attach = dict(computeId=comp_dict['id'], - netType='EXTNET', - netId=netrunner['id'], - ipAddr=netrunner.get('ip_addr', "")) + netType='EXTNET', + netId=netrunner['id'], + ipAddr=netrunner.get('ip_addr', "")) attach_list.append(net2attach) - + # detach is meaningful only if compute's interfaces list was not empty if vins_id_list or enet_id_list: # Build detach list by looking for ViNS/Ext net IDs that appear in current lists, but do not appear in new_networks - detach_list = [] # detach list holds both ViNS and Ext Net detachment specs, as API handles them the same way + detach_list = [] # detach list holds both ViNS and Ext Net detachment specs, as API handles them the same way - target_list = [ rec['id'] for rec in new_networks if rec['type'] == 'VINS' ] + target_list = [rec['id'] for rec in new_networks if rec['type'] == 'VINS'] for netrunner in vins_iface_list: if netrunner['id'] not in target_list: net2detach = dict(computeId=comp_dict['id'], - ipAddr=netrunner['ipAddress'], - mac=netrunner['mac']) + ipAddr=netrunner['ipAddress'], + mac=netrunner['mac']) detach_list.append(net2detach) - target_list = [ rec['id'] for rec in new_networks if rec['type'] == 'EXTNET' ] + target_list = [rec['id'] for rec in new_networks if rec['type'] == 'EXTNET'] for netrunner in enet_iface_list: if netrunner['id'] not in target_list: net2detach = dict(computeId=comp_dict['id'], - ipAddr=netrunner['ipAddress'], - mac=netrunner['mac']) + ipAddr=netrunner['ipAddress'], + mac=netrunner['mac']) detach_list.append(net2detach) - + for api_params in detach_list: self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/netDetach", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. @@ -1093,8 +1096,8 @@ class DecortController(object): if not new_cpu and not new_ram: # if both are 0 or Null - return immediately, as user did not mean to manage size self.result['failed'] = False - self.result['warning'] =("compute_resize: new CPU count and RAM size are both zero for Compute ID {}" - " - nothing to do.").format(comp_dict['id']) + self.result['warning'] = ("compute_resize: new CPU count and RAM size are both zero for Compute ID {}" + " - nothing to do.").format(comp_dict['id']) return if not new_cpu: @@ -1104,17 +1107,17 @@ class DecortController(object): # stupid hack? if new_ram > 1 and new_ram < 512: - new_ram = new_ram*1024 + new_ram = new_ram * 1024 if comp_dict['cpus'] == new_cpu and comp_dict['ram'] == new_ram: # no need to call API in this case, as requested size is not different from the current one self.result['failed'] = False - self.result['warning'] =("compute_resize: new CPU count and RAM size are the same for Compute ID {}" - " - nothing to do.").format(comp_dict['id']) + self.result['warning'] = ("compute_resize: new CPU count and RAM size are the same for Compute ID {}" + " - nothing to do.").format(comp_dict['id']) return if ((comp_dict['cpus'] > new_cpu or comp_dict['ram'] > new_ram) and - comp_dict['status'] in INVALID_STATES_FOR_HOT_DOWNSIZE): + comp_dict['status'] in INVALID_STATES_FOR_HOT_DOWNSIZE): while wait_for_state_change: time.sleep(5) fresh_comp_dict = self.compute_find(arg_vm_id=comp_dict['id']) @@ -1125,15 +1128,15 @@ class DecortController(object): if not wait_for_state_change: self.result['failed'] = True self.result['msg'] = ("compute_resize(): downsize of Compute ID {} from CPU:RAM {}:{} to {}:{} was " - "requested, but its current state '{}' is incompatible with downsize operation.").\ - format(comp_dict['id'], - comp_dict['cpus'], comp_dict['ram'], - new_cpu, new_ram, comp_dict['status']) + "requested, but its current state '{}' is incompatible with downsize operation."). \ + format(comp_dict['id'], + comp_dict['cpus'], comp_dict['ram'], + new_cpu, new_ram, comp_dict['status']) return api_params = dict(computeId=comp_dict['id'], ram=new_ram, - cpu=new_cpu,) + cpu=new_cpu, ) self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/resize", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -1200,7 +1203,6 @@ class DecortController(object): return image_id, ret_image_dict - def image_find(self, image_id, image_name, account_id, rg_id=0, sepid=0, pool=""): """Locates image specified by name and returns its facts as dictionary. Primary use of this function is to obtain the ID of the image identified by its name and, @@ -1228,10 +1230,10 @@ class DecortController(object): if image_id > 0: ret_image_id, ret_image_dict = self._image_get_by_id(image_id) - if ( ret_image_id and - (sepid == 0 or sepid == ret_image_dict['sepId']) and - (pool == "" or pool == ret_image_dict['pool']) ): - return ret_image_id, ret_image_dict + if (ret_image_id and + (sepid == 0 or sepid == ret_image_dict['sepId']) and + (pool == "" or pool == ret_image_dict['pool'])): + return ret_image_id, ret_image_dict else: validated_acc_id = account_id if account_id == 0: @@ -1263,10 +1265,10 @@ class DecortController(object): self.result['failed'] = True self.result['msg'] = ("Failed to find OS image by name '{}', SEP ID {}, pool '{}' for " "account ID '{}'.").format(image_name, - sepid, pool, - account_id) + sepid, pool, + account_id) return 0, None - + ################################### # Resource Group (RG) manipulation methods ################################### @@ -1292,7 +1294,7 @@ class DecortController(object): api_params = dict(rgId=rg_id, # force=True | False, - permanently=permanently,) + permanently=permanently, ) self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/delete", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -1317,7 +1319,7 @@ class DecortController(object): self.result['msg'] = "rg_get_by_id(): zero RG ID specified." self.amodule.fail_json(**self.result) - api_params = dict(rgId=rg_id,) + api_params = dict(rgId=rg_id, ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/get", api_params) if api_resp.status_code == 200: ret_rg_id = rg_id @@ -1381,7 +1383,7 @@ class DecortController(object): # try to locate RG by name - start with getting all RGs IDs within the specified account api_params['accountId'] = arg_account_id api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/listRG", api_params) - if api_resp.status_code == 200: + if api_resp.status_code == 200: account_specs = json.loads(api_resp.content.decode('utf8')) api_params.pop('accountId') for rg_item in account_specs: @@ -1402,7 +1404,6 @@ class DecortController(object): return ret_rg_id, ret_rg_dict - def rg_provision(self, arg_account_id, arg_rg_name, arg_username, arg_quota={}, arg_location="", arg_desc=""): """Provision new RG according to the specified arguments. If critical error occurs the embedded call to API function will abort further execution of the script @@ -1434,7 +1435,8 @@ class DecortController(object): target_gid = self.gid_get(arg_location) if not target_gid: self.result['failed'] = True - self.result['msg'] = ("rg_provision() failed to obtain valid Grid ID for location '{}'").format(arg_location) + self.result['msg'] = ("rg_provision() failed to obtain valid Grid ID for location '{}'").format( + arg_location) self.amodule.fail_json(**self.result) api_params = dict(accountId=arg_account_id, @@ -1454,7 +1456,7 @@ class DecortController(object): api_params['maxCPUCapacity'] = arg_quota['cpu'] if 'ext_ips' in arg_quota: api_params['maxNumPublicIP'] = arg_quota['ext_ips'] - + if arg_desc: api_params['desc'] = arg_desc @@ -1495,12 +1497,12 @@ class DecortController(object): query_key_map = dict(cpu='CU_C', ram='CU_M', disk='CU_D', - ext_ips='CU_I',) + ext_ips='CU_I', ) set_key_map = dict(cpu='maxCPUCapacity', ram='maxMemoryCapacity', disk='maxVDiskCapacity', - ext_ips='maxNumPublicIP',) - api_params = dict(rgId=arg_rg_dict['id'],) + ext_ips='maxNumPublicIP', ) + api_params = dict(rgId=arg_rg_dict['id'], ) quota_change_required = False for new_limit in ('cpu', 'ram', 'disk', 'ext_ips'): @@ -1543,7 +1545,7 @@ class DecortController(object): return api_params = dict(rgId=arg_rg_id, - reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username),) + reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username), ) self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/restore", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -1560,7 +1562,8 @@ class DecortController(object): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_state") - NOP_STATES_FOR_RG_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", "DESTROYED"] + NOP_STATES_FOR_RG_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", + "DESTROYED"] VALID_TARGET_STATES = ["enabled", "disabled"] if arg_rg_dict['status'] in NOP_STATES_FOR_RG_CHANGE: @@ -1573,7 +1576,7 @@ class DecortController(object): self.result['failed'] = False self.result['warning'] = ("rg_state(): unrecognized desired state '{}' requested " "for RG ID {}. No RG state change will be done.").format(arg_desired_state, - arg_rg_dict['id']) + arg_rg_dict['id']) return if self.amodule.check_mode: @@ -1677,7 +1680,7 @@ class DecortController(object): "requested.").format(arg_type, arg_mode, arg_vmid) return 0 - api_params=dict( + api_params = dict( machineId=arg_vmid, gpu_type=arg_type, gpu_mode=arg_mode, @@ -1709,7 +1712,7 @@ class DecortController(object): "requested.").format(arg_vgpuid, arg_vmid) return True - api_params=dict( + api_params = dict( machineId=arg_vmid, vgpuid=arg_vgpuid, ) @@ -1734,7 +1737,7 @@ class DecortController(object): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "gpu_list") - api_params=dict( + api_params = dict( machineId=arg_vmid, list_destroyed=arg_list_destroyed, ) @@ -1749,7 +1752,7 @@ class DecortController(object): ret_gpu_list = json.loads(api_resp.content.decode('utf8')) return ret_gpu_list - + ################################### # Workflow callback stub methods - not fully implemented yet ################################### @@ -1798,7 +1801,7 @@ class DecortController(object): ret_gid = 0 api_params = dict() api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/locations/list", api_params) - if api_resp.status_code == 200: + if api_resp.status_code == 200: locations = json.loads(api_resp.content.decode('utf8')) if location_code == "" and locations: ret_gid = locations[0]['gid'] @@ -1839,7 +1842,7 @@ class DecortController(object): api_params = dict(vinsId=vins_id, # force=True | False, - permanently=permanently,) + permanently=permanently, ) self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/delete", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -1867,7 +1870,7 @@ class DecortController(object): self.result['msg'] = "vins_get_by_id(): zero ViNS ID specified." self.amodule.fail_json(**self.result) - api_params = dict(vinsId=vins_id,) + api_params = dict(vinsId=vins_id, ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/get", api_params) if api_resp.status_code == 200: ret_vins_id = vins_id @@ -1949,12 +1952,12 @@ class DecortController(object): return ret_vins_id, ret_vins_facts else: return 0, None - else: # both Account ID and RG ID are zero - fail the module + else: # both Account ID and RG ID are zero - fail the module self.result['failed'] = True self.result['msg'] = ("vins_find(): cannot find ViNS by name '{}' " "when no account ID or RG ID is specified.").format(vins_name) self.amodule.fail_json(**self.result) - else: # ViNS ID is 0 and ViNS name is emtpy - fail the module + else: # ViNS ID is 0 and ViNS name is emtpy - fail the module self.result['failed'] = True self.result['msg'] = "vins_find(): cannot find ViNS by zero ID and empty name." self.amodule.fail_json(**self.result) @@ -2055,7 +2058,7 @@ class DecortController(object): return api_params = dict(vinsId=vins_id, - reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username),) + reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username), ) self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/restore", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -2072,7 +2075,8 @@ class DecortController(object): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_state") - NOP_STATES_FOR_VINS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", "DESTROYED"] + NOP_STATES_FOR_VINS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", + "DESTROYED"] VALID_TARGET_STATES = ["enabled", "disabled"] if vins_dict['status'] in NOP_STATES_FOR_VINS_CHANGE: @@ -2085,13 +2089,13 @@ class DecortController(object): self.result['failed'] = False self.result['warning'] = ("vins_state(): unrecognized desired state '{}' requested " "for ViNS ID {}. No ViNS state change will be done.").format(desired_state, - vins_dict['id']) + vins_dict['id']) return if self.amodule.check_mode: self.result['failed'] = False self.result['msg'] = ("vins_state() in check mode: setting state of ViNS ID {}, name '{}' to " - "'{}' was requested.").format(vins_dict['id'],vins_dict['name'], + "'{}' was requested.").format(vins_dict['id'], vins_dict['name'], desired_state) return @@ -2137,14 +2141,14 @@ class DecortController(object): recommended to update ViNS facts in the upstream code. """ - api_params = dict(vinsId=vins_dict['id'],) + api_params = dict(vinsId=vins_dict['id'], ) self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update") if self.amodule.check_mode: self.result['failed'] = False self.result['msg'] = ("vins_update() in check mode: updating ViNS ID {}, name '{}' " - "was requested.").format(vins_dict['id'],vins_dict['name']) + "was requested.").format(vins_dict['id'], vins_dict['name']) return if not vins_dict['rgId']: @@ -2184,7 +2188,7 @@ class DecortController(object): self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, " "ignore ext IP address change if any.").format(vins_dict['id'], ext_net_id) - else: # ext_net_id = 0, i.e. connect ViNS to default network + else: # ext_net_id = 0, i.e. connect ViNS to default network # we will connect ViNS to default network only if it is NOT connected to any ext network yet if not gw_config: api_params['netId'] = 0 @@ -2195,7 +2199,8 @@ class DecortController(object): self.result['failed'] = False self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, " "no reconnection to default network will be done.").format(vins_dict['id'], - gw_config['ext_net_id']) + gw_config[ + 'ext_net_id']) return @@ -2223,7 +2228,7 @@ class DecortController(object): api_params = dict(diskId=disk_id, detach=force_detach, - permanently=permanently,) + permanently=permanently, ) self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/delete", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -2251,7 +2256,7 @@ class DecortController(object): self.result['msg'] = "disk_get_by_id(): zero Disk ID specified." self.amodule.fail_json(**self.result) - api_params = dict(diskId=disk_id,) + api_params = dict(diskId=disk_id, ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/get", api_params) if api_resp.status_code == 200: ret_disk_id = disk_id @@ -2278,14 +2283,15 @@ class DecortController(object): if no Disk found and check_state=False, so make sure to check return values in the upstream code accordingly. """ - + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_find") DISK_INVALID_STATES = ["MODELED", "CREATING", "DELETING", "DESTROYING"] if self.amodule.check_mode: self.result['failed'] = False - self.result['msg'] = "disk_find() in check mode: find Disk ID {} / name '{}' was requested.".format(disk_id, disk_name) + self.result['msg'] = "disk_find() in check mode: find Disk ID {} / name '{}' was requested.".format(disk_id, + disk_name) return ret_disk_id = 0 @@ -2314,12 +2320,12 @@ class DecortController(object): return runner['id'], runner else: return 0, None - else: # we are missing meaningful account_id - fail the module + else: # we are missing meaningful account_id - fail the module self.result['failed'] = True self.result['msg'] = ("disk_find(): cannot find Disk by name '{}' " "when no account ID specified.").format(disk_name) self.amodule.fail_json(**self.result) - else: # Disk ID is 0 and Disk name is emtpy - fail the module + else: # Disk ID is 0 and Disk name is emtpy - fail the module self.result['failed'] = True self.result['msg'] = "disk_find(): cannot find Disk by zero ID and empty name." self.amodule.fail_json(**self.result) @@ -2348,7 +2354,8 @@ class DecortController(object): if self.amodule.check_mode: self.result['failed'] = False - self.result['msg'] = "disk_provision() in check mode: create Disk name '{}' was requested.".format(disk_name) + self.result['msg'] = "disk_provision() in check mode: create Disk name '{}' was requested.".format( + disk_name) return 0 target_gid = self.gid_get(location) @@ -2364,7 +2371,7 @@ class DecortController(object): desc=desc, size=size, type='D', - sepId=sep_id,) + sepId=sep_id, ) if pool_name != "": api_params['pool'] = pool_name api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/create", api_params) @@ -2400,13 +2407,14 @@ class DecortController(object): if not new_size: self.result['failed'] = False - self.result['warning'] = "disk_resize(): zero size requested for Disk ID {} - ignoring.".format(disk_facts['id']) + self.result['warning'] = "disk_resize(): zero size requested for Disk ID {} - ignoring.".format( + disk_facts['id']) return if new_size < disk_facts['sizeMax']: self.result['failed'] = True self.result['msg'] = ("disk_resize(): downsizing Disk ID {} is not allowed - current " - "size {}, requeste size {}.").format(disk_facts['id'], + "size {}, requeste size {}.").format(disk_facts['id'], disk_facts['sizeMax'], new_size) return @@ -2444,14 +2452,13 @@ class DecortController(object): return api_params = dict(diskId=disk_id, - reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username),) + reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username), ) self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/restore", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False self.result['changed'] = True return - ############################## # # Port Forward rules management @@ -2519,14 +2526,15 @@ class DecortController(object): ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id']) return ret_rules - iface_ipaddr = "" # keep IP address associated with Compute's connection to this ViNS - need this for natRuleDel API + iface_ipaddr = "" # keep IP address associated with Compute's connection to this ViNS - need this for natRuleDel API for iface in comp_facts['interfaces']: if iface['connType'] == 'VXLAN' and iface['connId'] == vins_facts['vxlanId']: iface_ipaddr = iface['ipAddress'] break else: decon.result['failed'] = True - decon.result['msg'] = "Compute ID {} is not connected to ViNS ID {}.".format(comp_facts['id'], vins_facts['id']) + decon.result['msg'] = "Compute ID {} is not connected to ViNS ID {}.".format(comp_facts['id'], + vins_facts['id']) return ret_rules existing_rules = [] @@ -2574,9 +2582,9 @@ class DecortController(object): rule['local_port'] = rule['public_port_start'] for runner in existing_rules: if (runner['publicPortStart'] == rule['public_port_start'] and - runner['publicPortEnd'] == rule_port_end and - runner['localPort'] == rule['local_port'] and - runner['protocol'] == rule['proto']): + runner['publicPortEnd'] == rule_port_end and + runner['localPort'] == rule['local_port'] and + runner['protocol'] == rule['proto']): rule['action'] = 'keep' break if rule['action'] == 'add': @@ -2594,9 +2602,9 @@ class DecortController(object): for runner in new_rules: runner_port_end = runner.get('public_port_end', runner['public_port_start']) if (rule['publicPortStart'] == runner['public_port_start'] and - rule['publicPortEnd'] == runner_port_end and - rule['localPort'] == runner['local_port'] and - rule['protocol'] == runner['proto']): + rule['publicPortEnd'] == runner_port_end and + rule['localPort'] == runner['local_port'] and + rule['protocol'] == runner['proto']): rule['action'] = 'keep' break if rule['action'] == 'del': @@ -2640,4 +2648,257 @@ class DecortController(object): self.result['failed'] = False ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id']) - return ret_rules \ No newline at end of file + return ret_rules + + def _k8s_get_by_id(self, k8s_id): + """Helper function that locates k8s by ID and returns k8s facts. + + @param (int) k8s_id: ID of the k8s to find and return facts for. + + @return: k8s ID and a dictionary of k8s facts as provided by k8s/get API call. Note that if it fails + to find the k8s with the specified ID, it may return 0 for ID and empty dictionary for the facts. So + it is suggested to check the return values accordingly. + """ + ret_k8s_id = 0 + ret_k8s_dict = dict() + + if not k8s_id: + self.result['failed'] = True + self.result['msg'] = "k8s_get_by_id(): zero k8s ID specified." + self.amodule.fail_json(**self.result) + + api_params = dict(k8sId=k8s_id, ) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/get", api_params) + if api_resp.status_code == 200: + ret_k8s_id = k8s_id + ret_k8s_dict = json.loads(api_resp.content.decode('utf8')) + else: + self.result['warning'] = ("k8s_get_by_id(): failed to get k8s by ID {}. HTTP code {}, " + "response {}.").format(k8s_id, api_resp.status_code, api_resp.reason) + + return ret_k8s_id, ret_k8s_dict + + def k8s_find(self, arg_k8s_id=0, arg_k8s_name="", arg_check_state=True): + """Returns non zero k8s ID and a dictionary with k8s details on success, 0 and empty dictionary otherwise. + This method does not fail the run if k8s cannot be located by its name (arg_k8s_name), because this could be + an indicator of the requested k8s never existed before. + However, it does fail the run if k8s cannot be located by arg_k8s_id (if non zero specified) or if API errors + occur. + @param (int) arg_k8s_id: integer ID of the k8s to be found. If non-zero k8s ID is passed, account ID and k8s name + are ignored. However, k8s must be present in this case, as knowing its ID implies it already exists, otherwise + method will fail. + @param (string) arg_k8s_name: string that defines the name of k8s to be found. This parameter is case sensitive. + @param (bool) arg_check_state: tells the method to report k8s in valid states only. + + @return: ID of the k8s, if found. Zero otherwise. + @return: dictionary with k8s facts if k8s is present. Empty dictionary otherwise. None on error. + """ + + # Resource group can be in one of the following states: + # MODELED, CREATED, DISABLING, DISABLED, ENABLING, DELETING, DELETED, DESTROYED, DESTROYED + # + # Transient state (ending with ING) are invalid from k8s manipulation viewpoint + # + + K8S_INVALID_STATES = ["MODELED"] + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_find") + + ret_k8s_id = 0 + api_params = dict(includedeleted=True) + ret_k8s_dict = None + + if arg_k8s_id > 0: + ret_k8s_id, ret_k8s_dict = self._k8s_get_by_id(arg_k8s_id) + if not ret_k8s_id: + self.result['failed'] = True + self.result['msg'] = "k8s_find(): cannot find k8s by ID {}.".format(arg_k8s_id) + self.amodule.fail_json(**self.result) + elif arg_k8s_name != "": + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/list", api_params) + if api_resp.status_code == 200: + account_specs = json.loads(api_resp.content.decode('utf8')) + for k8s_item in account_specs: + got_id, got_specs = self._k8s_get_by_id(k8s_item['id']) + if got_id and got_specs['name'] == arg_k8s_name: + # name matches + if not arg_check_state or got_specs['status'] not in K8S_INVALID_STATES: + ret_k8s_id = got_id + ret_k8s_dict = got_specs + break + # Note: we do not fail the run if k8s cannot be located by its name, because it could be a new k8s + # that never existed before. In this case ret_k8s_id=0 and empty ret_k8s_dict will be returned. + else: + # Both arg_k8s_id and arg_k8s_name are empty - there is no way to locate k8s in this case + self.result['failed'] = True + self.result['msg'] = "k8s_find(): either non-zero ID or a non-empty name must be specified." + self.amodule.fail_json(**self.result) + + return ret_k8s_id, ret_k8s_dict + + def k8s_state(self, arg_k8s_dict, arg_desired_state, arg_started=False): + """Enable or disable k8s cluster. + + @param arg_k8s_dict: dictionary with the target k8s facts as returned by k8s_find(...) method or + .../k8s/get API call. + @param arg_desired_state: the desired state for this k8s cluster. Valid states are 'enabled' and 'disabled'. + @param arg_started: the desired tech state for this k8s cluster. Valid states are 'True' and 'False'. + """ + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_state") + + NOP_STATES_FOR_K8S_CHANGE = ["MODELED", "DISABLING", + "ENABLING", "DELETING", + "DELETED", "DESTROYING", + "DESTROYED", "CREATING", + "RESTORING"] + VALID_TARGET_STATES = ["ENABLED", "DISABLED"] + + if arg_k8s_dict['status'] in NOP_STATES_FOR_K8S_CHANGE: + self.result['failed'] = False + self.result['msg'] = ("k8s_state(): no state change possible for k8s ID {} " + "in its current state '{}'.").format(arg_k8s_dict['id'], arg_k8s_dict['status']) + return + if arg_k8s_dict['status'] not in VALID_TARGET_STATES: + self.result['failed'] = False + self.result['warning'] = ("k8s_state(): unrecognized desired state '{}' requested " + "for k8s ID {}. No k8s state change will be done.").format(arg_desired_state, + arg_k8s_dict['id']) + return + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("k8s_state() in check mode: setting state of k8s ID {}, name '{}' to " + "'{}' was requested.").format(arg_k8s_dict['id'], arg_k8s_dict['name'], + arg_desired_state) + return + + k8s_state_api = "" # This string will also be used as a flag to indicate that API call is necessary + api_params = dict(k8sId=arg_k8s_dict['id']) + expected_state = "" + tech_state = "" + if arg_k8s_dict['status'] in ["CREATED", "ENABLED"] and arg_desired_state == 'disabled': + k8s_state_api = "/restmachine/cloudapi/k8s/disable" + expected_state = "DISABLED" + elif arg_k8s_dict['status'] in ["CREATED", "DISABLED"] and arg_desired_state == 'enabled': + k8s_state_api = "/restmachine/cloudapi/k8s/enable" + expected_state = "ENABLED" + elif arg_k8s_dict['status'] == "ENABLED" and arg_desired_state == 'enabled' and arg_started is True and arg_k8s_dict['techStatus'] == "STOPPED": + k8s_state_api = "/restmachine/cloudapi/k8s/start" + tech_state = "STARTED" + elif arg_k8s_dict['status'] == "ENABLED" and arg_desired_state == 'enabled' and arg_started is False and arg_k8s_dict['techStatus'] == "STARTED": + k8s_state_api = "/restmachine/cloudapi/k8s/stop" + tech_state = "STOPPED" + if k8s_state_api != "": + self.decort_api_call(requests.post, k8s_state_api, api_params) + # On success the above call will return here. On error it will abort execution by calling fail_json. + self.result['failed'] = False + self.result['changed'] = True + arg_k8s_dict['status'] = expected_state + arg_k8s_dict['started'] = tech_state + else: + self.result['failed'] = False + self.result['msg'] = ("k8s_state(): no state change required for k8s ID {} from current " + "state '{}' to desired state '{}'.").format(arg_k8s_dict['id'], + arg_k8s_dict['status'], + arg_desired_state) + return + def k8s_delete(self, k8s_id, permanently=False): + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_delete") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = "k8s_delete() in check mode: delete Compute ID {} was requested.".format(k8s_id) + return + + api_params = dict(k8sId=k8s_id, + permanently=permanently, + ) + self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/delete", api_params) + # On success the above call will return here. On error it will abort execution by calling fail_json. + self.result['failed'] = False + self.result['changed'] = True + return + def k8s_restore(self, k8s_id ): + """Restores a deleted k8s cluster identified by ID. + + @param k8s_id: ID of the k8s cluster to restore. + """ + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_restore") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = "k8s_restore() in check mode: restore k8s ID {} was requested.".format(k8s_id) + return + + api_params = dict(k8sId=k8s_id) + self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/restore", api_params) + # On success the above call will return here. On error it will abort execution by calling fail_json. + self.result['failed'] = False + self.result['changed'] = True + return + def k8s_provision(self, k8s_name, + wg_name, k8ci_id, + rg_id, master_count, + master_cpu, master_ram, + master_disk, worker_count, + worker_cpu, worker_ram, + worker_disk, extnet_id, + with_lb, annotation, ): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_provision") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("k8s_provision() in check mode. Provision k8s '{}' in RG ID {} " + "was requested.").format(k8s_name, rg_id) + return 0 + + api_url = "/restmachine/cloudapi/k8s/create" + api_params = dict(name=k8s_name, + rgId=rg_id, + k8ciId=k8ci_id, + workerGroupName=wg_name, + masterNum=master_count, + masterCpu=master_cpu, + masterRam=master_ram, + masterDisk=master_disk, + workerNum=worker_count, + workerCpu=worker_cpu, + workerRam=worker_ram, + workerDisk=worker_disk, + extnetId=extnet_id, + withLB=with_lb, + desc=annotation, + ) + api_resp = self.decort_api_call(requests.post, api_url, api_params) + k8s_id = "" + if api_resp.status_code == 200: + for i in range(300): + api_get_url = "/restmachine/cloudapi/tasks/get" + api_get_params = dict( + auditId=api_resp.content.decode('utf8').replace('"', '') + ) + api_get_resp = self.decort_api_call(requests.post, api_get_url, api_get_params) + ret_info = json.loads(api_get_resp.content.decode('utf8')) + if api_get_resp.status_code == 200: + if ret_info['status'] in ["PROCESSING", "SCHEDULED"]: + self.result['failed'] = False + time.sleep(30) + elif ret_info['status'] == "ERROR": + self.result['failed'] = True + return + elif ret_info['status'] == "OK": + k8s_id = ret_info['result'][0] + self.result['changed'] = True + return k8s_id + else: + k8s_id = ret_info['status'] + else: + self.result['failed'] = True + # Timeout + self.result['failed'] = True + else: + self.result['failed'] = True + return