Skip to content

Instantly share code, notes, and snippets.

@alevin
Forked from justenwalker/README.md
Created September 24, 2015 04:35
Show Gist options
  • Select an option

  • Save alevin/1615f11a86deb09213b8 to your computer and use it in GitHub Desktop.

Select an option

Save alevin/1615f11a86deb09213b8 to your computer and use it in GitHub Desktop.

Revisions

  1. @justenwalker justenwalker created this gist Aug 30, 2014.
    43 changes: 43 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,43 @@
    etcd dynamic inventory script
    =================================

    Generarates inventory for ansible from etcd using python-etcd library.

    The script assumes etcd.ini to be present alongside it. To choose a different
    path, set the ETCD_INI_PATH environment variable:

    export ETCD_INI_PATH=/path/to/etcd.ini

    All etcd variables are prefixed with /ansible by default, but this can be changed
    in the etcd.ini file.

    Some example keys to get an idea of how to store your data in etcd (assume prefix is '/ansible'):

    ## Group Variables

    {prefix}/groupvars/{group}/{key}

    Example:
    /ansible/groupvars/group1/foo


    ## Host Variables

    {prefix}/hostvars/{host}/{key}

    Example:
    /ansible/hostvars/host1/foo

    ## Host group membership

    {prefix}/hosts/{group}/{host}

    Example:
    /ansible/hosts/group1/host1

    ## Group children

    {prefix}/groups/{parent}/{child}

    Example:
    /ansible/groups/group1/group2
    33 changes: 33 additions & 0 deletions etcd.ini
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,33 @@
    # Ansible etcd external inventory script settings
    #

    [etcd]

    # Prefix for all ansible variables (default: /ansible)
    prefix = /ansible

    # Hostname or IP
    host = localhost

    # Port (default: 4001)
    port = 4001

    # If true, use https - otherwise use http (default: False)
    # secure = false

    # When using secure mode - specify path to the root ca certificate to validate connections
    # ca_cert = root-ca.crt

    # If client authentication is required, you must specify both client_cert and client_key
    # client_cert = etcd-client-chain.crt
    # client_key = etcd-client.key

    [cache]
    # If disabled, it will always do an etcd instead of using cache (default: True)
    enabled = true

    # Directory where cache files will be stored
    path = ~/.ansible/tmp

    # Max age of the cache files before they should be refreshed (in seconds)
    max_age = 300
    260 changes: 260 additions & 0 deletions etcd_inv.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,260 @@
    #! /usr/bin/env python

    '''
    etcd dynamic inventory script
    =================================
    Generarates inventory for ansible from etcd using python-etcd library.
    The script assumes etcd.ini to be present alongside it. To choose a different
    path, set the ETCD_INI_PATH environment variable:
    export ETCD_INI_PATH=/path/to/etcd.ini
    All etcd variables are prefixed with /ansible by default, but this can be changed
    in the etcd.ini file.
    Some example keys to get an idea of how to store your data in etcd (assume prefix is '/ansible'):
    ## Group Variables
    {prefix}/groupvars/{group}/{key}
    Example:
    /ansible/groupvars/group1/foo
    ## Host Variables
    {prefix}/hostvars/{host}/{key}
    Example:
    /ansible/hostvars/host1/foo
    ## Host group membership
    {prefix}/hosts/{group}/{host}
    Example:
    /ansible/hosts/group1/host1
    ## Group children
    {prefix}/groups/{parent}/{child}
    Example:
    /ansible/groups/group1/group2
    '''

    import sys
    import os
    import argparse
    import re
    from time import time
    import ConfigParser

    try:
    import json
    except ImportError:
    import simplejson as json

    try:
    import etcd
    except ImportError:
    raise ImportError("python-etcd library is required")

    class EtcdInventory:
    def _empty_inventory(self):
    return { '_meta': { 'hostvars': {} } }

    def __init__(self):
    ''' Main execution path '''
    self.inventory = self._empty_inventory()

    # Read settings and parse CLI arguments

    self.read_settings()
    self.parse_cli_args()

    # Cache
    if self.cache_enabled:
    if self.args.refresh_cache:
    self.refresh_cache()
    elif not self.is_cache_valid():
    self.refresh_cache()
    if self.inventory == self._empty_inventory():
    self.load_from_cache()
    else:
    self.get_inventory()

    # Data to print
    if self.args.host:
    data_to_print = self.get_host_info()
    elif self.args.list:
    data_to_print = self.json_format_dict(self.inventory, True)

    print data_to_print

    def get_host_info(self):
    ''' Get the hostvars for the given --host arg '''
    host = self.args.host
    hostvars = self.inventory['_meta']['hostvars']
    if host in hostvars:
    return self.json_format_dict(hostvars[host],True)
    return self.json_format_dict({}, True)

    def parse_cli_args(self):
    ''' Command line argument processing '''

    parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on etcd')
    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 etcd (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_cache):
    mod_time = os.path.getmtime(self.cache_path_cache)
    current_time = time()
    if (mod_time + self.cache_max_age) > current_time:
    return True
    return False

    def read_settings(self):
    ''' Reads the settings from the etcd.ini file '''

    config = ConfigParser.SafeConfigParser()
    etcd_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'etcd.ini')
    etcd_default_cache_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'etcd.cache')
    etcd_ini_path = os.environ.get('ETCD_INI_PATH', etcd_default_ini_path)
    config.read(etcd_ini_path)

    # Some sensible defaults
    self.prefix = '/ansible'
    self.host = 'localhost'
    self.port = 4001
    self.proto = 'http'
    self.ca_cert = None
    self.cert = None
    self.cache_max_age = 300
    self.cache_enabled = True
    self.cache_dir = os.path.expanduser('~/.ansible/tmp')
    secure = False

    # Connection to etcd
    if config.has_option('etcd','host'):
    self.host = config.get('etcd','host')

    if config.has_option('etcd','port'):
    self.port = config.getint('etcd','port')

    if config.has_option('etcd','secure'):
    secure = config.getboolean('etcd','secure')

    if secure:
    self.proto = 'https'
    if config.has_option('etcd','ca_cert'):
    self.ca_cert = config.get('etcd','ca_cert')
    if config.has_option('etcd','client_cert') and config.has_option('etcd','client_key'):
    self.cert = (config.get('etcd','client_cert'),config.get('etcd','client_key'))

    # Cache related
    if config.has_option('cache','enabled'):
    self.cache_enabled = config.getboolean('cache','enabled')

    if config.has_option('cache','path'):
    self.cache_dir = os.path.expanduser(config.get('cache', 'path'))

    self.cache_path_cache = self.cache_dir + "/ansible-etcd.cache"

    if config.has_option('cache','max_age'):
    self.cache_max_age = config.getint('cache', 'max_age')

    def add_group(self,group):
    if group not in self.inventory:
    self.inventory[group] = { 'hosts': [], 'vars': {}, 'children': [] }

    def get_inventory(self):
    ''' Get inventory from etcd '''
    client = etcd.Client(host=self.host,port=self.port,protocol=self.proto,ca_cert=self.ca_cert,cert=self.cert)
    try:
    inventory = client.read(self.prefix,recursive=True)
    except KeyError as e:
    raise Exception("Unable read inventory; " + str(e))
    except Exception as e:
    msg = str(e)
    if "alert bad certificate" in msg:
    raise Exception("Make sure client_cert and client_key are set correctly; " + msg)
    if "No JSON object could be decoded" in msg:
    raise Exception("Double check your secure = true setting; " + msg)
    if "No more machines in the cluster" in msg:
    raise Exception("Are your host and port correct?; " + msg)
    raise
    self.inventory = self._empty_inventory()
    for i in inventory.leaves:
    prefix = self.prefix + '/'
    relpath = i.key[len(prefix):]
    path_parts = relpath.split('/')
    t = path_parts[0]

    if len(path_parts) != 3:
    continue

    # Host Variables
    if t == 'hostvars':
    _,host,key = path_parts
    if host not in self.inventory['_meta']['hostvars']:
    self.inventory['_meta']['hostvars'][host] = {}
    self.inventory['_meta']['hostvars'][host][key] = i.value

    ## Add group variables
    if t == 'groupvars':
    _,group,key = path_parts
    self.add_group(group)
    self.inventory[group]['vars'][key] = i.value

    # Add host to group
    if t == 'hosts':
    _,group,host = path_parts
    self.add_group(group)
    self.inventory[group]['hosts'].append(host)

    # Group children
    if t == 'groups':
    _,group,child = path_parts
    self.add_group(group)
    self.inventory[group]['children'].append(child)

    def refresh_cache(self):
    ''' Get inventory from etcd and refresh the cache files '''

    self.get_inventory()
    if not os.path.exists(self.cache_dir):
    os.makedirs(self.cache_dir)
    json_data = self.json_format_dict(self.inventory, True)
    cache = open(self.cache_path_cache, 'w')
    cache.write(json_data)
    cache.close()

    def load_from_cache(self):
    ''' Reads the cached inventory file sets self.inventory '''

    cache = open(self.cache_path_cache, 'r')
    json_inventory = cache.read()
    self.inventory = json.loads(json_inventory)

    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)

    EtcdInventory()