diff --git a/lib/ansible/modules/extras/cloud/cloudstack/cs_firewall.py b/lib/ansible/modules/extras/cloud/cloudstack/cs_firewall.py index d47f9b0ff61..e1e0f4a503d 100644 --- a/lib/ansible/modules/extras/cloud/cloudstack/cs_firewall.py +++ b/lib/ansible/modules/extras/cloud/cloudstack/cs_firewall.py @@ -28,20 +28,35 @@ author: '"René Moser (@resmo)" ' options: ip_address: description: - - Public IP address the rule is assigned to. - required: true + - Public IP address the ingress rule is assigned to. + - Required if C(type=ingress). + required: false + default: null + network: + description: + - Network the egress rule is related to. + - Required if C(type=egress). + required: false + default: null state: description: - State of the firewall rule. required: false default: 'present' choices: [ 'present', 'absent' ] + type: + description: + - Type of the firewall rule. + required: false + default: 'ingress' + choices: [ 'ingress', 'egress' ] protocol: description: - Protocol of the firewall rule. + - C(all) is only available if C(type=egress) required: false default: 'tcp' - choices: [ 'tcp', 'udp', 'icmp' ] + choices: [ 'tcp', 'udp', 'icmp', 'all' ] cidr: description: - CIDR (full notation) to be used for firewall rule. @@ -83,6 +98,11 @@ options: - Name of the project the firewall rule is related to. required: false default: null + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true extends_documentation_fragment: cloudstack ''' @@ -114,15 +134,37 @@ EXAMPLES = ''' end_port: 8888 cidr: 17.0.0.0/8 state: absent + + +# Allow all outbound traffic +- local_action: + module: cs_firewall + network: my_network + type: egress + protocol: all + + +# Allow only HTTP outbound traffic for an IP +- local_action: + module: cs_firewall + network: my_network + type: egress + port: 80 + cidr: 10.101.1.20 ''' RETURN = ''' --- ip_address: - description: IP address of the rule. + description: IP address of the rule if C(type=ingress) returned: success type: string sample: 10.100.212.10 +type: + description: Type of the rule. + returned: success + type: string + sample: ingress cidr: description: CIDR of the rule. returned: success @@ -153,6 +195,11 @@ icmp_type: returned: success type: int sample: 1 +network: + description: Name of the network if C(type=egress) + returned: success + type: string + sample: my_network ''' try: @@ -180,32 +227,46 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): def get_firewall_rule(self): if not self.firewall_rule: - cidr = self.module.params.get('cidr') - protocol = self.module.params.get('protocol') - start_port = self.module.params.get('start_port') - end_port = self.get_end_port() - icmp_code = self.module.params.get('icmp_code') - icmp_type = self.module.params.get('icmp_type') + cidr = self.module.params.get('cidr') + protocol = self.module.params.get('protocol') + start_port = self.module.params.get('start_port') + end_port = self.get_end_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="no start_port or end_port set for protocol '%s'" % protocol) + self.module.fail_json(msg="missing required argument for protocol '%s': start_port or end_port" % protocol) if protocol == 'icmp' and not icmp_type: - self.module.fail_json(msg="no icmp_type set") + self.module.fail_json(msg="missing required argument for protocol 'icmp': icmp_type") + + if protocol == 'all' and fw_type != 'egress': + self.module.fail_json(msg="protocol 'all' could only be used for type 'egress'" ) args = {} - args['ipaddressid'] = self.get_ip_address('id') args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') args['projectid'] = self.get_project('id') - firewall_rules = self.cs.listFirewallRules(**args) + 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") + firewall_rules = self.cs.listEgressFirewallRules(**args) + else: + args['ipaddressid'] = self.get_ip_address('id') + if not args['ipaddressid']: + self.module.fail_json(msg="missing required argument for type ingress: ip_address") + firewall_rules = self.cs.listFirewallRules(**args) + if firewall_rules and 'firewallrule' in firewall_rules: for rule in firewall_rules['firewallrule']: type_match = self._type_cidr_match(rule, cidr) protocol_match = self._tcp_udp_match(rule, protocol, start_port, end_port) \ - or self._icmp_match(rule, protocol, icmp_code, icmp_type) + 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: self.firewall_rule = rule @@ -220,6 +281,12 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): 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' + + def _icmp_match(self, rule, protocol, icmp_code, icmp_type): return protocol == 'icmp' \ and protocol == rule['protocol'] \ @@ -231,6 +298,30 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): return cidr == rule['cidrlist'] + def get_network(self, key=None, network=None): + if not network: + network = self.module.params.get('network') + + if not network: + return None + + args = {} + args['account'] = self.get_account('name') + args['domainid'] = self.get_domain('id') + args['projectid'] = self.get_project('id') + args['zoneid'] = self.get_zone('id') + + networks = self.cs.listNetworks(**args) + if not networks: + self.module.fail_json(msg="No networks available") + + for n in networks['network']: + if network in [ n['displaytext'], n['name'], n['id'] ]: + return self._get_by_key(key, n) + break + self.module.fail_json(msg="Network '%s' not found" % network) + + def create_firewall_rule(self): firewall_rule = self.get_firewall_rule() if not firewall_rule: @@ -243,11 +334,22 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): args['endport'] = self.get_end_port() args['icmptype'] = self.module.params.get('icmp_type') args['icmpcode'] = self.module.params.get('icmp_code') - args['ipaddressid'] = self.get_ip_address('id') + fw_type = self.module.params.get('type') if not self.module.check_mode: - firewall_rule = self.cs.createFirewallRule(**args) - + if fw_type == 'egress': + args['networkid'] = self.get_network(key='id') + res = self.cs.createEgressFirewallRule(**args) + else: + args['ipaddressid'] = self.get_ip_address('id') + res = self.cs.createFirewallRule(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + firewall_rule = self._poll_job(res, 'firewallrule') return firewall_rule @@ -255,17 +357,29 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): firewall_rule = self.get_firewall_rule() if firewall_rule: self.result['changed'] = True - args = {} + + args = {} args['id'] = firewall_rule['id'] + fw_type = self.module.params.get('type') if not self.module.check_mode: - res = self.cs.deleteFirewallRule(**args) + if fw_type == 'egress': + res = self.cs.deleteEgressFirewallRule(**args) + else: + res = self.cs.deleteFirewallRule(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'firewallrule') return firewall_rule def get_result(self, firewall_rule): if firewall_rule: + self.result['type'] = self.module.params.get('type') if 'cidrlist' in firewall_rule: self.result['cidr'] = firewall_rule['cidrlist'] if 'startport' in firewall_rule: @@ -280,15 +394,19 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): self.result['icmp_code'] = int(firewall_rule['icmpcode']) if 'icmptype' in firewall_rule: self.result['icmp_type'] = int(firewall_rule['icmptype']) + if 'networkid' in firewall_rule: + self.result['network'] = self.get_network(key='displaytext', network=firewall_rule['networkid']) return self.result def main(): module = AnsibleModule( argument_spec = dict( - ip_address = dict(required=True), + ip_address = dict(default=None), + network = dict(default=None), cidr = dict(default='0.0.0.0/0'), - protocol = dict(choices=['tcp', 'udp', 'icmp'], default='tcp'), + protocol = dict(choices=['tcp', 'udp', 'icmp', 'all'], default='tcp'), + type = dict(choices=['ingress', 'egress'], default='ingress'), icmp_type = dict(type='int', default=None), icmp_code = dict(type='int', default=None), start_port = dict(type='int', aliases=['port'], default=None), @@ -297,6 +415,7 @@ def main(): domain = dict(default=None), account = dict(default=None), project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), @@ -305,6 +424,7 @@ def main(): mutually_exclusive = ( ['icmp_type', 'start_port'], ['icmp_type', 'end_port'], + ['ip_address', 'network'], ), supports_check_mode=True )