ansible-collections-openstack/plugins/modules/security_group.py
Will Szumski 0aedc268f1 Adds stateful parameter to security groups
This is a missing option.

Change-Id: Ic7b43093d9c35de8962978e9ee108cf7b5379fcd
2023-09-01 17:53:32 +00:00

579 lines
22 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.
- When I(security_group_rules) is not set, 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
stateful:
description:
- Should the resource be stateful or stateless.
type: bool
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: Create a stateless security group
openstack.cloud.security_group:
cloud: mordred
state: present
stateful: false
name: foo
description: stateless 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']),
stateful=dict(type="bool"),
)
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):
if self.params['security_group_rules'] is None:
# Consider a change of security group rules only when option
# 'security_group_rules' was defined explicitly, because undefined
# options in our Ansible modules denote "apply no change"
return {}
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', 'stateful']
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()