e0139fe940
Specifying CIDR during creation of subnet from subnet pool is a valid operation. Moreover, in case of use of a subnet pool with multiple subnets, cidr is a mandatory paramter for creating subnet. Following code should be valid: - name: Create subnet openstack.cloud.subnet: name: "subnet_name" network: "some_network" gateway_ip: "192.168.0.1" allocation_pool_start: "192.168.0.2" allocation_pool_end: "192.168.0.254" cidr: "192.168.0.0/24" ip_version: 4 subnet_pool: "192.168.0.0/24" This scenario is added as a subnet-pool.yaml test in the test role. Change-Id: I1163ba34ac3079f76dd0b7477a80a2135985a650
469 lines
16 KiB
Python
469 lines
16 KiB
Python
#!/usr/bin/python
|
|
# -*- 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)
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: subnet
|
|
short_description: Add/Remove subnet to an OpenStack network
|
|
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
|
|
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
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# Create a new (or update an existing) subnet on the specified network
|
|
- openstack.cloud.subnet:
|
|
state: present
|
|
network_name: network1
|
|
name: net1subnet
|
|
cidr: 192.168.0.0/24
|
|
dns_nameservers:
|
|
- 8.8.8.7
|
|
- 8.8.8.8
|
|
host_routes:
|
|
- destination: 0.0.0.0/0
|
|
nexthop: 12.34.56.78
|
|
- destination: 192.168.0.0/24
|
|
nexthop: 192.168.0.1
|
|
|
|
# Delete a subnet
|
|
- openstack.cloud.subnet:
|
|
state: absent
|
|
name: net1subnet
|
|
|
|
# Create an ipv6 stateless subnet
|
|
- openstack.cloud.subnet:
|
|
state: present
|
|
name: intv6
|
|
network_name: internal
|
|
ip_version: 6
|
|
cidr: 2db8:1::/64
|
|
dns_nameservers:
|
|
- 2001:4860:4860::8888
|
|
- 2001:4860:4860::8844
|
|
ipv6_ra_mode: dhcpv6-stateless
|
|
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(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_if=[
|
|
('state', 'present', ('network',)),
|
|
('state', 'present',
|
|
('cidr', 'use_default_subnet_pool', 'subnet_pool'), True),
|
|
],
|
|
mutually_exclusive=[
|
|
('use_default_subnet_pool', 'subnet_pool')
|
|
]
|
|
)
|
|
|
|
# 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)
|
|
|
|
def _build_pool(self):
|
|
pool_start = self.params['allocation_pool_start']
|
|
pool_end = self.params['allocation_pool_end']
|
|
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_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']
|
|
disable_gateway_ip = self.params['disable_gateway_ip']
|
|
|
|
# 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_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, network, project, subnet_pool))
|
|
|
|
changed = False
|
|
if state == 'present':
|
|
params = self._build_params(network, project, subnet_pool)
|
|
if subnet is None:
|
|
subnet = self.conn.network.create_subnet(**params)
|
|
changed = True
|
|
else:
|
|
updates = self._build_updates(subnet, params)
|
|
if updates:
|
|
self._validate_update(subnet, updates)
|
|
subnet = self.conn.network.update_subnet(subnet, **updates)
|
|
changed = True
|
|
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():
|
|
module = SubnetModule()
|
|
module()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|