Merge pull request #12139 from amousset/rudder_inventory_plugin
Add Rudder inventory plugin
This commit is contained in:
commit
7ce4903a8b
2 changed files with 337 additions and 0 deletions
35
contrib/inventory/rudder.ini
Normal file
35
contrib/inventory/rudder.ini
Normal file
|
@ -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
|
302
contrib/inventory/rudder.py
Executable file
302
contrib/inventory/rudder.py
Executable file
|
@ -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()
|
Loading…
Reference in a new issue