diff --git a/examples/disk_create.yaml b/examples/decort_disk/disk_create.yaml similarity index 100% rename from examples/disk_create.yaml rename to examples/decort_disk/disk_create.yaml diff --git a/examples/disk_delete.yaml b/examples/decort_disk/disk_delete.yaml similarity index 100% rename from examples/disk_delete.yaml rename to examples/decort_disk/disk_delete.yaml diff --git a/examples/disk_limitIO.yaml b/examples/decort_disk/disk_limitIO.yaml similarity index 100% rename from examples/disk_limitIO.yaml rename to examples/decort_disk/disk_limitIO.yaml diff --git a/examples/disk_rename.yaml b/examples/decort_disk/disk_rename.yaml similarity index 100% rename from examples/disk_rename.yaml rename to examples/decort_disk/disk_rename.yaml diff --git a/examples/disk_restore.yaml b/examples/decort_disk/disk_restore.yaml similarity index 100% rename from examples/disk_restore.yaml rename to examples/decort_disk/disk_restore.yaml diff --git a/examples/create-osimage.yaml b/examples/decort_osimage/create-osimage.yaml similarity index 100% rename from examples/create-osimage.yaml rename to examples/decort_osimage/create-osimage.yaml diff --git a/examples/create-virtual-osimage.yaml b/examples/decort_osimage/create-virtual-osimage.yaml similarity index 100% rename from examples/create-virtual-osimage.yaml rename to examples/decort_osimage/create-virtual-osimage.yaml diff --git a/examples/get-osimage.yaml b/examples/decort_osimage/get-osimage.yaml similarity index 100% rename from examples/get-osimage.yaml rename to examples/decort_osimage/get-osimage.yaml diff --git a/examples/rename-osimage.yaml b/examples/decort_osimage/rename-osimage.yaml similarity index 100% rename from examples/rename-osimage.yaml rename to examples/decort_osimage/rename-osimage.yaml diff --git a/examples/decort_rg/changeLimits_rg.yaml b/examples/decort_rg/changeLimits_rg.yaml new file mode 100644 index 0000000..462fc88 --- /dev/null +++ b/examples/decort_rg/changeLimits_rg.yaml @@ -0,0 +1,20 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "rg_created_by_module" + # or + #rg_id: 999 + account_id: 99 + quotas: + cpu: 8 + ram: 4096 + disk: 20 + ext_ips: 10 + net_transfer: 200 + state: present + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/changeResTypes_rg.yaml b/examples/decort_rg/changeResTypes_rg.yaml new file mode 100644 index 0000000..655ed4a --- /dev/null +++ b/examples/decort_rg/changeResTypes_rg.yaml @@ -0,0 +1,21 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "rg_created_by_module" + # or + #rg_id: 999 + account_id: 99 + resType: + - vins + - compute + - k8s + - openshift + - lb + - flipgroup + state: present + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/create_rg.yaml b/examples/decort_rg/create_rg.yaml new file mode 100644 index 0000000..b702613 --- /dev/null +++ b/examples/decort_rg/create_rg.yaml @@ -0,0 +1,30 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "rg_created_by_module" + account_id: 99 + owner: "user_1" #Leave blank to set current user as owner. + quotas: + cpu: 8 + ram: 4096 + disk: 20 + ext_ips: 10 + net_transfer: 200 + access: + action: "grant" + user: "user_2" + right: "RCX" + def_netType: "PRIVATE" + ipcidr: "" "192.168.1.1" + extNetId: 0 + extNetIp: "" "10.100.1.10" + resType: + - vins + - compute + state: present + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/delete_rg.yaml b/examples/decort_rg/delete_rg.yaml new file mode 100644 index 0000000..cb0ead2 --- /dev/null +++ b/examples/decort_rg/delete_rg.yaml @@ -0,0 +1,15 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "test_rg" + # or + #rg_id: 999 + account_id: 99 + state: present + permanently: True + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/disable_rg.yaml b/examples/decort_rg/disable_rg.yaml new file mode 100644 index 0000000..d62e2ba --- /dev/null +++ b/examples/decort_rg/disable_rg.yaml @@ -0,0 +1,12 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_id: 999 # rg can be restored only by rg id + account_id: 99 + state: present + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/enable_rg.yaml b/examples/decort_rg/enable_rg.yaml new file mode 100644 index 0000000..a562791 --- /dev/null +++ b/examples/decort_rg/enable_rg.yaml @@ -0,0 +1,14 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "rg_created_by_module" + # or + #rg_id: 999 + account_id: 99 + state: enabled + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/grantAccess_rg.yaml b/examples/decort_rg/grantAccess_rg.yaml new file mode 100644 index 0000000..5266a33 --- /dev/null +++ b/examples/decort_rg/grantAccess_rg.yaml @@ -0,0 +1,18 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "rg_created_by_module" + # or + #rg_id: 999 + account_id: 99 + access: + action: "grant" + user: "new_user" + right: "R" + state: present + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/rename_rg.yaml b/examples/decort_rg/rename_rg.yaml new file mode 100644 index 0000000..3fa03dd --- /dev/null +++ b/examples/decort_rg/rename_rg.yaml @@ -0,0 +1,15 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "old_rg_name" + # or + #rg_id: 1737 + account_id: 99 + rename: "new_rg_name" + state: present + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/revokeAccess_rg.yaml b/examples/decort_rg/revokeAccess_rg.yaml new file mode 100644 index 0000000..a16b6d1 --- /dev/null +++ b/examples/decort_rg/revokeAccess_rg.yaml @@ -0,0 +1,17 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "rg_created_by_module" + # or + #rg_id: 999 + account_id: 99 + access: + action: "revoke" + user: "old_user" + state: present + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/decort_rg/setDefNet_rg.yaml b/examples/decort_rg/setDefNet_rg.yaml new file mode 100644 index 0000000..dcb9854 --- /dev/null +++ b/examples/decort_rg/setDefNet_rg.yaml @@ -0,0 +1,16 @@ +- hosts: localhost + tasks: + - name: create + decort_rg: + authenticator: oauth2 + controller_url: "https://cloud.digitalenergy.online" + rg_name: "rg_created_by_module" + # or + #rg_id: 999 + account_id: 99 + def_netType: "PRIVATE" + def_netId: 199 + state: present + verify_ssl: false + register: my_rg + delegate_to: localhost diff --git a/examples/hashivault_create_engine.yaml b/examples/hashivault/hashivault_create_engine.yaml similarity index 100% rename from examples/hashivault_create_engine.yaml rename to examples/hashivault/hashivault_create_engine.yaml diff --git a/examples/hashivault_create_secret.yaml b/examples/hashivault/hashivault_create_secret.yaml similarity index 100% rename from examples/hashivault_create_secret.yaml rename to examples/hashivault/hashivault_create_secret.yaml diff --git a/examples/hashivault_example.yaml b/examples/hashivault/hashivault_example.yaml similarity index 100% rename from examples/hashivault_example.yaml rename to examples/hashivault/hashivault_example.yaml diff --git a/examples/hashivault_plugin_example.yaml b/examples/hashivault/hashivault_plugin_example.yaml similarity index 100% rename from examples/hashivault_plugin_example.yaml rename to examples/hashivault/hashivault_plugin_example.yaml diff --git a/examples/hashivault_plugin_login.yaml b/examples/hashivault/hashivault_plugin_login.yaml similarity index 100% rename from examples/hashivault_plugin_login.yaml rename to examples/hashivault/hashivault_plugin_login.yaml diff --git a/examples/hashivault_plugin_read_secret.yaml b/examples/hashivault/hashivault_plugin_read_secret.yaml similarity index 100% rename from examples/hashivault_plugin_read_secret.yaml rename to examples/hashivault/hashivault_plugin_read_secret.yaml diff --git a/examples/hashivault_read_secret.yaml b/examples/hashivault/hashivault_read_secret.yaml similarity index 100% rename from examples/hashivault_read_secret.yaml rename to examples/hashivault/hashivault_read_secret.yaml diff --git a/library/decort_rg.py b/library/decort_rg.py index 7b9bf11..87e5bf1 100644 --- a/library/decort_rg.py +++ b/library/decort_rg.py @@ -209,90 +209,253 @@ from ansible.module_utils.basic import env_fallback from ansible.module_utils.decort_utils import * +class decort_rg(DecortController): + def __init__(self,amodule): + super(decort_rg, self).__init__(amodule) -def decort_rg_package_facts(arg_rg_facts, arg_check_mode=False): - """Package a dictionary of RG facts according to the decort_rg module specification. This dictionary will - be returned to the upstream Ansible engine at the completion of the module run. + self.validated_acc_id = 0 + self.validated_rg_id = 0 + self.validated_rg_facts = None - @param arg_rg_facts: dictionary with RG facts as returned by API call to .../rg/get - @param arg_check_mode: boolean that tells if this Ansible module is run in check mode - """ + if self.amodule.params['account_id']: + self.validated_acc_id, _ = self.account_find("", amodule.params['account_id']) + elif amodule.params['account_name']: + self.validated_acc_id, _ = self.account_find(amodule.params['account_name']) + if not self.validated_acc_id: + # we failed to locate account by either name or ID - abort with an error + self.result['failed'] = True + self.result['msg'] = ("Current user does not have access to the requested account " + "or non-existent account specified.") + self.fail_json(**self.result) - ret_dict = dict(id=0, - name="none", - state="CHECK_MODE", - ) + if amodule.params['rg_id'] > 0: + self.validated_rg_id = amodule.params['rg_id'] - if arg_check_mode: - # in check mode return immediately with the default values - return ret_dict + # Check if the RG with the specified parameters already exists + self.validated_rg_id, self.rg_facts = self.rg_find(self.validated_acc_id, + arg_rg_id = self.validated_rg_id, + arg_rg_name=amodule.params['rg_name'], + arg_check_state=False) + + if amodule.params['state'] != "absent": + self.rg_should_exist = True + else: + self.rg_should_exist = False + + def access(self): + should_change_access = False + acc_granted = False + for rg_item in self.rg_facts['acl']: + if rg_item['userGroupId'] == self.amodule.params['access']['user']: + acc_granted = True + if self.amodule.params['access']['action'] == 'grant': + if rg_item['right'] != self.amodule.params['access']['right']: + should_change_access = True + if self.amodule.params['access']['action'] == 'revoke': + should_change_access = True + if acc_granted == False and self.amodule.params['access']['action'] == 'grant': + should_change_access = True + + if should_change_access == True: + self.rg_access(self.validated_rg_id, self.amodule.params['access']) + self.rg_facts['access'] = self.amodule.params['access'] + self.rg_should_exist = True + return + + def error(self): + self.result['failed'] = True + self.result['changed'] = False + if self.validated_rg_id > 0: + self.result['msg'] = ("Invalid target state '{}' requested for rg ID {} in the " + "current status '{}'.").format(self.validated_rg_id, + self.amodule.params['state'], + self.rg_facts['status']) + else: + self.result['msg'] = ("Invalid target state '{}' requested for non-existent rg name '{}' " + "in account ID {} ").format(self.amodule.params['state'], + self.amodule.params['rg_name'], + self.validated_acc_id) + return + + def update(self): + resources = self.rg_facts['Resources']['Reserved'] + incorrect_quota = dict(Requested=dict(), + Reserved=dict(),) + query_key_map = dict(cpu='cpu', + ram='ram', + disk='disksize', + ext_ips='extips', + net_transfer='exttraffic',) + if self.amodule.params['quotas']: + for quota_item in self.amodule.params['quotas']: + if self.amodule.params['quotas'][quota_item] < resources[query_key_map[quota_item]]: + incorrect_quota['Requested'][quota_item]=self.amodule.params['quotas'][quota_item] + incorrect_quota['Reserved'][quota_item]=resources[query_key_map[quota_item]] + + if incorrect_quota['Requested']: + self.result['msg'] = ("Cannot limit less than already reserved'{}'").format(incorrect_quota) + self.result['failed'] = True + + if self.result['failed'] != True: + self.rg_update(self.rg_facts, self.amodule.params['quotas'], + self.amodule.params['resType'], self.amodule.params['rename']) + self.rg_should_exist = True + return + + def setDefNet(self): + if self.amodule.params['def_netId'] != self.rg_facts['def_net_id']: + self.rg_setDefNet(self.validated_rg_id, + self.amodule.params['def_netType'], + self.amodule.params['def_netId']) + self.rg_should_exist = True + return + + def create(self): + self.validated_rg_id = self.rg_provision(self.validated_acc_id, + self.amodule.params['rg_name'], + self.amodule.params['owner'], + self.amodule.params['annotation'], + self.amodule.params['resType'], + self.amodule.params['def_netType'], + self.amodule.params['ipcidr'], + self.amodule.params['extNetId'], + self.amodule.params['extNetIp'], + self.amodule.params['quotas'], + "", # this is location code. TODO: add module argument + ) + + self.validated_rg_id, self.rg_facts = self.rg_find(self.validated_acc_id, + self.validated_rg_id, + arg_rg_name="", + arg_check_state=False) + self.rg_should_exist = True + return + + def enable(self): + self.rg_enable(self.validated_rg_id, + self.amodule.params['state']) + if self.amodule.params['state'] == "enabled": + self.rg_facts['status'] = 'CREATED' + else: + self.rg_facts['status'] = 'DISABLED' + self.rg_should_exist = True + return + + def restore(self): + self.rg_restore(self.validated_rg_id) + self.rg_facts['status'] = 'DISABLED' + self.rg_should_exist = True + return + + def destroy(self): + + self.rg_delete(self.validated_rg_id, self.amodule.params['permanently']) + if self.amodule.params['permanently'] == True: + self.rg_facts['status'] = 'DESTROYED' + else: + self.rg_facts['status'] = 'DELETED' + self.rg_should_exist = False + return + + def package_facts(self, check_mode=False): + """Package a dictionary of RG facts according to the decort_rg module specification. This dictionary will + be returned to the upstream Ansible engine at the completion of the module run. + + @param arg_rg_facts: dictionary with RG facts as returned by API call to .../rg/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 check_mode: + # in check mode return immediately with the default values + return ret_dict + + #if arg_rg_facts is None: + # # if void facts provided - change state value to ABSENT and return + # ret_dict['state'] = "ABSENT" + # return ret_dict + + ret_dict['id'] = self.rg_facts['id'] + ret_dict['name'] = self.rg_facts['name'] + ret_dict['state'] = self.rg_facts['status'] + ret_dict['account_id'] = self.rg_facts['accountId'] + ret_dict['gid'] = self.rg_facts['gid'] + ret_dict['quota'] = self.rg_facts['resourceLimits'] + ret_dict['resTypes'] = self.rg_facts['resourceTypes'] + ret_dict['defNetId'] = self.rg_facts['def_net_id'] + ret_dict['defNetType'] = self.rg_facts['def_net_type'] - if arg_rg_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_rg_facts['id'] - ret_dict['name'] = arg_rg_facts['name'] - ret_dict['state'] = arg_rg_facts['status'] - ret_dict['account_id'] = arg_rg_facts['accountId'] - ret_dict['gid'] = arg_rg_facts['gid'] - - return ret_dict - -def decort_rg_parameters(): - """Build and return a dictionary of parameters expected by decort_rg 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', + def parameters(): + """Build and return a dictionary of parameters expected by decort_rg 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=''), + access=dict(type='dict'), + annotation=dict(type='str', required=False, default=''), + app_id=dict(type='str', required=False, - fallback=(env_fallback, ['DECORT_OAUTH2_URL'])), - password=dict(type='str', + 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=''), + def_netType=dict(type='str', choices=['PRIVATE','PUBLIC', 'NONE'], default='PRIVATE'), + def_netId=dict(type='int', default=0), + extNetId=dict(type='int', default=0), + extNetIp=dict(type='str', default=""), + owner=dict(type='str', default=""), + ipcidr=dict(type='str', 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'])), + rename=dict(type='str', default=""), + password=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_PASSWORD']), + no_log=True), + quotas=dict(type='dict', required=False), + resType=dict(type='list'), + state=dict(type='str', + default='present', + choices=['absent', 'disabled', 'enabled', 'present']), + permanently=dict(type='bool', + default='False'), + user=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']), - user=dict(type='str', - required=False, - fallback=(env_fallback, ['DECORT_USER'])), - rg_name=dict(type='str', required=True,), - verify_ssl=dict(type='bool', required=False, default=True), - workflow_callback=dict(type='str', required=False), - 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 RG with the specified id or rg_name:name exists -# 3) if RG does not exist -> deploy -# 4) if RG exists: check desired state, desired configuration -> initiate action accordingly -# 5) report result to Ansible + fallback=(env_fallback, ['DECORT_USER'])), + rg_name=dict(type='str', required=False,), + rg_id=dict(type='int', required=False, default=0), + verify_ssl=dict(type='bool', required=False, default=True), + workflow_callback=dict(type='str', required=False), + 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 RG with the specified id or rg_name:name exists + # 3) if RG does not exist -> deploy + # 4) if RG exists: check desired state, desired configuration -> initiate action accordingly + # 5) report result to Ansible def main(): - module_parameters = decort_rg_parameters() + module_parameters = decort_rg.parameters() amodule = AnsibleModule(argument_spec=module_parameters, supports_check_mode=True, @@ -307,157 +470,52 @@ def main(): ], ) - decon = DecortController(amodule) - - # We need valid Account ID to manage RG. - # Account may be specified either by account_id or account_name. In both cases we - # have to validate account presence and accesibility by the current user. - validated_acc_id = 0 - if decon.check_amodule_argument('account_id', False): - validated_acc_id, _ = decon.account_find("", amodule.params['account_id']) - else: - decon.check_amodule_argument('account_name') # if no account_name, this function will abort module - validated_acc_id, _ = decon.account_find(amodule.params['account_name']) - - if not validated_acc_id: - # we failed to locate account by either name or ID - abort with an error - decon.result['failed'] = True - decon.result['msg'] = ("Current user does not have access to the requested account " - "or non-existent account specified.") - decon.fail_json(**decon.result) - - # Check if the RG with the specified parameters already exists - rg_id, rg_facts = decon.rg_find(validated_acc_id, - 0, arg_rg_name=amodule.params['rg_name'], - arg_check_state=False) - rg_should_exist = True - - if rg_id: - if rg_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]: - # error: nothing can be done to existing RG in the listed statii regardless of - # the requested state - decon.result['failed'] = True - decon.result['changed'] = False - decon.result['msg'] = ("No change can be done for existing RG ID {} because of its current " - "status '{}'").format(rg_id, rg_facts['status']) - elif rg_facts['status'] == "DISABLED": + decon = decort_rg(amodule) + #amodule.check_mode=True + if decon.validated_rg_id > 0: + if decon.rg_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING", "CONFIRMED"]: + decon.error() + elif decon.rg_facts['status'] in ("CREATED"): if amodule.params['state'] == 'absent': - decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True) - rg_facts['status'] = 'DESTROYED' - rg_should_exist = False - elif amodule.params['state'] in ('present', 'disabled'): - # update quotas - decon.rg_quotas(rg_facts, amodule.params['quotas']) - elif amodule.params['state'] == 'enabled': - # update quotas and enable - decon.rg_quotas(rg_facts, amodule.params['quotas']) - decon.rg_state(rg_facts, 'enabled') - elif rg_facts['status'] == "CREATED": - if amodule.params['state'] == 'absent': - decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True) - rg_facts['status'] = 'DESTROYED' - rg_should_exist = False - elif amodule.params['state'] in ('present', 'enabled'): - # update quotas - decon.rg_quotas(rg_facts, amodule.params['quotas']) - elif amodule.params['state'] == 'disabled': - # disable and update quotas - decon.rg_state(rg_facts, 'disabled') - decon.rg_quotas(rg_facts, amodule.params['quotas']) - elif rg_facts['status'] == "DELETED": + decon.destroy() + elif amodule.params['state'] == "disabled": + decon.enable() if amodule.params['state'] in ['present', 'enabled']: - # restore and enable - # TODO: check if restore RG API returns the new RG ID of the restored RG instance. - decon.rg_restore(arg_rg_id=rg_id) - decon.rg_state(rg_facts, 'enabled') - # TODO: Not sure what to do with the quotas after RG is restored. May need to update rg_facts. - rg_should_exist = True - elif amodule.params['state'] == 'absent': - # destroy permanently - decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True) - rg_facts['status'] = 'DESTROYED' - rg_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 RG ID {} in the " - "current status '{}'").format(rg_id, - amodule.params['state'], - rg_facts['status']) - rg_should_exist = False - elif rg_facts['status'] == "DESTROYED": - if amodule.params['state'] in ('present', 'enabled'): - # need to re-provision RG - decon.check_amodule_argument('rg_name') - # As we already have validated account ID we can create RG and get rg_id on success - # pass empty string for location code, rg_provision will select the 1st location - rg_id = decon.rg_provision(validated_acc_id, - amodule.params['rg_name'], decon.decort_username, - amodule.params['quotas'], - "", # this is location code. TODO: add module argument - amodule.params['annotation']) - rg_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 RG ID {} because of its " - "current status '{}'").format(rg_id, - rg_facts['status']) - rg_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 RG ID {} in the " - "current status '{}'").format(rg_id, - amodule.params['state'], - rg_facts['status']) + if amodule.params['quotas'] or amodule.params['resType'] or amodule.params['rename'] != "": + decon.update() + if amodule.params['access']: + decon.access() + if amodule.params['def_netId'] > 0: + decon.setDefNet() + + elif decon.rg_facts['status'] == "DELETED": + if amodule.params['state'] == 'absent' and amodule.params['permanently'] == True: + decon.destroy() + elif amodule.params['state'] == 'present': + decon.restore() + elif decon.rg_facts['status'] in ("DISABLED"): + if amodule.params['state'] == 'absent': + decon.destroy() + elif amodule.params['state'] == ("enabled"): + decon.enable() + else: - # Preexisting RG was not found. - rg_should_exist = False # we will change it back to True if RG is explicitly created or restored - # If requested state is 'absent' - nothing to do - 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 RG name '{}'").format(amodule.params['rg_name']) - elif amodule.params['state'] in ('present', 'enabled'): - # Target RG does not exist yet - create it and store the returned ID in rg_id variable for later use - # To create RG we need account name (or account ID) and RG name - check - # that these parameters are present and proceed. - decon.check_amodule_argument('rg_name') - # as we already have account ID we can create RG and get rg_id on success - # pass empty string for location code, rg_provision will select the 1st location - rg_id = decon.rg_provision(validated_acc_id, - amodule.params['rg_name'], decon.decort_username, - amodule.params['quotas'], - "", # this is location code. TODO: add module argument - amodule.params['annotation']) - rg_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 " - "RG name '{}' ").format(amodule.params['state'], - amodule.params['rg_name']) - # - # conditional switch end - complete module run + if amodule.params['state'] in ('present', 'enabled'): + decon.create() + if amodule.params['access']: + decon.access() + elif amodule.params['state'] in ('disabled'): + decon.error() + + if decon.result['failed']: amodule.fail_json(**decon.result) else: - # prepare RG facts to be returned as part of decon.result and then call exit_json(...) - # rg_facts = None - if rg_should_exist: - if decon.result['changed']: - # If we arrive here, there is a good chance that the RG is present - get fresh RG facts from - # the cloud by RG ID. - # Otherwise, RG facts from previous call (when the RG was still in existence) will be returned. - _, rg_facts = decon.rg_find(arg_account_id=0, arg_rg_id=rg_id) - decon.result['facts'] = decort_rg_package_facts(rg_facts, amodule.check_mode) - amodule.exit_json(**decon.result) - + if decon.rg_should_exist: + decon.result['facts'] = decon.package_facts(amodule.check_mode) + amodule.exit_json(**decon.result) + else: + amodule.exit_json(**decon.result) if __name__ == "__main__": main() diff --git a/module_utils/decort_utils.py b/module_utils/decort_utils.py index db5613a..044a281 100644 --- a/module_utils/decort_utils.py +++ b/module_utils/decort_utils.py @@ -1442,7 +1442,7 @@ class DecortController(object): ################################### # Resource Group (RG) manipulation methods ################################### - def rg_delete(self, rg_id, permanently=False): + def rg_delete(self, rg_id, permanently): """Deletes specified VDC. @param (int) rg_id: integer value that identifies the RG to be deleted. @@ -1500,7 +1500,27 @@ class DecortController(object): return ret_rg_id, ret_rg_dict - def rg_find(self, arg_account_id, arg_rg_id=0, arg_rg_name="", arg_check_state=True): + def rg_access(self, arg_rg_id, arg_access): + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_access") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("rg_access() in check mode: access to RG id '{}' was " + "requested with '{}'.").format(arg_rg_id, arg_access) + return 0 + + api_params=dict(rgId=arg_rg_id,) + if arg_access['action'] == "grant": + api_params['user']=arg_access['user'], + api_params['right']=arg_access['right'], + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/accessGrant", api_params) + else: + api_params['user']=arg_access['user'], + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/accessRevoke", api_params) + self.result['changed'] = True + return + + def rg_find(self, arg_account_id, arg_rg_id, arg_rg_name="", arg_check_state=True): """Returns non zero RG ID and a dictionary with RG details on success, 0 and empty dictionary otherwise. This method does not fail the run if RG cannot be located by its name (arg_rg_name), because this could be an indicator of the requested RG never existed before. @@ -1529,7 +1549,7 @@ class DecortController(object): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_find") - ret_rg_id = 0 + ret_rg_id = arg_rg_id api_params = dict() ret_rg_dict = None @@ -1574,7 +1594,44 @@ 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=""): + def rg_setDefNet(self, arg_rg_id, arg_net_type, arg_net_id): + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_setDefNet") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("rg_setDefNet() in check mode: setDefNet RG id '{}' was " + "requested.").format(arg_rg_id) + return 0 + + if arg_net_type == "NONE": + arg_net_type = "PRIVATE" + api_params = dict(rgId=arg_rg_id, + netType=arg_net_type, + netId=arg_net_id,) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/setDefNet", api_params) + self.result['changed'] = True + return + + + def rg_enable(self, arg_rg_id, arg_state): + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_enable") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("rg_enable() in check mode: '{}' RG id '{}' was " + "requested.").format(arg_state, arg_rg_id) + return 0 + + api_params = dict(rgId=arg_rg_id) + + if arg_state == "enabled": + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/enable", api_params) + else: + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/disable", api_params) + self.result['changed'] = True + return + + def rg_provision(self, arg_account_id, arg_rg_name, arg_username, arg_desc, restype, arg_net_type, ipcidr, arg_extNetId, arg_extIp, arg_quota={}, location=""): """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 and relay error to Ansible. @@ -1587,7 +1644,7 @@ class DecortController(object): access to the newly created RG. @param arg_quota: dictionary that defines quotas to set on the RG to be created. Valid keys are: cpu, ram, disk and ext_ips. - @param (string) arg_location: location code, which identifies the location where RG will be created. If + @param (string) location: location code, which identifies the location where RG will be created. If empty string is passed, the first location under current DECORT controller will be selected. @param (string) arg_desc: optional text description of this resource group. @@ -1602,20 +1659,23 @@ class DecortController(object): "requested.").format(arg_rg_name) return 0 - target_gid = self.gid_get(arg_location) + target_gid = self.gid_get(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) + location) self.amodule.fail_json(**self.result) api_params = dict(accountId=arg_account_id, gid=target_gid, name=arg_rg_name, owner=arg_username, - def_net="NONE", + def_net=arg_net_type, + extNetId=arg_extNetId, + extIp=arg_extIp, # maxMemoryCapacity=-1, maxVDiskCapacity=-1, # maxCPUCapacity=-1, maxNumPublicIP=-1, + # maxNetworkPeerTransfer=-1, ) if arg_quota: if 'ram' in arg_quota: @@ -1626,9 +1686,20 @@ class DecortController(object): api_params['maxCPUCapacity'] = arg_quota['cpu'] if 'ext_ips' in arg_quota: api_params['maxNumPublicIP'] = arg_quota['ext_ips'] + if 'net_transfer' in arg_quota: + api_params['maxNetworkPeerTransfer'] = arg_quota['net_transfer'] + + if restype: + api_params['resourceTypes'] = restype if arg_desc: api_params['desc'] = arg_desc + + api_params['def_net'] = arg_net_type + if arg_net_type == "PRIVATE" and ipcidr !="": + api_params['ipcidr'] = ipcidr + + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/create", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. @@ -1639,7 +1710,8 @@ class DecortController(object): return ret_rg_id # TODO: this method will not work in its current implementation. Update it for new .../rg/update specs. - def rg_quotas(self, arg_rg_dict, arg_quotas): + + def rg_update(self, arg_rg_dict, arg_quotas, arg_res_types, arg_newname): """Manage quotas for an existing RG. @param arg_rg_dict: dictionary with RG facts as returned by rg_find(...) method or .../rg/get API @@ -1653,46 +1725,59 @@ class DecortController(object): # TODO: this method may need update since we introduced GPU functionality and corresponding GPU quota management. # - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_quotas") + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_update") if self.amodule.check_mode: self.result['failed'] = False - self.result['msg'] = ("rg_quotas() in check mode: setting quotas on RG ID {}, RG name '{}' was " + self.result['msg'] = ("rg_update() in check mode: setting quotas on RG ID {}, RG name '{}' was " "requested.").format(arg_rg_dict['id'], arg_rg_dict['name']) return + update_required = False + api_params = dict(rgId=arg_rg_dict['id'],) + + if arg_res_types: + if arg_rg_dict['resourceTypes'] != arg_res_types: + api_params['resourceTypes'] = arg_res_types + update_required = True + else: + api_params['resourceTypes'] = arg_rg_dict['resourceTypes'] + + if arg_newname != "" and arg_newname!=arg_rg_dict['name']: + api_params['name'] = arg_newname + update_required = True + # One more inconsistency in API keys: # - when setting resource limits, the keys are in the form 'max{{ RESOURCE_NAME }}Capacity' # - when quering resource limits, the keys are in the form of cloud units (CU_*) query_key_map = dict(cpu='CU_C', ram='CU_M', disk='CU_D', - ext_ips='CU_I', ) + ext_ips='CU_I', + net_transfer='CU_NP',) set_key_map = dict(cpu='maxCPUCapacity', ram='maxMemoryCapacity', disk='maxVDiskCapacity', - ext_ips='maxNumPublicIP', ) - api_params = dict(rgId=arg_rg_dict['id'], ) - quota_change_required = False + ext_ips='maxNumPublicIP', + net_transfer='maxNetworkPeerTransfer',) - for new_limit in ('cpu', 'ram', 'disk', 'ext_ips'): + for new_limit in ('cpu', 'ram', 'disk', 'ext_ips', 'net_transfer'): if arg_quotas: if new_limit in arg_quotas: # If this resource type limit is found in the desired quotas, check if the desired setting is # different from the current settings of VDC. If it is different, set the new one. if arg_quotas[new_limit] != arg_rg_dict['resourceLimits'][query_key_map[new_limit]]: api_params[set_key_map[new_limit]] = arg_quotas[new_limit] - quota_change_required = True + update_required = True + elif arg_quotas[new_limit] == arg_rg_dict['resourceLimits'][query_key_map[new_limit]]: + api_params[set_key_map[new_limit]] = arg_quotas[new_limit] else: - # This resource type limit not found in the desired quotas. It means that no limit for this - # resource type - reset VDC limit for this resource type regardless of the current VDC settings. - api_params[set_key_map[new_limit]] = -1 - # quota_change_required = True + api_params[set_key_map[new_limit]] = arg_rg_dict['resourceLimits'][query_key_map[new_limit]] else: # if quotas dictionary is None, it means that no quotas should be set - reset the limits - api_params[set_key_map[new_limit]] = -1 + api_params[set_key_map[new_limit]] = arg_rg_dict['resourceLimits'][query_key_map[new_limit]] - if quota_change_required: + if update_required: self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/update", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -1722,66 +1807,6 @@ class DecortController(object): self.result['changed'] = True return - def rg_state(self, arg_rg_dict, arg_desired_state): - """Enable or disable RG. - - @param arg_rg_dict: dictionary with the target RG facts as returned by rg_find(...) method or - .../rg/get API call. - @param arg_desired_state: the desired state for this RG. Valid states are 'enabled' and 'disabled'. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_state") - - 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: - self.result['failed'] = False - self.result['msg'] = ("rg_state(): no state change possible for RG ID {} " - "in its current state '{}'.").format(arg_rg_dict['id'], arg_rg_dict['status']) - return - - if arg_desired_state not in VALID_TARGET_STATES: - 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']) - return - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("rg_state() in check mode: setting state of RG ID {}, name '{}' to " - "'{}' was requested.").format(arg_rg_dict['id'], arg_rg_dict['name'], - arg_desired_state) - return - - rgstate_api = "" # this string will also be used as a flag to indicate that API call is necessary - api_params = dict(rgId=arg_rg_dict['id'], - reason='Changed by DECORT Ansible module, rg_state method.') - expected_state = "" - - if arg_rg_dict['status'] in ["CREATED", "ENABLED"] and arg_desired_state == 'disabled': - rgstate_api = "/restmachine/cloudapi/rg/disable" - expected_state = "DISABLED" - elif arg_rg_dict['status'] == "DISABLED" and arg_desired_state == 'enabled': - rgstate_api = "/restmachine/cloudapi/rg/enable" - expected_state = "ENABLED" - - if rgstate_api != "": - self.decort_api_call(requests.post, rgstate_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_rg_dict['status'] = expected_state - else: - self.result['failed'] = False - self.result['msg'] = ("rg_state(): no state change required for RG ID {} from current " - "state '{}' to desired state '{}'.").format(arg_rg_dict['id'], - arg_rg_dict['status'], - arg_desired_state) - return - def account_find(self, account_name, account_id=0): """Find cloud account specified by the name and return facts about the account. Knowing account is required for certain cloud resource management tasks (e.g. creating new RG). @@ -1971,10 +1996,13 @@ 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: locations = json.loads(api_resp.content.decode('utf8')) if location_code == "" and locations: - ret_gid = locations['gid'] + ret_gid = locations[0]['gid'] else: for runner in locations: if runner['locationCode'] == location_code: