summaryrefslogtreecommitdiffstats
path: root/ansible_collections/openstack/cloud/plugins/modules/subnet.py
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/openstack/cloud/plugins/modules/subnet.py')
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/subnet.py652
1 files changed, 378 insertions, 274 deletions
diff --git a/ansible_collections/openstack/cloud/plugins/modules/subnet.py b/ansible_collections/openstack/cloud/plugins/modules/subnet.py
index dfe1eaca3..d8da4b5db 100644
--- a/ansible_collections/openstack/cloud/plugins/modules/subnet.py
+++ b/ansible_collections/openstack/cloud/plugins/modules/subnet.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-# coding: utf-8 -*-
+# -*- coding: utf-8 -*-
# (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)
@@ -12,108 +12,121 @@ author: OpenStack Ansible SIG
description:
- Add or Remove a subnet to an OpenStack network
options:
- state:
- description:
- - Indicate desired state of the resource
- choices: ['present', 'absent']
- default: present
- type: str
- network_name:
- description:
- - Name of the network to which the subnet should be attached
- - Required when I(state) is 'present'
- type: str
- name:
- description:
- - The name of the subnet that should be created. Although Neutron
- allows for non-unique subnet names, this module enforces subnet
- name uniqueness.
- required: true
- type: str
- cidr:
- description:
- - The CIDR representation of the subnet that should be assigned to
- the subnet. Required when I(state) is 'present' and a subnetpool
- is not specified.
- type: str
- ip_version:
- description:
- - The IP version of the subnet 4 or 6
- default: '4'
- type: str
- choices: ['4', '6']
- enable_dhcp:
- description:
- - Whether DHCP should be enabled for this subnet.
- type: bool
- default: 'yes'
- gateway_ip:
- description:
- - The ip that would be assigned to the gateway for this subnet
- type: str
- no_gateway_ip:
- description:
- - The gateway IP would not be assigned for this subnet
- type: bool
- default: 'no'
- dns_nameservers:
- description:
- - List of DNS nameservers for this subnet.
- type: list
- elements: str
- allocation_pool_start:
- description:
- - From the subnet pool the starting address from which the IP should
- be allocated.
- type: str
- allocation_pool_end:
- description:
- - From the subnet pool the last IP that should be assigned to the
- virtual machines.
- type: str
- host_routes:
- description:
- - A list of host route dictionaries for the subnet.
- type: list
- elements: dict
- suboptions:
- destination:
- description: The destination network (CIDR).
- type: str
- required: true
- nexthop:
- description: The next hop (aka gateway) for the I(destination).
- type: str
- required: true
- ipv6_ra_mode:
- description:
- - IPv6 router advertisement mode
- choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac']
- type: str
- ipv6_address_mode:
- description:
- - IPv6 address mode
- choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac']
- type: str
- use_default_subnetpool:
- description:
- - Use the default subnetpool for I(ip_version) to obtain a CIDR.
- type: bool
- default: 'no'
- project:
- description:
- - Project name or ID containing the subnet (name admin-only)
- type: str
- extra_specs:
- description:
- - Dictionary with extra key/value pairs passed to the API
- required: false
- default: {}
- type: dict
-requirements:
- - "python >= 3.6"
- - "openstacksdk"
-
+ state:
+ description:
+ - Indicate desired state of the resource
+ choices: ['present', 'absent']
+ default: present
+ type: str
+ allocation_pool_start:
+ description:
+ - From the subnet pool the starting address from which the IP
+ should be allocated.
+ type: str
+ allocation_pool_end:
+ description:
+ - From the subnet pool the last IP that should be assigned to the
+ virtual machines.
+ type: str
+ cidr:
+ description:
+ - The CIDR representation of the subnet that should be assigned to
+ the subnet. Required when I(state) is 'present' and a subnetpool
+ is not specified.
+ type: str
+ description:
+ description:
+ - Description of the subnet
+ type: str
+ disable_gateway_ip:
+ description:
+ - The gateway IP would not be assigned for this subnet
+ type: bool
+ aliases: ['no_gateway_ip']
+ default: 'false'
+ dns_nameservers:
+ description:
+ - List of DNS nameservers for this subnet.
+ type: list
+ elements: str
+ extra_attrs:
+ description:
+ - Dictionary with extra key/value pairs passed to the API
+ required: false
+ aliases: ['extra_specs']
+ default: {}
+ type: dict
+ host_routes:
+ description:
+ - A list of host route dictionaries for the subnet.
+ type: list
+ elements: dict
+ suboptions:
+ destination:
+ description: The destination network (CIDR).
+ type: str
+ required: true
+ nexthop:
+ description: The next hop (aka gateway) for the I(destination).
+ type: str
+ required: true
+ gateway_ip:
+ description:
+ - The ip that would be assigned to the gateway for this subnet
+ type: str
+ ip_version:
+ description:
+ - The IP version of the subnet 4 or 6
+ default: 4
+ type: int
+ choices: [4, 6]
+ is_dhcp_enabled:
+ description:
+ - Whether DHCP should be enabled for this subnet.
+ type: bool
+ aliases: ['enable_dhcp']
+ default: 'true'
+ ipv6_ra_mode:
+ description:
+ - IPv6 router advertisement mode
+ choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac']
+ type: str
+ ipv6_address_mode:
+ description:
+ - IPv6 address mode
+ choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac']
+ type: str
+ name:
+ description:
+ - The name of the subnet that should be created. Although Neutron
+ allows for non-unique subnet names, this module enforces subnet
+ name uniqueness.
+ required: true
+ type: str
+ network:
+ description:
+ - Name or id of the network to which the subnet should be attached
+ - Required when I(state) is 'present'
+ aliases: ['network_name']
+ type: str
+ project:
+ description:
+ - Project name or ID containing the subnet (name admin-only)
+ type: str
+ prefix_length:
+ description:
+ - The prefix length to use for subnet allocation from a subnet pool
+ type: str
+ use_default_subnet_pool:
+ description:
+ - Use the default subnetpool for I(ip_version) to obtain a CIDR.
+ type: bool
+ aliases: ['use_default_subnetpool']
+ subnet_pool:
+ description:
+ - The subnet pool name or ID from which to obtain a CIDR
+ type: str
+ required: false
extends_documentation_fragment:
- openstack.cloud.openstack
'''
@@ -153,206 +166,297 @@ EXAMPLES = '''
ipv6_address_mode: dhcpv6-stateless
'''
+RETURN = '''
+id:
+ description: Id of subnet
+ returned: On success when subnet exists.
+ type: str
+subnet:
+ description: Dictionary describing the subnet.
+ returned: On success when subnet exists.
+ type: dict
+ contains:
+ allocation_pools:
+ description: Allocation pools associated with this subnet.
+ returned: success
+ type: list
+ elements: dict
+ cidr:
+ description: Subnet's CIDR.
+ returned: success
+ type: str
+ created_at:
+ description: Created at timestamp
+ type: str
+ description:
+ description: Description
+ type: str
+ dns_nameservers:
+ description: DNS name servers for this subnet.
+ returned: success
+ type: list
+ elements: str
+ dns_publish_fixed_ip:
+ description: Whether to publish DNS records for fixed IPs.
+ returned: success
+ type: bool
+ gateway_ip:
+ description: Subnet's gateway ip.
+ returned: success
+ type: str
+ host_routes:
+ description: A list of host routes.
+ returned: success
+ type: str
+ id:
+ description: Unique UUID.
+ returned: success
+ type: str
+ ip_version:
+ description: IP version for this subnet.
+ returned: success
+ type: int
+ ipv6_address_mode:
+ description: |
+ The IPv6 address modes which are 'dhcpv6-stateful',
+ 'dhcpv6-stateless' or 'slaac'.
+ returned: success
+ type: str
+ ipv6_ra_mode:
+ description: |
+ The IPv6 router advertisements modes which can be 'slaac',
+ 'dhcpv6-stateful', 'dhcpv6-stateless'.
+ returned: success
+ type: str
+ is_dhcp_enabled:
+ description: DHCP enable flag for this subnet.
+ returned: success
+ type: bool
+ name:
+ description: Name given to the subnet.
+ returned: success
+ type: str
+ network_id:
+ description: Network ID this subnet belongs in.
+ returned: success
+ type: str
+ prefix_length:
+ description: |
+ The prefix length to use for subnet allocation from a subnet
+ pool.
+ returned: success
+ type: str
+ project_id:
+ description: Project id associated with this subnet.
+ returned: success
+ type: str
+ revision_number:
+ description: Revision number of the resource
+ returned: success
+ type: int
+ segment_id:
+ description: The ID of the segment this subnet is associated with.
+ returned: success
+ type: str
+ service_types:
+ description: Service types for this subnet
+ returned: success
+ type: list
+ subnet_pool_id:
+ description: The subnet pool ID from which to obtain a CIDR.
+ returned: success
+ type: str
+ tags:
+ description: Tags
+ type: str
+ updated_at:
+ description: Timestamp when the subnet was last updated.
+ returned: success
+ type: str
+ use_default_subnet_pool:
+ description: |
+ Whether to use the default subnet pool to obtain a CIDR.
+ returned: success
+ type: bool
+'''
+
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
class SubnetModule(OpenStackModule):
ipv6_mode_choices = ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac']
argument_spec = dict(
- name=dict(type='str', required=True),
- network_name=dict(type='str'),
- cidr=dict(type='str'),
- ip_version=dict(type='str', default='4', choices=['4', '6']),
- enable_dhcp=dict(type='bool', default=True),
- gateway_ip=dict(type='str'),
- no_gateway_ip=dict(type='bool', default=False),
- dns_nameservers=dict(type='list', default=None, elements='str'),
- allocation_pool_start=dict(type='str'),
- allocation_pool_end=dict(type='str'),
- host_routes=dict(type='list', default=None, elements='dict'),
- ipv6_ra_mode=dict(type='str', choices=ipv6_mode_choices),
- ipv6_address_mode=dict(type='str', choices=ipv6_mode_choices),
- use_default_subnetpool=dict(type='bool', default=False),
- extra_specs=dict(type='dict', default=dict()),
- state=dict(type='str', default='present', choices=['absent', 'present']),
- project=dict(type='str'),
+ name=dict(required=True),
+ network=dict(aliases=['network_name']),
+ cidr=dict(),
+ description=dict(),
+ ip_version=dict(type='int', default=4, choices=[4, 6]),
+ is_dhcp_enabled=dict(type='bool', default=True,
+ aliases=['enable_dhcp']),
+ gateway_ip=dict(),
+ disable_gateway_ip=dict(
+ type='bool', default=False, aliases=['no_gateway_ip']),
+ dns_nameservers=dict(type='list', elements='str'),
+ allocation_pool_start=dict(),
+ allocation_pool_end=dict(),
+ host_routes=dict(type='list', elements='dict'),
+ ipv6_ra_mode=dict(choices=ipv6_mode_choices),
+ ipv6_address_mode=dict(choices=ipv6_mode_choices),
+ subnet_pool=dict(),
+ prefix_length=dict(),
+ use_default_subnet_pool=dict(
+ type='bool', aliases=['use_default_subnetpool']),
+ extra_attrs=dict(type='dict', default=dict(), aliases=['extra_specs']),
+ state=dict(default='present',
+ choices=['absent', 'present']),
+ project=dict(),
)
module_kwargs = dict(
supports_check_mode=True,
- required_together=[['allocation_pool_end', 'allocation_pool_start']]
+ required_together=[['allocation_pool_end', 'allocation_pool_start']],
+ required_if=[
+ ('state', 'present', ('network',)),
+ ('state', 'present',
+ ('cidr', 'use_default_subnet_pool', 'subnet_pool'), True),
+ ],
+ mutually_exclusive=[
+ ('cidr', 'use_default_subnet_pool', 'subnet_pool')
+ ]
)
- def _can_update(self, subnet, filters=None):
- """Check for differences in non-updatable values"""
- network_name = self.params['network_name']
- ip_version = int(self.params['ip_version'])
- ipv6_ra_mode = self.params['ipv6_ra_mode']
- ipv6_a_mode = self.params['ipv6_address_mode']
-
- if network_name:
- network = self.conn.get_network(network_name, filters)
- if network:
- netid = network['id']
- if netid != subnet['network_id']:
- self.fail_json(msg='Cannot update network_name in existing subnet')
- else:
- self.fail_json(msg='No network found for %s' % network_name)
-
- if ip_version and subnet['ip_version'] != ip_version:
- self.fail_json(msg='Cannot update ip_version in existing subnet')
- if ipv6_ra_mode and subnet.get('ipv6_ra_mode', None) != ipv6_ra_mode:
- self.fail_json(msg='Cannot update ipv6_ra_mode in existing subnet')
- if ipv6_a_mode and subnet.get('ipv6_address_mode', None) != ipv6_a_mode:
- self.fail_json(msg='Cannot update ipv6_address_mode in existing subnet')
-
- def _needs_update(self, subnet, filters=None):
- """Check for differences in the updatable values."""
-
- # First check if we are trying to update something we're not allowed to
- self._can_update(subnet, filters)
+ # resource attributes obtainable directly from params
+ attr_params = ('cidr', 'description',
+ 'dns_nameservers', 'gateway_ip', 'host_routes',
+ 'ip_version', 'ipv6_address_mode', 'ipv6_ra_mode',
+ 'is_dhcp_enabled', 'name', 'prefix_length',
+ 'use_default_subnet_pool',)
+
+ def _validate_update(self, subnet, update):
+ """ Check for differences in non-updatable values """
+ # Ref.: https://docs.openstack.org/api-ref/network/v2/index.html#update-subnet
+ for attr in ('cidr', 'ip_version', 'ipv6_ra_mode', 'ipv6_address_mode',
+ 'prefix_length', 'use_default_subnet_pool'):
+ if attr in update and update[attr] != subnet[attr]:
+ self.fail_json(
+ msg='Cannot update {0} in existing subnet'.format(attr))
+
+ def _system_state_change(self, subnet, network, project, subnet_pool):
+ state = self.params['state']
+ if state == 'absent':
+ return subnet is not None
+ # else state is present
+ if not subnet:
+ return True
+ params = self._build_params(network, project, subnet_pool)
+ updates = self._build_updates(subnet, params)
+ self._validate_update(subnet, updates)
+ return bool(updates)
- # now check for the things we are allowed to update
- enable_dhcp = self.params['enable_dhcp']
- subnet_name = self.params['name']
+ def _build_pool(self):
pool_start = self.params['allocation_pool_start']
pool_end = self.params['allocation_pool_end']
- gateway_ip = self.params['gateway_ip']
- no_gateway_ip = self.params['no_gateway_ip']
- dns = self.params['dns_nameservers']
- host_routes = self.params['host_routes']
- if pool_start and pool_end:
- pool = dict(start=pool_start, end=pool_end)
- else:
- pool = None
-
- changes = dict()
- if subnet['enable_dhcp'] != enable_dhcp:
- changes['enable_dhcp'] = enable_dhcp
- if subnet_name and subnet['name'] != subnet_name:
- changes['subnet_name'] = subnet_name
- if pool and (not subnet['allocation_pools'] or subnet['allocation_pools'] != [pool]):
- changes['allocation_pools'] = [pool]
- if gateway_ip and subnet['gateway_ip'] != gateway_ip:
- changes['gateway_ip'] = gateway_ip
- if dns and sorted(subnet['dns_nameservers']) != sorted(dns):
- changes['dns_nameservers'] = dns
- if host_routes:
- curr_hr = sorted(subnet['host_routes'], key=lambda t: t.keys())
- new_hr = sorted(host_routes, key=lambda t: t.keys())
- if curr_hr != new_hr:
- changes['host_routes'] = host_routes
- if no_gateway_ip and subnet['gateway_ip']:
- changes['disable_gateway_ip'] = no_gateway_ip
- return changes
-
- def _system_state_change(self, subnet, filters=None):
- state = self.params['state']
- if state == 'present':
- if not subnet:
- return True
- return bool(self._needs_update(subnet, filters))
- if state == 'absent' and subnet:
- return True
- return False
+ if pool_start:
+ return [dict(start=pool_start, end=pool_end)]
+ return None
+
+ def _build_params(self, network, project, subnet_pool):
+ params = {attr: self.params[attr] for attr in self.attr_params}
+ params['network_id'] = network.id
+ if project:
+ params['project_id'] = project.id
+ if subnet_pool:
+ params['subnet_pool_id'] = subnet_pool.id
+ params['allocation_pools'] = self._build_pool()
+ params = self._add_extra_attrs(params)
+ params = {k: v for k, v in params.items() if v is not None}
+ return params
+
+ def _build_updates(self, subnet, params):
+ # Sort lists before doing comparisons comparisons
+ if 'dns_nameservers' in params:
+ params['dns_nameservers'].sort()
+ subnet['dns_nameservers'].sort()
+
+ if 'host_routes' in params:
+ params['host_routes'].sort(key=lambda r: sorted(r.items()))
+ subnet['host_routes'].sort(key=lambda r: sorted(r.items()))
+
+ updates = {k: params[k] for k in params if params[k] != subnet[k]}
+ if self.params['disable_gateway_ip'] and subnet.gateway_ip:
+ updates['gateway_ip'] = None
+ return updates
+
+ def _add_extra_attrs(self, params):
+ duplicates = set(self.params['extra_attrs']) & set(params)
+ if duplicates:
+ self.fail_json(msg='Duplicate key(s) {0} in extra_specs'
+ .format(list(duplicates)))
+ params.update(self.params['extra_attrs'])
+ return params
def run(self):
-
state = self.params['state']
- network_name = self.params['network_name']
- cidr = self.params['cidr']
- ip_version = self.params['ip_version']
- enable_dhcp = self.params['enable_dhcp']
+ network_name_or_id = self.params['network']
+ project_name_or_id = self.params['project']
+ subnet_pool_name_or_id = self.params['subnet_pool']
subnet_name = self.params['name']
gateway_ip = self.params['gateway_ip']
- no_gateway_ip = self.params['no_gateway_ip']
- dns = self.params['dns_nameservers']
- pool_start = self.params['allocation_pool_start']
- pool_end = self.params['allocation_pool_end']
- host_routes = self.params['host_routes']
- ipv6_ra_mode = self.params['ipv6_ra_mode']
- ipv6_a_mode = self.params['ipv6_address_mode']
- use_default_subnetpool = self.params['use_default_subnetpool']
- project = self.params.pop('project')
- extra_specs = self.params['extra_specs']
-
- # Check for required parameters when state == 'present'
- if state == 'present':
- if not self.params['network_name']:
- self.fail(msg='network_name required with present state')
- if (
- not self.params['cidr']
- and not use_default_subnetpool
- and not extra_specs.get('subnetpool_id', False)
- ):
- self.fail(msg='cidr or use_default_subnetpool or '
- 'subnetpool_id required with present state')
-
- if pool_start and pool_end:
- pool = [dict(start=pool_start, end=pool_end)]
- else:
- pool = None
-
- if no_gateway_ip and gateway_ip:
- self.fail_json(msg='no_gateway_ip is not allowed with gateway_ip')
+ disable_gateway_ip = self.params['disable_gateway_ip']
- if project is not None:
- proj = self.conn.get_project(project)
- if proj is None:
- self.fail_json(msg='Project %s could not be found' % project)
- project_id = proj['id']
- filters = {'tenant_id': project_id}
- else:
- project_id = None
- filters = None
+ # fail early if incompatible options have been specified
+ if disable_gateway_ip and gateway_ip:
+ self.fail_json(msg='no_gateway_ip is not allowed with gateway_ip')
- subnet = self.conn.get_subnet(subnet_name, filters=filters)
+ subnet_pool_filters = {}
+ filters = {}
+
+ project = None
+ if project_name_or_id:
+ project = self.conn.identity.find_project(project_name_or_id,
+ ignore_missing=False)
+ subnet_pool_filters['project_id'] = project.id
+ filters['project_id'] = project.id
+
+ network = None
+ if network_name_or_id:
+ # At this point filters can only contain project_id
+ network = self.conn.network.find_network(network_name_or_id,
+ ignore_missing=False,
+ **filters)
+ filters['network_id'] = network.id
+
+ subnet_pool = None
+ if subnet_pool_name_or_id:
+ subnet_pool = self.conn.network.find_subnet_pool(
+ subnet_pool_name_or_id,
+ ignore_missing=False,
+ **subnet_pool_filters)
+ filters['subnet_pool_id'] = subnet_pool.id
+
+ subnet = self.conn.network.find_subnet(subnet_name, **filters)
if self.ansible.check_mode:
- self.exit_json(changed=self._system_state_change(subnet, filters))
+ self.exit_json(changed=self._system_state_change(
+ subnet, network, project, subnet_pool))
+ changed = False
if state == 'present':
- if not subnet:
- kwargs = dict(
- cidr=cidr,
- ip_version=ip_version,
- enable_dhcp=enable_dhcp,
- subnet_name=subnet_name,
- gateway_ip=gateway_ip,
- disable_gateway_ip=no_gateway_ip,
- dns_nameservers=dns,
- allocation_pools=pool,
- host_routes=host_routes,
- ipv6_ra_mode=ipv6_ra_mode,
- ipv6_address_mode=ipv6_a_mode,
- tenant_id=project_id)
- dup_args = set(kwargs.keys()) & set(extra_specs.keys())
- if dup_args:
- raise ValueError('Duplicate key(s) {0} in extra_specs'
- .format(list(dup_args)))
- if use_default_subnetpool:
- kwargs['use_default_subnetpool'] = use_default_subnetpool
- kwargs = dict(kwargs, **extra_specs)
- subnet = self.conn.create_subnet(network_name, **kwargs)
+ params = self._build_params(network, project, subnet_pool)
+ if subnet is None:
+ subnet = self.conn.network.create_subnet(**params)
changed = True
else:
- changes = self._needs_update(subnet, filters)
- if changes:
- subnet = self.conn.update_subnet(subnet['id'], **changes)
+ updates = self._build_updates(subnet, params)
+ if updates:
+ self._validate_update(subnet, updates)
+ subnet = self.conn.network.update_subnet(subnet, **updates)
changed = True
- else:
- changed = False
- self.exit_json(changed=changed,
- subnet=subnet,
- id=subnet['id'])
-
- elif state == 'absent':
- if not subnet:
- changed = False
- else:
- changed = True
- self.conn.delete_subnet(subnet_name)
- self.exit_json(changed=changed)
+ self.exit_json(changed=changed, subnet=subnet, id=subnet.id)
+ elif state == 'absent' and subnet is not None:
+ self.conn.network.delete_subnet(subnet)
+ changed = True
+ self.exit_json(changed=changed)
def main():