Merge plugins tree

This commit is contained in:
Michael DeHaan 2012-10-08 07:45:25 -04:00
commit 41dd4a1f93
10 changed files with 1056 additions and 0 deletions

59
plugins/CONTRIBUTING.md Normal file
View file

@ -0,0 +1,59 @@
Contributing to Ansible
=======================
It is required that you read the following information to learn how to contribute to this project.
Branch Info
===========
Here's how to understand the branches.
* The devel branch corresponds to the latest ongoing release
* Various release-X.Y branches exist for previous releases
* All feature work happens on the development branch.
* Major bug fixes will be made to the last release branch only
* See CHANGELOG.md for release notes to track each release.
Patch Instructions
==================
Contributions to the core and modules are greatly welcome.
* Required Process:
* Submit github pull requests to the "ansible/devel" branch for features
* Fixes for bugs may also be submitted to "ansible/release-X.Y" for the last release
* Make sure "make tests" passes before submitting any requests.
* Bonus points:
* Joining the mailing list
* Fixing bugs instead of sending bug reports.
* Using squash merges
* Updating the "rst/*" files in the docs project and "docs/" manpage content
* Adding more unit tests
* Avoid:
* Sending patches to the mailing list directly.
* Sending feature pull requests to the 'release' branch instead of the devel branch
* Sending pull requests to mpdehaan's personal ansible fork.
* Sending pull requests about more than one feature in the same pull request.
* Whitespace restructuring
* Large scale refactoring without a discussion on the list
Coding Standards
================
We're not too strict on style considerations, but we require:
* python 2.6 compliant code, unless in ansible modules, then python *2.4* compliant code (no 'with', etc)
* 4-space indents, no tabs except in Makefiles
* under_scores for method names and variables, not camelCase
* GPLv3 license headers on all files, with copyright on new files with your name on it
* no single-line if statements, deeply nested list comprehensions, or clever use of metaclasses -- keep it simple
* comments where appropriate
Contributors License Agreement
==============================
By contributing you agree that these contributions are your own (or approved by your employer) and you grant a full, complete, irrevocable
copyright license to all users and developers of the project, present and future, persusant to the license of the project.

35
plugins/README.md Normal file
View file

@ -0,0 +1,35 @@
ansible-plugins
===============
Extend ansible with optional callback and connection plugins.
callbacks
=========
Callbacks can be used to add logging or monitoring capability, or just make
interesting sound effects.
Drop callback plugins in your ansible/lib/callback_plugins/ directory.
connections
===========
Connection plugins allow ansible to talk over different protocols.
Drop connection plugins in your ansible/lib/runner/connection_plugins/ directory.
inventory
=========
Inventory plugins allow you to store your hosts, groups, and variables in any way
you like. Examples include discovering inventory from EC2 or pulling it from
Cobbler. These could also be used to interface with LDAP or database.
chmod +x an inventory plugin and either name it /etc/ansible/hosts or use ansible
with -i to designate the path to the plugin.
contributions welcome
=====================
Send in pull requests to add plugins of your own. The sky is the limit!

View file

@ -0,0 +1,106 @@
# (C) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import os
import time
import json
TIME_FORMAT="%b %d %Y %H:%M:%S"
MSG_FORMAT="%(now)s - %(category)s - %(data)s\n\n"
if not os.path.exists("/var/log/ansible/hosts"):
os.makedirs("/var/log/ansible/hosts")
def log(host, category, data):
if type(data) == dict:
if 'verbose_override' in data:
# avoid logging extraneous data from facts
data = 'omitted'
else:
invocation = data.pop('invocation', None)
data = json.dumps(data)
if invocation is not None:
data = json.dumps(invocation) + " => %s " % data
path = os.path.join("/var/log/ansible/hosts", host)
now = time.strftime(TIME_FORMAT, time.localtime())
fd = open(path, "a")
fd.write(MSG_FORMAT % dict(now=now, category=category, data=data))
fd.close()
class CallbackModule(object):
"""
logs playbook results, per host, in /var/log/ansible/hosts
"""
def on_any(self, *args, **kwargs):
pass
def runner_on_failed(self, host, res, ignore_errors=False):
log(host, 'FAILED', res)
def runner_on_ok(self, host, res):
log(host, 'OK', res)
def runner_on_error(self, host, msg):
log(host, 'ERROR', msg)
def runner_on_skipped(self, host, item=None):
log(host, 'SKIPPED', '...')
def runner_on_unreachable(self, host, res):
log(host, 'UNREACHABLE', res)
def runner_on_no_hosts(self):
pass
def runner_on_async_poll(self, host, res, jid, clock):
pass
def runner_on_async_ok(self, host, res, jid):
pass
def runner_on_async_failed(self, host, res, jid):
log(host, 'ASYNC_FAILED', res)
def playbook_on_start(self):
pass
def playbook_on_notify(self, host, handler):
pass
def playbook_on_task_start(self, name, is_conditional):
pass
def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None):
pass
def playbook_on_setup(self):
pass
def playbook_on_import_for_host(self, host, imported_file):
log(host, 'IMPORTED', imported_file)
def playbook_on_not_import_for_host(self, host, missing_file):
log(host, 'NOTIMPORTED', missing_file)
def playbook_on_play_start(self, pattern):
pass
def playbook_on_stats(self, stats):
pass

View file

@ -0,0 +1,93 @@
# (C) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import subprocess
FAILED_VOICE="Zarvox"
REGULAR_VOICE="Trinoids"
HAPPY_VOICE="Cellos"
LASER_VOICE="Princess"
def say(msg, voice):
subprocess.call(["/usr/bin/say", msg, "--voice=%s" % (voice)])
class CallbackModule(object):
"""
makes Ansible much more exciting on OS X.
"""
def on_any(self, *args, **kwargs):
pass
def runner_on_failed(self, host, res, ignore_errors=False):
say("Failure on host %s" % host, FAILED_VOICE)
def runner_on_ok(self, host, res):
say("pew", LASER_VOICE)
def runner_on_error(self, host, msg):
pass
def runner_on_skipped(self, host, item=None):
say("pew", LASER_VOICE)
def runner_on_unreachable(self, host, res):
say("Failure on host %s" % host, FAILED_VOICE)
def runner_on_no_hosts(self):
pass
def runner_on_async_poll(self, host, res, jid, clock):
pass
def runner_on_async_ok(self, host, res, jid):
say("pew", LASER_VOICE)
def runner_on_async_failed(self, host, res, jid):
say("Failure on host %s" % host, FAILED_VOICE)
def playbook_on_start(self):
say("Running Playbook", REGULAR_VOICE)
def playbook_on_notify(self, host, handler):
say("pew", LASER_VOICE)
def playbook_on_task_start(self, name, is_conditional):
if not is_conditional:
say("Starting task: %s" % name, REGULAR_VOICE)
else:
say("Notifying task: %s" % name, REGULAR_VOICE)
def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None):
pass
def playbook_on_setup(self):
say("Gathering facts", REGULAR_VOICE)
def playbook_on_import_for_host(self, host, imported_file):
pass
def playbook_on_not_import_for_host(self, host, missing_file):
pass
def playbook_on_play_start(self, pattern):
say("Starting play: %s" % pattern, HAPPY_VOICE)
def playbook_on_stats(self, stats):
say("Play complete", HAPPY_VOICE)

View file

@ -0,0 +1,4 @@
Connections are also pluggable, see lib/ansible/runner/connections.py for the ones that ship with ansible.
When non-core alternatives are avialable, they can be shared here.

131
plugins/inventory/cobbler.py Executable file
View file

@ -0,0 +1,131 @@
#!/usr/bin/python
"""
Cobbler external inventory script
=================================
Ansible has a feature where instead of reading from /etc/ansible/hosts
as a text file, it can query external programs to obtain the list
of hosts, groups the hosts are in, and even variables to assign to each host.
To use this, copy this file over /etc/ansible/hosts and chmod +x the file.
This, more or less, allows you to keep one central database containing
info about all of your managed instances.
This script is an example of sourcing that data from Cobbler
(http://cobbler.github.com). With cobbler each --mgmt-class in cobbler
will correspond to a group in Ansible, and --ks-meta variables will be
passed down for use in templates or even in argument lines.
NOTE: The cobbler system names will not be used. Make sure a
cobbler --dns-name is set for each cobbler system. If a system
appears with two DNS names we do not add it twice because we don't want
ansible talking to it twice. The first one found will be used. If no
--dns-name is set the system will NOT be visible to ansible. We do
not add cobbler system names because there is no requirement in cobbler
that those correspond to addresses.
See http://ansible.github.com/api.html for more info
Tested with Cobbler 2.0.11.
"""
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
######################################################################
import sys
import xmlrpclib
import shlex
try:
import json
except:
import simplejson as json
# NOTE -- this file assumes Ansible is being accessed FROM the cobbler
# server, so it does not attempt to login with a username and password.
# this will be addressed in a future version of this script.
conn = xmlrpclib.Server("http://127.0.0.1/cobbler_api", allow_none=True)
###################################################
# executed with no parameters, return the list of
# all groups and hosts
if len(sys.argv) == 2 and (sys.argv[1] == '--list'):
systems = conn.get_item_names('system')
groups = { 'ungrouped' : [] }
for system in systems:
data = conn.get_blended_data(None, system)
dns_name = None
interfaces = data['interfaces']
for (iname, ivalue) in interfaces.iteritems():
this_dns_name = ivalue.get('dns_name', None)
if this_dns_name is not None:
dns_name = this_dns_name
if dns_name is None:
continue
classes = data['mgmt_classes']
for cls in classes:
if cls not in groups:
groups[cls] = []
# hostname is not really what we want to insert, really insert the
# first DNS name but no further DNS names
groups[cls].append(dns_name)
print json.dumps(groups)
sys.exit(0)
#####################################################
# executed with a hostname as a parameter, return the
# variables for that host
elif len(sys.argv) == 3 and (sys.argv[1] == '--host'):
# look up the system record for the given DNS name
result = conn.find_system_by_dns_name(sys.argv[2])
system = result.get('name', None)
data = {}
if system is None:
print json.dumps({})
sys.exit(1)
data = conn.get_system_for_koan(system)
# return the ksmeta data for that system
metadata = data['ks_meta']
tokens = shlex.split(metadata)
results = {}
for t in tokens:
if t.find("=") != -1:
(k,v) = t.split("=",1)
results[k]=v
print json.dumps(results)
sys.exit(0)
else:
print "usage: --list ..OR.. --host <hostname>"
sys.exit(1)

48
plugins/inventory/ec2.ini Normal file
View file

@ -0,0 +1,48 @@
# Ansible EC2 external inventory script settings
#
[ec2]
# to talk to a private eucalyptus instance uncomment these lines
# and edit edit eucalyptus_host to be the host name of your cloud controller
#eucalyptus = True
#eucalyptus_host = clc.cloud.domain.org
# AWS regions to make calls to. Set this to 'all' to make request to all regions
# in AWS and merge the results together. Alternatively, set this to a comma
# separated list of regions. E.g. 'us-east-1,us-west-1,us-west-2'
regions = all
# When generating inventory, Ansible needs to know how to address a server.
# Each EC2 instance has a lot of variables associated with it. Here is the list:
# http://docs.pythonboto.org/en/latest/ref/ec2.html#module-boto.ec2.instance
# Below are 2 variables that are used as the address of a server:
# - destination_variable
# - vpc_destination_variable
# This is the normal destination variable to use. If you are running Ansible
# from outside EC2, then 'public_dns_name' makes the most sense. If you are
# running Ansible from within EC2, then perhaps you want to use the internal
# address, and should set this to 'private_dns_name'.
destination_variable = public_dns_name
# For server inside a VPC, using DNS names may not make sense. When an instance
# has 'subnet_id' set, this variable is used. If the subnet is public, setting
# this to 'ip_address' will return the public IP address. For instances in a
# private subnet, this should be set to 'private_ip_address', and Ansible must
# be run from with EC2.
vpc_destination_variable = ip_address
# API calls to EC2 are slow. For this reason, we cache the results of an API
# call. Set this to the path you want cache files to be written to. Two files
# will be written to this directory:
# - ansible-ec2.cache
# - ansible-ec2.index
cache_path = /tmp
# The number of seconds a cache file is considered valid. After this many
# seconds, a new API call will be made, and the cache file will be updated.
cache_max_age = 300

405
plugins/inventory/ec2.py Executable file
View file

@ -0,0 +1,405 @@
#!/usr/bin/python -tt
'''
EC2 external inventory script
=================================
Generates inventory that Ansible can understand by making API request to
AWS EC2 using the Boto library.
NOTE: This script assumes Ansible is being executed where the environment
variables needed for Boto have already been set:
export AWS_ACCESS_KEY_ID='AK123'
export AWS_SECRET_ACCESS_KEY='abc123'
If you're using eucalyptus you need to set the above variables and
you need to define:
export EC2_URL=http://hostname_of_your_cc:port/services/Eucalyptus
For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html
When run against a specific host, this script returns the following variables:
- ec2_ami_launch_index
- ec2_architecture
- ec2_association
- ec2_attachTime
- ec2_attachment
- ec2_attachmentId
- ec2_client_token
- ec2_deleteOnTermination
- ec2_description
- ec2_deviceIndex
- ec2_dns_name
- ec2_eventsSet
- ec2_group_name
- ec2_hypervisor
- ec2_id
- ec2_image_id
- ec2_instanceState
- ec2_instance_type
- ec2_ipOwnerId
- ec2_ip_address
- ec2_item
- ec2_kernel
- ec2_key_name
- ec2_launch_time
- ec2_monitored
- ec2_monitoring
- ec2_networkInterfaceId
- ec2_ownerId
- ec2_persistent
- ec2_placement
- ec2_platform
- ec2_previous_state
- ec2_private_dns_name
- ec2_private_ip_address
- ec2_publicIp
- ec2_public_dns_name
- ec2_ramdisk
- ec2_reason
- ec2_region
- ec2_requester_id
- ec2_root_device_name
- ec2_root_device_type
- ec2_security_group_ids
- ec2_security_group_names
- ec2_shutdown_state
- ec2_sourceDestCheck
- ec2_spot_instance_request_id
- ec2_state
- ec2_state_code
- ec2_state_reason
- ec2_status
- ec2_subnet_id
- ec2_tenancy
- ec2_virtualization_type
- ec2_vpc_id
These variables are pulled out of a boto.ec2.instance object. There is a lack of
consistency with variable spellings (camelCase and underscores) since this
just loops through all variables the object exposes. It is preferred to use the
ones with underscores when multiple exist.
In addition, if an instance has AWS Tags associated with it, each tag is a new
variable named:
- ec2_tag_[Key] = [Value]
Security groups are comma-separated in 'ec2_security_group_ids' and
'ec2_security_group_names'.
'''
# (c) 2012, Peter Sankauskas
#
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
######################################################################
import os
import argparse
import re
from time import time
import boto
from boto import ec2
import ConfigParser
try:
import json
except ImportError:
import simplejson as json
class Ec2Inventory(object):
def __init__(self):
''' Main execution path '''
# Inventory grouped by instance IDs, tags, security groups, regions,
# and availability zones
self.inventory = {}
# Index of hostname (address) to instance ID
self.index = {}
# Read settings and parse CLI arguments
self.read_settings()
self.parse_cli_args()
# Cache
if self.args.refresh_cache:
self.do_api_calls_update_cache()
elif not self.is_cache_valid():
self.do_api_calls_update_cache()
# Data to print
if self.args.host:
data_to_print = self.get_host_info()
elif self.args.list:
# Display list of instances for inventory
if len(self.inventory) == 0:
data_to_print = self.get_inventory_from_cache()
else:
data_to_print = self.json_format_dict(self.inventory, True)
print data_to_print
def is_cache_valid(self):
''' Determines if the cache files have expired, or if it is still valid '''
if os.path.isfile(self.cache_path_cache):
mod_time = os.path.getmtime(self.cache_path_cache)
current_time = time()
if (mod_time + self.cache_max_age) > current_time:
if os.path.isfile(self.cache_path_index):
return True
return False
def read_settings(self):
''' Reads the settings from the ec2.ini file '''
config = ConfigParser.SafeConfigParser()
config.read(os.path.dirname(os.path.realpath(__file__)) + '/ec2.ini')
# is eucalyptus?
self.eucalyptus_host = None
self.eucalyptus = False
if config.has_option('ec2', 'eucalyptus'):
self.eucalyptus = config.getboolean('ec2', 'eucalyptus')
if self.eucalyptus and config.has_option('ec2', 'eucalyptus_host'):
self.eucalyptus_host = config.get('ec2', 'eucalyptus_host')
# Regions
self.regions = []
configRegions = config.get('ec2', 'regions')
if (configRegions == 'all'):
if self.eucalyptus_host:
self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name)
else:
for regionInfo in ec2.regions():
self.regions.append(regionInfo.name)
else:
self.regions = configRegions.split(",")
# Destination addresses
self.destination_variable = config.get('ec2', 'destination_variable')
self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable')
# Cache related
cache_path = config.get('ec2', 'cache_path')
self.cache_path_cache = cache_path + "/ansible-ec2.cache"
self.cache_path_index = cache_path + "/ansible-ec2.index"
self.cache_max_age = config.getint('ec2', 'cache_max_age')
def parse_cli_args(self):
''' Command line argument processing '''
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2')
parser.add_argument('--list', action='store_true', default=True,
help='List instances (default: True)')
parser.add_argument('--host', action='store',
help='Get all the variables about a specific instance')
parser.add_argument('--refresh-cache', action='store_true', default=False,
help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)')
self.args = parser.parse_args()
def do_api_calls_update_cache(self):
''' Do API calls to each region, and save data in cache files '''
for region in self.regions:
self.get_instances_by_region(region)
self.write_to_cache(self.inventory, self.cache_path_cache)
self.write_to_cache(self.index, self.cache_path_index)
def get_instances_by_region(self, region):
''' Makes an AWS EC2 API call to the list of instances in a particular
region '''
if self.eucalyptus:
conn = boto.connect_euca(host=self.eucalyptus_host)
conn.APIVersion = '2010-08-31'
else:
conn = ec2.connect_to_region(region)
reservations = conn.get_all_instances()
for reservation in reservations:
for instance in reservation.instances:
self.add_instance(instance, region)
def get_instance(self, region, instance_id):
''' Gets details about a specific instance '''
if self.eucalyptus:
conn = boto.connect_euca(self.eucalyptus_host)
conn.APIVersion = '2010-08-31'
else:
conn = ec2.connect_to_region(region)
reservations = conn.get_all_instances([instance_id])
for reservation in reservations:
for instance in reservation.instances:
return instance
def add_instance(self, instance, region):
''' Adds an instance to the inventory and index, as long as it is
addressable '''
# Only want running instances
if instance.state == 'terminated':
return
# Select the best destination address
if instance.subnet_id:
dest = getattr(instance, self.vpc_destination_variable)
else:
dest = getattr(instance, self.destination_variable)
if dest == None:
# Skip instances we cannot address (e.g. private VPC subnet)
return
# Add to index
self.index[dest] = [region, instance.id]
# Inventory: Group by instance ID (always a group of 1)
self.inventory[instance.id] = [dest]
# Inventory: Group by region
self.push(self.inventory, region, dest)
# Inventory: Group by availability zone
self.push(self.inventory, instance.placement, dest)
# Inventory: Group by security group
for group in instance.groups:
key = self.to_safe("security_group_" + group.name)
self.push(self.inventory, key, dest)
# Inventory: Group by tag keys
for k, v in instance.tags.iteritems():
key = self.to_safe("tag_" + k + "=" + v)
self.push(self.inventory, key, dest)
def get_host_info(self):
''' Get variables about a specific host '''
if len(self.index) == 0:
# Need to load index from cache
self.load_index_from_cache()
(region, instance_id) = self.index[self.args.host]
instance = self.get_instance(region, instance_id)
instance_vars = {}
for key in vars(instance):
value = getattr(instance, key)
key = self.to_safe('ec2_' + key)
# Handle complex types
if type(value) in [int, bool]:
instance_vars[key] = value
elif type(value) in [str, unicode]:
instance_vars[key] = value.strip()
elif type(value) == type(None):
instance_vars[key] = ''
elif key == 'ec2_region':
instance_vars[key] = value.name
elif key == 'ec2_tags':
for k, v in value.iteritems():
key = self.to_safe('ec2_tag_' + k)
instance_vars[key] = v
elif key == 'ec2_groups':
group_ids = []
group_names = []
for group in value:
group_ids.append(group.id)
group_names.append(group.name)
instance_vars["ec2_security_group_ids"] = ','.join(group_ids)
instance_vars["ec2_security_group_names"] = ','.join(group_names)
else:
pass
# TODO Product codes if someone finds them useful
#print key
#print type(value)
#print value
return self.json_format_dict(instance_vars, True)
def push(self, my_dict, key, element):
''' Pushed an element onto an array that may not have been defined in
the dict '''
if key in my_dict:
my_dict[key].append(element);
else:
my_dict[key] = [element]
def get_inventory_from_cache(self):
''' Reads the inventory from the cache file and returns it as a JSON
object '''
cache = open(self.cache_path_cache, 'r')
json_inventory = cache.read()
return json_inventory
def load_index_from_cache(self):
''' Reads the index from the cache file sets self.index '''
cache = open(self.cache_path_index, 'r')
json_index = cache.read()
self.index = json.loads(json_index)
def write_to_cache(self, data, filename):
''' Writes data in JSON format to a file '''
json_data = self.json_format_dict(data, True)
cache = open(filename, 'w')
cache.write(json_data)
cache.close()
def to_safe(self, word):
''' Converts 'bad' characters in a string to underscores so they can be
used as Ansible groups '''
return re.sub("[^A-Za-z0-9\-]", "_", word)
def json_format_dict(self, data, pretty=False):
''' Converts a dict to a JSON object and dumps it as a formatted
string '''
if pretty:
return json.dumps(data, sort_keys=True, indent=2)
else:
return json.dumps(data)
# Run the script
Ec2Inventory()

View file

@ -0,0 +1,26 @@
# Ansible OpenStack external inventory script
[openstack]
# API version
version = 2
# OpenStack nova username
username =
# OpenStack nova api_key
api_key =
# OpenStack nova auth_url
# For use with the new RackSpace API use https://identity.api.rackspacecloud.com/v2.0/
auth_url =
# OpenStack nova project_id
project_id = None
# TODO: Some other options
# insecure =
# region_name =
# endpoint_type =
# extensions =
# service_type =
# service_name =

149
plugins/inventory/nova.py Executable file
View file

@ -0,0 +1,149 @@
#!/usr/bin/python
"""
OpenStack external inventory script
=================================
Generates inventory that Ansible can understand by making API request to
OpenStack endpoint using the novaclient library.
NOTE: This script assumes Ansible is being executed where the environment
variables needed for novaclient have already been set on nova.ini file
For more details, see: https://github.com/openstack/python-novaclient
When run against a specific host, this script returns the following variables:
os_os-ext-sts_task_state
os_addresses
os_links
os_image
os_os-ext-sts_vm_state
os_flavor
os_id
os_rax-bandwidth_bandwidth
os_user_id
os_os-dcf_diskconfig
os_accessipv4
os_accessipv6
os_progress
os_os-ext-sts_power_state
os_metadata
os_status
os_updated
os_hostid
os_name
os_created
os_tenant_id
os__info
os__loaded
where some item can have nested structure.
"""
# (c) 2012, Marco Vito Moscaritolo <marco@agavee.com>
#
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
######################################################################
import sys
import re
import os
import ConfigParser
from novaclient import client as nova_client
try:
import json
except:
import simplejson as json
###################################################
# executed with no parameters, return the list of
# all groups and hosts
def nova_load_config_file():
p = ConfigParser.SafeConfigParser()
path1 = os.getcwd() + "/nova.ini"
path2 = os.path.expanduser(os.environ.get('ANSIBLE_CONFIG', "~/nova.ini"))
path3 = "/etc/ansible/nova.ini"
if os.path.exists(path1):
p.read(path1)
elif os.path.exists(path2):
p.read(path2)
elif os.path.exists(path3):
p.read(path3)
else:
return None
return p
config = nova_load_config_file()
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'),
project_id = config.get('openstack', 'project_id')
)
if len(sys.argv) == 2 and (sys.argv[1] == '--list'):
groups = {}
# Cycle on servers
for f in client.servers.list():
# Define group (or set to empty string)
group = f.metadata['group'] if f.metadata.has_key('group') else 'undefined'
# Create group if not exist
if group not in groups:
groups[group] = []
# Append group to list
groups[group].append(f.accessIPv4)
# Return server list
print json.dumps(groups)
sys.exit(0)
#####################################################
# executed with a hostname as a parameter, return the
# variables for that host
elif len(sys.argv) == 3 and (sys.argv[1] == '--host'):
results = {}
for instance in client.servers.list():
if instance.accessIPv4 == sys.argv[2]:
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
print json.dumps(results)
sys.exit(0)
else:
print "usage: --list ..OR.. --host <hostname>"
sys.exit(1)