From 5d7b6aae04480ed3289a2eea0a617bdc5c55c1ea Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Mar 2015 17:05:43 -0400 Subject: [PATCH 1/3] Add OpenStack Subnet module Also deprecated old quantum_subnet module --- .../{quantum_subnet.py => _quantum_subnet.py} | 0 cloud/openstack/os_subnet | 213 ++++++++++++++++++ 2 files changed, 213 insertions(+) rename cloud/openstack/{quantum_subnet.py => _quantum_subnet.py} (100%) create mode 100644 cloud/openstack/os_subnet diff --git a/cloud/openstack/quantum_subnet.py b/cloud/openstack/_quantum_subnet.py similarity index 100% rename from cloud/openstack/quantum_subnet.py rename to cloud/openstack/_quantum_subnet.py diff --git a/cloud/openstack/os_subnet b/cloud/openstack/os_subnet new file mode 100644 index 00000000000..ad3a27c2816 --- /dev/null +++ b/cloud/openstack/os_subnet @@ -0,0 +1,213 @@ +#!/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 + + +DOCUMENTATION = ''' +--- +module: os_subnet +short_description: Add/Remove subnet to an OpenStack network +extends_documentation_fragment: openstack +version_added: "1.10" +description: + - Add or Remove a subnet to an OpenStack network +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + network_name: + description: + - Name of the network to which the subnet should be attached + required: true + default: None + name: + description: + - The name of the subnet that should be created + required: true + default: None + cidr: + description: + - The CIDR representation of the subnet that should be assigned to the subnet + required: true + 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 + dns_nameservers: + description: + - DNS nameservers for this subnet, comma-separated + required: false + default: None + version_added: "1.4" + 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 +requirements: ["shade"] +''' + +EXAMPLES = ''' +# Create a subnet with the specified network +- os_subnet: state=present username=admin password=admin + project_name=admin + network_name=network1 name=net1subnet cidr=192.168.0.0/24" +''' + +_os_network_id = None + +def _get_net_id(neutron, module): + kwargs = { + 'name': module.params['network_name'], + } + try: + networks = neutron.list_networks(**kwargs) + except Exception, e: + module.fail_json("Error in listing neutron networks: %s" % e.message) + if not networks['networks']: + return None + return networks['networks'][0]['id'] + + +def _get_subnet_id(module, neutron): + global _os_network_id + subnet_id = None + _os_network_id = _get_net_id(neutron, module) + if not _os_network_id: + module.fail_json(msg = "network id of network not found.") + else: + kwargs = { + 'name': module.params['name'], + } + try: + subnets = neutron.list_subnets(**kwargs) + except Exception, e: + module.fail_json( msg = " Error in getting the subnet list:%s " % e.message) + if not subnets['subnets']: + return None + return subnets['subnets'][0]['id'] + +def _create_subnet(module, neutron): + neutron.format = 'json' + subnet = { + 'name': module.params['name'], + 'ip_version': module.params['ip_version'], + 'enable_dhcp': module.params['enable_dhcp'], + 'gateway_ip': module.params['gateway_ip'], + 'dns_nameservers': module.params['dns_nameservers'], + 'network_id': _os_network_id, + 'cidr': module.params['cidr'], + } + if module.params['allocation_pool_start'] and module.params['allocation_pool_end']: + allocation_pools = [ + { + 'start' : module.params['allocation_pool_start'], + 'end' : module.params['allocation_pool_end'] + } + ] + subnet.update({'allocation_pools': allocation_pools}) + if not module.params['gateway_ip']: + subnet.pop('gateway_ip') + if module.params['dns_nameservers']: + subnet['dns_nameservers'] = module.params['dns_nameservers'].split(',') + else: + subnet.pop('dns_nameservers') + try: + new_subnet = neutron.create_subnet(dict(subnet=subnet)) + except Exception, e: + module.fail_json(msg = "Failure in creating subnet: %s" % e.message) + return new_subnet['subnet']['id'] + + +def _delete_subnet(module, neutron, subnet_id): + try: + neutron.delete_subnet(subnet_id) + except Exception, e: + module.fail_json( msg = "Error in deleting subnet: %s" % e.message) + return True + + +def main(): + + argument_spec = openstack_full_argument_spec( + name = dict(required=True), + network_name = dict(required=True), + cidr = dict(required=True), + ip_version = dict(default='4', choices=['4', '6']), + enable_dhcp = dict(default='true', type='bool'), + gateway_ip = dict(default=None), + dns_nameservers = dict(default=None), + allocation_pool_start = dict(default=None), + allocation_pool_end = dict(default=None), + ) + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + try: + cloud = shade.openstack_cloud(**module.params) + neutron = cloud.neutron_client + if module.params['state'] == 'present': + subnet_id = _get_subnet_id(module, neutron) + if not subnet_id: + subnet_id = _create_subnet(module, neutron) + module.exit_json(changed = True, result = "Created" , id = subnet_id) + else: + module.exit_json(changed = False, result = "success" , id = subnet_id) + else: + subnet_id = _get_subnet_id(module, neutron) + if not subnet_id: + module.exit_json(changed = False, result = "success") + else: + _delete_subnet(module, neutron, subnet_id) + module.exit_json(changed = True, result = "deleted") + except shade.OpenStackCloudException as e: + module.fail_json(msg=e.message) + + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * +main() + From b854a28dfb4f1be8e0585b450232c275eef85169 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 16 Apr 2015 09:17:57 -0400 Subject: [PATCH 2/3] Rename os_subnet to os_subnet.py --- cloud/openstack/{os_subnet => os_subnet.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cloud/openstack/{os_subnet => os_subnet.py} (100%) diff --git a/cloud/openstack/os_subnet b/cloud/openstack/os_subnet.py similarity index 100% rename from cloud/openstack/os_subnet rename to cloud/openstack/os_subnet.py From c241296a204465a6c42c65d556f5245a1ac547cc Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 16 Apr 2015 09:19:29 -0400 Subject: [PATCH 3/3] Update os_subnet module for latest shade Shade can now handle creating, updating and deleting subnets. This cleans up the module to take advantage of that. --- cloud/openstack/os_subnet.py | 255 ++++++++++++++++++++--------------- 1 file changed, 147 insertions(+), 108 deletions(-) diff --git a/cloud/openstack/os_subnet.py b/cloud/openstack/os_subnet.py index ad3a27c2816..e6f02816e1d 100644 --- a/cloud/openstack/os_subnet.py +++ b/cloud/openstack/os_subnet.py @@ -40,18 +40,18 @@ options: network_name: description: - Name of the network to which the subnet should be attached - required: true - default: None + required: true when state is 'present' name: description: - - The name of the subnet that should be created + - 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 - default: None cidr: description: - - The CIDR representation of the subnet that should be assigned to the subnet - required: true - default: None + - The CIDR representation of the subnet that should be assigned to + the subnet. + required: true when state is 'present' ip_version: description: - The IP version of the subnet 4 or 6 @@ -69,139 +69,179 @@ options: default: None dns_nameservers: description: - - DNS nameservers for this subnet, comma-separated + - List of DNS nameservers for this subnet. required: false default: None - version_added: "1.4" allocation_pool_start: description: - - From the subnet pool the starting address from which the IP should be allocated + - 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 + - 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 requirements: ["shade"] ''' EXAMPLES = ''' -# Create a subnet with the specified network -- os_subnet: state=present username=admin password=admin - project_name=admin - network_name=network1 name=net1subnet cidr=192.168.0.0/24" +# 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: 123.456.78.9 + - destination: 192.168.0.0/24 + nexthop: 192.168.0.1 + +# Delete a subnet +- os_subnet: + state=absent + name=net1subnet ''' -_os_network_id = None -def _get_net_id(neutron, module): - kwargs = { - 'name': module.params['network_name'], - } - try: - networks = neutron.list_networks(**kwargs) - except Exception, e: - module.fail_json("Error in listing neutron networks: %s" % e.message) - if not networks['networks']: - return None - return networks['networks'][0]['id'] +def _needs_update(subnet, module): + """Check for differences in the updatable values.""" + 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'] + 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 + return False -def _get_subnet_id(module, neutron): - global _os_network_id - subnet_id = None - _os_network_id = _get_net_id(neutron, module) - if not _os_network_id: - module.fail_json(msg = "network id of network not found.") - else: - kwargs = { - 'name': module.params['name'], - } - try: - subnets = neutron.list_subnets(**kwargs) - except Exception, e: - module.fail_json( msg = " Error in getting the subnet list:%s " % e.message) - if not subnets['subnets']: - return None - return subnets['subnets'][0]['id'] - -def _create_subnet(module, neutron): - neutron.format = 'json' - subnet = { - 'name': module.params['name'], - 'ip_version': module.params['ip_version'], - 'enable_dhcp': module.params['enable_dhcp'], - 'gateway_ip': module.params['gateway_ip'], - 'dns_nameservers': module.params['dns_nameservers'], - 'network_id': _os_network_id, - 'cidr': module.params['cidr'], - } - if module.params['allocation_pool_start'] and module.params['allocation_pool_end']: - allocation_pools = [ - { - 'start' : module.params['allocation_pool_start'], - 'end' : module.params['allocation_pool_end'] - } - ] - subnet.update({'allocation_pools': allocation_pools}) - if not module.params['gateway_ip']: - subnet.pop('gateway_ip') - if module.params['dns_nameservers']: - subnet['dns_nameservers'] = module.params['dns_nameservers'].split(',') - else: - subnet.pop('dns_nameservers') - try: - new_subnet = neutron.create_subnet(dict(subnet=subnet)) - except Exception, e: - module.fail_json(msg = "Failure in creating subnet: %s" % e.message) - return new_subnet['subnet']['id'] - - -def _delete_subnet(module, neutron, subnet_id): - try: - neutron.delete_subnet(subnet_id) - except Exception, e: - module.fail_json( msg = "Error in deleting subnet: %s" % e.message) - return True +def _system_state_change(module, subnet): + state = module.params['state'] + if state == 'present': + if not subnet: + return True + return _needs_update(subnet, module) + if state == 'absent' and subnet: + return True + return False def main(): - argument_spec = openstack_full_argument_spec( - name = dict(required=True), - network_name = dict(required=True), - cidr = dict(required=True), - ip_version = dict(default='4', choices=['4', '6']), - enable_dhcp = dict(default='true', type='bool'), - gateway_ip = dict(default=None), - dns_nameservers = dict(default=None), - allocation_pool_start = dict(default=None), - allocation_pool_end = dict(default=None), + 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), + 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'), ) + module_kwargs = openstack_module_kwargs() - module = AnsibleModule(argument_spec, **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'] + 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'] + + # 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 + try: cloud = shade.openstack_cloud(**module.params) - neutron = cloud.neutron_client - if module.params['state'] == 'present': - subnet_id = _get_subnet_id(module, neutron) - if not subnet_id: - subnet_id = _create_subnet(module, neutron) - module.exit_json(changed = True, result = "Created" , id = subnet_id) + subnet = cloud.get_subnet(subnet_name) + + if module.check_mode: + module.exit_json(changed=_system_state_change(module, subnet)) + + 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, + dns_nameservers=dns, + allocation_pools=pool, + host_routes=host_routes) + module.exit_json(changed=True, result="created") else: - module.exit_json(changed = False, result = "success" , id = subnet_id) - else: - subnet_id = _get_subnet_id(module, neutron) - if not subnet_id: - module.exit_json(changed = False, result = "success") + if _needs_update(subnet, module): + cloud.update_subnet(subnet['id'], + subnet_name=subnet_name, + enable_dhcp=enable_dhcp, + gateway_ip=gateway_ip, + dns_nameservers=dns, + allocation_pools=pool, + host_routes=host_routes) + module.exit_json(changed=True, result="updated") + else: + module.exit_json(changed=False, result="success") + + elif state == 'absent': + if not subnet: + module.exit_json(changed=False, result="success") else: - _delete_subnet(module, neutron, subnet_id) - module.exit_json(changed = True, result = "deleted") + cloud.delete_subnet(subnet_name) + module.exit_json(changed=True, result="deleted") + except shade.OpenStackCloudException as e: module.fail_json(msg=e.message) @@ -210,4 +250,3 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.openstack import * main() -