Added NSoT Inventory script to pull from Device resources
This commit is contained in:
parent
97b99e4517
commit
c05970df2c
2 changed files with 367 additions and 0 deletions
345
contrib/inventory/nsot.py
Normal file
345
contrib/inventory/nsot.py
Normal file
|
@ -0,0 +1,345 @@
|
|||
#!/bin/env python2.7
|
||||
|
||||
'''
|
||||
nsot
|
||||
====
|
||||
|
||||
Ansible Dynamic Inventory to pull hosts from NSoT, a flexible CMDB by Dropbox
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Define host groups in form of NSoT device attribute criteria
|
||||
|
||||
* All parameters defined by the spec as of 2015-09-05 are supported.
|
||||
|
||||
+ ``--list``: Returns JSON hash of host groups -> hosts and top-level
|
||||
``_meta`` -> ``hostvars`` which correspond to all device attributes.
|
||||
|
||||
Group vars can be specified in the YAML configuration, noted below.
|
||||
|
||||
+ ``--host <hostname>``: Returns JSON hash where every item is a device
|
||||
attribute.
|
||||
|
||||
* In addition to all attributes assigned to resource being returned, script
|
||||
will also append ``site_id`` and ``id`` as facts to utilize.
|
||||
|
||||
|
||||
Confguration
|
||||
------------
|
||||
|
||||
Since it'd be annoying and failure prone to guess where you're configuration
|
||||
file is, use ``NSOT_INVENTORY_CONFIG`` to specify the path to it.
|
||||
|
||||
This file should adhere to the YAML spec. All top-level variable must be
|
||||
desired Ansible group-name hashed with single 'query' item to define the NSoT
|
||||
attribute query.
|
||||
|
||||
Queries follow the normal NSoT query syntax, `shown here`_
|
||||
|
||||
.. _shown here: https://github.com/dropbox/pynsot#set-queries
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
routers:
|
||||
query: 'deviceType=ROUTER'
|
||||
vars:
|
||||
a: b
|
||||
c: d
|
||||
|
||||
juniper_fw:
|
||||
query: 'deviceType=FIREWALL manufacturer=JUNIPER'
|
||||
|
||||
not_f10:
|
||||
query: '-manufacturer=FORCE10'
|
||||
|
||||
The inventory will automatically use your ``.pynsotrc`` like normal pynsot from
|
||||
cli would, so make sure that's configured appropriately.
|
||||
|
||||
.. note::
|
||||
|
||||
Attributes I'm showing above are influenced from ones that the Trigger
|
||||
project likes. As is the spirit of NSoT, use whichever attributes work best
|
||||
for your workflow.
|
||||
|
||||
If config file is blank or absent, the following default groups will be
|
||||
created:
|
||||
|
||||
* ``routers``: deviceType=ROUTER
|
||||
* ``switches``: deviceType=SWITCH
|
||||
* ``firewalls``: deviceType=FIREWALL
|
||||
|
||||
These are likely not useful for everyone so please use the configuration. :)
|
||||
|
||||
.. note::
|
||||
|
||||
By default, resources will only be returned for what your default
|
||||
site is set for in your ``~/.pynsotrc``.
|
||||
|
||||
If you want to specify, add an extra key under the group for ``site: n``.
|
||||
|
||||
Output Examples
|
||||
---------------
|
||||
|
||||
Here are some examples shown from just calling the command directly::
|
||||
|
||||
$ NSOT_INVENTORY_CONFIG=$PWD/test.yaml ansible_nsot --list | jq '.'
|
||||
{
|
||||
"routers": {
|
||||
"hosts": [
|
||||
"test1.example.com"
|
||||
],
|
||||
"vars": {
|
||||
"cool_level": "very",
|
||||
"group": "routers"
|
||||
}
|
||||
},
|
||||
"firewalls": {
|
||||
"hosts": [
|
||||
"test2.example.com"
|
||||
],
|
||||
"vars": {
|
||||
"cool_level": "enough",
|
||||
"group": "firewalls"
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"hostvars": {
|
||||
"test2.example.com": {
|
||||
"make": "SRX",
|
||||
"site_id": 1,
|
||||
"id": 108
|
||||
},
|
||||
"test1.example.com": {
|
||||
"make": "MX80",
|
||||
"site_id": 1,
|
||||
"id": 107
|
||||
}
|
||||
}
|
||||
},
|
||||
"rtr_and_fw": {
|
||||
"hosts": [
|
||||
"test1.example.com",
|
||||
"test2.example.com"
|
||||
],
|
||||
"vars": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$ NSOT_INVENTORY_CONFIG=$PWD/test.yaml ansible_nsot --host test1 | jq '.'
|
||||
{
|
||||
"make": "MX80",
|
||||
"site_id": 1,
|
||||
"id": 107
|
||||
}
|
||||
|
||||
'''
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import os
|
||||
import pkg_resources
|
||||
import argparse
|
||||
import json
|
||||
import yaml
|
||||
from textwrap import dedent
|
||||
from pynsot.client import get_api_client
|
||||
from pynsot.app import HttpServerError
|
||||
from click.exceptions import UsageError
|
||||
|
||||
# Version source of truth is in setup.py
|
||||
__version__ = pkg_resources.require('ansible_nsot')[0].version
|
||||
|
||||
|
||||
def warning(*objs):
|
||||
print("WARNING: ", *objs, file=sys.stderr)
|
||||
|
||||
|
||||
class NSoTInventory(object):
|
||||
'''NSoT Client object for gather inventory'''
|
||||
|
||||
def __init__(self):
|
||||
self.config = dict()
|
||||
config_env = os.environ.get('NSOT_INVENTORY_CONFIG')
|
||||
if config_env:
|
||||
try:
|
||||
config_file = os.path.abspath(config_env)
|
||||
except IOError: # If file non-existent, use default config
|
||||
self._config_default()
|
||||
except Exception as e:
|
||||
sys.exit('%s\n' % e)
|
||||
|
||||
with open(config_file) as f:
|
||||
try:
|
||||
self.config.update(yaml.safe_load(f))
|
||||
except TypeError: # If empty file, use default config
|
||||
warning('Empty config file')
|
||||
self._config_default()
|
||||
except Exception as e:
|
||||
sys.exit('%s\n' % e)
|
||||
else: # Use defaults if env var missing
|
||||
self._config_default()
|
||||
self.groups = self.config.keys()
|
||||
self.client = get_api_client()
|
||||
self._meta = {'hostvars': dict()}
|
||||
|
||||
def _config_default(self):
|
||||
default_yaml = '''
|
||||
---
|
||||
routers:
|
||||
query: deviceType=ROUTER
|
||||
switches:
|
||||
query: deviceType=SWITCH
|
||||
firewalls:
|
||||
query: deviceType=FIREWALL
|
||||
'''
|
||||
self.config = yaml.safe_load(dedent(default_yaml))
|
||||
|
||||
def do_list(self):
|
||||
'''Direct callback for when ``--list`` is provided
|
||||
|
||||
Relies on the configuration generated from init to run
|
||||
_inventory_group()
|
||||
'''
|
||||
inventory = dict()
|
||||
for group, contents in self.config.iteritems():
|
||||
group_response = self._inventory_group(group, contents)
|
||||
inventory.update(group_response)
|
||||
inventory.update({'_meta': self._meta})
|
||||
return json.dumps(inventory)
|
||||
|
||||
def do_host(self, host):
|
||||
return json.dumps(self._hostvars(host))
|
||||
|
||||
def _hostvars(self, host):
|
||||
'''Return dictionary of all device attributes
|
||||
|
||||
Depending on number of devices in NSoT, could be rather slow since this
|
||||
has to request every device resource to filter through
|
||||
'''
|
||||
device = [i for i in self.client.devices.get()['data']['devices']
|
||||
if host in i['hostname']][0]
|
||||
attributes = device['attributes']
|
||||
attributes.update({'site_id': device['site_id'], 'id': device['id']})
|
||||
return attributes
|
||||
|
||||
def _inventory_group(self, group, contents):
|
||||
'''Takes a group and returns inventory for it as dict
|
||||
|
||||
:param group: Group name
|
||||
:type group: str
|
||||
:param contents: The contents of the group's YAML config
|
||||
:type contents: dict
|
||||
|
||||
contents param should look like::
|
||||
|
||||
{
|
||||
'query': 'xx',
|
||||
'vars':
|
||||
'a': 'b'
|
||||
}
|
||||
|
||||
Will return something like::
|
||||
|
||||
{ group: {
|
||||
hosts: [],
|
||||
vars: {},
|
||||
}
|
||||
'''
|
||||
query = contents.get('query')
|
||||
hostvars = contents.get('vars', dict())
|
||||
site = contents.get('site', dict())
|
||||
obj = {group: dict()}
|
||||
obj[group]['hosts'] = []
|
||||
obj[group]['vars'] = hostvars
|
||||
try:
|
||||
assert isinstance(query, basestring)
|
||||
except:
|
||||
sys.exit('ERR: Group queries must be a single string\n'
|
||||
' Group: %s\n'
|
||||
' Query: %s\n' % (group, query)
|
||||
)
|
||||
try:
|
||||
if site:
|
||||
site = self.client.sites(site)
|
||||
devices = site.devices.query.get(query=query)
|
||||
else:
|
||||
devices = self.client.devices.query.get(query=query)
|
||||
except HttpServerError as e:
|
||||
if '500' in str(e.response):
|
||||
_site = 'Correct site id?'
|
||||
_attr = 'Queried attributes actually exist?'
|
||||
questions = _site + '\n' + _attr
|
||||
sys.exit('ERR: 500 from server.\n%s' % questions)
|
||||
else:
|
||||
raise
|
||||
except UsageError:
|
||||
sys.exit('ERR: Could not connect to server. Running?')
|
||||
|
||||
# Would do a list comprehension here, but would like to save code/time
|
||||
# and also acquire attributes in this step
|
||||
for host in devices['data']['devices']:
|
||||
# Iterate through each device that matches query, assign hostname
|
||||
# to the group's hosts array and then use this single iteration as
|
||||
# a chance to update self._meta which will be used in the final
|
||||
# return
|
||||
hostname = host['hostname']
|
||||
obj[group]['hosts'].append(hostname)
|
||||
attributes = host['attributes']
|
||||
attributes.update({'site_id': host['site_id'], 'id': host['id']})
|
||||
self._meta['hostvars'].update({hostname: attributes})
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def parse_args():
|
||||
desc = __doc__.splitlines()[4] # Just to avoid being redundant
|
||||
|
||||
# Establish parser with options and error out if no action provided
|
||||
parser = argparse.ArgumentParser(
|
||||
description=desc,
|
||||
version=__version__,
|
||||
conflict_handler='resolve',
|
||||
)
|
||||
|
||||
# Arguments
|
||||
#
|
||||
# Currently accepting (--list | -l) and (--host | -h)
|
||||
# These must not be allowed together
|
||||
parser.add_argument(
|
||||
'--list', '-l',
|
||||
help='Print JSON object containing hosts to STDOUT',
|
||||
action='store_true',
|
||||
dest='list_', # Avoiding syntax highlighting for list
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--host', '-h',
|
||||
help='Print JSON object containing hostvars for <host>',
|
||||
action='store',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.list_ and not args.host: # Require at least one option
|
||||
parser.exit(status=1, message='No action requested')
|
||||
|
||||
if args.list_ and args.host: # Do not allow multiple options
|
||||
parser.exit(status=1, message='Too many actions requested')
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
'''Set up argument handling and callback routing'''
|
||||
args = parse_args()
|
||||
client = NSoTInventory()
|
||||
|
||||
# Callback condition
|
||||
if args.list_:
|
||||
print(client.do_list())
|
||||
elif args.host:
|
||||
print(client.do_host(args.host))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
22
contrib/inventory/nsot.yaml
Normal file
22
contrib/inventory/nsot.yaml
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
juniper_routers:
|
||||
query: 'deviceType=ROUTER manufacturer=JUNIPER'
|
||||
vars:
|
||||
group: juniper_routers
|
||||
netconf: true
|
||||
os: junos
|
||||
|
||||
cisco_asa:
|
||||
query: 'manufacturer=CISCO deviceType=FIREWALL'
|
||||
vars:
|
||||
group: cisco_asa
|
||||
routed_vpn: false
|
||||
stateful: true
|
||||
|
||||
old_cisco_asa:
|
||||
query: 'manufacturer=CISCO deviceType=FIREWALL -softwareVersion=8.3+'
|
||||
vars:
|
||||
old_nat: true
|
||||
|
||||
not_f10:
|
||||
query: '-manufacturer=FORCE10'
|
Loading…
Reference in a new issue