summaryrefslogtreecommitdiffstats
path: root/contrib/inventory/nsot.py
blob: 1c4b474362ef2903ff4eb8c09ac914dc23e2c9bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#!/usr/bin/env python

'''
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

from six import string_types


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.items():
            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()
                  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, string_types)
        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:
            # 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,
        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()