From 2c7542e33312cb7697dc14a7a2b240cfceaa94ab Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 19 May 2015 09:33:04 +0200 Subject: [PATCH] cloudstack: cs_firewall: add egress support Added functionality to set rules for egress using this module at these are very similar. The only real difference is that egress firewall API uses the networkid. That is why the new arguments `type` for choosing `egress` or `ingress` and `network` was added. For `type=ingress`, which is the default, `ip_address` is required and for `type=egress` the argument `network` is required. --- .../extras/cloud/cloudstack/cs_firewall.py | 164 +++++++++++++++--- 1 file changed, 142 insertions(+), 22 deletions(-) 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 )