summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Coca <bcoca@ansible.com>2015-11-12 08:12:08 -0800
committerBrian Coca <bcoca@ansible.com>2015-11-12 08:12:08 -0800
commit7ce4903a8bc9ed4a777a9e1b88dcadd1336dc3be (patch)
treedcd1d46c2041fa775e4beb3ef14cbebc0dafe5be
parent2dad974048a4eb6ed14bab739162a2a33c414a81 (diff)
parentcc95068dc956d61f1fac37bacb5d1977e130e478 (diff)
downloadansible-7ce4903a8bc9ed4a777a9e1b88dcadd1336dc3be.tar.gz
Merge pull request #12139 from amousset/rudder_inventory_plugin
Add Rudder inventory plugin
-rw-r--r--contrib/inventory/rudder.ini35
-rwxr-xr-xcontrib/inventory/rudder.py302
2 files changed, 337 insertions, 0 deletions
diff --git a/contrib/inventory/rudder.ini b/contrib/inventory/rudder.ini
new file mode 100644
index 0000000000..376674bb90
--- /dev/null
+++ b/contrib/inventory/rudder.ini
@@ -0,0 +1,35 @@
+# Rudder external inventory script settings
+#
+
+[rudder]
+
+# Your Rudder server API URL, typically:
+# https://rudder.local/rudder/api
+uri = https://rudder.local/rudder/api
+
+# By default, Rudder uses a self-signed certificate. Set this to True
+# to disable certificate validation.
+disable_ssl_certificate_validation = True
+
+# Your Rudder API token, created in the Web interface.
+token = aaabbbccc
+
+# Rudder API version to use, use "latest" for lastest available
+# version.
+version = latest
+
+# Property to use as group name in the output.
+# Can generally be "id" or "displayName".
+group_name = displayName
+
+# Fail if there are two groups with the same name or two hosts with the
+# same hostname in the output.
+fail_if_name_collision = True
+
+# We cache the results of Rudder API in a local file
+cache_path = /tmp/ansible-rudder.cache
+
+# 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.
+# Set to 0 to disable cache.
+cache_max_age = 500
diff --git a/contrib/inventory/rudder.py b/contrib/inventory/rudder.py
new file mode 100755
index 0000000000..b13f406d5c
--- /dev/null
+++ b/contrib/inventory/rudder.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2015, Normation SAS
+#
+# Inspired by the EC2 inventory plugin:
+# https://github.com/ansible/ansible/blob/devel/contrib/inventory/ec2.py
+#
+# 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/>.
+
+######################################################################
+
+'''
+Rudder external inventory script
+=================================
+
+Generates inventory that Ansible can understand by making API request to
+a Rudder server. This script is compatible with Rudder 2.10 or later.
+
+The output JSON includes all your Rudder groups, containing the hostnames of
+their nodes. Groups and nodes have a variable called rudder_group_id and
+rudder_node_id, which is the Rudder internal id of the item, allowing to identify
+them uniquely. Hosts variables also include your node properties, which are
+key => value properties set by the API and specific to each node.
+
+This script assumes there is an rudder.ini file alongside it. To specify a
+different path to rudder.ini, define the RUDDER_INI_PATH environment variable:
+
+ export RUDDER_INI_PATH=/path/to/my_rudder.ini
+
+You have to configure your Rudder server information, either in rudder.ini or
+by overriding it with environment variables:
+
+ export RUDDER_API_VERSION='latest'
+ export RUDDER_API_TOKEN='my_token'
+ export RUDDER_API_URI='https://rudder.local/rudder/api'
+'''
+
+
+import sys
+import os
+import re
+import argparse
+import six
+import httplib2 as http
+from time import time
+from six.moves import configparser
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+
+class RudderInventory(object):
+ def __init__(self):
+ ''' Main execution path '''
+
+ # Empty inventory by default
+ self.inventory = {}
+
+ # Read settings and parse CLI arguments
+ self.read_settings()
+ self.parse_cli_args()
+
+ # Create connection
+ self.conn = http.Http(disable_ssl_certificate_validation=self.disable_ssl_validation)
+
+ # Cache
+ if self.args.refresh_cache:
+ self.update_cache()
+ elif not self.is_cache_valid():
+ self.update_cache()
+ else:
+ self.load_cache()
+
+ data_to_print = {}
+
+ if self.args.host:
+ data_to_print = self.get_host_info(self.args.host)
+ elif self.args.list:
+ data_to_print = self.get_list_info()
+
+ print(self.json_format_dict(data_to_print, True))
+
+ def read_settings(self):
+ ''' Reads the settings from the rudder.ini file '''
+ if six.PY2:
+ config = configparser.SafeConfigParser()
+ else:
+ config = configparser.ConfigParser()
+ rudder_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'rudder.ini')
+ rudder_ini_path = os.path.expanduser(os.path.expandvars(os.environ.get('RUDDER_INI_PATH', rudder_default_ini_path)))
+ config.read(rudder_ini_path)
+
+ self.token = os.environ.get('RUDDER_API_TOKEN', config.get('rudder', 'token'))
+ self.version = os.environ.get('RUDDER_API_VERSION', config.get('rudder', 'version'))
+ self.uri = os.environ.get('RUDDER_API_URI', config.get('rudder', 'uri'))
+
+ self.disable_ssl_validation = config.getboolean('rudder', 'disable_ssl_certificate_validation')
+ self.group_name = config.get('rudder', 'group_name')
+ self.fail_if_name_collision = config.getboolean('rudder', 'fail_if_name_collision')
+
+ self.cache_path = config.get('rudder', 'cache_path')
+ self.cache_max_age = config.getint('rudder', 'cache_max_age')
+
+ def parse_cli_args(self):
+ ''' Command line argument processing '''
+
+ parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on Rudder inventory')
+ 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 Rudder (default: False - use cache files)')
+ self.args = parser.parse_args()
+
+ 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):
+ mod_time = os.path.getmtime(self.cache_path)
+ current_time = time()
+ if (mod_time + self.cache_max_age) > current_time:
+ return True
+
+ return False
+
+ def load_cache(self):
+ ''' Reads the cache from the cache file sets self.cache '''
+
+ cache = open(self.cache_path, 'r')
+ json_cache = cache.read()
+
+ try:
+ self.inventory = json.loads(json_cache)
+ except ValueError, e:
+ self.fail_with_error('Could not parse JSON response from local cache', 'parsing local cache')
+
+ def write_cache(self):
+ ''' Writes data in JSON format to a file '''
+
+ json_data = self.json_format_dict(self.inventory, True)
+ cache = open(self.cache_path, 'w')
+ cache.write(json_data)
+ cache.close()
+
+ def get_nodes(self):
+ ''' Gets the nodes list from Rudder '''
+
+ path = '/nodes?select=nodeAndPolicyServer'
+ result = self.api_call(path)
+
+ nodes = {}
+
+ for node in result['data']['nodes']:
+ nodes[node['id']] = {}
+ nodes[node['id']]['hostname'] = node['hostname']
+ if 'properties' in node:
+ nodes[node['id']]['properties'] = node['properties']
+ else:
+ nodes[node['id']]['properties'] = []
+
+ return nodes
+
+ def get_groups(self):
+ ''' Gets the groups list from Rudder '''
+
+ path = '/groups'
+ result = self.api_call(path)
+
+ groups = {}
+
+ for group in result['data']['groups']:
+ groups[group['id']] = {'hosts': group['nodeIds'], 'name': self.to_safe(group[self.group_name])}
+
+ return groups
+
+ def update_cache(self):
+ ''' Fetches the inventory information from Rudder and creates the inventory '''
+
+ nodes = self.get_nodes()
+ groups = self.get_groups()
+
+ inventory = {}
+
+ for group in groups:
+ # Check for name collision
+ if self.fail_if_name_collision:
+ if groups[group]['name'] in inventory:
+ self.fail_with_error('Name collision on groups: "%s" appears twice' % groups[group]['name'], 'creating groups')
+ # Add group to inventory
+ inventory[groups[group]['name']] = {}
+ inventory[groups[group]['name']]['hosts'] = []
+ inventory[groups[group]['name']]['vars'] = {}
+ inventory[groups[group]['name']]['vars']['rudder_group_id'] = group
+ for node in groups[group]['hosts']:
+ # Add node to group
+ inventory[groups[group]['name']]['hosts'].append(nodes[node]['hostname'])
+
+ properties = {}
+
+ for node in nodes:
+ # Check for name collision
+ if self.fail_if_name_collision:
+ if nodes[node]['hostname'] in properties:
+ self.fail_with_error('Name collision on hosts: "%s" appears twice' % nodes[node]['hostname'], 'creating hosts')
+ # Add node properties to inventory
+ properties[nodes[node]['hostname']] = {}
+ properties[nodes[node]['hostname']]['rudder_node_id'] = node
+ for node_property in nodes[node]['properties']:
+ properties[nodes[node]['hostname']][self.to_safe(node_property['name'])] = node_property['value']
+
+ inventory['_meta'] = {}
+ inventory['_meta']['hostvars'] = properties
+
+ self.inventory = inventory
+
+ if self.cache_max_age > 0:
+ self.write_cache()
+
+ def get_list_info(self):
+ ''' Gets inventory information from local cache '''
+
+ return self.inventory
+
+ def get_host_info(self, hostname):
+ ''' Gets information about a specific host from local cache '''
+
+ if hostname in self.inventory['_meta']['hostvars']:
+ return self.inventory['_meta']['hostvars'][hostname]
+ else:
+ return {}
+
+ def api_call(self, path):
+ ''' Performs an API request '''
+
+ headers = {
+ 'X-API-Token': self.token,
+ 'X-API-Version': self.version,
+ 'Content-Type': 'application/json;charset=utf-8'
+ }
+
+ target = urlparse(self.uri + path)
+ method = 'GET'
+ body = ''
+
+ try:
+ response, content = self.conn.request(target.geturl(), method, body, headers)
+ except:
+ self.fail_with_error('Error connecting to Rudder server')
+
+ try:
+ data = json.loads(content)
+ except ValueError, e:
+ self.fail_with_error('Could not parse JSON response from Rudder API', 'reading API response')
+
+ return data
+
+ def fail_with_error(self, err_msg, err_operation=None):
+ ''' Logs an error to std err for ansible-playbook to consume and exit '''
+ if err_operation:
+ err_msg = 'ERROR: "{err_msg}", while: {err_operation}'.format(
+ err_msg=err_msg, err_operation=err_operation)
+ sys.stderr.write(err_msg)
+ sys.exit(1)
+
+ 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)
+
+ def to_safe(self, word):
+ ''' Converts 'bad' characters in a string to underscores so they can be
+ used as Ansible variable names '''
+
+ return re.sub('[^A-Za-z0-9\_]', '_', word)
+
+# Run the script
+RudderInventory()