diff --git a/lib/ansible/module_utils/openstack.py b/lib/ansible/module_utils/openstack.py index c70eb9fbfa8..64f95437143 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 86ea57d8996..236ce6ab572 100644 --- a/library/cloud/nova_compute +++ b/library/cloud/nova_compute @@ -17,11 +17,14 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +import operator import os 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'") @@ -71,14 +74,34 @@ 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 + version_added: "1.7" + 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_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: - - 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 + version_added: "1.7" + 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_include 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 @@ -94,6 +117,30 @@ options: - A list of network id's to which the VM's interface should be attached required: false default: None + auto_floating_ip: + description: + - 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 @@ -109,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 @@ -135,8 +187,86 @@ EXAMPLES = ''' meta: hostname: test1 group: uge_master + +# 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: + - 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 + security_groups: default + auto_floating_ip: yes + +# 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_exclude: 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_include: Performance ''' + + def _delete_server(module, nova): name = None server_list = None @@ -158,18 +288,142 @@ 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): + + # 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'] + + # 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_list(module, server, ips): + # add ip(s) to instance + for ip in 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_auto_floating_ip(module, nova, server): + + 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, module.params['floating_ips']) + 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 + # 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 _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_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'] + + +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_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'] + + 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'], - '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'], + 'config_drive': module.params['config_drive'], } - if not module.params['key_name']: - del bootkwargs['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: server = nova.servers.create(*bootargs, **bootkwargs) server = nova.servers.get(server.id) @@ -181,11 +435,16 @@ 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': - 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'] + server = _add_floating_ip(module, nova, server) + + 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) + if server.status == 'ERROR': module.fail_json(msg = "Error in creating the server, please check logs") time.sleep(2) @@ -193,11 +452,46 @@ 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 = 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) +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 = 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 + 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: @@ -214,9 +508,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) - 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'] - module.exit_json(changed = False, id = server.id, public_ip = ''.join(public), private_ip = ''.join(private), info = server._info) + (ip_changed, server) = _check_floating_ips(module, nova, server) + 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 if module.params['state'] == 'absent': @@ -230,7 +525,11 @@ def main(): argument_spec.update(dict( name = dict(required=True), image_id = dict(default=None), + image_name = dict(default=None), + image_exclude = dict(default='(deprecated)'), flavor_id = dict(default=1), + flavor_ram = dict(default=None, type='int'), + flavor_include = dict(default=None), key_name = dict(default=None), security_groups = dict(default='default'), nics = dict(default=None), @@ -239,8 +538,21 @@ 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), )) - 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'], + ['image_id','image_name'], + ['flavor_id','flavor_ram'], + ], + ) nova = nova_client.Client(module.params['login_username'], module.params['login_password'], @@ -256,8 +568,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) @@ -265,7 +577,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() 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() 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))