From 6da266f64cc2aea78c098a9aed8b484c83ce5ca5 Mon Sep 17 00:00:00 2001 From: CaptTofu Date: Fri, 11 Apr 2014 12:01:14 -0700 Subject: [PATCH 01/17] Added floating IP functionality to nova_compute --- library/cloud/nova_compute | 139 ++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 37df3665ea4..ed717c82624 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -19,7 +19,9 @@ try: from novaclient.v1_1 import client as nova_client + from novaclient.v1_1 import floating_ips from novaclient import exceptions + from novaclient import utils import time except ImportError: print("failed=True msg='novaclient is required for this module'") @@ -92,6 +94,11 @@ options: - A list of network id's to which the VM's interface should be attached required: false default: None + floating_ip: + description: + - list of key value pairs that determine how to assign, if specified, floating IPs. Either use an explicite list of valid floating IPs, list of floating IP pools to choose from, or auto-assign + required: false + default: None meta: description: - A list of key value pairs that should be provided as a metadata to the new VM @@ -133,8 +140,38 @@ EXAMPLES = ''' meta: hostname: test1 group: uge_master + +# Creates a new VM in HP Cloud AE1 region and automatically assigns a floating IP +- name: launch a nova instance + hosts: localhost + tasks: + - name: launch an instance + nova_compute: + state: present + login_username: username + login_password: Equality7-2521 + login_tenant_name: username-project1 + name: vm1 + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ + region_name: region-b.geo-1 + image_id: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + wait_for: 200 + flavor_id: 101 + security_groups: default + floating_ip: + auto: True + +# If one wants to specify a floating ip to use: + + floating_ip: + ips: + - 15.126.238.160 + ''' + + def _delete_server(module, nova): name = None server_list = None @@ -157,6 +194,21 @@ def _delete_server(module, nova): def _create_server(module, nova): + # issue an error early on and not launch the instance + if module.params['floating_ip'] != None: + if module.params['floating_ip'].has_key('ips'): + # can't specify "ips" and "auto" both + if module.params['floating_ip'].has_key('auto') and \ + module.params['floating_ip']['auto'] is True: + err_msg = "For floating_ips - " + err_msg += "you cannot specify both 'auto' and 'ips'!" + module.fail_json(msg = err_msg) + # can't specify "ips" and "pools" both + if module.params['floating_ip'].has_key('pools'): + err_msg = "For floating_ips - " + err_msg += "you cannot specify both 'ips' and 'pools'!" + module.fail_json(msg = err_msg) + bootargs = [module.params['name'], module.params['image_id'], module.params['flavor_id']] bootkwargs = { 'nics' : module.params['nics'], @@ -179,11 +231,92 @@ def _create_server(module, nova): try: server = nova.servers.get(server.id) except Exception, e: - module.fail_json( msg = "Error in getting info from instance: %s " % e.message) + module.fail_json(msg = \ + "Error in getting info from instance: %s"\ + % e.message + ) if server.status == 'ACTIVE': + # if floating_ip is specified, then attach + if module.params['floating_ip'] != None: + # instantiate FloatingIPManager object + floating_ip_obj = floating_ips.FloatingIPManager(nova) + # empty dict and list + usable_floating_ips = {} + pools = [] + + # if floating_ip pools are defined, then make that + # the list of pools + if module.params['floating_ip'].has_key('pools'): + # user specified + pools = module.params['floating_ip']['pools'] + else: + # otherwise all + pools = [''] + + # if there is a list of IP addresses, make that the list + if module.params['floating_ip'].has_key('ips'): + usable_floating_ips[''] = \ + module.params['floating_ip']['ips'] + + # if 'auto', then assign automatically, no pool needed + if module.params['floating_ip'].has_key('auto') and \ + module.params['floating_ip']['auto'] is True: + # get the list of all floating IPs. Mileage may + # vary according to Nova Compute configuration + # per cloud provider + all_floating_ips = floating_ip_obj.list() + + # iterate through all pools of IP address. Empty + # string means all and is the default value + for pool in pools: + # temporary list per pool + pool_ips = [] + # loop through all floating IPs + for f_ip in all_floating_ips: + # if not reserved and the correct pool, add + if f_ip.instance_id == None and \ + (f_ip.pool == pool or pool == ''): + pool_ips.append(f_ip.ip) + # one per pool + break + # if the list is empty, add for this pool + if len(pool_ips) == 0: + try: + new_ip = nova.floating_ips.create(pool) + except Exception, e: + module.fail_json(msg = \ + "Unable to create \ + floating ip") + pool_ips.append(new_ip.ip) + # Add to the main list + usable_floating_ips[pool] = pool_ips + + # finally, add ip(s) to instance for each pool + for pool in usable_floating_ips: + for ip in usable_floating_ips[pool]: + try: + server.add_floating_ip(ip) + except Exception, e: + module.fail_json(msg = \ + "Error attaching IP %s to \ + instance %s: %s " % \ + (ip, server.id, e.message)) + + # this may look redundant, but if there is now a + # floating IP, then it needs to be obtained from + # a recent server object if the above code path exec'd + try: + server = nova.servers.get(server.id) + except Exception, e: + module.fail_json(msg = \ + "Error in getting info from \ + instance: %s " % e.message) + private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'fixed'] public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'floating'] + # now exit with info module.exit_json(changed = True, id = server.id, private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info) + if server.status == 'ERROR': module.fail_json(msg = "Error in creating the server, please check logs") time.sleep(2) @@ -193,6 +326,7 @@ def _create_server(module, nova): module.fail_json(msg = "Error in creating the server.. Please check manually") private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'fixed'] public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'floating'] + module.exit_json(changed = True, id = info['id'], private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info) @@ -241,7 +375,8 @@ def main(): wait = dict(default='yes', choices=['yes', 'no']), wait_for = dict(default=180), state = dict(default='present', choices=['absent', 'present']), - user_data = dict(default=None) + user_data = dict(default=None), + floating_ip = dict(default=None) ), ) From 10a50b4a61ee324d59bac92f9b382b8337e32029 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 15:05:42 -0700 Subject: [PATCH 02/17] Split nova floating-ip pool logic The desires around getting a floating ip associated with a pool and getting a floating ip not associated with a pool is just different enough that following it as one set of nested ifs is tricky. Split the function into two, one for the pool and one for the non-pool logic. --- library/cloud/nova_compute | 194 ++++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 87 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index ed717c82624..f7fd55123ed 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -193,21 +193,116 @@ def _delete_server(module, nova): module.fail_json(msg = "Timed out waiting for server to get deleted, please check manually") +def _add_floating_ip_from_pool(module, nova, server, floating_ip_obj): + + # empty dict and list + usable_floating_ips = {} + pools = [] + + # user specified + pools = module.params['floating_ip']['pools'] + + # get the list of all floating IPs. Mileage may + # vary according to Nova Compute configuration + # per cloud provider + all_floating_ips = floating_ip_obj.list() + + # iterate through all pools of IP address. Empty + # string means all and is the default value + for pool in pools: + # temporary list per pool + pool_ips = [] + # loop through all floating IPs + for f_ip in all_floating_ips: + # if not reserved and the correct pool, add + if f_ip.instance_id is None and (f_ip.pool == pool): + pool_ips.append(f_ip.ip) + # only need one + break + + # if the list is empty, add for this pool + if not pool_ips: + try: + new_ip = nova.floating_ips.create(pool) + except Exception, e: + module.fail_json(msg = "Unable to create floating ip") + pool_ips.append(new_ip.ip) + # Add to the main list + usable_floating_ips[pool] = pool_ips + + # finally, add ip(s) to instance for each pool + for pool in usable_floating_ips: + for ip in usable_floating_ips[pool]: + try: + server.add_floating_ip(ip) + # We only need to assign one ip - but there is an inherent + # race condition and some other cloud operation may have + # stolen an available floating ip + break + except Exception, e: + module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message)) + + +def _add_floating_ip_no_pool(module, nova, server, floating_ip_obj): + + usable_floating_ips = list() + + # if there is a list of IP addresses, make that the list + if module.params['floating_ip'].has_key('ips'): + usable_floating_ips = module.params['floating_ip']['ips'] + else: + # get the list of all floating IPs. Mileage may + # vary according to Nova Compute configuration + # per cloud provider + for f_ip in floating_ip_obj.list(): + # if not reserved and the correct pool, add + if f_ip.instance_id is None: + usable_floating_ips.append(f_ip.ip) + + if not usable_floating_ips: + try: + new_ip = nova.floating_ips.create() + except Exception, e: + module.fail_json(msg = "Unable to create floating ip") + usable_floating_ips.append(new_ip.ip) + + # finally, add ip(s) to instance + for ip in usable_floating_ips: + try: + server.add_floating_ip(ip) + except Exception, e: + module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message)) + + +def _add_floating_ip(module, nova, server): + # instantiate FloatingIPManager object + floating_ip_obj = floating_ips.FloatingIPManager(nova) + + if module.params['floating_ip'].has_key('pools'): + _add_floating_ip_from_pool(module, nova, server, floating_ip_obj) + else: + _add_floating_ip_no_pool(module, nova, server, floating_ip_obj) + + # this may look redundant, but if there is now a + # floating IP, then it needs to be obtained from + # a recent server object if the above code path exec'd + try: + server = nova.servers.get(server.id) + except Exception, e: + module.fail_json(msg = "Error in getting info from instance: %s " % e.message) + return server + + def _create_server(module, nova): # issue an error early on and not launch the instance - if module.params['floating_ip'] != None: + if module.params['floating_ip'] is not None: if module.params['floating_ip'].has_key('ips'): # can't specify "ips" and "auto" both - if module.params['floating_ip'].has_key('auto') and \ - module.params['floating_ip']['auto'] is True: - err_msg = "For floating_ips - " - err_msg += "you cannot specify both 'auto' and 'ips'!" - module.fail_json(msg = err_msg) + if module.params['floating_ip'].has_key('auto') and module.params['floating_ip']['auto']: + module.fail_json(msg = "For floating_ips - you cannot specify both 'auto' and 'ips'!") # can't specify "ips" and "pools" both if module.params['floating_ip'].has_key('pools'): - err_msg = "For floating_ips - " - err_msg += "you cannot specify both 'ips' and 'pools'!" - module.fail_json(msg = err_msg) + module.fail_json(msg = "For floating_ips - you cannot specify both 'pools' and 'ips'!") bootargs = [module.params['name'], module.params['image_id'], module.params['flavor_id']] bootkwargs = { @@ -231,86 +326,11 @@ def _create_server(module, nova): try: server = nova.servers.get(server.id) except Exception, e: - module.fail_json(msg = \ - "Error in getting info from instance: %s"\ - % e.message - ) + module.fail_json( msg = "Error in getting info from instance: %s" % e.message) if server.status == 'ACTIVE': # if floating_ip is specified, then attach - if module.params['floating_ip'] != None: - # instantiate FloatingIPManager object - floating_ip_obj = floating_ips.FloatingIPManager(nova) - # empty dict and list - usable_floating_ips = {} - pools = [] - - # if floating_ip pools are defined, then make that - # the list of pools - if module.params['floating_ip'].has_key('pools'): - # user specified - pools = module.params['floating_ip']['pools'] - else: - # otherwise all - pools = [''] - - # if there is a list of IP addresses, make that the list - if module.params['floating_ip'].has_key('ips'): - usable_floating_ips[''] = \ - module.params['floating_ip']['ips'] - - # if 'auto', then assign automatically, no pool needed - if module.params['floating_ip'].has_key('auto') and \ - module.params['floating_ip']['auto'] is True: - # get the list of all floating IPs. Mileage may - # vary according to Nova Compute configuration - # per cloud provider - all_floating_ips = floating_ip_obj.list() - - # iterate through all pools of IP address. Empty - # string means all and is the default value - for pool in pools: - # temporary list per pool - pool_ips = [] - # loop through all floating IPs - for f_ip in all_floating_ips: - # if not reserved and the correct pool, add - if f_ip.instance_id == None and \ - (f_ip.pool == pool or pool == ''): - pool_ips.append(f_ip.ip) - # one per pool - break - # if the list is empty, add for this pool - if len(pool_ips) == 0: - try: - new_ip = nova.floating_ips.create(pool) - except Exception, e: - module.fail_json(msg = \ - "Unable to create \ - floating ip") - pool_ips.append(new_ip.ip) - # Add to the main list - usable_floating_ips[pool] = pool_ips - - # finally, add ip(s) to instance for each pool - for pool in usable_floating_ips: - for ip in usable_floating_ips[pool]: - try: - server.add_floating_ip(ip) - except Exception, e: - module.fail_json(msg = \ - "Error attaching IP %s to \ - instance %s: %s " % \ - (ip, server.id, e.message)) - - # this may look redundant, but if there is now a - # floating IP, then it needs to be obtained from - # a recent server object if the above code path exec'd - try: - server = nova.servers.get(server.id) - except Exception, e: - module.fail_json(msg = \ - "Error in getting info from \ - instance: %s " % e.message) + if module.params['floating_ip'] is not None: + server = _add_floating_ip(module, nova, server) private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'fixed'] public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'floating'] From 8938222029550c53f49c880d8ea2fc9f1b259d8a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 15:15:18 -0700 Subject: [PATCH 03/17] Pass through nova region name If the region name is specified in the config, we need to pass it in to the nova client constructor. Since key_name is similarly optional, go ahead and handle both parameters the same. --- library/cloud/nova_compute | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index f7fd55123ed..e87ec8af12e 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -308,13 +308,14 @@ def _create_server(module, nova): bootkwargs = { 'nics' : module.params['nics'], 'meta' : module.params['meta'], - 'key_name': module.params['key_name'], 'security_groups': module.params['security_groups'].split(','), #userdata is unhyphenated in novaclient, but hyphenated here for consistency with the ec2 module: 'userdata': module.params['user_data'], } - if not module.params['key_name']: - del bootkwargs['key_name'] + + for optional_param in ('region_name', 'key_name'): + if module.params[optional_param]: + bootkwargs[optional_param] = module.params[optional_param] try: server = nova.servers.create(*bootargs, **bootkwargs) server = nova.servers.get(server.id) From 1e0e7a6b28895b4fcb0e599a7105d05f02762624 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 15:39:50 -0700 Subject: [PATCH 04/17] Add support for nova availability zones If a cloud has availability zone support, it's possible to specify an availability zone via the nova client. Add that as a config param. --- library/cloud/nova_compute | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index e87ec8af12e..905295c1d21 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -99,6 +99,11 @@ options: - list of key value pairs that determine how to assign, if specified, floating IPs. Either use an explicite list of valid floating IPs, list of floating IP pools to choose from, or auto-assign required: false default: None + availability_zone: + description: + - Name of the availability zone + required: false + default: None meta: description: - A list of key value pairs that should be provided as a metadata to the new VM @@ -141,7 +146,7 @@ EXAMPLES = ''' hostname: test1 group: uge_master -# Creates a new VM in HP Cloud AE1 region and automatically assigns a floating IP +# Creates a new VM in HP Cloud AE1 region availability zone az2 and automatically assigns a floating IP - name: launch a nova instance hosts: localhost tasks: @@ -154,6 +159,7 @@ EXAMPLES = ''' name: vm1 auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ region_name: region-b.geo-1 + availability_zone: az2 image_id: 9302692b-b787-4b52-a3a6-daebb79cb498 key_name: test wait_for: 200 @@ -313,7 +319,7 @@ def _create_server(module, nova): 'userdata': module.params['user_data'], } - for optional_param in ('region_name', 'key_name'): + for optional_param in ('region_name', 'key_name', 'availability_zone'): if module.params[optional_param]: bootkwargs[optional_param] = module.params[optional_param] try: @@ -397,7 +403,8 @@ def main(): wait_for = dict(default=180), state = dict(default='present', choices=['absent', 'present']), user_data = dict(default=None), - floating_ip = dict(default=None) + floating_ip = dict(default=None), + availability_zone = dict(default=None), ), ) From 6045923cabe3435ad5d734b7ed1fb0ae0d17f036 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 15:42:12 -0700 Subject: [PATCH 05/17] Find IPs on OpenStack clouds without floating-ips The floating-ip extension, while pretty ubiquitous, is not a foregone conclusion. Specifically, Rackspace, while also served by the rax module, is a valid OpenStack cloud and can be interacted with directly via nova interfaces. Add support for determining public and private IPs for OpenStack clouds that don't use floating ips by reading the public and private keys from the addresses dict. --- library/cloud/nova_compute | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 905295c1d21..fb74ce15e29 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -299,6 +299,19 @@ def _add_floating_ip(module, nova, server): return server +def _get_ips(addresses, ext_tag, key_name): + + ret = [] + for (k, v) in addresses.iteritems(): + if k == key_name: + ret.extend([addrs['addr'] for addrs in v]) + else: + for interface_spec in v: + if 'OS-EXT-IPS:type' in interface_spec and interface_spec['OS-EXT-IPS:type'] == ext_tag: + ret.append(interface_spec['addr']) + return ret + + def _create_server(module, nova): # issue an error early on and not launch the instance if module.params['floating_ip'] is not None: @@ -339,8 +352,9 @@ def _create_server(module, nova): if module.params['floating_ip'] is not None: server = _add_floating_ip(module, nova, server) - private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'fixed'] - public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'floating'] + private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private') + public = _get_ips(getattr(server, 'addresses'), 'floating', 'public') + # now exit with info module.exit_json(changed = True, id = server.id, private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info) @@ -351,8 +365,8 @@ def _create_server(module, nova): module.fail_json(msg = "Timeout waiting for the server to come up.. Please check manually") if server.status == 'ERROR': module.fail_json(msg = "Error in creating the server.. Please check manually") - private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'fixed'] - public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'floating'] + private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private') + public = _get_ips(getattr(server, 'addresses'), 'floating', 'public') module.exit_json(changed = True, id = info['id'], private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info) @@ -373,8 +387,8 @@ def _get_server_state(module, nova): if server and module.params['state'] == 'present': if server.status != 'ACTIVE': module.fail_json( msg="The VM is available but not Active. state:" + server.status) - private = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'fixed'] - public = [ x['addr'] for x in getattr(server, 'addresses').itervalues().next() if 'OS-EXT-IPS:type' in x and x['OS-EXT-IPS:type'] == 'floating'] + private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private') + public = _get_ips(getattr(server, 'addresses'), 'floating', 'public') module.exit_json(changed = False, id = server.id, public_ip = ''.join(public), private_ip = ''.join(private), info = server._info) if server and module.params['state'] == 'absent': return True From 2611246b89d8f67dd26f22c634bd1731d0e577db Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 15:51:01 -0700 Subject: [PATCH 06/17] Cleanup in nova after a failed floating ip There is a potential leak of resources if there is somehow a failure adding a floating ip to a server. Clean up after ourselves. --- library/cloud/nova_compute | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index fb74ce15e29..34abc47801d 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -255,7 +255,7 @@ def _add_floating_ip_no_pool(module, nova, server, floating_ip_obj): # if there is a list of IP addresses, make that the list if module.params['floating_ip'].has_key('ips'): - usable_floating_ips = module.params['floating_ip']['ips'] + usable_floating_ips = [dict(ip=f, created=False) for f in module.params['floating_ip']['ips']] else: # get the list of all floating IPs. Mileage may # vary according to Nova Compute configuration @@ -263,20 +263,23 @@ def _add_floating_ip_no_pool(module, nova, server, floating_ip_obj): for f_ip in floating_ip_obj.list(): # if not reserved and the correct pool, add if f_ip.instance_id is None: - usable_floating_ips.append(f_ip.ip) + usable_floating_ips.append(dict(ip=f_ip.ip, created=False)) if not usable_floating_ips: try: new_ip = nova.floating_ips.create() except Exception, e: module.fail_json(msg = "Unable to create floating ip") - usable_floating_ips.append(new_ip.ip) + usable_floating_ips.append(dict(ip=new_ip.ip, created=True)) # finally, add ip(s) to instance for ip in usable_floating_ips: try: - server.add_floating_ip(ip) + server.add_floating_ip(ip['ip']) except Exception, e: + # Clean up - we auto-created this ip, and it's not attached + # to the server, so the cloud will not know what to do with it + server.floating_ips.delete(ip['ip']) module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message)) From ac420fd483daab8c2778a8f8573ddda8bf2ec84c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 15:31:10 -0700 Subject: [PATCH 07/17] Consume standard OpenStack environment settings The OpenStack client utilities consume a set of input environment variables for things like username and auth_url, so it's very common for OpenStack users to have such settings set in their environment. Indeed, things like devstack also output a shell file to be sourced to set them. Although in a playbook it's entirely expected that variables should be used to pass in system settings like api passwords, for ad-hoc command line usage, needing to pass in five parameters which are almost certainly in the environment already reduces the utility. Grab the environment variables and inject them as default. Special care is taken to ensure that in the case where the values are not found, the behavior of which parameters are required is not altered. --- library/cloud/nova_compute | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 34abc47801d..61d05f84e7f 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +import os + try: from novaclient.v1_1 import client as nova_client from novaclient.v1_1 import floating_ips @@ -402,13 +404,32 @@ def _get_server_state(module, nova): def main(): + # Consume standard OpenStack environment variables. + # This is mainly only useful for ad-hoc command line operation as + # in playbooks one would assume variables would be used appropriately + OS_USERNAME=os.environ.get('OS_USERNAME', 'admin') + OS_PASSWORD=os.environ.get('OS_PASSWORD', None) + login_password_arg = dict() + if OS_PASSWORD: + login_password_arg['default'] = OS_PASSWORD + else: + login_password_arg['required'] = True + OS_TENANT_NAME=os.environ.get('OS_TENANT_NAME', None) + tenant_name_arg = dict() + if OS_TENANT_NAME: + tenant_name_arg['default'] = OS_TENANT_NAME + else: + tenant_name_arg['required'] = True + OS_REGION_NAME=os.environ.get('OS_REGION_NAME', None) + OS_AUTH_URL=os.environ.get('OS_AUTH_URL', 'http://127.0.0.1:35357/v2.0/') + module = AnsibleModule( argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + login_username = dict(default=OS_USERNAME), + login_password = login_password_arg, + login_tenant_name = tenant_name_arg, + auth_url = dict(default=OS_AUTH_URL), + region_name = dict(default=OS_REGION_NAME), name = dict(required=True), image_id = dict(default=None), flavor_id = dict(default=1), From d17a1b5c19ac00783cbe3614905215f354afeb37 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 17:12:24 -0700 Subject: [PATCH 08/17] Add common auth handling across openstack modules Taking a page out of the ec2 config, make sure that all of the OpenStack modules handle the inbound auth config in the same way. The one outlier is keystone wrt auth_url. --- lib/ansible/module_utils/openstack.py | 56 +++++++++++++++++++++ library/cloud/glance_image | 36 +++++++------ library/cloud/keystone_user | 14 +++--- library/cloud/nova_compute | 34 ++----------- library/cloud/nova_keypair | 14 ++---- library/cloud/quantum_floating_ip | 14 ++---- library/cloud/quantum_floating_ip_associate | 14 ++---- library/cloud/quantum_network | 14 ++---- library/cloud/quantum_router | 14 ++---- library/cloud/quantum_router_gateway | 14 ++---- library/cloud/quantum_router_interface | 14 ++---- library/cloud/quantum_subnet | 14 ++---- 12 files changed, 126 insertions(+), 126 deletions(-) create mode 100644 lib/ansible/module_utils/openstack.py diff --git a/lib/ansible/module_utils/openstack.py b/lib/ansible/module_utils/openstack.py new file mode 100644 index 00000000000..c70eb9fbfa8 --- /dev/null +++ b/lib/ansible/module_utils/openstack.py @@ -0,0 +1,56 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os + + +def openstack_argument_spec(): + # Consume standard OpenStack environment variables. + # This is mainly only useful for ad-hoc command line operation as + # in playbooks one would assume variables would be used appropriately + OS_AUTH_URL=os.environ.get('OS_AUTH_URL', 'http://127.0.0.1:35357/v2.0/') + OS_PASSWORD=os.environ.get('OS_PASSWORD', None) + OS_REGION_NAME=os.environ.get('OS_REGION_NAME', None) + OS_USERNAME=os.environ.get('OS_USERNAME', 'admin') + OS_TENANT_NAME=os.environ.get('OS_TENANT_NAME', OS_USERNAME) + + spec = dict( + login_username = dict(default=OS_USERNAME), + auth_url = dict(default=OS_AUTH_URL), + region_name = dict(default=OS_REGION_NAME), + availability_zone = dict(default=None), + ) + if OS_PASSWORD: + spec['login_password'] = dict(default=OS_PASSWORD) + else: + spec['login_password'] = dict(required=True) + if OS_TENANT_NAME: + spec['login_tenant_name'] = dict(default=OS_TENANT_NAME) + else: + spec['login_tenant_name'] = dict(required=True) + return spec diff --git a/library/cloud/glance_image b/library/cloud/glance_image index b73b3bfea7a..d8b02602feb 100644 --- a/library/cloud/glance_image +++ b/library/cloud/glance_image @@ -217,26 +217,23 @@ def _glance_delete_image(module, params, client): def main(): + argument_spec = openstack_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + disk_format = dict(default='qcow2', choices=['aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso']), + container_format = dict(default='bare', choices=['aki', 'ari', 'bare', 'ovf']), + owner = dict(default=None), + min_disk = dict(default=None), + min_ram = dict(default=None), + is_public = dict(default=True), + copy_from = dict(default= None), + timeout = dict(default=180), + file = dict(default=None), + endpoint_type = dict(default='publicURL', choices=['publicURL', 'internalURL']), + state = dict(default='present', choices=['absent', 'present']) + )) module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required=True), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), - name = dict(required=True), - disk_format = dict(default='qcow2', choices=['aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso']), - container_format = dict(default='bare', choices=['aki', 'ari', 'bare', 'ovf']), - owner = dict(default=None), - min_disk = dict(default=None), - min_ram = dict(default=None), - is_public = dict(default=True), - copy_from = dict(default= None), - timeout = dict(default=180), - file = dict(default=None), - endpoint_type = dict(default='publicURL', choices=['publicURL', 'internalURL']), - state = dict(default='present', choices=['absent', 'present']) - ), + argument_spec=argument_spec, mutually_exclusive = [['file','copy_from']], ) if module.params['state'] == 'present': @@ -258,4 +255,5 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/keystone_user b/library/cloud/keystone_user index f3295b7f19b..a5d3aa940d8 100644 --- a/library/cloud/keystone_user +++ b/library/cloud/keystone_user @@ -287,11 +287,8 @@ def ensure_role_absent(keystone, uesr, tenant, role, check_mode): def main(): - module = AnsibleModule( - argument_spec=dict( - user=dict(required=False), - password=dict(required=False), - tenant=dict(required=False), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( tenant_description=dict(required=False), email=dict(required=False), role=dict(required=False), @@ -302,7 +299,11 @@ def main(): login_user=dict(required=False), login_password=dict(required=False), login_tenant_name=dict(required=False) - ), + )) + # keystone operations themselves take an endpoint, not a keystone auth_url + del(argument_spec['auth_url']) + module = AnsibleModule( + argument_spec=argument_spec, supports_check_mode=True, mutually_exclusive=[['token', 'login_user'], ['token', 'login_password'], @@ -388,5 +389,6 @@ def dispatch(keystone, user=None, password=None, tenant=None, # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * if __name__ == '__main__': main() diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 61d05f84e7f..757caaf16f4 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -404,32 +404,8 @@ def _get_server_state(module, nova): def main(): - # Consume standard OpenStack environment variables. - # This is mainly only useful for ad-hoc command line operation as - # in playbooks one would assume variables would be used appropriately - OS_USERNAME=os.environ.get('OS_USERNAME', 'admin') - OS_PASSWORD=os.environ.get('OS_PASSWORD', None) - login_password_arg = dict() - if OS_PASSWORD: - login_password_arg['default'] = OS_PASSWORD - else: - login_password_arg['required'] = True - OS_TENANT_NAME=os.environ.get('OS_TENANT_NAME', None) - tenant_name_arg = dict() - if OS_TENANT_NAME: - tenant_name_arg['default'] = OS_TENANT_NAME - else: - tenant_name_arg['required'] = True - OS_REGION_NAME=os.environ.get('OS_REGION_NAME', None) - OS_AUTH_URL=os.environ.get('OS_AUTH_URL', 'http://127.0.0.1:35357/v2.0/') - - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default=OS_USERNAME), - login_password = login_password_arg, - login_tenant_name = tenant_name_arg, - auth_url = dict(default=OS_AUTH_URL), - region_name = dict(default=OS_REGION_NAME), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( name = dict(required=True), image_id = dict(default=None), flavor_id = dict(default=1), @@ -442,9 +418,8 @@ def main(): state = dict(default='present', choices=['absent', 'present']), user_data = dict(default=None), floating_ip = dict(default=None), - availability_zone = dict(default=None), - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) nova = nova_client.Client(module.params['login_username'], module.params['login_password'], @@ -471,5 +446,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/nova_keypair b/library/cloud/nova_keypair index 553683d3a89..be2bbb1d93d 100644 --- a/library/cloud/nova_keypair +++ b/library/cloud/nova_keypair @@ -87,18 +87,13 @@ EXAMPLES = ''' ''' def main(): - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( name = dict(required=True), public_key = dict(default=None), state = dict(default='present', choices=['absent', 'present']) - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) nova = nova_client.Client(module.params['login_username'], module.params['login_password'], @@ -138,5 +133,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/quantum_floating_ip b/library/cloud/quantum_floating_ip index 9bde712576d..17f78effffd 100644 --- a/library/cloud/quantum_floating_ip +++ b/library/cloud/quantum_floating_ip @@ -220,19 +220,14 @@ def _update_floating_ip(neutron, module, port_id, floating_ip_id): def main(): - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( network_name = dict(required=True), instance_name = dict(required=True), state = dict(default='present', choices=['absent', 'present']), internal_network_name = dict(default=None), - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) try: nova = nova_client.Client(module.params['login_username'], module.params['login_password'], @@ -266,5 +261,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/quantum_floating_ip_associate b/library/cloud/quantum_floating_ip_associate index 29df6856571..91df2690b62 100644 --- a/library/cloud/quantum_floating_ip_associate +++ b/library/cloud/quantum_floating_ip_associate @@ -178,18 +178,13 @@ def _update_floating_ip(neutron, module, port_id, floating_ip_id): def main(): - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( ip_address = dict(required=True), instance_name = dict(required=True), state = dict(default='present', choices=['absent', 'present']) - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) try: nova = nova_client.Client(module.params['login_username'], module.params['login_password'], @@ -218,5 +213,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/quantum_network b/library/cloud/quantum_network index 744fe44d8dc..606c493f398 100644 --- a/library/cloud/quantum_network +++ b/library/cloud/quantum_network @@ -230,13 +230,8 @@ def _delete_network(module, net_id, neutron): def main(): - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( name = dict(required=True), tenant_name = dict(default=None), provider_network_type = dict(default=None, choices=['local', 'vlan', 'flat', 'gre']), @@ -246,8 +241,8 @@ def main(): shared = dict(default=False, type='bool'), admin_state_up = dict(default=True, type='bool'), state = dict(default='present', choices=['absent', 'present']) - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) if module.params['provider_network_type'] in ['vlan' , 'flat']: if not module.params['provider_physical_network']: @@ -279,5 +274,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/quantum_router b/library/cloud/quantum_router index 36e027eccd1..d5f5d56a362 100644 --- a/library/cloud/quantum_router +++ b/library/cloud/quantum_router @@ -175,19 +175,14 @@ def _delete_router(module, neutron, router_id): return True def main(): - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( name = dict(required=True), tenant_name = dict(default=None), state = dict(default='present', choices=['absent', 'present']), admin_state_up = dict(type='bool', default=True), - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) neutron = _get_neutron_client(module, module.params) _set_tenant_id(module) @@ -210,5 +205,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/quantum_router_gateway b/library/cloud/quantum_router_gateway index 55295f76e40..5de19fd4785 100644 --- a/library/cloud/quantum_router_gateway +++ b/library/cloud/quantum_router_gateway @@ -174,18 +174,13 @@ def _remove_gateway_router(neutron, module, router_id): def main(): - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( router_name = dict(required=True), network_name = dict(required=True), state = dict(default='present', choices=['absent', 'present']), - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) neutron = _get_neutron_client(module, module.params) router_id = _get_router_id(module, neutron) @@ -213,5 +208,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/quantum_router_interface b/library/cloud/quantum_router_interface index 47e5f6b211a..8fdad4f8954 100644 --- a/library/cloud/quantum_router_interface +++ b/library/cloud/quantum_router_interface @@ -208,19 +208,14 @@ def _remove_interface_router(neutron, module, router_id, subnet_id): return True def main(): - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( router_name = dict(required=True), subnet_name = dict(required=True), tenant_name = dict(default=None), state = dict(default='present', choices=['absent', 'present']), - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) neutron = _get_neutron_client(module, module.params) _set_tenant_id(module) @@ -249,5 +244,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() diff --git a/library/cloud/quantum_subnet b/library/cloud/quantum_subnet index 5afe566c8f8..5034a736a10 100644 --- a/library/cloud/quantum_subnet +++ b/library/cloud/quantum_subnet @@ -252,13 +252,8 @@ def _delete_subnet(module, neutron, subnet_id): def main(): - module = AnsibleModule( - argument_spec = dict( - login_username = dict(default='admin'), - login_password = dict(required=True), - login_tenant_name = dict(required='True'), - auth_url = dict(default='http://127.0.0.1:35357/v2.0/'), - region_name = dict(default=None), + argument_spec = openstack_argument_spec() + argument_spec.update(dict( name = dict(required=True), network_name = dict(required=True), cidr = dict(required=True), @@ -270,8 +265,8 @@ def main(): dns_nameservers = dict(default=None), allocation_pool_start = dict(default=None), allocation_pool_end = dict(default=None), - ), - ) + )) + module = AnsibleModule(argument_spec=argument_spec) neutron = _get_neutron_client(module, module.params) _set_tenant_id(module) if module.params['state'] == 'present': @@ -291,5 +286,6 @@ def main(): # this is magic, see lib/ansible/module.params['common.py from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * main() From 3fe1083192819f595bf9047ecd1987ec3f9c9d1e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 18:26:23 -0700 Subject: [PATCH 09/17] Use mutually exclusive on nova floating ip params ansible knows how to deal with mutually exclusive parameters, so instead of coding that ourselves, use it. --- library/cloud/nova_compute | 113 +++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 757caaf16f4..9146df42442 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -96,9 +96,19 @@ options: - A list of network id's to which the VM's interface should be attached required: false default: None - floating_ip: + auto_floating_ip: description: - - list of key value pairs that determine how to assign, if specified, floating IPs. Either use an explicite list of valid floating IPs, list of floating IP pools to choose from, or auto-assign + - Should a floating ip be auto created and assigned + required: false + default: 'yes' + floating_ips: + decription: + - list of valid floating IPs that pre-exist to assign to this node + required: false + default: None + floating_ip_pools: + description: + - list of floating IP pools from which to choose a floating IP required: false default: None availability_zone: @@ -201,14 +211,17 @@ def _delete_server(module, nova): module.fail_json(msg = "Timed out waiting for server to get deleted, please check manually") -def _add_floating_ip_from_pool(module, nova, server, floating_ip_obj): +def _add_floating_ip_from_pool(module, nova, server): + + # instantiate FloatingIPManager object + floating_ip_obj = floating_ips.FloatingIPManager(nova) # empty dict and list usable_floating_ips = {} pools = [] # user specified - pools = module.params['floating_ip']['pools'] + pools = module.params['floating_ip_pools'] # get the list of all floating IPs. Mileage may # vary according to Nova Compute configuration @@ -251,48 +264,41 @@ def _add_floating_ip_from_pool(module, nova, server, floating_ip_obj): module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message)) -def _add_floating_ip_no_pool(module, nova, server, floating_ip_obj): - - usable_floating_ips = list() - - # if there is a list of IP addresses, make that the list - if module.params['floating_ip'].has_key('ips'): - usable_floating_ips = [dict(ip=f, created=False) for f in module.params['floating_ip']['ips']] - else: - # get the list of all floating IPs. Mileage may - # vary according to Nova Compute configuration - # per cloud provider - for f_ip in floating_ip_obj.list(): - # if not reserved and the correct pool, add - if f_ip.instance_id is None: - usable_floating_ips.append(dict(ip=f_ip.ip, created=False)) - - if not usable_floating_ips: - try: - new_ip = nova.floating_ips.create() - except Exception, e: - module.fail_json(msg = "Unable to create floating ip") - usable_floating_ips.append(dict(ip=new_ip.ip, created=True)) - - # finally, add ip(s) to instance - for ip in usable_floating_ips: +def _add_floating_ip_list(module, server): + # add ip(s) to instance + for ip in module.params['floating_ips']: try: - server.add_floating_ip(ip['ip']) + server.add_floating_ip(ip) except Exception, e: - # Clean up - we auto-created this ip, and it's not attached - # to the server, so the cloud will not know what to do with it - server.floating_ips.delete(ip['ip']) module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message)) -def _add_floating_ip(module, nova, server): - # instantiate FloatingIPManager object - floating_ip_obj = floating_ips.FloatingIPManager(nova) +def _add_auto_floating_ip(module, nova, server): - if module.params['floating_ip'].has_key('pools'): - _add_floating_ip_from_pool(module, nova, server, floating_ip_obj) - else: - _add_floating_ip_no_pool(module, nova, server, floating_ip_obj) + try: + new_ip = nova.floating_ips.create() + except Exception as e: + module.fail_json(msg = "Unable to create floating ip: %s" % (e.message)) + + try: + server.add_floating_ip(new_ip) + except Exception as e: + # Clean up - we auto-created this ip, and it's not attached + # to the server, so the cloud will not know what to do with it + server.floating_ips.delete(new_ip) + module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message)) + + +def _add_floating_ip(module, nova, server): + + if module.params['floating_ip_pools']: + _add_floating_ip_from_pool(module, nova, server) + elif module.params['floating_ips']: + _add_floating_ip_list(module, server) + elif module.params['auto_floating_ip']: + _add_auto_floating_ip(module, nova, server) + else: + return server # this may look redundant, but if there is now a # floating IP, then it needs to be obtained from @@ -318,16 +324,6 @@ def _get_ips(addresses, ext_tag, key_name): def _create_server(module, nova): - # issue an error early on and not launch the instance - if module.params['floating_ip'] is not None: - if module.params['floating_ip'].has_key('ips'): - # can't specify "ips" and "auto" both - if module.params['floating_ip'].has_key('auto') and module.params['floating_ip']['auto']: - module.fail_json(msg = "For floating_ips - you cannot specify both 'auto' and 'ips'!") - # can't specify "ips" and "pools" both - if module.params['floating_ip'].has_key('pools'): - module.fail_json(msg = "For floating_ips - you cannot specify both 'pools' and 'ips'!") - bootargs = [module.params['name'], module.params['image_id'], module.params['flavor_id']] bootkwargs = { 'nics' : module.params['nics'], @@ -353,9 +349,7 @@ def _create_server(module, nova): except Exception, e: module.fail_json( msg = "Error in getting info from instance: %s" % e.message) if server.status == 'ACTIVE': - # if floating_ip is specified, then attach - if module.params['floating_ip'] is not None: - server = _add_floating_ip(module, nova, server) + server = _add_floating_ip(module, nova, server) private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private') public = _get_ips(getattr(server, 'addresses'), 'floating', 'public') @@ -417,9 +411,18 @@ def main(): wait_for = dict(default=180), state = dict(default='present', choices=['absent', 'present']), user_data = dict(default=None), - floating_ip = dict(default=None), + auto_floating_ip = dict(default=False, type='bool'), + floating_ips = dict(default=None), + floating_ip_pools = dict(default=None), )) - module = AnsibleModule(argument_spec=argument_spec) + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['auto_floating_ip','floating_ips'], + ['auto_floating_ip','floating_ip_pools'], + ['floating_ips','floating_ip_pools'], + ], + ) nova = nova_client.Client(module.params['login_username'], module.params['login_password'], From 65adb6465bcb4f9ba3d4c09f2762ceb2b4c4a4fd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 18:51:27 -0700 Subject: [PATCH 10/17] Specify nova image and flavor by name Putting uuid and numberic identifies in playbooks is fragile, especially with cloud providers who change them out from under you. Asking for Ubuntu 14.04 is consistent, the UUID associated with that is not. Add mutually exclusive parameters to allow for specifying images by name and flavors by RAM amount. --- library/cloud/nova_compute | 51 ++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 9146df42442..6adb5b59145 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -73,12 +73,22 @@ options: default: None image_id: description: - - The id of the image that has to be cloned + - The id of the base image to boot. Mutually exclusive with image_name + required: true + default: None + image_name: + description: + - The name of the base image to boot. Mutually exclusive with image_id required: true default: None flavor_id: description: - - The id of the flavor in which the new VM has to be created + - The id of the flavor in which the new VM has to be created. Mutually exclusive with flavor_ram + required: false + default: 1 + flavor_ram: + description: + - The minimum amount of ram in MB that the flavor in which the new VM has to be created must have. Mutually exclusive with flavor_id required: false default: 1 key_name: @@ -323,8 +333,35 @@ def _get_ips(addresses, ext_tag, key_name): return ret +def _get_image_id(module, nova): + if module.params['image_name']: + image = None + for img in nova.images.list(): + if img.name == module.params['image_name']: + image = img + break + if img.name.startswith(module.params['image_name']) and '(deprecated)' not in img.name: + image = img + if not image: + module.fail_json(msg = "Error finding image id from name(%s)" % module.params['image_name']) + return image.id + return module.params['image_id'] + + +def _get_flavor_id(module, nova): + if module.params['flavor_ram']: + try: + flavor = nova.flavors.find(ram=module.params['flavor_ram']) + except exceptions.NotFound as e: + module.fail_json(msg = "Error finding flavor with %sMB of RAM" % module.params['flavor_ram']) + return flavor.id + return module.params['flavor_id'] + + def _create_server(module, nova): - bootargs = [module.params['name'], module.params['image_id'], module.params['flavor_id']] + image_id = _get_image_id(module, nova) + flavor_id = _get_flavor_id(module, nova) + bootargs = [module.params['name'], image_id, flavor_id] bootkwargs = { 'nics' : module.params['nics'], 'meta' : module.params['meta'], @@ -402,7 +439,9 @@ def main(): argument_spec.update(dict( name = dict(required=True), image_id = dict(default=None), + image_name = dict(default=None), flavor_id = dict(default=1), + flavor_ram = dict(default=None, type='int'), key_name = dict(default=None), security_groups = dict(default='default'), nics = dict(default=None), @@ -421,6 +460,8 @@ def main(): ['auto_floating_ip','floating_ips'], ['auto_floating_ip','floating_ip_pools'], ['floating_ips','floating_ip_pools'], + ['image_id','image_name'], + ['flavor_id','flavor_ram'], ], ) @@ -438,8 +479,8 @@ def main(): module.fail_json(msg = "Unable to authorize user: %s" % e.message) if module.params['state'] == 'present': - if not module.params['image_id']: - module.fail_json( msg = "Parameter 'image_id' is required if state == 'present'") + if not module.params['image_id'] and not module.params['image_name']: + module.fail_json( msg = "Parameter 'image_id' or `image_name` is required if state == 'present'") else: _get_server_state(module, nova) _create_server(module, nova) From b80be42ed9275f0f6f0f57797031b81a80d3944c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 20:31:31 -0700 Subject: [PATCH 11/17] Add ability to filter on image and flavor names The fun part about having multiple vendors providing the same cloud is that while their APIs are the same, what they do with their metadata tends to be ... fun. So in order to be able to express sanely what you want without needing to stick tons of unreadable uuids in your config, it turns out what sometimes you need to further filter image and flavor names. Specific examples are (deprecated) images in HP Cloud and the Standard and Performance flavors on Rackspace. --- library/cloud/nova_compute | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 6adb5b59145..e514fa22ee9 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +import operator import os try: @@ -81,6 +82,9 @@ options: - The name of the base image to boot. Mutually exclusive with image_id required: true default: None + image_filter: + description: + - Text to use to filter image names, for the case, such as HP, where there are multiple image names matching the common identifying portions. image_filter is a negative match filter - it is text that may not exist in the image name. Defaults to "(deprecated)" flavor_id: description: - The id of the flavor in which the new VM has to be created. Mutually exclusive with flavor_ram @@ -91,6 +95,9 @@ options: - The minimum amount of ram in MB that the flavor in which the new VM has to be created must have. Mutually exclusive with flavor_id required: false default: 1 + flavor_filter: + description: + - Text to use to filter flavor names, for the case, such as Rackspace, where there are multiple flavors that have the same ram count. flavor_filter is a positive match filter - it must exist in the flavor name. key_name: description: - The key pair name to be used when creating a VM @@ -335,26 +342,22 @@ def _get_ips(addresses, ext_tag, key_name): def _get_image_id(module, nova): if module.params['image_name']: - image = None - for img in nova.images.list(): - if img.name == module.params['image_name']: - image = img - break - if img.name.startswith(module.params['image_name']) and '(deprecated)' not in img.name: - image = img - if not image: - module.fail_json(msg = "Error finding image id from name(%s)" % module.params['image_name']) - return image.id + for image in nova.images.list(): + if (module.params['image_name'] in img.name and ( + not module.params['image_filter'] + or module.params['image_filter'] not in img.name)): + return image.id + module.fail_json(msg = "Error finding image id from name(%s)" % module.params['image_name']) return module.params['image_id'] def _get_flavor_id(module, nova): if module.params['flavor_ram']: - try: - flavor = nova.flavors.find(ram=module.params['flavor_ram']) - except exceptions.NotFound as e: + for flavor in sorted(nova.flavors.list(), key=operator.attrgetter('ram')): + if (flavor.ram >= module.params['flavor_ram'] and + (not module.params['flavor_filter'] or module.params['flavor_filter'] in flavor.name)): + return flavor.id module.fail_json(msg = "Error finding flavor with %sMB of RAM" % module.params['flavor_ram']) - return flavor.id return module.params['flavor_id'] @@ -440,8 +443,10 @@ def main(): name = dict(required=True), image_id = dict(default=None), image_name = dict(default=None), + image_filter = dict(default='(deprecated)'), flavor_id = dict(default=1), flavor_ram = dict(default=None, type='int'), + flavor_filter = dict(default=None), key_name = dict(default=None), security_groups = dict(default='default'), nics = dict(default=None), @@ -488,7 +493,7 @@ def main(): _get_server_state(module, nova) _delete_server(module, nova) -# this is magic, see lib/ansible/module.params['common.py +# this is magic, see lib/ansible/module_common.py from ansible.module_utils.basic import * from ansible.module_utils.openstack import * main() From c6975552d797e5f4fef04075e51758ec4f9a4839 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 20:45:35 -0700 Subject: [PATCH 12/17] Cleaned up nova_compute documentation Added version_added tags as well as a few more examples. --- library/cloud/nova_compute | 73 +++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index e514fa22ee9..2df5942a260 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -82,9 +82,11 @@ options: - The name of the base image to boot. Mutually exclusive with image_id required: true default: None + version_added: "1.7" image_filter: description: - Text to use to filter image names, for the case, such as HP, where there are multiple image names matching the common identifying portions. image_filter is a negative match filter - it is text that may not exist in the image name. Defaults to "(deprecated)" + version_added: "1.7" flavor_id: description: - The id of the flavor in which the new VM has to be created. Mutually exclusive with flavor_ram @@ -95,9 +97,11 @@ options: - The minimum amount of ram in MB that the flavor in which the new VM has to be created must have. Mutually exclusive with flavor_id required: false default: 1 + version_added: "1.7" flavor_filter: description: - Text to use to filter flavor names, for the case, such as Rackspace, where there are multiple flavors that have the same ram count. flavor_filter is a positive match filter - it must exist in the flavor name. + version_added: "1.7" key_name: description: - The key pair name to be used when creating a VM @@ -118,21 +122,25 @@ options: - Should a floating ip be auto created and assigned required: false default: 'yes' + version_added: "1.7" floating_ips: decription: - list of valid floating IPs that pre-exist to assign to this node required: false default: None + version_added: "1.7" floating_ip_pools: description: - list of floating IP pools from which to choose a floating IP required: false default: None + version_added: "1.7" availability_zone: description: - Name of the availability zone required: false default: None + version_added: "1.7" meta: description: - A list of key value pairs that should be provided as a metadata to the new VM @@ -182,8 +190,8 @@ EXAMPLES = ''' - name: launch an instance nova_compute: state: present - login_username: username - login_password: Equality7-2521 + login_username: username + login_password: Equality7-2521 login_tenant_name: username-project1 name: vm1 auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ @@ -194,15 +202,62 @@ EXAMPLES = ''' wait_for: 200 flavor_id: 101 security_groups: default - floating_ip: - auto: True + auto_floating_ip: yes -# If one wants to specify a floating ip to use: - - floating_ip: - ips: - - 15.126.238.160 +# Creates a new VM in HP Cloud AE1 region availability zone az2 and assigns a pre-known floating IP +- name: launch a nova instance + hosts: localhost + tasks: + - name: launch an instance + nova_compute: + state: present + login_username: username + login_password: Equality7-2521 + login_tenant_name: username-project1 + name: vm1 + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ + region_name: region-b.geo-1 + availability_zone: az2 + image_id: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + wait_for: 200 + flavor_id: 101 + floating-ips: + - 12.34.56.79 +# Creates a new VM with 4G of RAM on Ubuntu Trusty, ignoring deprecated images +- name: launch a nova instance + hosts: localhost + tasks: + - name: launch an instance + nova_compute: + name: vm1 + state: present + login_username: username + login_password: Equality7-2521 + login_tenant_name: username-project1 + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ + region_name: region-b.geo-1 + image_name: Ubuntu Server 14.04 + image_filter: deprecated + flavor_ram: 4096 + +# Creates a new VM with 4G of RAM on Ubuntu Trusty on a Rackspace Performance node in DFW +- name: launch a nova instance + hosts: localhost + tasks: + - name: launch an instance + nova_compute: + name: vm1 + state: present + login_username: username + login_password: Equality7-2521 + login_tenant_name: username-project1 + auth_url: https://identity.api.rackspacecloud.com/v2.0/ + region_name: DFW + image_name: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM) + flavor_ram: 4096 + flavor_filter: Performance ''' From a12086f1bff2dae69bb510ad9264287a1fb7e5ba Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 21:13:17 -0700 Subject: [PATCH 13/17] Update existing nova instances with current ip During the state check, check IP address information. This gets us two things. The most obvious is that for direct IP management, a change to the config will reflect in the config of the instance. But also, if we succeed in creating the instance but fail in adding an IP, this should let us re-run and arrive in the state we were expecting. --- library/cloud/nova_compute | 47 +++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 2df5942a260..933d5714930 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -336,9 +336,9 @@ def _add_floating_ip_from_pool(module, nova, server): module.fail_json(msg = "Error attaching IP %s to instance %s: %s " % (ip, server.id, e.message)) -def _add_floating_ip_list(module, server): +def _add_floating_ip_list(module, server, ips): # add ip(s) to instance - for ip in module.params['floating_ips']: + for ip in ips: try: server.add_floating_ip(ip) except Exception, e: @@ -366,7 +366,7 @@ def _add_floating_ip(module, nova, server): if module.params['floating_ip_pools']: _add_floating_ip_from_pool(module, nova, server) elif module.params['floating_ips']: - _add_floating_ip_list(module, server) + _add_floating_ip_list(module, server, module.params['floating_ips']) elif module.params['auto_floating_ip']: _add_auto_floating_ip(module, nova, server) else: @@ -382,11 +382,11 @@ def _add_floating_ip(module, nova, server): return server -def _get_ips(addresses, ext_tag, key_name): +def _get_ips(addresses, ext_tag, key_name=None): ret = [] for (k, v) in addresses.iteritems(): - if k == key_name: + if key_name and k == key_name: ret.extend([addrs['addr'] for addrs in v]) else: for interface_spec in v: @@ -465,6 +465,40 @@ def _create_server(module, nova): module.exit_json(changed = True, id = info['id'], private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info) +def _delete_floating_ip_list(module, nova, server, extra_ips): + for ip in extra_ips: + nova.servers.remove_floating_ip(server=server.id, address=ip) + + +def _check_floating_ips(module, nova, server): + changed = False + if module.params['floating_ip_pools'] or module.params['floating_ips'] or module.params['auto_floating_ip']: + ips = _get_ips(server.addresses, 'floating') + if not ips: + # If we're configured to have a floating but we don't have one, + # let's add one + server = _add_floating_ip(module, nova, server) + changed = True + elif module.params['floating_ips']: + # we were configured to have specific ips, let's make sure we have + # those + missing_ips = [] + for ip in module.params['floating_ips']: + if ip not in ips: + missing_ips.append(ip) + if missing_ips: + server = _add_floating_ip_list(module, server, missing_ips) + changed = True + extra_ips = [] + for ip in ips: + if ip not in module.params['floating_ips']: + extra_ips.append(ip) + if extra_ips: + _delete_floating_ip_list(module, server, extra_ips) + changed = True + return (changed, server) + + def _get_server_state(module, nova): server = None try: @@ -481,9 +515,10 @@ def _get_server_state(module, nova): if server and module.params['state'] == 'present': if server.status != 'ACTIVE': module.fail_json( msg="The VM is available but not Active. state:" + server.status) + (ip_changed, server) = _check_floating_ips(module, nova, server) private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private') public = _get_ips(getattr(server, 'addresses'), 'floating', 'public') - module.exit_json(changed = False, id = server.id, public_ip = ''.join(public), private_ip = ''.join(private), info = server._info) + module.exit_json(changed = ip_changed, id = server.id, public_ip = ''.join(public), private_ip = ''.join(private), info = server._info) if server and module.params['state'] == 'absent': return True if module.params['state'] == 'absent': From a05804bf8d384cb5ff0916f7f4994679e137acca Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 2 Aug 2014 22:02:29 -0700 Subject: [PATCH 14/17] Update the nova inventory plugin with new ip code The provisioning module knows more about how nova deals with IP addresses now. Ensure that the inventory module is similarly as smart by separating out the logic into the openstack/module_utils. --- lib/ansible/module_utils/openstack.py | 13 +++++++++++++ library/cloud/nova_compute | 27 +++++++------------------- plugins/inventory/nova.py | 28 ++++++++++++++------------- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/lib/ansible/module_utils/openstack.py b/lib/ansible/module_utils/openstack.py index c70eb9fbfa8..7f955a112d3 100644 --- a/lib/ansible/module_utils/openstack.py +++ b/lib/ansible/module_utils/openstack.py @@ -54,3 +54,16 @@ def openstack_argument_spec(): else: spec['login_tenant_name'] = dict(required=True) return spec + + +def openstack_find_nova_addresses(addresses, ext_tag, key_name=None): + + ret = [] + for (k, v) in addresses.iteritems(): + if key_name and k == key_name: + ret.extend([addrs['addr'] for addrs in v]) + else: + for interface_spec in v: + if 'OS-EXT-IPS:type' in interface_spec and interface_spec['OS-EXT-IPS:type'] == ext_tag: + ret.append(interface_spec['addr']) + return ret diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 933d5714930..2358d7cb639 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -382,19 +382,6 @@ def _add_floating_ip(module, nova, server): return server -def _get_ips(addresses, ext_tag, key_name=None): - - ret = [] - for (k, v) in addresses.iteritems(): - if key_name and k == key_name: - ret.extend([addrs['addr'] for addrs in v]) - else: - for interface_spec in v: - if 'OS-EXT-IPS:type' in interface_spec and interface_spec['OS-EXT-IPS:type'] == ext_tag: - ret.append(interface_spec['addr']) - return ret - - def _get_image_id(module, nova): if module.params['image_name']: for image in nova.images.list(): @@ -446,8 +433,8 @@ def _create_server(module, nova): if server.status == 'ACTIVE': server = _add_floating_ip(module, nova, server) - private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private') - public = _get_ips(getattr(server, 'addresses'), 'floating', 'public') + private = openstack_find_nova_addresses(getattr(server, 'addresses'), 'fixed', 'private') + public = openstack_find_nova_addresses(getattr(server, 'addresses'), 'floating', 'public') # now exit with info module.exit_json(changed = True, id = server.id, private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info) @@ -459,8 +446,8 @@ def _create_server(module, nova): module.fail_json(msg = "Timeout waiting for the server to come up.. Please check manually") if server.status == 'ERROR': module.fail_json(msg = "Error in creating the server.. Please check manually") - private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private') - public = _get_ips(getattr(server, 'addresses'), 'floating', 'public') + private = openstack_find_nova_addresses(getattr(server, 'addresses'), 'fixed', 'private') + public = openstack_find_nova_addresses(getattr(server, 'addresses'), 'floating', 'public') module.exit_json(changed = True, id = info['id'], private_ip=''.join(private), public_ip=''.join(public), status = server.status, info = server._info) @@ -473,7 +460,7 @@ def _delete_floating_ip_list(module, nova, server, extra_ips): def _check_floating_ips(module, nova, server): changed = False if module.params['floating_ip_pools'] or module.params['floating_ips'] or module.params['auto_floating_ip']: - ips = _get_ips(server.addresses, 'floating') + ips = openstack_find_nova_addresses(server.addresses, 'floating') if not ips: # If we're configured to have a floating but we don't have one, # let's add one @@ -516,8 +503,8 @@ def _get_server_state(module, nova): if server.status != 'ACTIVE': module.fail_json( msg="The VM is available but not Active. state:" + server.status) (ip_changed, server) = _check_floating_ips(module, nova, server) - private = _get_ips(getattr(server, 'addresses'), 'fixed', 'private') - public = _get_ips(getattr(server, 'addresses'), 'floating', 'public') + private = openstack_find_nova_addresses(getattr(server, 'addresses'), 'fixed', 'private') + public = openstack_find_nova_addresses(getattr(server, 'addresses'), 'floating', 'public') module.exit_json(changed = ip_changed, id = server.id, public_ip = ''.join(public), private_ip = ''.join(private), info = server._info) if server and module.params['state'] == 'absent': return True diff --git a/plugins/inventory/nova.py b/plugins/inventory/nova.py index 3ae19d0d177..585e26732ed 100755 --- a/plugins/inventory/nova.py +++ b/plugins/inventory/nova.py @@ -28,6 +28,8 @@ try: except: import simplejson as json +from ansible.module_utils.openstack import * + ################################################### # executed with no parameters, return the list of # all groups and hosts @@ -57,12 +59,12 @@ if not config: sys.exit('Unable to find configfile in %s' % ', '.join(NOVA_CONFIG_FILES)) client = nova_client.Client( - version = config.get('openstack', 'version'), - username = config.get('openstack', 'username'), - api_key = config.get('openstack', 'api_key'), - auth_url = config.get('openstack', 'auth_url'), + config.get('openstack', 'version'), + config.get('openstack', 'username'), + config.get('openstack', 'api_key'), + config.get('openstack', 'project_id'), + config.get('openstack', 'auth_url'), region_name = config.get('openstack', 'region_name'), - project_id = config.get('openstack', 'project_id'), auth_system = config.get('openstack', 'auth_system') ) @@ -70,20 +72,20 @@ if len(sys.argv) == 2 and (sys.argv[1] == '--list'): groups = {} # Cycle on servers - for f in client.servers.list(): - private = [ x['addr'] for x in getattr(f, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'fixed'] - public = [ x['addr'] for x in getattr(f, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'floating'] + for server in client.servers.list(): + private = openstack_find_nova_addresses(getattr(server, 'addresses'), 'fixed', 'private') + public = openstack_find_nova_addresses(getattr(server, 'addresses'), 'floating', 'public') # Define group (or set to empty string) - group = f.metadata['group'] if f.metadata.has_key('group') else 'undefined' + group = server.metadata['group'] if server.metadata.has_key('group') else 'undefined' # Create group if not exist if group not in groups: groups[group] = [] # Append group to list - if f.accessIPv4: - groups[group].append(f.accessIPv4) + if server.accessIPv4: + groups[group].append(server.accessIPv4) continue if public: groups[group].append(''.join(public)) @@ -104,8 +106,8 @@ elif len(sys.argv) == 3 and (sys.argv[1] == '--host'): results = {} ips = [] for instance in client.servers.list(): - private = [ x['addr'] for x in getattr(instance, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'fixed'] - public = [ x['addr'] for x in getattr(instance, 'addresses').itervalues().next() if x['OS-EXT-IPS:type'] == 'floating'] + private = openstack_find_nova_addresses(getattr(instance, 'addresses'), 'fixed', 'private') + public = openstack_find_nova_addresses(getattr(instance, 'addresses'), 'floating', 'public') ips.append( instance.accessIPv4) ips.append(''.join(private)) ips.append(''.join(public)) From 38beae3b196cc825ed1e03cc88c016ee702a096a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 3 Aug 2014 09:05:45 -0700 Subject: [PATCH 15/17] Add nova config drive support Config drive can be enabled on nova by passing config_drive=True. That's a pretty easy feature to support. --- library/cloud/nova_compute | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index 2358d7cb639..abddc582c93 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -156,6 +156,11 @@ options: - The amount of time the module should wait for the VM to get into active state required: false default: 180 + config_drive: + description: + - Whether to boot the server with config drive enabled + required: false + default: 'no' user_data: description: - Opaque blob of data which is made available to the instance @@ -385,9 +390,9 @@ def _add_floating_ip(module, nova, server): def _get_image_id(module, nova): if module.params['image_name']: for image in nova.images.list(): - if (module.params['image_name'] in img.name and ( + if (module.params['image_name'] in image.name and ( not module.params['image_filter'] - or module.params['image_filter'] not in img.name)): + or module.params['image_filter'] not in image.name)): return image.id module.fail_json(msg = "Error finding image id from name(%s)" % module.params['image_name']) return module.params['image_id'] @@ -413,6 +418,7 @@ def _create_server(module, nova): 'security_groups': module.params['security_groups'].split(','), #userdata is unhyphenated in novaclient, but hyphenated here for consistency with the ec2 module: 'userdata': module.params['user_data'], + 'config_drive': module.params['config_drive'], } for optional_param in ('region_name', 'key_name', 'availability_zone'): @@ -532,6 +538,7 @@ def main(): wait_for = dict(default=180), state = dict(default='present', choices=['absent', 'present']), user_data = dict(default=None), + config_drive = dict(default=False, type='bool'), auto_floating_ip = dict(default=False, type='bool'), floating_ips = dict(default=None), floating_ip_pools = dict(default=None), From b81a37ad9b1b2fec59667698c8cd65f01b119609 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 3 Aug 2014 09:21:14 -0700 Subject: [PATCH 16/17] Rename nova filters to include and exclude jeblair says: "having 'flavor_filter' be inclusive, and 'image_filter' be exclusive is kind of mind blowing" and I agree. Let's be more explicit. --- library/cloud/nova_compute | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/library/cloud/nova_compute b/library/cloud/nova_compute index abddc582c93..236ce6ab572 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -83,9 +83,9 @@ options: required: true default: None version_added: "1.7" - image_filter: + image_exclude: description: - - Text to use to filter image names, for the case, such as HP, where there are multiple image names matching the common identifying portions. image_filter is a negative match filter - it is text that may not exist in the image name. Defaults to "(deprecated)" + - Text to use to filter image names, for the case, such as HP, where there are multiple image names matching the common identifying portions. image_exclude is a negative match filter - it is text that may not exist in the image name. Defaults to "(deprecated)" version_added: "1.7" flavor_id: description: @@ -98,9 +98,9 @@ options: required: false default: 1 version_added: "1.7" - flavor_filter: + flavor_include: description: - - Text to use to filter flavor names, for the case, such as Rackspace, where there are multiple flavors that have the same ram count. flavor_filter is a positive match filter - it must exist in the flavor name. + - Text to use to filter flavor names, for the case, such as Rackspace, where there are multiple flavors that have the same ram count. flavor_include is a positive match filter - it must exist in the flavor name. version_added: "1.7" key_name: description: @@ -244,7 +244,7 @@ EXAMPLES = ''' auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ region_name: region-b.geo-1 image_name: Ubuntu Server 14.04 - image_filter: deprecated + image_exclude: deprecated flavor_ram: 4096 # Creates a new VM with 4G of RAM on Ubuntu Trusty on a Rackspace Performance node in DFW @@ -262,7 +262,7 @@ EXAMPLES = ''' region_name: DFW image_name: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM) flavor_ram: 4096 - flavor_filter: Performance + flavor_include: Performance ''' @@ -391,8 +391,8 @@ def _get_image_id(module, nova): if module.params['image_name']: for image in nova.images.list(): if (module.params['image_name'] in image.name and ( - not module.params['image_filter'] - or module.params['image_filter'] not in image.name)): + not module.params['image_exclude'] + or module.params['image_exclude'] not in image.name)): return image.id module.fail_json(msg = "Error finding image id from name(%s)" % module.params['image_name']) return module.params['image_id'] @@ -402,7 +402,7 @@ def _get_flavor_id(module, nova): if module.params['flavor_ram']: for flavor in sorted(nova.flavors.list(), key=operator.attrgetter('ram')): if (flavor.ram >= module.params['flavor_ram'] and - (not module.params['flavor_filter'] or module.params['flavor_filter'] in flavor.name)): + (not module.params['flavor_include'] or module.params['flavor_include'] in flavor.name)): return flavor.id module.fail_json(msg = "Error finding flavor with %sMB of RAM" % module.params['flavor_ram']) return module.params['flavor_id'] @@ -526,10 +526,10 @@ def main(): name = dict(required=True), image_id = dict(default=None), image_name = dict(default=None), - image_filter = dict(default='(deprecated)'), + image_exclude = dict(default='(deprecated)'), flavor_id = dict(default=1), flavor_ram = dict(default=None, type='int'), - flavor_filter = dict(default=None), + flavor_include = dict(default=None), key_name = dict(default=None), security_groups = dict(default='default'), nics = dict(default=None), From f3357099a738366a084928cc8abb47ca6cfb6a1a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 3 Aug 2014 10:43:39 -0700 Subject: [PATCH 17/17] Pass region_name to nova_keypair Again, on things with regions, when we're configuring it, we should actually pass it in to the compute constructor. --- library/cloud/nova_keypair | 1 + 1 file changed, 1 insertion(+) diff --git a/library/cloud/nova_keypair b/library/cloud/nova_keypair index be2bbb1d93d..c7c9affb3e6 100644 --- a/library/cloud/nova_keypair +++ b/library/cloud/nova_keypair @@ -99,6 +99,7 @@ def main(): module.params['login_password'], module.params['login_tenant_name'], module.params['auth_url'], + region_name=module.params['region_name'], service_type='compute') try: nova.authenticate()