Jakob Meng 92c3e87467 Respect description option and delete security group rules first
The description option of security group rules will now be used properly
when creating new rules.

Security group rules have to be deleted first before new ones get
created, because if one changes one rule attribute such as its
description, then the old rule must be deleted before recreating it,
as rules cannot be updated.

Story: 2010605
Task: 47486

Change-Id: I75b900e6675f7ec33532089738a6c2bfc10a898b
2023-02-23 21:20:51 +01:00

560 lines
21 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
# Copyright (c) 2013, Benno Joy <benno@ansible.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = r'''
---
module: security_group
short_description: Manage Neutron security groups of an OpenStack cloud.
author: OpenStack Ansible SIG
description:
- Add or remove Neutron security groups to/from an OpenStack cloud.
options:
description:
description:
- Long description of the purpose of the security group.
type: str
name:
description:
- Name that has to be given to the security group. This module
requires that security group names be unique.
required: true
type: str
project:
description:
- Unique name or ID of the project.
type: str
security_group_rules:
description:
- List of security group rules.
- When I(security_group_rules) is not defined, Neutron might create this
security group with a default set of rules.
- Security group rules which are listed in I(security_group_rules)
but not defined in this security group will be created.
- Existing security group rules which are not listed in
I(security_group_rules) will be deleted.
- When updating a security group, one has to explicitly list rules from
Neutron's defaults in I(security_group_rules) if those rules should be
kept. Rules which are not listed in I(security_group_rules) will be
deleted.
type: list
elements: dict
suboptions:
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
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
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.
- When a netmask such as C(/32) is missing from I(remote_ip_prefix),
then this module will fail on updates with OpenStack error message
C(Security group rule already exists.).
- Mutually exclusive with I(remote_group).
type: str
state:
description:
- Should the resource be present or absent.
choices: [present, absent]
default: present
type: str
extends_documentation_fragment:
- openstack.cloud.openstack
'''
RETURN = r'''
security_group:
description: Dictionary describing the security group.
type: dict
returned: On success when I(state) is C(present).
contains:
created_at:
description: Creation time of the security group
type: str
sample: "yyyy-mm-dd hh:mm:ss"
description:
description: Description of the security group
type: str
sample: "My security group"
id:
description: ID of the security group
type: str
sample: "d90e55ba-23bd-4d97-b722-8cb6fb485d69"
name:
description: Name of the security group.
type: str
sample: "my-sg"
project_id:
description: Project ID where the security group is located in.
type: str
sample: "25d24fc8-d019-4a34-9fff-0a09fde6a567"
revision_number:
description: The revision number of the resource.
type: int
tenant_id:
description: Tenant ID where the security group is located in. Deprecated
type: str
sample: "25d24fc8-d019-4a34-9fff-0a09fde6a567"
security_group_rules:
description: Specifies the security group rule list
type: list
sample: [
{
"id": "d90e55ba-23bd-4d97-b722-8cb6fb485d69",
"direction": "ingress",
"protocol": null,
"ethertype": "IPv4",
"description": null,
"remote_group_id": "0431c9c5-1660-42e0-8a00-134bec7f03e2",
"remote_ip_prefix": null,
"tenant_id": "bbfe8c41dd034a07bebd592bf03b4b0c",
"port_range_max": null,
"port_range_min": null,
"security_group_id": "0431c9c5-1660-42e0-8a00-134bec7f03e2"
},
{
"id": "aecff4d4-9ce9-489c-86a3-803aedec65f7",
"direction": "egress",
"protocol": null,
"ethertype": "IPv4",
"description": null,
"remote_group_id": null,
"remote_ip_prefix": null,
"tenant_id": "bbfe8c41dd034a07bebd592bf03b4b0c",
"port_range_max": null,
"port_range_min": null,
"security_group_id": "0431c9c5-1660-42e0-8a00-134bec7f03e2"
}
]
stateful:
description: Indicates if the security group is stateful or stateless.
type: bool
tags:
description: The list of tags on the resource.
type: list
updated_at:
description: Update time of the security group
type: str
sample: "yyyy-mm-dd hh:mm:ss"
'''
EXAMPLES = r'''
- name: Create a security group
openstack.cloud.security_group:
cloud: mordred
state: present
name: foo
description: security group for foo servers
- name: Update the existing 'foo' security group description
openstack.cloud.security_group:
cloud: mordred
state: present
name: foo
description: updated description for the foo security group
- name: Create a security group for a given project
openstack.cloud.security_group:
cloud: mordred
state: present
name: foo
project: myproj
- name: Create (or update) a security group with security group rules
openstack.cloud.security_group:
cloud: mordred
state: present
name: foo
security_group_rules:
- ether_type: IPv6
direction: egress
- ether_type: IPv4
direction: egress
- name: Create (or update) security group without security group rules
openstack.cloud.security_group:
cloud: mordred
state: present
name: foo
security_group_rules: []
'''
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
class SecurityGroupModule(OpenStackModule):
# NOTE: Keep handling of security group rules synchronized with
# security_group_rule.py!
argument_spec = dict(
description=dict(),
name=dict(required=True),
project=dict(),
security_group_rules=dict(
type="list", elements="dict",
options=dict(
description=dict(),
direction=dict(default="ingress",
choices=["egress", "ingress"]),
ether_type=dict(default="IPv4", choices=["IPv4", "IPv6"]),
port_range_max=dict(type="int"),
port_range_min=dict(type="int"),
protocol=dict(),
remote_group=dict(),
remote_ip_prefix=dict(),
),
),
state=dict(default='present', choices=['absent', 'present']),
)
module_kwargs = dict(
supports_check_mode=True,
)
def run(self):
state = self.params['state']
security_group = self._find()
if self.ansible.check_mode:
self.exit_json(changed=self._will_change(state, security_group))
if state == 'present' and not security_group:
# Create security_group
security_group = self._create()
self.exit_json(
changed=True,
security_group=security_group.to_dict(computed=False))
elif state == 'present' and security_group:
# Update security_group
update = self._build_update(security_group)
if update:
security_group = self._update(security_group, update)
self.exit_json(
changed=bool(update),
security_group=security_group.to_dict(computed=False))
elif state == 'absent' and security_group:
# Delete security_group
self._delete(security_group)
self.exit_json(changed=True)
elif state == 'absent' and not security_group:
# Do nothing
self.exit_json(changed=False)
def _build_update(self, security_group):
return {
**self._build_update_security_group(security_group),
**self._build_update_security_group_rules(security_group)}
def _build_update_security_group(self, security_group):
update = {}
# module options name and project are used to find security group
# and thus cannot be updated
non_updateable_keys = [k for k in []
if self.params[k] is not None
and self.params[k] != security_group[k]]
if non_updateable_keys:
self.fail_json(msg='Cannot update parameters {0}'
.format(non_updateable_keys))
attributes = dict((k, self.params[k])
for k in ['description']
if self.params[k] is not None
and self.params[k] != security_group[k])
if attributes:
update['attributes'] = attributes
return update
def _build_update_security_group_rules(self, security_group):
def find_security_group_rule_match(prototype, security_group_rules):
matches = [r for r in security_group_rules
if is_security_group_rule_match(prototype, r)]
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 matches[0]
else: # len(matches) == 0
return None
def is_security_group_rule_match(prototype, security_group_rule):
skip_keys = ['ether_type']
if 'ether_type' in prototype \
and security_group_rule['ethertype'] != prototype['ether_type']:
return False
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]:
if security_group_rule['port_range_max'] is not None:
return False
skip_keys.append('port_range_max')
if 'port_range_min' in prototype \
and prototype['port_range_min'] in [-1, 1]:
if security_group_rule['port_range_min'] is not None:
return False
skip_keys.append('port_range_min')
if all(security_group_rule[k] == prototype[k]
for k in (set(prototype.keys()) - set(skip_keys))):
return security_group_rule
else:
return None
update = {}
keep_security_group_rules = {}
create_security_group_rules = []
delete_security_group_rules = []
for prototype in self._generate_security_group_rules(security_group):
match = find_security_group_rule_match(
prototype, security_group.security_group_rules)
if match:
keep_security_group_rules[match['id']] = match
else:
create_security_group_rules.append(prototype)
for security_group_rule in security_group.security_group_rules:
if (security_group_rule['id']
not in keep_security_group_rules.keys()):
delete_security_group_rules.append(security_group_rule)
if create_security_group_rules:
update['create_security_group_rules'] = create_security_group_rules
if delete_security_group_rules:
update['delete_security_group_rules'] = delete_security_group_rules
return update
def _create(self):
kwargs = dict((k, self.params[k])
for k in ['description', 'name']
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(
name_or_id=project_name_or_id, ignore_missing=False)
kwargs['project_id'] = project.id
security_group = self.conn.network.create_security_group(**kwargs)
update = self._build_update_security_group_rules(security_group)
if update:
security_group = self._update_security_group_rules(security_group,
update)
return security_group
def _delete(self, security_group):
self.conn.network.delete_security_group(security_group.id)
def _find(self):
kwargs = dict(name_or_id=self.params['name'])
project_name_or_id = self.params['project']
if project_name_or_id is not None:
project = self.conn.identity.find_project(
name_or_id=project_name_or_id, ignore_missing=False)
kwargs['project_id'] = project.id
return self.conn.network.find_security_group(**kwargs)
def _generate_security_group_rules(self, security_group):
security_group_cache = {}
security_group_cache[security_group.name] = security_group
security_group_cache[security_group.id] = security_group
def _generate_security_group_rule(params):
prototype = dict(
(k, params[k])
for k in ['description', 'direction', 'remote_ip_prefix']
if params[k] is not None)
# When remote_ip_prefix is missing a netmask, then Neutron will add
# a netmask using Python library netaddr [0] and its IPNetwork
# class [1]. We do not want to introduce additional Python
# dependencies to our code base and neither want to replicate
# netaddr's parse_ip_network code here. So we do not handle
# remote_ip_prefix without a netmask and instead let Neutron handle
# it.
# [0] https://opendev.org/openstack/neutron/src/commit/\
# 43d94640568828f5e98bbb1e9df985ec3f1bb2d2/neutron/db/securitygroups_db.py#L775
# [1] https://github.com/netaddr/netaddr/blob/\
# b1d8f016abee00c8a93e35b928acdc22797c800a/netaddr/ip/__init__.py#L841
# [2] https://github.com/netaddr/netaddr/blob/\
# b1d8f016abee00c8a93e35b928acdc22797c800a/netaddr/ip/__init__.py#L773
prototype['project_id'] = security_group.project_id
prototype['security_group_id'] = security_group.id
remote_group_name_or_id = params['remote_group']
if remote_group_name_or_id is not None:
if remote_group_name_or_id in security_group_cache:
remote_group = \
security_group_cache[remote_group_name_or_id]
else:
remote_group = self.conn.network.find_security_group(
remote_group_name_or_id, ignore_missing=False)
security_group_cache[remote_group_name_or_id] = \
remote_group
prototype['remote_group_id'] = remote_group.id
ether_type = params['ether_type']
if ether_type is not None:
prototype['ether_type'] = ether_type
protocol = params['protocol']
if protocol is not None and protocol not in ['any', '0']:
prototype['protocol'] = protocol
port_range_max = params['port_range_max']
port_range_min = 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
return [_generate_security_group_rule(r)
for r in (self.params['security_group_rules'] or [])]
def _update(self, security_group, update):
security_group = self._update_security_group(security_group, update)
return self._update_security_group_rules(security_group, update)
def _update_security_group(self, security_group, update):
attributes = update.get('attributes')
if attributes:
security_group = self.conn.network.update_security_group(
security_group.id, **attributes)
return security_group
def _update_security_group_rules(self, security_group, update):
delete_security_group_rules = update.get('delete_security_group_rules')
if delete_security_group_rules:
for security_group_rule in delete_security_group_rules:
self.conn.network.\
delete_security_group_rule(security_group_rule['id'])
create_security_group_rules = update.get('create_security_group_rules')
if create_security_group_rules:
self.conn.network.\
create_security_group_rules(create_security_group_rules)
if create_security_group_rules or delete_security_group_rules:
# Update security group with created and deleted rules
return self.conn.network.get_security_group(security_group.id)
else:
return security_group
def _will_change(self, state, security_group):
if state == 'present' and not security_group:
return True
elif state == 'present' and security_group:
return bool(self._build_update(security_group))
elif state == 'absent' and security_group:
return True
else:
# state == 'absent' and not security_group:
return False
def main():
module = SecurityGroupModule()
module()
if __name__ == '__main__':
main()