#!/usr/bin/python #coding: utf-8 -*- # (c) 2013, Benno Joy # # This module is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This software is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this software. If not, see . try: import shade HAS_SHADE = True except ImportError: HAS_SHADE = False ANSIBLE_METADATA = {'status': ['preview'], 'supported_by': 'community', 'version': '1.0'} DOCUMENTATION = ''' --- module: os_subnet short_description: Add/Remove subnet to an OpenStack network extends_documentation_fragment: openstack version_added: "2.0" author: "Monty Taylor (@emonty)" description: - Add or Remove a subnet to an OpenStack network options: state: description: - Indicate desired state of the resource choices: ['present', 'absent'] required: false default: present network_name: description: - Name of the network to which the subnet should be attached - Required when I(state) is 'present' required: false 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 cidr: description: - The CIDR representation of the subnet that should be assigned to the subnet. Required when I(state) is 'present' required: false default: None ip_version: description: - The IP version of the subnet 4 or 6 required: false default: 4 enable_dhcp: description: - Whether DHCP should be enabled for this subnet. required: false default: true gateway_ip: description: - The ip that would be assigned to the gateway for this subnet required: false default: None no_gateway_ip: description: - The gateway IP would not be assigned for this subnet required: false default: false version_added: "2.2" dns_nameservers: description: - List of DNS nameservers for this subnet. required: false default: None allocation_pool_start: description: - From the subnet pool the starting address from which the IP should be allocated. required: false default: None allocation_pool_end: description: - From the subnet pool the last IP that should be assigned to the virtual machines. required: false default: None host_routes: description: - A list of host route dictionaries for the subnet. required: false default: None ipv6_ra_mode: description: - IPv6 router advertisement mode choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] required: false default: None ipv6_address_mode: description: - IPv6 address mode choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] required: false default: None project: description: - Project name or ID containing the subnet (name admin-only) required: false default: None version_added: "2.1" requirements: - "python >= 2.6" - "shade" ''' EXAMPLES = ''' # Create a new (or update an existing) subnet on the specified network - os_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 - os_subnet: state: absent name: net1subnet # Create an ipv6 stateless subnet - os_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 ''' def _can_update(subnet, module, cloud): """Check for differences in non-updatable values""" network_name = module.params['network_name'] cidr = module.params['cidr'] ip_version = int(module.params['ip_version']) ipv6_ra_mode = module.params['ipv6_ra_mode'] ipv6_a_mode = module.params['ipv6_address_mode'] if network_name: network = cloud.get_network(network_name) if network: netid = network['id'] else: module.fail_json(msg='No network found for %s' % network_name) if netid != subnet['network_id']: module.fail_json(msg='Cannot update network_name in existing \ subnet') if ip_version and subnet['ip_version'] != ip_version: module.fail_json(msg='Cannot update ip_version in existing subnet') if ipv6_ra_mode and subnet.get('ipv6_ra_mode', None) != ipv6_ra_mode: module.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: module.fail_json(msg='Cannot update ipv6_address_mode in existing \ subnet') def _needs_update(subnet, module, cloud): """Check for differences in the updatable values.""" # First check if we are trying to update something we're not allowed to _can_update(subnet, module, cloud) # now check for the things we are allowed to update enable_dhcp = module.params['enable_dhcp'] subnet_name = module.params['name'] pool_start = module.params['allocation_pool_start'] pool_end = module.params['allocation_pool_end'] gateway_ip = module.params['gateway_ip'] no_gateway_ip = module.params['no_gateway_ip'] dns = module.params['dns_nameservers'] host_routes = module.params['host_routes'] curr_pool = subnet['allocation_pools'][0] if subnet['enable_dhcp'] != enable_dhcp: return True if subnet_name and subnet['name'] != subnet_name: return True if pool_start and curr_pool['start'] != pool_start: return True if pool_end and curr_pool['end'] != pool_end: return True if gateway_ip and subnet['gateway_ip'] != gateway_ip: return True if dns and sorted(subnet['dns_nameservers']) != sorted(dns): return True 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 sorted(curr_hr) != sorted(new_hr): return True if no_gateway_ip and subnet['gateway_ip']: return True return False def _system_state_change(module, subnet, cloud): state = module.params['state'] if state == 'present': if not subnet: return True return _needs_update(subnet, module, cloud) if state == 'absent' and subnet: return True return False def main(): ipv6_mode_choices = ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] argument_spec = openstack_full_argument_spec( name=dict(required=True), network_name=dict(default=None), cidr=dict(default=None), ip_version=dict(default='4', choices=['4', '6']), enable_dhcp=dict(default='true', type='bool'), gateway_ip=dict(default=None), no_gateway_ip=dict(default=False, type='bool'), dns_nameservers=dict(default=None, type='list'), allocation_pool_start=dict(default=None), allocation_pool_end=dict(default=None), host_routes=dict(default=None, type='list'), ipv6_ra_mode=dict(default=None, choice=ipv6_mode_choices), ipv6_address_mode=dict(default=None, choice=ipv6_mode_choices), state=dict(default='present', choices=['absent', 'present']), project=dict(default=None) ) module_kwargs = openstack_module_kwargs() module = AnsibleModule(argument_spec, supports_check_mode=True, **module_kwargs) if not HAS_SHADE: module.fail_json(msg='shade is required for this module') state = module.params['state'] network_name = module.params['network_name'] cidr = module.params['cidr'] ip_version = module.params['ip_version'] enable_dhcp = module.params['enable_dhcp'] subnet_name = module.params['name'] gateway_ip = module.params['gateway_ip'] no_gateway_ip = module.params['no_gateway_ip'] dns = module.params['dns_nameservers'] pool_start = module.params['allocation_pool_start'] pool_end = module.params['allocation_pool_end'] host_routes = module.params['host_routes'] ipv6_ra_mode = module.params['ipv6_ra_mode'] ipv6_a_mode = module.params['ipv6_address_mode'] project = module.params.pop('project') # Check for required parameters when state == 'present' if state == 'present': for p in ['network_name', 'cidr']: if not module.params[p]: module.fail_json(msg='%s required with present state' % p) if pool_start and pool_end: pool = [dict(start=pool_start, end=pool_end)] elif pool_start or pool_end: module.fail_json(msg='allocation pool requires start and end values') else: pool = None if no_gateway_ip and gateway_ip: module.fail_json(msg='no_gateway_ip is not allowed with gateway_ip') try: cloud = shade.openstack_cloud(**module.params) if project is not None: proj = cloud.get_project(project) if proj is None: module.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 subnet = cloud.get_subnet(subnet_name, filters=filters) if module.check_mode: module.exit_json(changed=_system_state_change(module, subnet, cloud)) if state == 'present': if not subnet: subnet = cloud.create_subnet(network_name, 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) changed = True else: if _needs_update(subnet, module, cloud): cloud.update_subnet(subnet['id'], subnet_name=subnet_name, enable_dhcp=enable_dhcp, gateway_ip=gateway_ip, disable_gateway_ip=no_gateway_ip, dns_nameservers=dns, allocation_pools=pool, host_routes=host_routes) changed = True else: changed = False module.exit_json(changed=changed, subnet=subnet, id=subnet['id']) elif state == 'absent': if not subnet: changed = False else: changed = True cloud.delete_subnet(subnet_name) module.exit_json(changed=changed) except shade.OpenStackCloudException as e: module.fail_json(msg=str(e)) # this is magic, see lib/ansible/module_common.py from ansible.module_utils.basic import * from ansible.module_utils.openstack import * if __name__ == '__main__': main()