diff --git a/library/decort_disk.py b/library/decort_disk.py index e13cff3..d8b1749 100644 --- a/library/decort_disk.py +++ b/library/decort_disk.py @@ -241,7 +241,7 @@ from ansible.module_utils.decort_utils import * def decort_disk_package_facts(disk_facts, check_mode=False): - """Package a dictionary of disk facts according to the decort_vins module specification. + """Package a dictionary of disk facts according to the decort_disk module specification. This dictionary will be returned to the upstream Ansible engine at the completion of the module run. @@ -329,13 +329,6 @@ def decort_disk_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 -# 3) if ViNS does not exist -> deploy -# 4) if ViNS exists: check desired state, desired configuration -> initiate action(s) accordingly -# 5) report result to Ansible - def main(): module_parameters = decort_disk_parameters() diff --git a/library/decort_kvmvm.py b/library/decort_kvmvm.py index 86694c1..56856e0 100644 --- a/library/decort_kvmvm.py +++ b/library/decort_kvmvm.py @@ -107,7 +107,7 @@ options: required: no id: description: - - ID of the VM. + - ID of the KVM VM to manage. - 'Either I(id) or a combination of VM name I(name) and RG related parameters (either I(rg_id) or a pair of I(account_name) and I(rg_name) is required to manage an existing VM.' - 'This parameter is not required (and ignored) when creating new VM as VM ID is assigned by cloud platform diff --git a/library/decort_pfw.py b/library/decort_pfw.py new file mode 100644 index 0000000..97d3133 --- /dev/null +++ b/library/decort_pfw.py @@ -0,0 +1,322 @@ +#!/usr/bin/python +# +# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible +# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC +# +# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt) +# + +# +# Author: Sergey Shubin (sergey.shubin@digitalenergy.online) +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: decort_disk +short_description: Manage network Port Forward rules for Compute instances in DECORT cloud +description: > + This module can be used to create new port forwarding rules in DECORT cloud platform, + modify and delete them. +version_added: "2.2" +author: + - Sergey Shubin +requirements: + - python >= 2.6 + - PyJWT module + - requests module + - decort_utils utility library (module) + - DECORT cloud platform version 3.4.1 or higher +notes: + - Environment variables can be used to pass selected parameters to the module, see details below. + - Specified Oauth2 provider must be trusted by the DECORT cloud controller on which JWT will be used. + - 'Similarly, JWT supplied in I(authenticator=jwt) mode should be received from Oauth2 provider trusted by + the DECORT cloud controller on which this JWT will be used.' +options: + account_id: + description: + - ID of the account, which owns this disk. This is the alternative to I(account_name) option. + - If both I(account_id) and I(account_name) specified, then I(account_name) is ignored. + default: 0 + required: no + account_name: + description: + - 'Name of the account, which will own this disk.' + - 'This parameter is ignored if I(account_id) is specified.' + default: empty string + required: no + app_id: + description: + - 'Application ID for authenticating to the DECORT controller when I(authenticator=oauth2).' + - 'Required if I(authenticator=oauth2).' + - 'If not found in the playbook or command line arguments, the value will be taken from DECORT_APP_ID + environment variable.' + required: no + app_secret: + description: + - 'Application API secret used for authenticating to the DECORT controller when I(authenticator=oauth2).' + - This parameter is required when I(authenticator=oauth2) and ignored in other modes. + - 'If not found in the playbook or command line arguments, the value will be taken from DECORT_APP_SECRET + environment variable.' + required: no + authenticator: + description: + - Authentication mechanism to be used when accessing DECORT controller and authorizing API call. + default: jwt + choices: [ jwt, oauth2, legacy ] + required: yes + controller_url: + description: + - URL of the DECORT controller that will be contacted to manage the RG according to the specification. + - 'This parameter is always required regardless of the specified I(authenticator) type.' + required: yes + compute_id: + description: + - ID of the Compute instance to manage network port forwarding rules for. + required: yes + jwt: + description: + - 'JWT (access token) for authenticating to the DECORT controller when I(authenticator=jwt).' + - 'This parameter is required if I(authenticator=jwt) and ignored for other authentication modes.' + - If not specified in the playbook, the value will be taken from DECORT_JWT environment variable. + required: no + oauth2_url: + description: + - 'URL of the oauth2 authentication provider to use when I(authenticator=oauth2).' + - 'This parameter is required when when I(authenticator=oauth2).' + - 'If not specified in the playbook, the value will be taken from DECORT_OAUTH2_URL environment variable.' + password: + description: + - 'Password for authenticating to the DECORT controller when I(authenticator=legacy).' + - 'This parameter is required if I(authenticator=legacy) and ignored in other authentication modes.' + - If not specified in the playbook, the value will be taken from DECORT_PASSWORD environment variable. + required: no + rules: + description: + - 'Set of rules to configure for the Compute instance identidied by I(compute_id) in the virtual + network segment identidied by I(vins_id).' + - The set is specified as a list of dictionaries with the following structure: + - ' - (int) public_port_start - starting port number on the ViNS external interface.' + - ' - (int) public_port_end - optional end port number of the ViNS external interface. If not specified + or set equal to I(public_port_start), a one-to-one rule is created. Otherwise a ranged rule will + be created, which maps specified external port range to local ports starting from I(local_port).' + - ' - (int) local_port - port number on the local interface of the Compute. For ranged rule it is + interpreted as a base port to translate public port range to internal port range.' + - ' - (string) porot - protocol, specify either I(tcp) or I(udp).' + state: + description: + - 'Specify the desired state of the port forwarding rules set for the Compute instance identified by + I(compute_id).' + - 'If I(state=present), the rules will be applied according to the I(rules) parameter.' + - 'If I(state=absent), all rules for the specified Compute instance will be deleted regardless of + I(rules) parameter.' + default: present + choices: [ absent, present ] + verify_ssl: + description: + - 'Controls SSL verification mode when making API calls to DECORT controller. Set it to False if you + want to disable SSL certificate verification. Intended use case is when you run module in a trusted + environment that uses self-signed certificates. Note that disabling SSL verification in any other + scenario can lead to security issues, so please know what you are doing.' + default: True + required: no + vins_id: + description: + - ID of the virtual network segment (ViNS), where port forwarding rules will be set up. + - This ViNS must have connection to external network. + - Compute instance specified by I(compute_id) must be connected to this ViNS. + workflow_callback: + description: + - 'Callback URL that represents an application, which invokes this module (e.g. up-level orchestrator or + end-user portal) and may except out-of-band updates on progress / exit status of the module run.' + - API call at this URL will be used to relay such information to the application. + - 'API call payload will include module-specific details about this module run and I(workflow_context).' + required: no + workflow_context: + description: + - 'Context data that will be included into the payload of the API call directed at I(workflow_callback) URL.' + - 'This context data is expected to uniquely identify the task carried out by this module invocation so + that up-level orchestrator could match returned information to the its internal entities.' + required: no +''' + +EXAMPLES = ''' +- name: configure one-toone rule for SSH protocol on Compute ID 100 connected to ViNS ID 5. + decort_pfw: + authenticator: oauth2 + app_id: "{{ MY_APP_ID }}" + app_secret: "{{ MY_APP_SECRET }}" + controller_url: "https://cloud.digitalenergy.online" + compute_id: 100 + vins_id: 5 + rules: + - public_port_start: 10022 + local_port: 22 + proto: tcp + state: present + delegate_to: localhost + register: my_pfw +''' + +RETURN = ''' +facts: + description: facts about created PFW rules + returned: always + type: dict + sample: + facts: + compute_id: 100 + vins_id: 5 + rules: + - +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback + +from ansible.module_utils.decort_utils import * + + +def decort_pfw_package_facts(pfw_facts, check_mode=False): + """Package a dictionary of PFW rules facts according to the decort_pfw module specification. + This dictionary will be returned to the upstream Ansible engine at the completion of + the module run. + + @param (dict) pfw_facts: dictionary with PFW facts as returned by API call to .../???/get + @param (bool) check_mode: boolean that tells if this Ansible module is run in check mode + """ + + ret_dict = dict(id=0, + name="none", + state="CHECK_MODE", + comp_id=0, + vins_id=0, + ) + + if check_mode: + # in check mode return immediately with the default values + return ret_dict + + if pfw_facts is None: + # if void facts provided - change state value to ABSENT and return + ret_dict['state'] = "ABSENT" + return ret_dict + + ret_dict['comp_id'] = pfw_facts['com_id'] + + return ret_dict + +def decort_pfw_parameters(): + """Build and return a dictionary of parameters expected by decort_pfw module in a form accepted + by AnsibleModule utility class.""" + + return dict( + 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']), + compute_id=dict(type='int', required=True), + controller_url=dict(type='str', required=True), + 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), + rules=dict(type='list', required=False, default=[]), + state=dict(type='str', + default='present', + choices=['absent', 'present']), + user=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_USER'])), + verify_ssl=dict(type='bool', required=False, default=True), + vins_id=dict(type='int', required=True), + workflow_callback=dict(type='str', required=False), + workflow_context=dict(type='str', required=False), + ) + +def main(): + module_parameters = decort_pfw_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) + + pfw_facts = None # will hold PFW facts + + # + # Validate module arguments: + # 1) specified Compute instance exists in correct state + # 2) specified ViNS exists + # 3) ViNS has GW function + # 4) Compute is connected to this ViNS + # + + validated_comp_id, comp_facts, rg_id = decon.compute_find(amodule.params['comp_id']) + if not validated_comp_id: + decon.result['failed'] = True + decon.result['msg'] = "Cannot find specified Compute ID {}.".format(amodule.params['comp_id']) + amodule.fail_json(**decon.result) + + validated_vins_id, vins_facts = decon.vins_find(amodule.params['vins_id']) + if not validated_vins_id: + decon.result['failed'] = True + decon.result['msg'] = "Cannot find specified ViNS ID {}.".format(amodule.params['vins_id']) + amodule.fail_json(**decon.result) + + gw_vnf_facts = vins_facts['vnfs'].get('GW') + if not gw_vnf_facts or gw_vnf_facts['status'] == "DESTROYED": + decon.result['failed'] = True + decon.result['msg'] = "ViNS ID {} does not have a configured external connection.".format(validated_vins_id) + amodule.fail_json(**decon.result) + + # + # Initial validation of module arguments is complete + # + + if amodule.params['state'] == 'absent': + # ignore amodule.params['rules'] and remove all rules associated with this Compute + pfw_facts = decon.pfw_configure(comp_facts, vins_facts, None) + else: + # manage PFW rules accodring to the module arguments + pfw_facts = decon.pfw_configure(comp_facts, vins_facts, amodule.params['rules']) + + # + # complete module run + # + if decon.result['failed']: + amodule.fail_json(**decon.result) + else: + # prepare PFW facts to be returned as part of decon.result and then call exit_json(...) + decon.result['facts'] = decort_pfw_package_facts(pfw_facts, amodule.check_mode) + amodule.exit_json(**decon.result) + + +if __name__ == "__main__": + main() diff --git a/module_utils/decort_utils.py b/module_utils/decort_utils.py index 20f67af..ffab63b 100644 --- a/module_utils/decort_utils.py +++ b/module_utils/decort_utils.py @@ -645,157 +645,6 @@ class DecortController(object): return ret_comp_id, ret_comp_dict, validated_rg_id - def vm_portforwards(self, arg_vm_dict, arg_pfw_specs): - """Manage VM port forwarding rules in a smart way. This method takes desired port forwarding rules as - an argument and compares it with the existing port forwarding rules - - @param arg_vm_dict: dictionary with VM facts. It identifies the VM for which network configuration is - requested. - @param arg_pfw_specs: desired network specifications. - """ - - # - # - # Strategy for port forwards management: - # 1) obtain current port forwarding rules for the target VM - # 2) create a delta list of port forwards (rules to add and rules to remove) - # - full match between existing & requested = ignore, no update of pfw_delta - # - existing rule not present in requested list => copy to pfw_delta and mark as 'delete' - # - requested rule not present in the existing list => copy to pfw_delta and mark as 'create' - # 3) provision delta list (first delete rules marked for deletion, next add rules mark for creation) - # - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vm_portforwards") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['changed'] = False - self.result['msg'] = "vm_portforwards() in check mode: port forwards configuration change requested." - return - - pfw_api_base = "/restmachine/cloudapi/portforwarding/" - pfw_api_params = dict(rgId=arg_vm_dict['rgId'], - machineId=arg_vm_dict['id']) - api_resp = self.decort_api_call(requests.post, pfw_api_base + "list", pfw_api_params) - existing_pfw_list = json.loads(api_resp.content.decode('utf8')) - - if not len(arg_pfw_specs) and not len(existing_pfw_list): - # Desired & existing port forwarding rules both empty - exit - self.result['failed'] = False - self.result['msg'] = ("vm_portforwards(): new and existing port forwarding lists both are empty - " - "nothing to do. No change applied to VM ID {}.").format(arg_vm_dict['id']) - return - - # pfw_delta_list will be a list of dictionaries that describe _changes_ to the port forwarding rules - # that existed for the target VM at the moment we entered this method. - # The dictionary has the following keys: - # ext_port - integer, external port number - # int_port - integer, internal port number - # proto - string, either 'tcp' or 'udp' - # action - string, either 'delete' or 'create' - # id - the ID of existing port forwarding rule that should be deleted (applicable when action='delete') - # NOTE: not all keys may exist in the resulting list! - pfw_delta_list = [] - - # Mark all requested pfw rules as new - if we find a match later, we will mark corresponding rule - # as 'new'=False - for requested_pfw in arg_pfw_specs: - requested_pfw['new'] = True - - for existing_pfw in existing_pfw_list: - existing_pfw['matched'] = False - for requested_pfw in arg_pfw_specs: - # TODO: portforwarding API needs refactoring. - # NOTE!!! Another glitch in the API implementation - .../portforwarding/list returns port numbers as strings, - # while .../portforwarding/create expects them as integers!!! - # Also: added type casting to int for requested_pfw in case the value comes as string from a complex - # variable in a loop - if (int(existing_pfw['publicPort']) == int(requested_pfw['ext_port']) and - int(existing_pfw['localPort']) == int(requested_pfw['int_port']) and - existing_pfw['protocol'] == requested_pfw['proto']): - # full match - existing rule stays as is: - # mark requested rule spec as 'new'=False, existing rule spec as 'macthed'=True - requested_pfw['new'] = False - existing_pfw['matched'] = True - - # Scan arg_pfw_specs, find all records that have been marked 'new'=True, then copy them the pfw_delta_list - # marking as action='create' - for requested_pfw in arg_pfw_specs: - if requested_pfw['new']: - pfw_delta = dict(ext_port=requested_pfw['ext_port'], - int_port=requested_pfw['int_port'], - proto=requested_pfw['proto'], - action='create') - pfw_delta_list.append(pfw_delta) - - # Scan existing_pfw_list, find all records that have 'matched'=False, then copy them to pfw_delta_list - # marking as action='delete' - for existing_pfw in existing_pfw_list: - if not existing_pfw['matched']: - pfw_delta = dict(ext_port=int(existing_pfw['publicPort']), - int_port=int(existing_pfw['localPort']), - proto=existing_pfw['protocol'], - action='delete') - pfw_delta_list.append(pfw_delta) - - if not len(pfw_delta_list): - # nothing to do - self.result['failed'] = False - self.result['msg'] = ("vm_portforwards() no difference between current and requested port " - "forwarding rules found. No change applied to VM ID {}.").format(arg_vm_dict['id']) - return - - # Need VDC facts to extract VDC external IP - it is needed to create new port forwarding rules - # Note that in a scenario when VM and VDC are created in the same task we may arrive to here - # when VDC is still in DEPLOYING state. Attempt to configure port forward rules in this will generate - # an error. So we have to check VDC status and loop for max ~60 seconds here so that the newly VDC - # created enters DEPLOYED state - max_retries = 5 - retry_counter = max_retries - while retry_counter > 0: - _, vdc_facts = self.vdc_find(arg_rg_id=arg_vm_dict['rgId']) - if vdc_facts['status'] == "DEPLOYED": - break - retry_timeout = 5 + 10 * (max_retries - retry_counter) - time.sleep(retry_timeout) - retry_counter = retry_counter - 1 - - if vdc_facts['status'] != "DEPLOYED": - # We still cannot manage port forwards due to incompatible VDC state. This is not necessarily an - # error that should lead to the task failure, so we register this fact in the module message and - # return from the method. - # - # self.result['failed'] = True - self.result['msg'] = ("vm_portforwards(): target VDC ID {} is still in '{}' state, " - "setting port forwarding rules is not possible.").format(arg_vm_dict['rgId'], - vdc_facts['status']) - return - - # Iterate over pfw_delta_list and first delete port forwarding rules marked for deletion, - # next create the rules marked for creation. - sorted_pfw_delta_list = sorted(pfw_delta_list, key=lambda i: i['action'], reverse=True) - for pfw_delta in sorted_pfw_delta_list: - if pfw_delta['action'] == 'delete': - pfw_api_params = dict(rgId=arg_vm_dict['rgId'], - publicIp=vdc_facts['externalnetworkip'], - publicPort=pfw_delta['ext_port'], - proto=pfw_delta['proto']) - self.decort_api_call(requests.post, pfw_api_base + 'deleteByPort', pfw_api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - elif pfw_delta['action'] == 'create': - pfw_api_params = dict(rgId=arg_vm_dict['rgId'], - publicIp=vdc_facts['externalnetworkip'], - publicPort=pfw_delta['ext_port'], - machineId=arg_vm_dict['id'], - localPort=pfw_delta['int_port'], - protocol=pfw_delta['proto']) - self.decort_api_call(requests.post, pfw_api_base + 'create', pfw_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 compute_powerstate(self, comp_facts, target_state, force_change=True): """Manage Compute power state transitions or its guest OS restarts. @@ -2322,9 +2171,6 @@ class DecortController(object): return ret_disk_id, ret_disk_dict - # - # TODO: method is not fully implemented - need ../disks/list or ../disks/search API function! - # def disk_find(self, disk_id, disk_name="", account_id=0, check_state=False): """Find specified Disk. @@ -2510,3 +2356,158 @@ class DecortController(object): self.result['failed'] = False self.result['changed'] = True return + + + ############################## + # + # Port Forward rules management + # + ############################## + + def pfw_configure(self, comp_facts, vins_facts, new_rules=None): + """Manage port forwarding rules for Compute in a smart way. The method will try to match existing + rules against the new rules set and calculate the delta settings to apply to the corresponding + virtual network function. + + @param (dict) comp_facts: dictionary with Compute facts as returned by .../compute/get. It describes + the Compute instance for which PFW rules will be managed. + @param (dict) vins_facts: dictionary with ViNS facts as returned by .../vins/get. It described ViNS + to which PFW rules set will be applied. + @param (list of dicts) new_rules: new PFW rules set. If None is passed, remove all existing + PFW rules for the Compute. + """ + + # At the entry to this method we assume that initial validations are already passed, namely: + # 1) Compute instance exists + # 2) ViNS exists and has GW VNS in valid state + # 3) Compute is connected to this ViNS + + # + # + # Strategy for port forwards management: + # 1) obtain current port forwarding rules for the target VM + # 2) create a delta list of port forwards (rules to add and rules to remove) + # - full match between existing & requested = ignore, no update of pfw_delta + # - existing rule not present in requested list => copy to pfw_delta and mark as 'delete' + # - requested rule not present in the existing list => copy to pfw_delta and mark as 'create' + # 3) provision delta list (first delete rules marked for deletion, next add rules mark for creation) + # + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "pfw_configure") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("pfw_configure() in check mode: port forwards configuration requested " + "for Compute ID {} / ViNS ID {}").format(comp_facts['id'], vins_facts['id']) + return None + + 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']) + return None + + existing_rules = [] + for runner in vins_facts['vnfs']['NAT']['config']['rules']: + if runner['vmId'] == comp_facts['id']: + existing_rules.append(runner) + + if not len(existing_rules) and not len(new_rules): + self.result['failed'] = False + self.result['warning'] = ("pfw_configure(): both existing and new port forwarding rule lists " + "for Compute ID {} are empty - nothing to do.").format(comp_facts['id']) + return None + + if not len(new_rules): + # delete all existing rules for this Compute + api_params = dict(vinsId=vins_facts['id']) + for runner in existing_rules: + api_params['ruleId'] = runner['id'] + self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/natRuleDel", api_params) + self.result['failed'] = False + self.result['chnaged'] = True + return None + + # + # delta_list will be a list of dictionaries that describe _changes_ to the port forwarding rules + # of the Compute in hands. + # The dictionary has the following keys - values: + # (int) publicPortStart - external port range start + # (int) publicPortEnd - external port range end + # (int) localPort - internal port number + # (string) protocol - protocol, either 'tcp' or 'udp' + # (string) action - string, either 'del' or 'add' + # (int) id - the ID of existing PFW rule that should be deleted (applicable only for action='del') + # + delta_list = [] + # select from new_rules the rules to add - those not found in existing rules + for rule in new_rules: + rule['action'] = 'add' + rule_port_end = rule.get('public_port_end', 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']): + rule['action'] = 'keep' + break + if rule['action'] == 'add': + delta_rule = dict(publicPortStart=rule['public_port_start'], + publicPortEnd=rule_port_end, + localPort=rule['local_port'], + protocol=rule['proto'], + action='add', + id='-1') + delta_list.append(delta_rule) + + # select from existing_rules the rules to delete - those not found in new_rules + for rule in existing_rules: + rule['action'] = 'del' + 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['action'] = 'keep' + break + if rule['action'] == 'del': + delta_list.append(rule) + + if not len(delta_list): + # strange, but still nothing to do? + self.result['failed'] = False + self.result['warning'] = ("pfw_configure() no difference between current and new PFW rules " + "found. No change applied to Compute ID {}.").format(comp_facts['id']) + return + + # now delta_list contains a list of enriched rule dictionaries with extra key 'action', which + # tells what kind of action is expected on this rule - 'add' or 'del' + # We first iterate to delete, then iterate again to add rules + + # Iterate over pfw_delta_list and first delete port forwarding rules marked for deletion, + # next create the rules marked for creation. + api_base = "/restmachine/cloudapi/vins/" + for delta_rule in sorted(delta_list, key=lambda i: i['action'], reverse=True): + if delta_rule['action'] == 'del': + api_params = dict(vinsId=vins_facts['id'], + ruleId=delta_rule['id']) + self.decort_api_call(requests.post, api_base + 'natRuleDel', api_params) + # On success the above call will return here. On error it will abort execution by calling fail_json. + elif delta_rule['action'] == 'add': + api_params = dict(vinsId=vins_facts['id'], + intIp=iface_ipaddr, + intPort=delta_rule['localPort'], + extPortStart=delta_rule['publicPortStart'], + extPortEnd=delta_rule['publicPortEnd'], + proto=delta_rule['protocol']) + self.decort_api_call(requests.post, api_base + 'natRuleAdd', 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 \ No newline at end of file