diff --git a/ci/roles/security_group_rule/tasks/main.yml b/ci/roles/security_group_rule/tasks/main.yml index c8220eda..0ad4c148 100644 --- a/ci/roles/security_group_rule/tasks/main.yml +++ b/ci/roles/security_group_rule/tasks/main.yml @@ -1,18 +1,17 @@ --- - name: Create security group openstack.cloud.security_group: - cloud: "{{ cloud }}" - name: ansible_security_group - state: present - description: Created from Ansible playbook + cloud: "{{ cloud }}" + name: ansible_security_group + state: present - name: Create empty ICMP rule openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: icmp - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 register: security_group_rule - name: Assert return values of security_group_rule module @@ -23,11 +22,11 @@ - name: Assert changed assert: - that: security_group_rule is changed + that: security_group_rule is changed - name: Fetch all security group rule openstack.cloud.security_group_rule_info: - cloud: "{{ cloud }}" + cloud: "{{ cloud }}" register: security_group_rules - name: Assert return values of security_group_rule_info module @@ -39,8 +38,8 @@ - name: Fetch security group rule based on rule openstack.cloud.security_group_rule_info: - cloud: "{{ cloud }}" - id: "{{ security_group_rule.rule.id }}" + cloud: "{{ cloud }}" + id: "{{ security_group_rule.rule.id }}" register: security_group_rules - name: Assert return fields security_group_rule_info @@ -49,157 +48,157 @@ - name: Create empty ICMP rule again openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: icmp - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 register: security_group_rule - name: Assert not changed assert: - that: security_group_rule is not changed + that: security_group_rule is not changed - name: Create -1 ICMP rule openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: icmp - port_range_min: -1 - port_range_max: -1 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 register: security_group_rule - name: Assert not changed assert: - that: security_group_rule is not changed + that: security_group_rule is not changed - name: Create -1 ICMP rule again openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: icmp - port_range_min: -1 - port_range_max: -1 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 register: security_group_rule - name: Assert not changed assert: - that: security_group_rule is not changed + that: security_group_rule is not changed - name: Create empty TCP rule openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: tcp - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 register: security_group_rule - name: Assert changed assert: - that: security_group_rule is changed + that: security_group_rule is changed - name: Create TCP rule again with port range (1, 65535) openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: tcp - port_range_min: 1 - port_range_max: 65535 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: tcp + port_range_min: 1 + port_range_max: 65535 + remote_ip_prefix: 0.0.0.0/0 register: security_group_rule - name: Assert changed assert: - that: security_group_rule is not changed + that: security_group_rule is not changed - name: Create TCP rule again with port range (-1, -1) openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: tcp - port_range_min: -1 - port_range_max: -1 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: tcp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 register: security_group_rule - name: Assert changed assert: - that: security_group_rule is not changed + that: security_group_rule is not changed - name: Create TCP rule again with defined range openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: tcp - port_range_min: 8000 - port_range_max: 9000 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: tcp + port_range_min: 8000 + port_range_max: 9000 + remote_ip_prefix: 0.0.0.0/0 register: security_group_rule - name: Assert changed assert: - that: security_group_rule is changed + that: security_group_rule is changed - name: Create empty UDP rule openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: udp - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: udp + remote_ip_prefix: 0.0.0.0/0 - name: Create UDP rule again with port range (1, 65535) openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: udp - port_range_min: 1 - port_range_max: 65535 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: udp + port_range_min: 1 + port_range_max: 65535 + remote_ip_prefix: 0.0.0.0/0 - name: Create UDP rule again with port range (-1, -1) openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: udp - port_range_min: -1 - port_range_max: -1 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: udp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 - name: Create HTTP rule openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: tcp - port_range_min: 80 - port_range_max: 80 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 - name: Create egress rule openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: present - protocol: tcp - port_range_min: 30000 - port_range_max: 30001 - remote_ip_prefix: 0.0.0.0/0 - direction: egress + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: present + protocol: tcp + port_range_min: 30000 + port_range_max: 30001 + remote_ip_prefix: 0.0.0.0/0 + direction: egress - name: List all available rules of all security groups in a project openstack.cloud.security_group_rule_info: - cloud: "{{ cloud }}" + cloud: "{{ cloud }}" register: security_group_rules - name: Check - List all available rules of all security groups in a project @@ -209,8 +208,8 @@ - name: List all available rules of a specific security group openstack.cloud.security_group_rule_info: - cloud: "{{ cloud }}" - security_group: ansible_security_group + cloud: "{{ cloud }}" + security_group: ansible_security_group register: security_group_rules - name: Check - List all available rules of a specific security group @@ -220,70 +219,80 @@ - name: List all available rules with filters openstack.cloud.security_group_rule_info: - cloud: "{{ cloud }}" - security_group: ansible_security_group - protocol: tcp - port_range_min: 80 - port_range_max: 80 - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete empty ICMP rule - openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: absent - protocol: icmp - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete -1 ICMP rule - openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: absent - protocol: icmp - port_range_min: -1 - port_range_max: -1 - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete empty TCP rule - openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: absent - protocol: tcp - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete empty UDP rule - openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: absent - protocol: udp - remote_ip_prefix: 0.0.0.0/0 - -- name: Delete HTTP rule - openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: absent - protocol: tcp - port_range_min: 80 - port_range_max: 80 - remote_ip_prefix: 0.0.0.0/0 + cloud: "{{ cloud }}" + security_group: ansible_security_group + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 - name: Delete egress rule openstack.cloud.security_group_rule: - cloud: "{{ cloud }}" - security_group: ansible_security_group - state: absent - protocol: tcp - port_range_min: 30000 - port_range_max: 30001 - remote_ip_prefix: 0.0.0.0/0 - direction: egress + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: absent + protocol: tcp + port_range_min: 30000 + port_range_max: 30001 + remote_ip_prefix: 0.0.0.0/0 + direction: egress + +- name: Delete HTTP rule + openstack.cloud.security_group_rule: + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: absent + protocol: tcp + port_range_min: 80 + port_range_max: 80 + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete empty UDP rule + openstack.cloud.security_group_rule: + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: absent + protocol: udp + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete TCP rule again with defined range + openstack.cloud.security_group_rule: + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: absent + protocol: tcp + port_range_min: 8000 + port_range_max: 9000 + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete empty TCP rule + openstack.cloud.security_group_rule: + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: absent + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete -1 ICMP rule + openstack.cloud.security_group_rule: + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: absent + protocol: icmp + port_range_min: -1 + port_range_max: -1 + remote_ip_prefix: 0.0.0.0/0 + +- name: Delete empty ICMP rule + openstack.cloud.security_group_rule: + cloud: "{{ cloud }}" + security_group: ansible_security_group + state: absent + protocol: icmp + remote_ip_prefix: 0.0.0.0/0 - name: Delete security group openstack.cloud.security_group: - cloud: "{{ cloud }}" - name: ansible_security_group - state: absent + cloud: "{{ cloud }}" + name: ansible_security_group + state: absent diff --git a/plugins/modules/security_group_rule.py b/plugins/modules/security_group_rule.py index 3d542822..d302a30a 100644 --- a/plugins/modules/security_group_rule.py +++ b/plugins/modules/security_group_rule.py @@ -5,82 +5,101 @@ # Copyright (c) 2013, Benno Joy # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: security_group_rule -short_description: Add/Delete rule from an existing security group +short_description: Manage security group rules in OpenStack network (Neutron) author: OpenStack Ansible SIG description: - - Add or Remove rule from an existing security group + - Add or remove security group rule to/from OpenStack network (Neutron) + service. options: - security_group: - description: - - Name or ID of the security group - required: true - type: str - protocol: - description: - - IP protocols ANY TCP UDP ICMP and others, also number in range 0-255 - type: str - port_range_min: - description: - - Starting port - type: int - port_range_max: - description: - - Ending port - type: int - remote_ip_prefix: - description: - - Source IP address(es) in CIDR notation (exclusive with remote_group) - type: str - remote_group: - description: - - Name or ID of the Security group to link (exclusive with - remote_ip_prefix) - type: str - ether_type: - description: - - Must be IPv4 or IPv6, and addresses represented in CIDR must - match the ingress or egress rules. Not all providers support IPv6. - choices: ['IPv4', 'IPv6'] - default: IPv4 - type: str - aliases: [ethertype] - direction: - description: - - The direction in which the security group rule is applied. Not - all providers support egress. - choices: ['egress', 'ingress'] - default: ingress - type: str - state: - description: - - Should the resource be present or absent. - choices: [present, absent] - default: present - type: str - project: - description: - - Unique name or ID of the project. - required: false - type: str - description: - required: false - description: - - Description of the rule. - type: str + description: + description: + - Description of the security group rule. + type: str + direction: + description: + - The direction in which the security group rule is applied. + - Not all providers support C(egress). + choices: ['egress', 'ingress'] + default: ingress + type: str + ether_type: + description: + - Must be IPv4 or IPv6, and addresses represented in CIDR must + match the ingress or egress rules. Not all providers support IPv6. + choices: ['IPv4', 'IPv6'] + default: IPv4 + type: str + aliases: ['ethertype'] + port_range_max: + description: + - The maximum port number in the range that is matched by the security + group rule. + - If the protocol is TCP, UDP, DCCP, SCTP or UDP-Lite this value must be + greater than or equal to the I(port_range_min) attribute value. + - If the protocol is ICMP, this value must be an ICMP code. + type: int + port_range_min: + description: + - The minimum port number in the range that is matched by the security + group rule. + - If the protocol is TCP, UDP, DCCP, SCTP or UDP-Lite this value must be + less than or equal to the port_range_max attribute value. + - If the protocol is ICMP, this value must be an ICMP type. + type: int + project: + description: + - Unique name or ID of the project. + type: str + protocol: + description: + - The IP protocol can be represented by a string, an integer, or null. + - Valid string or integer values are C(any) or C(0), C(ah) or C(51), + C(dccp) or C(33), C(egp) or C(8), C(esp) or C(50), C(gre) or C(47), + C(icmp) or C(1), C(icmpv6) or C(58), C(igmp) or C(2), C(ipip) or C(4), + C(ipv6-encap) or C(41), C(ipv6-frag) or C(44), C(ipv6-icmp) or C(58), + C(ipv6-nonxt) or C(59), C(ipv6-opts) or C(60), C(ipv6-route) or C(43), + C(ospf) or C(89), C(pgm) or C(113), C(rsvp) or C(46), C(sctp) or + C(132), C(tcp) or C(6), C(udp) or C(17), C(udplite) or C(136), C(vrrp) + or C(112). + - Additionally, any integer value between C([0-255]) is also valid. + - The string any (or integer 0) means all IP protocols. + - See the constants in neutron_lib.constants for the most up-to-date + list of supported strings. + type: str + remote_group: + description: + - Name or ID of the security group to link. + - Mutually exclusive with I(remote_ip_prefix). + type: str + remote_ip_prefix: + description: + - Source IP address(es) in CIDR notation. + - Mutually exclusive with I(remote_group). + type: str + security_group: + description: + - Name or ID of the security group. + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str requirements: - - "python >= 3.6" - - "openstacksdk" - + - "python >= 3.6" + - "openstacksdk" extends_documentation_fragment: -- openstack.cloud.openstack + - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Create a security group rule -- openstack.cloud.security_group_rule: +EXAMPLES = r''' +- name: Create a security group rule + openstack.cloud.security_group_rule: cloud: mordred security_group: foo protocol: tcp @@ -88,15 +107,15 @@ EXAMPLES = ''' port_range_max: 80 remote_ip_prefix: 0.0.0.0/0 -# Create a security group rule for ping -- openstack.cloud.security_group_rule: +- name: Create a security group rule for ping + openstack.cloud.security_group_rule: cloud: mordred security_group: foo protocol: icmp remote_ip_prefix: 0.0.0.0/0 -# Another way to create the ping rule -- openstack.cloud.security_group_rule: +- name: Another way to create the ping rule + openstack.cloud.security_group_rule: cloud: mordred security_group: foo protocol: icmp @@ -104,8 +123,8 @@ EXAMPLES = ''' port_range_max: -1 remote_ip_prefix: 0.0.0.0/0 -# Create a TCP rule covering all ports -- openstack.cloud.security_group_rule: +- name: Create a TCP rule covering all ports + openstack.cloud.security_group_rule: cloud: mordred security_group: foo protocol: tcp @@ -113,326 +132,278 @@ EXAMPLES = ''' port_range_max: 65535 remote_ip_prefix: 0.0.0.0/0 -# Another way to create the TCP rule above (defaults to all ports) -- openstack.cloud.security_group_rule: +- name: Another way to create the TCP rule above (defaults to all ports) + openstack.cloud.security_group_rule: cloud: mordred security_group: foo protocol: tcp remote_ip_prefix: 0.0.0.0/0 -# Create a rule for VRRP with numbered protocol 112 -- openstack.cloud.security_group_rule: +- name: Create a rule for VRRP with numbered protocol 112 + openstack.cloud.security_group_rule: security_group: loadbalancer_sg protocol: 112 remote_group: loadbalancer-node_sg -# Create a security group rule for a given project -- openstack.cloud.security_group_rule: +- name: Create a security group rule for a given project + openstack.cloud.security_group_rule: cloud: mordred security_group: foo protocol: icmp remote_ip_prefix: 0.0.0.0/0 project: myproj -# Remove the default created egress rule for IPv4 -- openstack.cloud.security_group_rule: - cloud: mordred - security_group: foo - protocol: any - remote_ip_prefix: 0.0.0.0/0 +- name: Remove the default created egress rule for IPv4 + openstack.cloud.security_group_rule: + cloud: mordred + security_group: foo + protocol: any + remote_ip_prefix: 0.0.0.0/0 ''' -RETURN = ''' +RETURN = r''' rule: - description: Representation of the security group rule + description: Dictionary describing the security group rule type: dict - returned: when I(state) is present + returned: On success when I(state) is C(present). contains: created_at: description: Timestamp when the resource was created type: str - returned: always description: description: Description of the resource type: str - returned: always direction: description: The direction in which the security group rule is applied. type: str sample: 'egress' - returned: always ether_type: description: Either IPv4 or IPv6 type: str - returned: always id: description: Unique rule UUID. type: str - returned: always name: description: Name of the resource. type: str - returned: always port_range_max: description: The maximum port number in the range that is matched by the security group rule. type: int sample: 8000 - returned: always port_range_min: description: The minimum port number in the range that is matched by the security group rule. type: int sample: 8000 - returned: always project_id: description: ID of the project the resource belongs to. type: str - returned: always protocol: description: The protocol that is matched by the security group rule. type: str sample: 'tcp' - returned: always remote_address_group_id: description: The remote address group ID to be associated with this security group rule. type: str sample: '0.0.0.0/0' - returned: always remote_group_id: description: The remote security group ID to be associated with this security group rule. type: str sample: '0.0.0.0/0' - returned: always remote_ip_prefix: description: The remote IP prefix to be associated with this security group rule. type: str sample: '0.0.0.0/0' - returned: always revision_number: description: Revision number type: int sample: 0 - returned: always security_group_id: description: The security group ID to associate with this security group rule. type: str - returned: always tags: description: Tags associated with resource. type: list elements: str - returned: always tenant_id: description: ID of the project the resource belongs to. Deprecated. type: str - returned: always updated_at: description: Timestamp when the security group rule was last updated. type: str - returned: always ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( OpenStackModule) -def _ports_match(protocol, module_min, module_max, rule_min, rule_max): - """ - Capture the complex port matching logic. - - The port values coming in for the module might be -1 (for ICMP), - which will work only for Nova, but this is handled by sdk. Likewise, - they might be None, which works for Neutron, but not Nova. This too is - handled by sdk. Since sdk will consistently return these port - values as None, we need to convert any -1 values input to the module - to None here for comparison. - - For TCP and UDP protocols, None values for both min and max are - represented as the range 1-65535 for Nova, but remain None for - Neutron. sdk returns the full range when Nova is the backend (since - that is how Nova stores them), and None values for Neutron. If None - values are input to the module for both values, then we need to adjust - for comparison. - """ - - # Check if the user is supplying -1 for ICMP. - if protocol in ['icmp', 'ipv6-icmp']: - if module_min and int(module_min) == -1: - module_min = None - if module_max and int(module_max) == -1: - module_max = None - - # Rules with 'any' protocol do not match ports - if protocol == 'any': - return True - - # Check if the user is supplying -1, 1 to 65535 or None values for full TPC/UDP port range. - if protocol in ['tcp', 'udp'] or protocol is None: - if ( - not module_min and not module_max - or (int(module_min) in [-1, 1] - and int(module_max) in [-1, 65535]) - ): - if ( - not rule_min and not rule_max - or (int(rule_min) in [-1, 1] - and int(rule_max) in [-1, 65535]) - ): - # (None, None) == (1, 65535) == (-1, -1) - return True - - # Sanity check to make sure we don't have type comparison issues. - if module_min: - module_min = int(module_min) - if module_max: - module_max = int(module_max) - if rule_min: - rule_min = int(rule_min) - if rule_max: - rule_max = int(rule_max) - - return module_min == rule_min and module_max == rule_max - - class SecurityGroupRuleModule(OpenStackModule): - argument_spec = dict( - security_group=dict(required=True), - protocol=dict(), - port_range_min=dict(type='int'), - port_range_max=dict(type='int'), - remote_ip_prefix=dict(), - remote_group=dict(), - ether_type=dict(default='IPv4', - choices=['IPv4', 'IPv6'], - aliases=['ethertype']), - direction=dict(default='ingress', - choices=['egress', 'ingress']), - state=dict(default='present', - choices=['absent', 'present']), description=dict(), + direction=dict(default='ingress', choices=['egress', 'ingress']), + ether_type=dict(default='IPv4', choices=['IPv4', 'IPv6'], + aliases=['ethertype']), + port_range_max=dict(type='int'), + port_range_min=dict(type='int'), project=dict(), + protocol=dict(), + remote_group=dict(), + remote_ip_prefix=dict(), + security_group=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), ) module_kwargs = dict( mutually_exclusive=[ ['remote_ip_prefix', 'remote_group'], - ] + ], + supports_check_mode=True, ) - def _build_kwargs(self, secgroup, remote_group, project): - kwargs = dict( - security_group_id=secgroup.id, - description=self.params['description'], - port_range_max=self.params['port_range_max'], - port_range_min=self.params['port_range_min'], - protocol=self.params['protocol'], - remote_ip_prefix=self.params['remote_ip_prefix'], - direction=self.params['direction'], - ether_type=self.params['ether_type'], - ) - if self.params['port_range_min'] != -1: - kwargs['port_range_min'] = self.params['port_range_min'] - if self.params['port_range_max'] != -1: - kwargs['port_range_max'] = self.params['port_range_max'] - if project: - kwargs['project_id'] = project.id - if remote_group: - kwargs['remote_group_id'] = remote_group.id - return {k: v for k, v in kwargs.items() if v is not None} - - def _find_matching_rule(self, kwargs, secgroup): - """ - Find a rule in the group that matches the module parameters. - :returns: The matching rule dict, or None if no matches. - """ - fields = ('protocol', 'remote_ip_prefix', 'direction', - 'remote_group_id') - for rule in secgroup['security_group_rules']: - if ('ether_type' in kwargs - and rule['ethertype'] != kwargs['ether_type']): - continue - if any(field in kwargs and rule[field] != kwargs[field] - for field in fields): - continue - if _ports_match( - self.params['protocol'], - self.params['port_range_min'], - self.params['port_range_max'], - rule['port_range_min'], - rule['port_range_max'] - ): - return rule - return None - - def _system_state_change(self, secgroup, rule): - state = self.params['state'] - if not secgroup: - return False - - if state == 'present' and not rule: - return True - if state == 'absent' and rule: - return True - return False - def run(self): state = self.params['state'] - security_group = self.params['security_group'] - remote_group_name_or_id = self.params['remote_group'] - project_name_or_id = self.params['project'] - project = None - if project_name_or_id: - project = self.conn.identity.find_project(project_name_or_id, - ignore_missing=False) - - filters = {} - if project and not remote_group_name_or_id: - filters = {'project_id': project.id} - - secgroup = self.conn.network.find_security_group( - security_group, ignore_missing=(state == 'absent'), **filters) - - remote_group = None - if remote_group_name_or_id: - remote_group = self.conn.network.find_security_group( - remote_group_name_or_id, ignore_missing=False, filters=filters) - - kwargs = self._build_kwargs(secgroup, remote_group, project) - - rule = None - if secgroup: - # TODO: Replace with self.conn.network.find_security_group_rule()? - rule = self._find_matching_rule(kwargs, secgroup) - if rule: - rule = self.conn.network.get_security_group_rule(rule['id']) + security_group_rule = self._find() if self.ansible.check_mode: - self.exit_json(changed=self._system_state_change(secgroup, rule)) + self.exit_json( + changed=self._will_change(state, security_group_rule)) - changed = False - if state == 'present': - if self.params['protocol'] == 'any': - self.params['protocol'] = None + if state == 'present' and not security_group_rule: + # Create security_group_rule + security_group_rule = self._create() + self.exit_json(changed=True, + rule=security_group_rule.to_dict(computed=False)) - if not rule: - rule = self.conn.network.create_security_group_rule(**kwargs) - changed = True + elif state == 'present' and security_group_rule: + # Only exact matches will cause security_group_rule to be not None + self.exit_json(changed=False, + rule=security_group_rule.to_dict(computed=False)) + elif state == 'absent' and security_group_rule: + # Delete security_group_rule + self._delete(security_group_rule) + self.exit_json(changed=True) - rule = rule.to_dict(computed=False) - self.exit_json(changed=changed, rule=rule) + elif state == 'absent' and not security_group_rule: + # Do nothing + self.exit_json(changed=False) - if state == 'absent' and rule: - self.conn.network.delete_security_group_rule(rule['id']) - changed = True + def _create(self): + prototype = self._define_prototype() + return self.conn.network.create_security_group_rule(**prototype) - self.exit_json(changed=changed) + def _define_prototype(self): + filters = {} + prototype = dict((k, self.params[k]) + for k in ['direction', 'protocol', 'remote_ip_prefix'] + if self.params[k] is not None) + + project_name_or_id = self.params['project'] + if project_name_or_id is not None: + project = self.conn.identity.find_project(project_name_or_id, + ignore_missing=False) + filters = {'project_id': project.id} + prototype['project_id'] = project.id + + security_group_name_or_id = self.params['security_group'] + security_group = self.conn.network.find_security_group( + security_group_name_or_id, ignore_missing=False, **filters) + prototype['security_group_id'] = security_group.id + + remote_group = None + remote_group_name_or_id = self.params['remote_group'] + if remote_group_name_or_id is not None: + remote_group = self.conn.network.find_security_group( + remote_group_name_or_id, ignore_missing=False) + prototype['remote_group_id'] = remote_group.id + + ether_type = self.params['ether_type'] + if ether_type is not None: + prototype['ether_type'] = ether_type + + protocol = self.params['protocol'] + port_range_max = self.params['port_range_max'] + port_range_min = self.params['port_range_min'] + + if protocol in ['icmp', 'ipv6-icmp']: + # Check if the user is supplying -1 for ICMP. + if port_range_max is not None and int(port_range_max) != -1: + prototype['port_range_max'] = int(port_range_max) + if port_range_min is not None and int(port_range_min) != -1: + prototype['port_range_min'] = int(port_range_min) + elif protocol in ['tcp', 'udp']: + if port_range_max is not None and int(port_range_max) != -1: + prototype['port_range_max'] = int(port_range_max) + if port_range_min is not None and int(port_range_min) != -1: + prototype['port_range_min'] = int(port_range_min) + elif protocol in ['any', '0']: + # Rules with 'any' protocol do not match ports + pass + else: + if port_range_max is not None: + prototype['port_range_max'] = int(port_range_max) + if port_range_min is not None: + prototype['port_range_min'] = int(port_range_min) + + return prototype + + def _delete(self, security_group_rule): + self.conn.network.delete_security_group_rule(security_group_rule.id) + + def _find(self): + # Replacing this code with self.conn.network.find_security_group_rule() + # is not possible because the latter requires an id or name. + matches = self._find_matches() + if len(matches) > 1: + self.fail_json(msg='Found more a single matching security group' + ' rule which match the given parameters.') + elif len(matches) == 1: + return self.conn.network.get_security_group_rule(matches[0]['id']) + else: # len(matches) == 0 + return None + + def _find_matches(self): + prototype = self._define_prototype() + + security_group = self.conn.network.\ + get_security_group(prototype['security_group_id']) + + if 'ether_type' in prototype: + prototype['ethertype'] = prototype.pop('ether_type') + + if 'protocol' in prototype and prototype['protocol'] in ['any', '0']: + prototype.pop('protocol') + + if 'protocol' in prototype and prototype['protocol'] in ['tcp', 'udp']: + # Check if the user is supplying -1, 1 to 65535 or None values + # for full TPC or UDP port range. + # (None, None) == (1, 65535) == (-1, -1) + if 'port_range_max' in prototype \ + and prototype['port_range_max'] in [-1, 65535]: + prototype.pop('port_range_max') + if 'port_range_min' in prototype \ + and prototype['port_range_min'] in [-1, 1]: + prototype.pop('port_range_min') + + return [r for r in security_group.security_group_rules + if all(r[k] == prototype[k] for k in prototype.keys())] + + def _will_change(self, state, security_group_rule): + if state == 'present' and not security_group_rule: + return True + elif state == 'present' and security_group_rule: + # Only exact matches will cause security_group_rule to be not None + return False + elif state == 'absent' and security_group_rule: + return True + else: + # state == 'absent' and not security_group_rule: + return False def main(): diff --git a/plugins/modules/security_group_rule_info.py b/plugins/modules/security_group_rule_info.py index 4d0e57f3..b309f384 100644 --- a/plugins/modules/security_group_rule_info.py +++ b/plugins/modules/security_group_rule_info.py @@ -4,13 +4,13 @@ # Copyright (c) 2020 by Tino Schreiber (Open Telekom Cloud), operated by T-Systems International GmbH # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: security_group_rule_info -short_description: Querying security group rules +short_description: Fetch OpenStack network (Neutron) security group rules author: OpenStack Ansible SIG description: - - Querying security group rules + - Fetch security group rules from OpenStack network (Neutron) API. options: description: description: @@ -69,23 +69,20 @@ options: description: - Name or ID of the security group type: str - requirements: - "python >= 3.6" - "openstacksdk" - extends_documentation_fragment: - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Get all security group rules -- openstack.cloud.security_group_rule_info: +EXAMPLES = r''' +- name: Fetch all security group rules + openstack.cloud.security_group_rule_info: cloud: devstack - register: sg -# Filter security group rules for port 80 and name -- openstack.cloud.security_group_rule_info: +- name: Filter security group rules for port 80 and name + openstack.cloud.security_group_rule_info: cloud: devstack security_group: foo protocol: tcp @@ -93,13 +90,13 @@ EXAMPLES = ''' port_range_max: 80 remote_ip_prefix: 0.0.0.0/0 -# Filter for ICMP rules -- openstack.cloud.security_group_rule_info: +- name: Filter for ICMP rules + openstack.cloud.security_group_rule_info: cloud: devstack protocol: icmp ''' -RETURN = ''' +RETURN = r''' security_group_rules: description: List of dictionaries describing security group rules. type: list @@ -213,29 +210,32 @@ class SecurityGroupRuleInfoModule(OpenStackModule): def run(self): filters = dict((k, self.params[k]) for k in ['description', 'direction', 'ether_type', - 'port_range_min', 'port_range_max', + 'id', 'port_range_min', 'port_range_max', 'protocol', 'remote_group', 'revision_number', 'remote_ip_prefix'] if self.params[k] is not None) - if self.params['id']: - filters['id'] = self.params['id'] - if self.params['project']: - proj = self.conn.find_project(self.params['project'], - ignore_missing=False) - filters['project_id'] = proj.id - if self.params['security_group']: - sec_grp = self.conn.network.find_security_group( - name_or_id=self.params['security_group'], - ignore_missing=False) - filters['security_group_id'] = sec_grp.id + project_name_or_id = self.params['project'] + if project_name_or_id is not None: + project = self.conn.find_project(project_name_or_id) + if not project: + self.exit_json(changed=False, security_group_rules=[]) + filters['project_id'] = project.id - security_group_rules = [ - rule.to_dict(computed=False) - for rule in self.conn.network.security_group_rules(**filters)] + security_group_name_or_id = self.params['security_group'] + if security_group_name_or_id is not None: + security_group = self.conn.network.\ + find_security_group(security_group_name_or_id) + if not security_group: + self.exit_json(changed=False, security_group_rules=[]) + filters['security_group_id'] = security_group.id + + security_group_rules = \ + self.conn.network.security_group_rules(**filters) self.exit_json(changed=False, - security_group_rules=security_group_rules) + security_group_rules=[r.to_dict(computed=False) + for r in security_group_rules]) def main():