1#!/usr/bin/python 2# coding: utf-8 -*- 3 4# (c) 2013, Benno Joy <benno@ansible.com> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11ANSIBLE_METADATA = {'metadata_version': '1.1', 12 'status': ['preview'], 13 'supported_by': 'community'} 14 15 16DOCUMENTATION = ''' 17--- 18module: os_subnet 19short_description: Add/Remove subnet to an OpenStack network 20extends_documentation_fragment: openstack 21version_added: "2.0" 22author: "Monty Taylor (@emonty)" 23description: 24 - Add or Remove a subnet to an OpenStack network 25options: 26 state: 27 description: 28 - Indicate desired state of the resource 29 choices: ['present', 'absent'] 30 default: present 31 network_name: 32 description: 33 - Name of the network to which the subnet should be attached 34 - Required when I(state) is 'present' 35 name: 36 description: 37 - The name of the subnet that should be created. Although Neutron 38 allows for non-unique subnet names, this module enforces subnet 39 name uniqueness. 40 required: true 41 cidr: 42 description: 43 - The CIDR representation of the subnet that should be assigned to 44 the subnet. Required when I(state) is 'present' and a subnetpool 45 is not specified. 46 ip_version: 47 description: 48 - The IP version of the subnet 4 or 6 49 default: 4 50 enable_dhcp: 51 description: 52 - Whether DHCP should be enabled for this subnet. 53 type: bool 54 default: 'yes' 55 gateway_ip: 56 description: 57 - The ip that would be assigned to the gateway for this subnet 58 no_gateway_ip: 59 description: 60 - The gateway IP would not be assigned for this subnet 61 type: bool 62 default: 'no' 63 version_added: "2.2" 64 dns_nameservers: 65 description: 66 - List of DNS nameservers for this subnet. 67 allocation_pool_start: 68 description: 69 - From the subnet pool the starting address from which the IP should 70 be allocated. 71 allocation_pool_end: 72 description: 73 - From the subnet pool the last IP that should be assigned to the 74 virtual machines. 75 host_routes: 76 description: 77 - A list of host route dictionaries for the subnet. 78 ipv6_ra_mode: 79 description: 80 - IPv6 router advertisement mode 81 choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] 82 ipv6_address_mode: 83 description: 84 - IPv6 address mode 85 choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] 86 use_default_subnetpool: 87 description: 88 - Use the default subnetpool for I(ip_version) to obtain a CIDR. 89 type: bool 90 default: 'no' 91 project: 92 description: 93 - Project name or ID containing the subnet (name admin-only) 94 version_added: "2.1" 95 availability_zone: 96 description: 97 - Ignored. Present for backwards compatibility 98 extra_specs: 99 description: 100 - Dictionary with extra key/value pairs passed to the API 101 required: false 102 default: {} 103 version_added: "2.7" 104requirements: 105 - "python >= 2.7" 106 - "openstacksdk" 107''' 108 109EXAMPLES = ''' 110# Create a new (or update an existing) subnet on the specified network 111- os_subnet: 112 state: present 113 network_name: network1 114 name: net1subnet 115 cidr: 192.168.0.0/24 116 dns_nameservers: 117 - 8.8.8.7 118 - 8.8.8.8 119 host_routes: 120 - destination: 0.0.0.0/0 121 nexthop: 12.34.56.78 122 - destination: 192.168.0.0/24 123 nexthop: 192.168.0.1 124 125# Delete a subnet 126- os_subnet: 127 state: absent 128 name: net1subnet 129 130# Create an ipv6 stateless subnet 131- os_subnet: 132 state: present 133 name: intv6 134 network_name: internal 135 ip_version: 6 136 cidr: 2db8:1::/64 137 dns_nameservers: 138 - 2001:4860:4860::8888 139 - 2001:4860:4860::8844 140 ipv6_ra_mode: dhcpv6-stateless 141 ipv6_address_mode: dhcpv6-stateless 142''' 143 144from ansible.module_utils.basic import AnsibleModule 145from ansible.module_utils.openstack import openstack_full_argument_spec, openstack_module_kwargs, openstack_cloud_from_module 146 147 148def _can_update(subnet, module, cloud, filters=None): 149 """Check for differences in non-updatable values""" 150 network_name = module.params['network_name'] 151 ip_version = int(module.params['ip_version']) 152 ipv6_ra_mode = module.params['ipv6_ra_mode'] 153 ipv6_a_mode = module.params['ipv6_address_mode'] 154 155 if network_name: 156 network = cloud.get_network(network_name, filters) 157 if network: 158 netid = network['id'] 159 else: 160 module.fail_json(msg='No network found for %s' % network_name) 161 if netid != subnet['network_id']: 162 module.fail_json(msg='Cannot update network_name in existing \ 163 subnet') 164 if ip_version and subnet['ip_version'] != ip_version: 165 module.fail_json(msg='Cannot update ip_version in existing subnet') 166 if ipv6_ra_mode and subnet.get('ipv6_ra_mode', None) != ipv6_ra_mode: 167 module.fail_json(msg='Cannot update ipv6_ra_mode in existing subnet') 168 if ipv6_a_mode and subnet.get('ipv6_address_mode', None) != ipv6_a_mode: 169 module.fail_json(msg='Cannot update ipv6_address_mode in existing \ 170 subnet') 171 172 173def _needs_update(subnet, module, cloud, filters=None): 174 """Check for differences in the updatable values.""" 175 176 # First check if we are trying to update something we're not allowed to 177 _can_update(subnet, module, cloud, filters) 178 179 # now check for the things we are allowed to update 180 enable_dhcp = module.params['enable_dhcp'] 181 subnet_name = module.params['name'] 182 pool_start = module.params['allocation_pool_start'] 183 pool_end = module.params['allocation_pool_end'] 184 gateway_ip = module.params['gateway_ip'] 185 no_gateway_ip = module.params['no_gateway_ip'] 186 dns = module.params['dns_nameservers'] 187 host_routes = module.params['host_routes'] 188 curr_pool = subnet['allocation_pools'][0] 189 190 if subnet['enable_dhcp'] != enable_dhcp: 191 return True 192 if subnet_name and subnet['name'] != subnet_name: 193 return True 194 if pool_start and curr_pool['start'] != pool_start: 195 return True 196 if pool_end and curr_pool['end'] != pool_end: 197 return True 198 if gateway_ip and subnet['gateway_ip'] != gateway_ip: 199 return True 200 if dns and sorted(subnet['dns_nameservers']) != sorted(dns): 201 return True 202 if host_routes: 203 curr_hr = sorted(subnet['host_routes'], key=lambda t: t.keys()) 204 new_hr = sorted(host_routes, key=lambda t: t.keys()) 205 if sorted(curr_hr) != sorted(new_hr): 206 return True 207 if no_gateway_ip and subnet['gateway_ip']: 208 return True 209 return False 210 211 212def _system_state_change(module, subnet, cloud, filters=None): 213 state = module.params['state'] 214 if state == 'present': 215 if not subnet: 216 return True 217 return _needs_update(subnet, module, cloud, filters) 218 if state == 'absent' and subnet: 219 return True 220 return False 221 222 223def main(): 224 ipv6_mode_choices = ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] 225 argument_spec = openstack_full_argument_spec( 226 name=dict(type='str', required=True), 227 network_name=dict(type='str'), 228 cidr=dict(type='str'), 229 ip_version=dict(type='str', default='4', choices=['4', '6']), 230 enable_dhcp=dict(type='bool', default=True), 231 gateway_ip=dict(type='str'), 232 no_gateway_ip=dict(type='bool', default=False), 233 dns_nameservers=dict(type='list', default=None), 234 allocation_pool_start=dict(type='str'), 235 allocation_pool_end=dict(type='str'), 236 host_routes=dict(type='list', default=None), 237 ipv6_ra_mode=dict(type='str', choice=ipv6_mode_choices), 238 ipv6_address_mode=dict(type='str', choice=ipv6_mode_choices), 239 use_default_subnetpool=dict(type='bool', default=False), 240 extra_specs=dict(type='dict', default=dict()), 241 state=dict(type='str', default='present', choices=['absent', 'present']), 242 project=dict(type='str'), 243 ) 244 245 module_kwargs = openstack_module_kwargs() 246 module = AnsibleModule(argument_spec, 247 supports_check_mode=True, 248 required_together=[ 249 ['allocation_pool_end', 'allocation_pool_start'], 250 ], 251 **module_kwargs) 252 253 state = module.params['state'] 254 network_name = module.params['network_name'] 255 cidr = module.params['cidr'] 256 ip_version = module.params['ip_version'] 257 enable_dhcp = module.params['enable_dhcp'] 258 subnet_name = module.params['name'] 259 gateway_ip = module.params['gateway_ip'] 260 no_gateway_ip = module.params['no_gateway_ip'] 261 dns = module.params['dns_nameservers'] 262 pool_start = module.params['allocation_pool_start'] 263 pool_end = module.params['allocation_pool_end'] 264 host_routes = module.params['host_routes'] 265 ipv6_ra_mode = module.params['ipv6_ra_mode'] 266 ipv6_a_mode = module.params['ipv6_address_mode'] 267 use_default_subnetpool = module.params['use_default_subnetpool'] 268 project = module.params.pop('project') 269 extra_specs = module.params['extra_specs'] 270 271 # Check for required parameters when state == 'present' 272 if state == 'present': 273 if not module.params['network_name']: 274 module.fail_json(msg='network_name required with present state') 275 if (not module.params['cidr'] and not use_default_subnetpool and 276 not extra_specs.get('subnetpool_id', False)): 277 module.fail_json(msg='cidr or use_default_subnetpool or ' 278 'subnetpool_id required with present state') 279 280 if pool_start and pool_end: 281 pool = [dict(start=pool_start, end=pool_end)] 282 else: 283 pool = None 284 285 if no_gateway_ip and gateway_ip: 286 module.fail_json(msg='no_gateway_ip is not allowed with gateway_ip') 287 288 sdk, cloud = openstack_cloud_from_module(module) 289 try: 290 if project is not None: 291 proj = cloud.get_project(project) 292 if proj is None: 293 module.fail_json(msg='Project %s could not be found' % project) 294 project_id = proj['id'] 295 filters = {'tenant_id': project_id} 296 else: 297 project_id = None 298 filters = None 299 300 subnet = cloud.get_subnet(subnet_name, filters=filters) 301 302 if module.check_mode: 303 module.exit_json(changed=_system_state_change(module, subnet, 304 cloud, filters)) 305 306 if state == 'present': 307 if not subnet: 308 kwargs = dict( 309 cidr=cidr, 310 ip_version=ip_version, 311 enable_dhcp=enable_dhcp, 312 subnet_name=subnet_name, 313 gateway_ip=gateway_ip, 314 disable_gateway_ip=no_gateway_ip, 315 dns_nameservers=dns, 316 allocation_pools=pool, 317 host_routes=host_routes, 318 ipv6_ra_mode=ipv6_ra_mode, 319 ipv6_address_mode=ipv6_a_mode, 320 tenant_id=project_id) 321 dup_args = set(kwargs.keys()) & set(extra_specs.keys()) 322 if dup_args: 323 raise ValueError('Duplicate key(s) {0} in extra_specs' 324 .format(list(dup_args))) 325 if use_default_subnetpool: 326 kwargs['use_default_subnetpool'] = use_default_subnetpool 327 kwargs = dict(kwargs, **extra_specs) 328 subnet = cloud.create_subnet(network_name, **kwargs) 329 changed = True 330 else: 331 if _needs_update(subnet, module, cloud, filters): 332 cloud.update_subnet(subnet['id'], 333 subnet_name=subnet_name, 334 enable_dhcp=enable_dhcp, 335 gateway_ip=gateway_ip, 336 disable_gateway_ip=no_gateway_ip, 337 dns_nameservers=dns, 338 allocation_pools=pool, 339 host_routes=host_routes) 340 changed = True 341 else: 342 changed = False 343 module.exit_json(changed=changed, 344 subnet=subnet, 345 id=subnet['id']) 346 347 elif state == 'absent': 348 if not subnet: 349 changed = False 350 else: 351 changed = True 352 cloud.delete_subnet(subnet_name) 353 module.exit_json(changed=changed) 354 355 except sdk.exceptions.OpenStackCloudException as e: 356 module.fail_json(msg=str(e)) 357 358 359if __name__ == '__main__': 360 main() 361