Merge pull request #9530 from sivel/rax-inventory-access-network

rax.py inventory: improvements
This commit is contained in:
Michael DeHaan 2014-11-17 12:12:13 -08:00
commit c9ecc51a5e
2 changed files with 261 additions and 68 deletions

57
plugins/inventory/rax.ini Normal file
View file

@ -0,0 +1,57 @@
# 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. Accepts a comma-separated
# list of network names, the first found wins.
# 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. Accepts a comma separated list,
# the first found wins.
# access_ip_version = 4

272
plugins/inventory/rax.py Executable file → Normal file
View file

@ -1,8 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
# (c) 2013, Jesse Keating <jesse.keating@rackspace.com> # (c) 2013, Jesse Keating <jesse.keating@rackspace.com,
# Paul Durivage <paul.durivage@rackspace.com>,
# Matt Martz <matt@sivel.net>
# #
# This file is part of Ansible, # This file is part of Ansible.
# #
# Ansible is free software: you can redistribute it and/or modify # Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # 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 # You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
DOCUMENTATION = ''' """
--- Rackspace Cloud Inventory
inventory: rax
short_description: Rackspace Public Cloud external inventory script Authors:
description: Jesse Keating <jesse.keating@rackspace.com,
- Generates inventory that Ansible can understand by making API request to Paul Durivage <paul.durivage@rackspace.com>,
Matt Martz <matt@sivel.net>
Description:
Generates inventory that Ansible can understand by making API request to
Rackspace Public Cloud API Rackspace Public Cloud API
- |
When run against a specific host, this script returns the following When run against a specific host, this script returns variables similar to:
variables:
rax_os-ext-sts_task_state rax_os-ext-sts_task_state
rax_addresses rax_addresses
rax_links rax_links
@ -50,72 +56,131 @@ description:
rax_tenant_id rax_tenant_id
rax_loaded rax_loaded
where some item can have nested structure. Configuration:
- credentials are set in a credentials file rax.py can be configured using a rax.ini file or via environment
version_added: None variables. The rax.ini file should live in the same directory along side
options: this script.
creds_file:
description: The section header for configuration values related to this
- File to find the Rackspace Public Cloud credentials in inventory plugin is [rax]
required: true
default: null [rax]
region: creds_file = ~/.rackspace_cloud_credentials
description: regions = IAD,ORD,DFW
- An optional value to narrow inventory scope, i.e. DFW, ORD, IAD, LON env = prod
required: false meta_prefix = meta
default: null access_network = public
authors: access_ip_version = 4
- Jesse Keating <jesse.keating@rackspace.com>
- Paul Durivage <paul.durivage@rackspace.com> Each of these configurations also has a corresponding environment variable.
- Matt Martz <matt@sivel.net> An environment variable will override a configuration file value.
notes:
- RAX_CREDS_FILE is an optional environment variable that points to a creds_file:
pyrax-compatible credentials file. Environment Variable: RAX_CREDS_FILE
- If RAX_CREDS_FILE is not supplied, rax.py will look for a credentials file
at ~/.rackspace_cloud_credentials. An optional configuration that points to a pyrax-compatible credentials
- See https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md#authenticating file.
- RAX_REGION is an optional environment variable to narrow inventory search
scope If not supplied, rax.py will look for a credentials file
- RAX_REGION, if used, needs a value like ORD, DFW, SYD (a Rackspace at ~/.rackspace_cloud_credentials. It uses the Rackspace Python SDK,
datacenter) and optionally accepts a comma-separated list and therefore requires a file formatted per the SDK's specifications.
- 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
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 regions:
for meta key/value groups. For compatibility with ec2.py set to Environment Variable: RAX_REGION
RAX_META_PREFIX=tag
requirements: [ "pyrax" ] An optional environment variable to narrow inventory search
examples: scope. If used, needs a value like ORD, DFW, SYD (a Rackspace
- description: List server instances datacenter) and optionally accepts a comma-separated list.
code: RAX_CREDS_FILE=~/.raxpub rax.py --list
- description: List servers in ORD datacenter only environment:
code: RAX_CREDS_FILE=~/.raxpub RAX_REGION=ORD rax.py --list Environment Variable: RAX_ENV
- description: List servers in ORD and DFW datacenters
code: RAX_CREDS_FILE=~/.raxpub RAX_REGION=ORD,DFW rax.py --list A configuration that will use an environment as configured in
- description: Get server details for server named "server.example.com" ~/.pyrax.cfg, see
code: RAX_CREDS_FILE=~/.raxpub rax.py --host server.example.com 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. Accepts a comma-separated
list of network names, the first found wins.
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. Accepts a comma-separated list,
the first found wins.
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_ACCESS_NETWORK=private rax.py --list
"""
import os import os
import re import re
import sys import sys
import argparse import argparse
import warnings
import collections import collections
import ConfigParser
from types import NoneType from ansible.constants import get_config, mk_boolean
try: try:
import json import json
except: except ImportError:
import simplejson as json import simplejson as json
try: try:
import pyrax import pyrax
from pyrax.utils import slugify
except ImportError: except ImportError:
print('pyrax is required for this module') print('pyrax is required for this module')
sys.exit(1) 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): def rax_slugify(value):
@ -126,7 +191,7 @@ def to_dict(obj):
instance = {} instance = {}
for key in dir(obj): for key in dir(obj):
value = getattr(obj, key) 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) key = rax_slugify(key)
instance[key] = value instance[key] = value
@ -153,11 +218,33 @@ def _list(regions):
groups = collections.defaultdict(list) groups = collections.defaultdict(list)
hostvars = collections.defaultdict(dict) hostvars = collections.defaultdict(dict)
images = {} images = {}
cbs_attachments = collections.defaultdict(dict)
prefix = get_config(p, 'rax', 'meta_prefix', 'RAX_META_PREFIX', 'meta')
networks = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK',
'public', islist=True)
try:
ip_versions = map(int, get_config(p, 'rax', 'access_ip_version',
'RAX_ACCESS_IP_VERSION', 4,
islist=True))
except:
ip_versions = [4]
else:
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 # Go through all the regions looking for servers
for region in regions: for region in regions:
# Connect to the region # Connect to the region
cs = pyrax.connect_to_cloudservers(region=region) cs = pyrax.connect_to_cloudservers(region=region)
if cs is None:
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(): for server in cs.servers.list():
# Create a group on region # Create a group on region
groups[region].append(server.name) groups[region].append(server.name)
@ -178,11 +265,33 @@ def _list(regions):
hostvars[server.name]['rax_region'] = region hostvars[server.name]['rax_region'] = region
for key, value in server.metadata.iteritems(): 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['%s_%s_%s' % (prefix, key, value)].append(server.name)
groups['instance-%s' % server.id].append(server.name) groups['instance-%s' % server.id].append(server.name)
groups['flavor-%s' % server.flavor['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: try:
imagegroup = 'image-%s' % images[server.image['id']] imagegroup = 'image-%s' % images[server.image['id']]
groups[imagegroup].append(server.name) groups[imagegroup].append(server.name)
@ -198,7 +307,30 @@ def _list(regions):
groups['image-%s' % server.image['id']].append(server.name) groups['image-%s' % server.image['id']].append(server.name)
# And finally, add an IP address # 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'
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
if hostvars: if hostvars:
groups['_meta'] = {'hostvars': hostvars} groups['_meta'] = {'hostvars': hostvars}
@ -218,16 +350,18 @@ def parse_args():
def setup(): def setup():
default_creds_file = os.path.expanduser('~/.rackspace_cloud_credentials') 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: if env:
pyrax.set_environment(env) pyrax.set_environment(env)
keyring_username = pyrax.get_setting('keyring_username') keyring_username = pyrax.get_setting('keyring_username')
# Attempt to grab credentials from environment first # Attempt to grab credentials from environment first
try: creds_file = get_config(p, 'rax', 'creds_file',
creds_file = os.path.expanduser(os.environ['RAX_CREDS_FILE']) 'RAX_CREDS_FILE', None)
except KeyError, e: if creds_file is not None:
creds_file = os.path.expanduser(creds_file)
else:
# But if that fails, use the default location of # But if that fails, use the default location of
# ~/.rackspace_cloud_credentials # ~/.rackspace_cloud_credentials
if os.path.isfile(default_creds_file): if os.path.isfile(default_creds_file):
@ -235,7 +369,7 @@ def setup():
elif not keyring_username: elif not keyring_username:
sys.stderr.write('No value in environment variable %s and/or no ' sys.stderr.write('No value in environment variable %s and/or no '
'credentials file at %s\n' 'credentials file at %s\n'
% (e.message, default_creds_file)) % ('RAX_CREDS_FILE', default_creds_file))
sys.exit(1) sys.exit(1)
identity_type = pyrax.get_setting('identity_type') identity_type = pyrax.get_setting('identity_type')
@ -256,7 +390,9 @@ def setup():
if region: if region:
regions.append(region) regions.append(region)
else: 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() region = region.strip().upper()
if region == 'ALL': if region == 'ALL':
regions = pyrax.regions regions = pyrax.regions