#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2013, Benno Joy # 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=[ ('cidr', '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()