From 2bd927fd818c3cd645d5d21f4550a47b4ecb1dd2 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 22 Oct 2014 14:40:20 -0500 Subject: [PATCH 1/4] Support RackConnect v3 by allowing a network to be specified for use in determining ansible_ssh_host --- plugins/inventory/rax.py | 149 ++++++++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 55 deletions(-) mode change 100755 => 100644 plugins/inventory/rax.py diff --git a/plugins/inventory/rax.py b/plugins/inventory/rax.py old mode 100755 new mode 100644 index 457c20962a..87b7f9cafc --- a/plugins/inventory/rax.py +++ b/plugins/inventory/rax.py @@ -1,8 +1,10 @@ #!/usr/bin/env python -# (c) 2013, Jesse Keating +# (c) 2013, Jesse Keating , +# Matt Martz # -# This file is part of Ansible, +# This file is part of Ansible. # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,16 +19,20 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -DOCUMENTATION = ''' ---- -inventory: rax -short_description: Rackspace Public Cloud external inventory script -description: - - Generates inventory that Ansible can understand by making API request to +""" +Rackspace Cloud Inventory + +Authors: + Jesse Keating , + Matt Martz + + +Description: + Generates inventory that Ansible can understand by making API request to Rackspace Public Cloud API - - | - When run against a specific host, this script returns the following - variables: + + When run against a specific host, this script returns variables similar to: rax_os-ext-sts_task_state rax_addresses rax_links @@ -50,63 +56,67 @@ description: rax_tenant_id rax_loaded - where some item can have nested structure. - - credentials are set in a credentials file -version_added: None -options: - creds_file: - description: - - File to find the Rackspace Public Cloud credentials in - required: true - default: null - region: - description: - - An optional value to narrow inventory scope, i.e. DFW, ORD, IAD, LON - required: false - default: null -authors: - - Jesse Keating - - Paul Durivage - - Matt Martz -notes: - - RAX_CREDS_FILE is an optional environment variable that points to a +Notes: + RAX_CREDS_FILE is an optional environment variable that points to a pyrax-compatible credentials file. - - If RAX_CREDS_FILE is not supplied, rax.py will look for a credentials file - at ~/.rackspace_cloud_credentials. - - See https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md#authenticating - - RAX_REGION is an optional environment variable to narrow inventory search - scope - - RAX_REGION, if used, needs a value like ORD, DFW, SYD (a Rackspace - datacenter) and optionally accepts a comma-separated list - - RAX_ENV is an environment variable that will use an environment as + + If RAX_CREDS_FILE is not supplied, rax.py will look for a credentials file + at ~/.rackspace_cloud_credentials. It uses the Rackspace Python SDK, and + therefore requires a file formatted per the SDK's specifications. See + https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md + #authenticating + + RAX_REGION is an optional environment variable to narrow inventory search + scope. RAX_REGION, if used, needs a value like ORD, DFW, SYD (a Rackspace + datacenter) and optionally accepts a comma-separated list. + + RAX_ENV is an environment variable that will use an environment as configured in ~/.pyrax.cfg, see - https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md#pyrax-configuration - - RAX_META_PREFIX is an environment variable that changes the prefix used + https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md + + RAX_META_PREFIX is an environment variable that changes the prefix used for meta key/value groups. For compatibility with ec2.py set to RAX_META_PREFIX=tag -requirements: [ "pyrax" ] -examples: - - description: List server instances - code: RAX_CREDS_FILE=~/.raxpub rax.py --list - - description: List servers in ORD datacenter only - code: RAX_CREDS_FILE=~/.raxpub RAX_REGION=ORD rax.py --list - - description: List servers in ORD and DFW datacenters - code: RAX_CREDS_FILE=~/.raxpub RAX_REGION=ORD,DFW rax.py --list - - description: Get server details for server named "server.example.com" - code: RAX_CREDS_FILE=~/.raxpub rax.py --host server.example.com -''' + + RAX_ACCESS_NETWORK is an environment variable that will tell the inventory + script to use a specific server network to determine the ansible_ssh_host + value. If no address is found, ansible_ssh_host will not be set. + + RAX_ACCESS_IP_VERSION is an environment variable related to + RAX_ACCESS_NETWORK that will attempt to determine the ansible_ssh_host + value for either IPv4 or IPv6. If no address is found, ansible_ssh_host + will not be set. Acceptable values are: 4 or 6. Values other than 4 or 6 + will be ignored, and 4 will be used. + +Examples: + List server instances + $ RAX_CREDS_FILE=~/.raxpub rax.py --list + + List servers in ORD datacenter only + $ RAX_CREDS_FILE=~/.raxpub RAX_REGION=ORD rax.py --list + + List servers in ORD and DFW datacenters + $ RAX_CREDS_FILE=~/.raxpub RAX_REGION=ORD,DFW rax.py --list + + Get server details for server named "server.example.com" + $ RAX_CREDS_FILE=~/.raxpub rax.py --host server.example.com + + Use the instance private IP to connect (instead of public IP) + $ RAX_CREDS_FILE=~/.raxpub RAX_PRIVATE_IP=yes rax.py --list +""" import os import re import sys import argparse +import warnings import collections from types import NoneType try: import json -except: +except ImportError: import simplejson as json try: @@ -126,7 +136,7 @@ def to_dict(obj): instance = {} for key in dir(obj): value = getattr(obj, key) - if (isinstance(value, NON_CALLABLES) and not key.startswith('_')): + if isinstance(value, NON_CALLABLES) and not key.startswith('_'): key = rax_slugify(key) instance[key] = value @@ -154,10 +164,25 @@ def _list(regions): hostvars = collections.defaultdict(dict) images = {} + network = os.getenv('RAX_ACCESS_NETWORK', 'public') + try: + ip_version = int(os.getenv('RAX_ACCESS_IP_VERSION', 4)) + except: + ip_version = 4 + else: + if ip_version not in [4, 6]: + ip_version = 4 + # Go through all the regions looking for servers for region in regions: # Connect to the region cs = pyrax.connect_to_cloudservers(region=region) + if isinstance(cs, NoneType): + warnings.warn( + 'Connecting to Rackspace region "%s" has caused Pyrax to ' + 'return a NoneType. Is this a valid region?' % region, + RuntimeWarning) + continue for server in cs.servers.list(): # Create a group on region groups[region].append(server.name) @@ -198,7 +223,21 @@ def _list(regions): groups['image-%s' % server.image['id']].append(server.name) # And finally, add an IP address - hostvars[server.name]['ansible_ssh_host'] = server.accessIPv4 + ansible_ssh_host = None + # use accessIPv[46] instead of looping address for 'public' + if network == 'public': + if ip_version == 6 and server.accessIPv6: + ansible_ssh_host = server.accessIPv6 + elif server.accessIPv4: + ansible_ssh_host = server.accessIPv4 + else: + addresses = server.addresses.get(network, []) + for address in addresses: + if address.get('version') == ip_version: + ansible_ssh_host = address.get('addr') + break + if ansible_ssh_host: + hostvars[server.name]['ansible_ssh_host'] = ansible_ssh_host if hostvars: groups['_meta'] = {'hostvars': hostvars} From 1e92aadb5a00cfb2a7e066a73248aa83397b51df Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 3 Nov 2014 10:34:01 -0600 Subject: [PATCH 2/4] Add support for reading from a config file --- plugins/inventory/rax.ini | 55 ++++++++++++++++ plugins/inventory/rax.py | 133 +++++++++++++++++++++++++++----------- 2 files changed, 151 insertions(+), 37 deletions(-) create mode 100644 plugins/inventory/rax.ini diff --git a/plugins/inventory/rax.ini b/plugins/inventory/rax.ini new file mode 100644 index 0000000000..5215d0d291 --- /dev/null +++ b/plugins/inventory/rax.ini @@ -0,0 +1,55 @@ +# Ansible Rackspace external inventory script settings +# + +[rax] + +# Environment Variable: RAX_CREDS_FILE +# +# An optional configuration that points to a pyrax-compatible credentials +# file. +# +# If not supplied, rax.py will look for a credentials file +# at ~/.rackspace_cloud_credentials. It uses the Rackspace Python SDK, +# and therefore requires a file formatted per the SDK's specifications. +# +# https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md +# creds_file = ~/.rackspace_cloud_credentials + +# Environment Variable: RAX_REGION +# +# An optional environment variable to narrow inventory search +# scope. If used, needs a value like ORD, DFW, SYD (a Rackspace +# datacenter) and optionally accepts a comma-separated list. +# regions = IAD,ORD,DFW + +# Environment Variable: RAX_ENV +# +# A configuration that will use an environment as configured in +# ~/.pyrax.cfg, see +# https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md +# env = prod + +# Environment Variable: RAX_META_PREFIX +# Default: meta +# +# A configuration that changes the prefix used for meta key/value groups. +# For compatibility with ec2.py set to "tag" +# meta_prefix = meta + +# Environment Variable: RAX_ACCESS_NETWORK +# Default: public +# +# A configuration that will tell the inventory script to use a specific +# server network to determine the ansible_ssh_host value. If no address +# is found, ansible_ssh_host will not be set. +# access_network = public + +# Environment Variable: RAX_ACCESS_IP_VERSION +# Default: 4 +# +# A configuration related to "access_network" that will attempt to +# determine the ansible_ssh_host value for either IPv4 or IPv6. If no +# address is found, ansible_ssh_host will not be set. +# Acceptable values are: 4 or 6. Values other than 4 or 6 +# will be ignored, and 4 will be used. +# access_ip_version = 4 diff --git a/plugins/inventory/rax.py b/plugins/inventory/rax.py index 87b7f9cafc..778f903216 100644 --- a/plugins/inventory/rax.py +++ b/plugins/inventory/rax.py @@ -56,37 +56,75 @@ Description: rax_tenant_id rax_loaded -Notes: - RAX_CREDS_FILE is an optional environment variable that points to a - pyrax-compatible credentials file. +Configuration: + rax.py can be configured using a rax.ini file or via environment + variables. The rax.ini file should live in the same directory along side + this script. - If RAX_CREDS_FILE is not supplied, rax.py will look for a credentials file - at ~/.rackspace_cloud_credentials. It uses the Rackspace Python SDK, and - therefore requires a file formatted per the SDK's specifications. See - https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md - #authenticating + The section header for configuration values related to this + inventory plugin is [rax] - RAX_REGION is an optional environment variable to narrow inventory search - scope. RAX_REGION, if used, needs a value like ORD, DFW, SYD (a Rackspace - datacenter) and optionally accepts a comma-separated list. + [rax] + creds_file = ~/.rackspace_cloud_credentials + regions = IAD,ORD,DFW + env = prod + meta_prefix = meta + access_network = public + access_ip_version = 4 - RAX_ENV is an environment variable that will use an environment as - configured in ~/.pyrax.cfg, see - https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md + Each of these configurations also has a corresponding environment variable. + An environment variable will override a configuration file value. - RAX_META_PREFIX is an environment variable that changes the prefix used - for meta key/value groups. For compatibility with ec2.py set to - RAX_META_PREFIX=tag + creds_file: + Environment Variable: RAX_CREDS_FILE - RAX_ACCESS_NETWORK is an environment variable that will tell the inventory - script to use a specific server network to determine the ansible_ssh_host - value. If no address is found, ansible_ssh_host will not be set. + An optional configuration that points to a pyrax-compatible credentials + file. - RAX_ACCESS_IP_VERSION is an environment variable related to - RAX_ACCESS_NETWORK that will attempt to determine the ansible_ssh_host - value for either IPv4 or IPv6. If no address is found, ansible_ssh_host - will not be set. Acceptable values are: 4 or 6. Values other than 4 or 6 - will be ignored, and 4 will be used. + If not supplied, rax.py will look for a credentials file + at ~/.rackspace_cloud_credentials. It uses the Rackspace Python SDK, + and therefore requires a file formatted per the SDK's specifications. + + https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md + + regions: + Environment Variable: RAX_REGION + + An optional environment variable to narrow inventory search + scope. If used, needs a value like ORD, DFW, SYD (a Rackspace + datacenter) and optionally accepts a comma-separated list. + + environment: + Environment Variable: RAX_ENV + + A configuration that will use an environment as configured in + ~/.pyrax.cfg, see + https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md + + meta_prefix: + Environment Variable: RAX_META_PREFIX + Default: meta + + A configuration that changes the prefix used for meta key/value groups. + For compatibility with ec2.py set to "tag" + + access_network: + Environment Variable: RAX_ACCESS_NETWORK + Default: public + + A configuration that will tell the inventory script to use a specific + server network to determine the ansible_ssh_host value. If no address + is found, ansible_ssh_host will not be set. + + access_ip_version: + Environment Variable: RAX_ACCESS_IP_VERSION + Default: 4 + + A configuration related to "access_network" that will attempt to + determine the ansible_ssh_host value for either IPv4 or IPv6. If no + address is found, ansible_ssh_host will not be set. + Acceptable values are: 4 or 6. Values other than 4 or 6 + will be ignored, and 4 will be used. Examples: List server instances @@ -102,7 +140,7 @@ Examples: $ RAX_CREDS_FILE=~/.raxpub rax.py --host server.example.com Use the instance private IP to connect (instead of public IP) - $ RAX_CREDS_FILE=~/.raxpub RAX_PRIVATE_IP=yes rax.py --list + $ RAX_CREDS_FILE=~/.raxpub RAX_ACCESS_NETWORK=private rax.py --list """ import os @@ -111,8 +149,9 @@ import sys import argparse import warnings import collections +import ConfigParser -from types import NoneType +from ansible.constants import get_config, mk_boolean try: import json @@ -125,7 +164,20 @@ except ImportError: print('pyrax is required for this module') sys.exit(1) -NON_CALLABLES = (basestring, bool, dict, int, list, NoneType) +NON_CALLABLES = (basestring, bool, dict, int, list, type(None)) + + +def load_config_file(): + p = ConfigParser.ConfigParser() + config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'rax.ini') + try: + p.read(config_file) + except ConfigParser.Error: + return None + else: + return p +p = load_config_file() def rax_slugify(value): @@ -163,10 +215,13 @@ def _list(regions): groups = collections.defaultdict(list) hostvars = collections.defaultdict(dict) images = {} + prefix = get_config(p, 'rax', 'meta_prefix', 'RAX_META_PREFIX', 'meta') - network = os.getenv('RAX_ACCESS_NETWORK', 'public') + network = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK', + 'public') try: - ip_version = int(os.getenv('RAX_ACCESS_IP_VERSION', 4)) + ip_version = get_config(p, 'rax', 'access_ip_version', + 'RAX_ACCESS_IP_VERSION', 4, integer=True) except: ip_version = 4 else: @@ -177,7 +232,7 @@ def _list(regions): for region in regions: # Connect to the region cs = pyrax.connect_to_cloudservers(region=region) - if isinstance(cs, NoneType): + if cs is None: warnings.warn( 'Connecting to Rackspace region "%s" has caused Pyrax to ' 'return a NoneType. Is this a valid region?' % region, @@ -257,16 +312,18 @@ def parse_args(): def setup(): default_creds_file = os.path.expanduser('~/.rackspace_cloud_credentials') - env = os.getenv('RAX_ENV', None) + env = get_config(p, 'rax', 'environment', 'RAX_ENV', None) if env: pyrax.set_environment(env) keyring_username = pyrax.get_setting('keyring_username') # Attempt to grab credentials from environment first - try: - creds_file = os.path.expanduser(os.environ['RAX_CREDS_FILE']) - except KeyError, e: + creds_file = get_config(p, 'rax', 'creds_file', + 'RAX_CREDS_FILE', None) + if creds_file is not None: + creds_file = os.path.expanduser(creds_file) + else: # But if that fails, use the default location of # ~/.rackspace_cloud_credentials if os.path.isfile(default_creds_file): @@ -274,7 +331,7 @@ def setup(): elif not keyring_username: sys.stderr.write('No value in environment variable %s and/or no ' 'credentials file at %s\n' - % (e.message, default_creds_file)) + % ('RAX_CREDS_FILE', default_creds_file)) sys.exit(1) identity_type = pyrax.get_setting('identity_type') @@ -295,7 +352,9 @@ def setup(): if region: regions.append(region) else: - for region in os.getenv('RAX_REGION', 'all').split(','): + region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all', + islist=True) + for region in region_list: region = region.strip().upper() if region == 'ALL': regions = pyrax.regions From b9b3c0ded6bc87420c8891ed28fb175f66d273f9 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 3 Nov 2014 10:34:59 -0600 Subject: [PATCH 3/4] Support boot from volume discovery --- plugins/inventory/rax.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/inventory/rax.py b/plugins/inventory/rax.py index 778f903216..ef45148c5b 100644 --- a/plugins/inventory/rax.py +++ b/plugins/inventory/rax.py @@ -160,6 +160,7 @@ except ImportError: try: import pyrax + from pyrax.utils import slugify except ImportError: print('pyrax is required for this module') sys.exit(1) @@ -215,6 +216,8 @@ def _list(regions): groups = collections.defaultdict(list) hostvars = collections.defaultdict(dict) images = {} + cbs_attachments = collections.defaultdict(dict) + prefix = get_config(p, 'rax', 'meta_prefix', 'RAX_META_PREFIX', 'meta') network = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK', @@ -258,11 +261,33 @@ def _list(regions): hostvars[server.name]['rax_region'] = region for key, value in server.metadata.iteritems(): - prefix = os.getenv('RAX_META_PREFIX', 'meta') groups['%s_%s_%s' % (prefix, key, value)].append(server.name) groups['instance-%s' % server.id].append(server.name) groups['flavor-%s' % server.flavor['id']].append(server.name) + + # Handle boot from volume + if not server.image: + if not cbs_attachments[region]: + cbs = pyrax.connect_to_cloud_blockstorage(region) + for vol in cbs.list(): + if mk_boolean(vol.bootable): + for attachment in vol.attachments: + metadata = vol.volume_image_metadata + server_id = attachment['server_id'] + cbs_attachments[region][server_id] = { + 'id': metadata['image_id'], + 'name': slugify(metadata['image_name']) + } + image = cbs_attachments[region].get(server.id) + if image: + server.image = {'id': image['id']} + hostvars[server.name]['rax_image'] = server.image + hostvars[server.name]['rax_boot_source'] = 'volume' + images[image['id']] = image['name'] + else: + hostvars[server.name]['rax_boot_source'] = 'local' + try: imagegroup = 'image-%s' % images[server.image['id']] groups[imagegroup].append(server.name) From 2f03e0c90619394e962f986ad2cc2f9a779b215f Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 10 Nov 2014 11:49:22 -0600 Subject: [PATCH 4/4] Support fallbacks for access network and access ip version --- plugins/inventory/rax.ini | 6 +++-- plugins/inventory/rax.py | 53 ++++++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/plugins/inventory/rax.ini b/plugins/inventory/rax.ini index 5215d0d291..5a269e16a3 100644 --- a/plugins/inventory/rax.ini +++ b/plugins/inventory/rax.ini @@ -41,7 +41,8 @@ # # A configuration that will tell the inventory script to use a specific # server network to determine the ansible_ssh_host value. If no address -# is found, ansible_ssh_host will not be set. +# is found, ansible_ssh_host will not be set. Accepts a comma-separated +# list of network names, the first found wins. # access_network = public # Environment Variable: RAX_ACCESS_IP_VERSION @@ -51,5 +52,6 @@ # determine the ansible_ssh_host value for either IPv4 or IPv6. If no # address is found, ansible_ssh_host will not be set. # Acceptable values are: 4 or 6. Values other than 4 or 6 -# will be ignored, and 4 will be used. +# will be ignored, and 4 will be used. Accepts a comma separated list, +# the first found wins. # access_ip_version = 4 diff --git a/plugins/inventory/rax.py b/plugins/inventory/rax.py index ef45148c5b..10b72d322b 100644 --- a/plugins/inventory/rax.py +++ b/plugins/inventory/rax.py @@ -114,7 +114,8 @@ Configuration: A configuration that will tell the inventory script to use a specific server network to determine the ansible_ssh_host value. If no address - is found, ansible_ssh_host will not be set. + is found, ansible_ssh_host will not be set. Accepts a comma-separated + list of network names, the first found wins. access_ip_version: Environment Variable: RAX_ACCESS_IP_VERSION @@ -124,7 +125,8 @@ Configuration: determine the ansible_ssh_host value for either IPv4 or IPv6. If no address is found, ansible_ssh_host will not be set. Acceptable values are: 4 or 6. Values other than 4 or 6 - will be ignored, and 4 will be used. + will be ignored, and 4 will be used. Accepts a comma-separated list, + the first found wins. Examples: List server instances @@ -220,16 +222,18 @@ def _list(regions): prefix = get_config(p, 'rax', 'meta_prefix', 'RAX_META_PREFIX', 'meta') - network = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK', - 'public') + networks = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK', + 'public', islist=True) try: - ip_version = get_config(p, 'rax', 'access_ip_version', - 'RAX_ACCESS_IP_VERSION', 4, integer=True) + ip_versions = map(int, get_config(p, 'rax', 'access_ip_version', + 'RAX_ACCESS_IP_VERSION', 4, + islist=True)) except: - ip_version = 4 + ip_versions = [4] else: - if ip_version not in [4, 6]: - ip_version = 4 + ip_versions = [v for v in ip_versions if v in [4, 6]] + if not ip_versions: + ip_versions = [4] # Go through all the regions looking for servers for region in regions: @@ -305,17 +309,26 @@ def _list(regions): # And finally, add an IP address ansible_ssh_host = None # use accessIPv[46] instead of looping address for 'public' - if network == 'public': - if ip_version == 6 and server.accessIPv6: - ansible_ssh_host = server.accessIPv6 - elif server.accessIPv4: - ansible_ssh_host = server.accessIPv4 - else: - addresses = server.addresses.get(network, []) - for address in addresses: - if address.get('version') == ip_version: - ansible_ssh_host = address.get('addr') - break + for network_name in networks: + if ansible_ssh_host: + break + if network_name == 'public': + for version_name in ip_versions: + if ansible_ssh_host: + break + if version_name == 6 and server.accessIPv6: + ansible_ssh_host = server.accessIPv6 + elif server.accessIPv4: + ansible_ssh_host = server.accessIPv4 + if not ansible_ssh_host: + addresses = server.addresses.get(network_name, []) + for address in addresses: + for version_name in ip_versions: + if ansible_ssh_host: + break + if address.get('version') == version_name: + ansible_ssh_host = address.get('addr') + break if ansible_ssh_host: hostvars[server.name]['ansible_ssh_host'] = ansible_ssh_host