From eccb48c8da77bf9ba884cc989251ed5d5209b1e1 Mon Sep 17 00:00:00 2001 From: Carson Gee Date: Sat, 17 May 2014 22:10:24 -0400 Subject: [PATCH 1/4] Improvements to OpenStack inventory script --- plugins/inventory/nova.ini | 9 +- plugins/inventory/nova.py | 166 ++++++++++++++++++++++++++----------- 2 files changed, 124 insertions(+), 51 deletions(-) mode change 100755 => 100644 plugins/inventory/nova.py diff --git a/plugins/inventory/nova.ini b/plugins/inventory/nova.ini index e648e5f143c..040c52bcee9 100644 --- a/plugins/inventory/nova.ini +++ b/plugins/inventory/nova.ini @@ -14,7 +14,7 @@ api_key = auth_url = # Authentication system -auth_system = +auth_system = keystone # OpenStack nova project_id project_id = @@ -22,6 +22,13 @@ project_id = # Serverarm region name to use region_name = +# Specify a preference for public or private IPs (public is default) +prefer_private = False + +# What service type (required for newer nova client) +service_type = compute + + # TODO: Some other options # insecure = # endpoint_type = diff --git a/plugins/inventory/nova.py b/plugins/inventory/nova.py old mode 100755 new mode 100644 index 585e26732ed..b1094c72887 --- a/plugins/inventory/nova.py +++ b/plugins/inventory/nova.py @@ -25,11 +25,9 @@ from novaclient import client as nova_client try: import json -except: +except ImportError: import simplejson as json -from ansible.module_utils.openstack import * - ################################################### # executed with no parameters, return the list of # all groups and hosts @@ -54,45 +52,129 @@ def nova_load_config_file(): return None + +def get_fallback(config, value, section="openstack"): + """ + Get value from config object and return the value + or false + """ + try: + return config.get(section, value) + except ConfigParser.NoOptionError: + return False + + +def push(data, key, element): + """ + Assist in items to a dictionary of lists + """ + if (not element) or (not key): + return + + if key in data: + data[key].append(element) + else: + data[key] = [element] + + +def to_safe(word): + ''' + Converts 'bad' characters in a string to underscores so they can + be used as Ansible groups + ''' + return re.sub(r"[^A-Za-z0-9\-]", "_", word) + + +def get_ips(server, access_ip=True): + """ + Returns a list of the server's IPs, or the preferred + access IP + """ + private = [] + public = [] + address_list = [] + # Iterate through each servers network(s), get addresses and get type + addresses = getattr(server, 'addresses', {}) + if len(addresses) > 0: + for network in addresses.itervalues(): + for address in network: + if address.get('OS-EXT-IPS:type', False) == 'fixed': + private.append(address['addr']) + elif address.get('OS-EXT-IPS:type', False) == 'floating': + public.append(address['addr']) + + if not access_ip: + address_list.append(server.accessIPv4) + address_list.extend(private) + address_list.extend(public) + return address_list + + access_ip = None + # Append group to list + if server.accessIPv4: + access_ip = server.accessIPv4 + if (not access_ip) and public and not (private and prefer_private): + access_ip = public[0] + if private and not access_ip: + access_ip = private[0] + + return access_ip + + +def get_metadata(server): + """Returns dictionary of all host metadata""" + get_ips(server, False) + results = {} + for key in vars(server): + # Extract value + value = getattr(server, key) + + # Generate sanitized key + key = 'os_' + re.sub(r"[^A-Za-z0-9\-]", "_", key).lower() + + # Att value to instance result (exclude manager class) + #TODO: maybe use value.__class__ or similar inside of key_name + if key != 'os_manager': + results[key] = value + return results + config = nova_load_config_file() if not config: sys.exit('Unable to find configfile in %s' % ', '.join(NOVA_CONFIG_FILES)) client = nova_client.Client( - config.get('openstack', 'version'), - config.get('openstack', 'username'), - config.get('openstack', 'api_key'), - config.get('openstack', 'project_id'), - config.get('openstack', 'auth_url'), + version = config.get('openstack', 'version'), + username = config.get('openstack', 'username'), + api_key = config.get('openstack', 'api_key'), + auth_url = 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') ) -if len(sys.argv) == 2 and (sys.argv[1] == '--list'): - groups = {} - +# Default or added list option +if (len(sys.argv) == 2 and sys.argv[1] == '--list') or len(sys.argv) == 1: + groups = {'_meta': {'hostvars': {}}} # Cycle on servers 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 = 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 server.accessIPv4: - groups[group].append(server.accessIPv4) - continue - if public: - groups[group].append(''.join(public)) - continue - if private: - groups[group].append(''.join(private)) - continue + access_ip = get_ips(server) + + # Push to name group of 1 + push(groups, server.name, access_ip) + + # Run through each metadata item and add instance to it + for key, value in server.metadata.iteritems(): + composed_key = to_safe('tag_{0}_{1}'.format(key, value)) + push(groups, composed_key, access_ip) + + # Do special handling of group for backwards compat + # inventory groups + group = server.metadata['group'] if 'group' in server.metadata else 'undefined' + push(groups, group, access_ip) + + # Add vars to _meta key for performance optimization in + # Ansible 1.3+ + groups['_meta']['hostvars'][access_ip] = get_metadata(server) # Return server list print(json.dumps(groups, sort_keys=True, indent=2)) @@ -105,25 +187,9 @@ if len(sys.argv) == 2 and (sys.argv[1] == '--list'): elif len(sys.argv) == 3 and (sys.argv[1] == '--host'): results = {} ips = [] - for instance in client.servers.list(): - 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)) - if sys.argv[2] in ips: - for key in vars(instance): - # Extract value - value = getattr(instance, key) - - # Generate sanitized key - key = 'os_' + re.sub("[^A-Za-z0-9\-]", "_", key).lower() - - # Att value to instance result (exclude manager class) - #TODO: maybe use value.__class__ or similar inside of key_name - if key != 'os_manager': - results[key] = value - + for server in client.servers.list(): + if sys.argv[2] in (get_ips(server) or []): + results = get_metadata(server) print(json.dumps(results, sort_keys=True, indent=2)) sys.exit(0) From cd5edc416c810354704c5b41701b1bcebb42305c Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 1 Jul 2014 09:41:55 -0700 Subject: [PATCH 2/4] nova.py: Set defaults for OpenStack settings - auth_system - region_name - service_type These are config settings that could be left out in many scenarios, but the current code is requiring them. In particular, "service_type" is a new one in PR #7444 so if we add that and don't set a default, then existing .ini files won't work: ``` File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ConfigParser.py", line 618, in get raise NoOptionError(option, section) ConfigParser.NoOptionError: No option 'service_type' in section: 'openstack' ``` --- plugins/inventory/nova.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/inventory/nova.py b/plugins/inventory/nova.py index b1094c72887..48e720184f5 100644 --- a/plugins/inventory/nova.py +++ b/plugins/inventory/nova.py @@ -39,6 +39,7 @@ NOVA_CONFIG_FILES = [os.getcwd() + "/nova.ini", NOVA_DEFAULTS = { 'auth_system': None, 'region_name': None, + 'service_type': 'compute', } From 1560b963aa2b5188cf138a1f0be0e27b22f4915a Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 1 Jul 2014 12:20:15 -0700 Subject: [PATCH 3/4] nova.py: Support OS_AUTH_SYSTEM and OS_REGION_NAME --- plugins/inventory/nova.py | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/plugins/inventory/nova.py b/plugins/inventory/nova.py index 48e720184f5..7e58390ee1a 100644 --- a/plugins/inventory/nova.py +++ b/plugins/inventory/nova.py @@ -143,14 +143,37 @@ config = nova_load_config_file() if not config: sys.exit('Unable to find configfile in %s' % ', '.join(NOVA_CONFIG_FILES)) +# Load up connections info based on config and then environment +# variables +username = (get_fallback(config, 'username') or + os.environ.get('OS_USERNAME', None)) +api_key = (get_fallback(config, 'api_key') or + os.environ.get('OS_PASSWORD', None)) +auth_url = (get_fallback(config, 'auth_url') or + os.environ.get('OS_AUTH_URL', None)) +project_id = (get_fallback(config, 'project_id') or + os.environ.get('OS_TENANT_NAME', None)) +region_name = (get_fallback(config, 'region_name') or + os.environ.get('OS_REGION_NAME', None)) +auth_system = (get_fallback(config, 'auth_system') or + os.environ.get('OS_AUTH_SYSTEM', None)) + +# Determine what type of IP is preferred to return +prefer_private = False +try: + prefer_private = config.getboolean('openstack', 'prefer_private') +except ConfigParser.NoOptionError: + pass + 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'), - region_name = config.get('openstack', 'region_name'), - project_id = config.get('openstack', 'project_id'), - auth_system = config.get('openstack', 'auth_system') + version=config.get('openstack', 'version'), + username=username, + api_key=api_key, + auth_url=auth_url, + region_name=region_name, + project_id=project_id, + auth_system=auth_system, + service_type=config.get('openstack', 'service_type'), ) # Default or added list option From 7cc5ecae527588dde572ddbace1d13e4a4b62bdf Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 1 Jul 2014 12:47:25 -0700 Subject: [PATCH 4/4] nova.ini: Distinguish between required and optional settings Put them in separate sections of config to make it more clear what is essential and what is not. Also comment out the optional settings. And remove duplicate mention of `service_type`. --- plugins/inventory/nova.ini | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/inventory/nova.ini b/plugins/inventory/nova.ini index 040c52bcee9..4900c496516 100644 --- a/plugins/inventory/nova.ini +++ b/plugins/inventory/nova.ini @@ -1,37 +1,45 @@ # Ansible OpenStack external inventory script [openstack] + +#------------------------------------------------------------------------- +# Required settings +#------------------------------------------------------------------------- + # API version version = 2 # OpenStack nova username username = -# OpenStack nova api_key +# OpenStack nova api_key or password api_key = # OpenStack nova auth_url auth_url = -# Authentication system -auth_system = keystone +# OpenStack nova project_id or tenant name +project_id = -# OpenStack nova project_id -project_id = +#------------------------------------------------------------------------- +# Optional settings +#------------------------------------------------------------------------- + +# Authentication system +# auth_system = keystone # Serverarm region name to use -region_name = +# region_name = # Specify a preference for public or private IPs (public is default) -prefer_private = False +# prefer_private = False # What service type (required for newer nova client) -service_type = compute +# service_type = compute # TODO: Some other options # insecure = # endpoint_type = # extensions = -# service_type = # service_name =