diff --git a/changelogs/fragments/84-cs_instance_extend_ip_to_networks.yml b/changelogs/fragments/84-cs_instance_extend_ip_to_networks.yml new file mode 100644 index 0000000..9f453ac --- /dev/null +++ b/changelogs/fragments/84-cs_instance_extend_ip_to_networks.yml @@ -0,0 +1,2 @@ +minor_changes: +- cs_firewall - implemented support for ``dest_cidrs`` (https://github.com/ngine-io/ansible-collection-cloudstack/issues/76). diff --git a/plugins/modules/cs_firewall.py b/plugins/modules/cs_firewall.py index fda792a..e63dfe8 100644 --- a/plugins/modules/cs_firewall.py +++ b/plugins/modules/cs_firewall.py @@ -5,10 +5,11 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function + __metaclass__ = type -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: cs_firewall short_description: Manages firewall rules on Apache CloudStack based clouds. @@ -49,11 +50,17 @@ cidrs: description: - List of CIDRs (full notation) to be used for firewall rule. - - Since version 2.5, it is a list of CIDR. elements: str type: list default: 0.0.0.0/0 aliases: [ cidr ] + dest_cidrs: + description: + - List of destination CIDRs (full notation) to forward traffic to if I(type=egress). + elements: str + type: list + aliases: [ dest_cidr ] + version_added: 2.2.0 start_port: description: - Start port for this rule. @@ -106,9 +113,9 @@ aliases: [ tag ] extends_documentation_fragment: - ngine_io.cloudstack.cloudstack -''' +""" -EXAMPLES = ''' +EXAMPLES = """ - name: Allow inbound port 80/tcp from 1.2.3.4 to 4.3.2.1 ngine_io.cloudstack.cs_firewall: ip_address: 4.3.2.1 @@ -149,9 +156,9 @@ type: egress port: 80 cidr: 10.101.1.20 -''' +""" -RETURN = ''' +RETURN = """ --- id: description: UUID of the rule. @@ -178,6 +185,12 @@ returned: success type: list sample: [ '0.0.0.0/0' ] +dest_cidrs: + description: CIDR list of the rule to forward traffic to. + returned: success + type: list + sample: [ '0.0.0.0/0' ] + version_added: 2.2.0 protocol: description: Protocol of the rule. returned: success @@ -208,14 +221,11 @@ returned: success type: str sample: my_network -''' +""" from ansible.module_utils.basic import AnsibleModule -from ..module_utils.cloudstack import ( - AnsibleCloudStack, - cs_argument_spec, - cs_required_together -) + +from ..module_utils.cloudstack import AnsibleCloudStack, cs_argument_spec, cs_required_together class AnsibleCloudStackFirewall(AnsibleCloudStack): @@ -223,69 +233,71 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): def __init__(self, module): super(AnsibleCloudStackFirewall, self).__init__(module) self.returns = { - 'cidrlist': 'cidr', - 'startport': 'start_port', - 'endport': 'end_port', - 'protocol': 'protocol', - 'ipaddress': 'ip_address', - 'icmpcode': 'icmp_code', - 'icmptype': 'icmp_type', + "cidrlist": "cidr", + "destcidrlist": "dest_cidrs", + "startport": "start_port", + "endport": "end_port", + "protocol": "protocol", + "ipaddress": "ip_address", + "icmpcode": "icmp_code", + "icmptype": "icmp_type", } self.firewall_rule = None self.network = None def get_firewall_rule(self): if not self.firewall_rule: - cidrs = self.module.params.get('cidrs') - protocol = self.module.params.get('protocol') - start_port = self.module.params.get('start_port') - end_port = self.get_or_fallback('end_port', 'start_port') - icmp_code = self.module.params.get('icmp_code') - icmp_type = self.module.params.get('icmp_type') - fw_type = self.module.params.get('type') - - if protocol in ['tcp', 'udp'] and not (start_port and end_port): + cidrs = self.module.params.get("cidrs") + dest_cidrs = self.module.params.get("destcidrs") + protocol = self.module.params.get("protocol") + start_port = self.module.params.get("start_port") + end_port = self.get_or_fallback("end_port", "start_port") + icmp_code = self.module.params.get("icmp_code") + icmp_type = self.module.params.get("icmp_type") + fw_type = self.module.params.get("type") + + if protocol in ["tcp", "udp"] and not (start_port and end_port): self.module.fail_json(msg="missing required argument for protocol '%s': start_port or end_port" % protocol) - if protocol == 'icmp' and not icmp_type: + if protocol == "icmp" and not icmp_type: self.module.fail_json(msg="missing required argument for protocol 'icmp': icmp_type") - if protocol == 'all' and fw_type != 'egress': + if protocol == "all" and fw_type != "egress": self.module.fail_json(msg="protocol 'all' could only be used for type 'egress'") args = { - 'account': self.get_account('name'), - 'domainid': self.get_domain('id'), - 'projectid': self.get_project('id'), - 'fetch_list': True, + "account": self.get_account("name"), + "domainid": self.get_domain("id"), + "projectid": self.get_project("id"), + "fetch_list": True, } - if fw_type == 'egress': - args['networkid'] = self.get_network(key='id') - if not args['networkid']: + if fw_type == "egress": + args["networkid"] = self.get_network(key="id") + if not args["networkid"]: self.module.fail_json(msg="missing required argument for type egress: network") # CloudStack 4.11 use the network cidr for 0.0.0.0/0 in egress # That is why we need to replace it. - network_cidr = self.get_network(key='cidr') - egress_cidrs = [network_cidr if cidr == '0.0.0.0/0' else cidr for cidr in cidrs] + network_cidr = self.get_network(key="cidr") + egress_cidrs = [network_cidr if cidr == "0.0.0.0/0" else cidr for cidr in cidrs] - firewall_rules = self.query_api('listEgressFirewallRules', **args) + firewall_rules = self.query_api("listEgressFirewallRules", **args) else: - args['ipaddressid'] = self.get_ip_address('id') - if not args['ipaddressid']: + args["ipaddressid"] = self.get_ip_address("id") + if not args["ipaddressid"]: self.module.fail_json(msg="missing required argument for type ingress: ip_address") egress_cidrs = None - firewall_rules = self.query_api('listFirewallRules', **args) + firewall_rules = self.query_api("listFirewallRules", **args) if firewall_rules: for rule in firewall_rules: - type_match = self._type_cidrs_match(rule, cidrs, egress_cidrs) + type_match = self._type_cidrs_match(rule, cidrs, egress_cidrs) and self._type_dest_cidrs_match(rule, dest_cidrs) protocol_match = ( - self._tcp_udp_match(rule, protocol, start_port, end_port) or - self._icmp_match(rule, protocol, icmp_code, icmp_type) or - self._egress_all_match(rule, protocol, fw_type) + self._tcp_udp_match(rule, protocol, start_port, end_port) + or self._icmp_match(rule, protocol, icmp_code, icmp_type) + or self._egress_all_match(rule, protocol, fw_type) ) if type_match and protocol_match: @@ -294,63 +306,56 @@ def get_firewall_rule(self): return self.firewall_rule def _tcp_udp_match(self, rule, protocol, start_port, end_port): - return ( - protocol in ['tcp', 'udp'] and - protocol == rule['protocol'] and - start_port == int(rule['startport']) and - end_port == int(rule['endport']) - ) + return protocol in ["tcp", "udp"] and protocol == rule["protocol"] and start_port == int(rule["startport"]) and end_port == int(rule["endport"]) def _egress_all_match(self, rule, protocol, fw_type): - return ( - protocol in ['all'] and - protocol == rule['protocol'] and - fw_type == 'egress' - ) + return protocol in ["all"] and protocol == rule["protocol"] and fw_type == "egress" def _icmp_match(self, rule, protocol, icmp_code, icmp_type): - return ( - protocol == 'icmp' and - protocol == rule['protocol'] and - icmp_code == rule['icmpcode'] and - icmp_type == rule['icmptype'] - ) + return protocol == "icmp" and protocol == rule["protocol"] and icmp_code == rule["icmpcode"] and icmp_type == rule["icmptype"] def _type_cidrs_match(self, rule, cidrs, egress_cidrs): if egress_cidrs is not None: - return ",".join(egress_cidrs) == rule['cidrlist'] or ",".join(cidrs) == rule['cidrlist'] + return ",".join(egress_cidrs) == rule["cidrlist"] or ",".join(cidrs) == rule["cidrlist"] + else: + return ",".join(cidrs) == rule["cidrlist"] + + def _type_dest_cidrs_match(self, rule, dest_cidrs): + if dest_cidrs is not None and "destcidrlist" in rule: + return set(dest_cidrs) == set(rule["destcidrlist"].split(",")) else: - return ",".join(cidrs) == rule['cidrlist'] + return True def create_firewall_rule(self): firewall_rule = self.get_firewall_rule() if not firewall_rule: - self.result['changed'] = True + self.result["changed"] = True args = { - 'cidrlist': self.module.params.get('cidrs'), - 'protocol': self.module.params.get('protocol'), - 'startport': self.module.params.get('start_port'), - 'endport': self.get_or_fallback('end_port', 'start_port'), - 'icmptype': self.module.params.get('icmp_type'), - 'icmpcode': self.module.params.get('icmp_code') + "cidrlist": self.module.params.get("cidrs"), + "destcidrlist": self.module.params.get("dest_cidrs"), + "protocol": self.module.params.get("protocol"), + "startport": self.module.params.get("start_port"), + "endport": self.get_or_fallback("end_port", "start_port"), + "icmptype": self.module.params.get("icmp_type"), + "icmpcode": self.module.params.get("icmp_code"), } - fw_type = self.module.params.get('type') + fw_type = self.module.params.get("type") if not self.module.check_mode: - if fw_type == 'egress': - args['networkid'] = self.get_network(key='id') - res = self.query_api('createEgressFirewallRule', **args) + if fw_type == "egress": + args["networkid"] = self.get_network(key="id") + res = self.query_api("createEgressFirewallRule", **args) else: - args['ipaddressid'] = self.get_ip_address('id') - res = self.query_api('createFirewallRule', **args) + args["ipaddressid"] = self.get_ip_address("id") + res = self.query_api("createFirewallRule", **args) - poll_async = self.module.params.get('poll_async') + poll_async = self.module.params.get("poll_async") if poll_async: - firewall_rule = self.poll_job(res, 'firewallrule') + firewall_rule = self.poll_job(res, "firewallrule") if firewall_rule: - firewall_rule = self.ensure_tags(resource=firewall_rule, resource_type='Firewallrule') + firewall_rule = self.ensure_tags(resource=firewall_rule, resource_type="Firewallrule") self.firewall_rule = firewall_rule return firewall_rule @@ -358,79 +363,80 @@ def create_firewall_rule(self): def remove_firewall_rule(self): firewall_rule = self.get_firewall_rule() if firewall_rule: - self.result['changed'] = True + self.result["changed"] = True - args = { - 'id': firewall_rule['id'] - } + args = {"id": firewall_rule["id"]} - fw_type = self.module.params.get('type') + fw_type = self.module.params.get("type") if not self.module.check_mode: - if fw_type == 'egress': - res = self.query_api('deleteEgressFirewallRule', **args) + if fw_type == "egress": + res = self.query_api("deleteEgressFirewallRule", **args) else: - res = self.query_api('deleteFirewallRule', **args) + res = self.query_api("deleteFirewallRule", **args) - poll_async = self.module.params.get('poll_async') + poll_async = self.module.params.get("poll_async") if poll_async: - self.poll_job(res, 'firewallrule') + self.poll_job(res, "firewallrule") return firewall_rule def get_result(self, firewall_rule): super(AnsibleCloudStackFirewall, self).get_result(firewall_rule) if firewall_rule: - self.result['type'] = self.module.params.get('type') - if self.result['type'] == 'egress': - self.result['network'] = self.get_network(key='displaytext') - if 'cidrlist' in firewall_rule: - self.result['cidrs'] = firewall_rule['cidrlist'].split(',') or [firewall_rule['cidrlist']] + self.result["type"] = self.module.params.get("type") + if self.result["type"] == "egress": + self.result["network"] = self.get_network(key="displaytext") + if "cidrlist" in firewall_rule: + self.result["cidrs"] = firewall_rule["cidrlist"].split(",") or [firewall_rule["cidrlist"]] return self.result def main(): argument_spec = cs_argument_spec() - argument_spec.update(dict( - ip_address=dict(), - network=dict(), - cidrs=dict(type='list', elements='str', default='0.0.0.0/0', aliases=['cidr']), - protocol=dict(choices=['tcp', 'udp', 'icmp', 'all'], default='tcp'), - type=dict(choices=['ingress', 'egress'], default='ingress'), - icmp_type=dict(type='int'), - icmp_code=dict(type='int'), - start_port=dict(type='int', aliases=['port']), - end_port=dict(type='int'), - state=dict(choices=['present', 'absent'], default='present'), - zone=dict(required=True), - domain=dict(), - account=dict(), - project=dict(), - poll_async=dict(type='bool', default=True), - tags=dict(type='list', elements='dict', aliases=['tag']), - )) + argument_spec.update( + dict( + ip_address=dict(), + network=dict(), + cidrs=dict(type="list", elements="str", default="0.0.0.0/0", aliases=["cidr"]), + dest_cidrs=dict(type="list", elements="str", aliases=["dest_cidr"]), + protocol=dict(choices=["tcp", "udp", "icmp", "all"], default="tcp"), + type=dict(choices=["ingress", "egress"], default="ingress"), + icmp_type=dict(type="int"), + icmp_code=dict(type="int"), + start_port=dict(type="int", aliases=["port"]), + end_port=dict(type="int"), + state=dict(choices=["present", "absent"], default="present"), + zone=dict(required=True), + domain=dict(), + account=dict(), + project=dict(), + poll_async=dict(type="bool", default=True), + tags=dict(type="list", elements="dict", aliases=["tag"]), + ) + ) required_together = cs_required_together() - required_together.extend([ - ['icmp_type', 'icmp_code'], - ]) + required_together.extend( + [ + ["icmp_type", "icmp_code"], + ] + ) module = AnsibleModule( argument_spec=argument_spec, required_together=required_together, - required_one_of=( - ['ip_address', 'network'], - ), + required_one_of=(["ip_address", "network"],), mutually_exclusive=( - ['icmp_type', 'start_port'], - ['icmp_type', 'end_port'], - ['ip_address', 'network'], + ["icmp_type", "start_port"], + ["icmp_type", "end_port"], + ["ip_address", "network"], ), - supports_check_mode=True + supports_check_mode=True, ) acs_fw = AnsibleCloudStackFirewall(module) - state = module.params.get('state') - if state in ['absent']: + state = module.params.get("state") + if state in ["absent"]: fw_rule = acs_fw.remove_firewall_rule() else: fw_rule = acs_fw.create_firewall_rule() @@ -440,5 +446,5 @@ def main(): module.exit_json(**result) -if __name__ == '__main__': +if __name__ == "__main__": main()