1#
2# Copyright: (c), Ansible Project
3#
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9DOCUMENTATION = r'''
10    name: servicenow.servicenow.now
11    plugin_type: inventory
12    author:
13      - Will Tome (@willtome)
14      - Alex Mittell (@alex_mittell)
15    short_description: ServiceNow Inventory Plugin
16    version_added: "2.10"
17    description:
18        - ServiceNow Inventory plugin.
19    extends_documentation_fragment:
20        - constructed
21        - inventory_cache
22    requirements:
23        - python requests (requests)
24        - netaddr
25    options:
26        plugin:
27            description: The name of the ServiceNow Inventory Plugin, this should always be 'servicenow.servicenow.now'.
28            required: True
29            choices: ['servicenow.servicenow.now']
30        instance:
31          description:
32          - The ServiceNow instance name, without the domain, service-now.com.
33          - If the value is not specified in the task, the value of environment variable C(SN_INSTANCE) will be used instead.
34          required: false
35          type: str
36          env:
37            - name: SN_INSTANCE
38        host:
39          description:
40          - The ServiceNow hostname.
41          - This value is FQDN for ServiceNow host.
42          - If the value is not specified in the task, the value of environment variable C(SN_HOST) will be used instead.
43          - Mutually exclusive with C(instance).
44          type: str
45          required: false
46          env:
47            - name: SN_HOST
48        username:
49          description:
50          - Name of user for connection to ServiceNow.
51          - If the value is not specified, the value of environment variable C(SN_USERNAME) will be used instead.
52          required: false
53          type: str
54          env:
55            - name: SN_USERNAME
56        password:
57          description:
58          - Password for username.
59          - If the value is not specified, the value of environment variable C(SN_PASSWORD) will be used instead.
60          required: true
61          type: str
62          env:
63            - name: SN_PASSWORD
64        table:
65            description: The ServiceNow table to query.
66            type: string
67            default: cmdb_ci_server
68        fields:
69            description: Comma seperated string providing additional table columns to add as host vars to each inventory host.
70            type: list
71            default: 'ip_address,fqdn,host_name,sys_class_name,name'
72        selection_order:
73            description: Comma seperated string providing ability to define selection preference order.
74            type: list
75            default: 'ip_address,fqdn,host_name,name'
76        filter_results:
77            description: Filter results with sysparm_query encoded query string syntax. Complete list of operators available for filters and queries.
78            type: string
79            default: ''
80        proxy:
81            description: Proxy server to use for requests to ServiceNow.
82            type: string
83            default: ''
84        enhanced:
85            description:
86             - Enable enhanced inventory which provides relationship information from CMDB.
87             - Requires installation of Update Set located in update_sets directory.
88            type: bool
89            default: False
90        enhanced_groups:
91            description: enable enhanced groups from CMDB relationships. Only used if enhanced is enabled.
92            type: bool
93            default: True
94
95'''
96
97EXAMPLES = r'''
98# Simple Inventory Plugin example
99plugin: servicenow.servicenow.now
100instance: dev89007
101username: admin
102password: password
103keyed_groups:
104  - key: sn_sys_class_name | lower
105    prefix: ''
106    separator: ''
107
108# Using Keyed Groups
109plugin: servicenow.servicenow.now
110host: servicenow.mydomain.com
111username: admin
112password: password
113fields: [name,host_name,fqdn,ip_address,sys_class_name, install_status, classification,vendor]
114keyed_groups:
115  - key: sn_classification | lower
116    prefix: 'env'
117  - key: sn_vendor | lower
118    prefix: ''
119    separator: ''
120  - key: sn_sys_class_name | lower
121    prefix: ''
122    separator: ''
123  - key: sn_install_status | lower
124    prefix: 'status'
125
126# Compose hostvars
127plugin: servicenow.servicenow.now
128instance: dev89007
129username: admin
130password: password
131fields:
132  - name
133  - sys_tags
134compose:
135  sn_tags: sn_sys_tags.replace(" ", "").split(',')
136  ansible_host: sn_ip_address
137keyed_groups:
138  - key: sn_tags | lower
139    prefix: 'tag'
140'''
141
142try:
143    import netaddr
144    HAS_NETADDR = True
145except ImportError:
146    HAS_NETADDR = False
147
148try:
149    import requests
150    HAS_REQUESTS = True
151except ImportError:
152    HAS_REQUESTS = False
153
154from ansible.errors import AnsibleError, AnsibleParserError
155from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable, to_safe_group_name
156
157
158class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
159
160    NAME = 'servicenow.servicenow.now'
161
162    def verify_file(self, path):
163        valid = False
164        if super(InventoryModule, self).verify_file(path):
165            if path.endswith(('now.yaml', 'now.yml')):
166                valid = True
167            else:
168                self.display.vvv(
169                    'Skipping due to inventory source not ending in "now.yaml" nor "now.yml"')
170        return valid
171
172    def invoke(self, verb, path, data):
173        auth = requests.auth.HTTPBasicAuth(self.get_option('username'),
174                                           self.get_option('password'))
175        headers = {
176            "Accept": "application/json",
177            "Content-Type": "application/json",
178        }
179        proxy = self.get_option('proxy')
180
181        if self.get_option('instance'):
182            fqdn = "%s.service-now.com" % (self.get_option('instance'))
183        elif self.get_option('host'):
184            fqdn = self.get_option('host')
185        else:
186            raise AnsibleError("instance or host must be defined")
187
188        # build url
189        self.url = "https://%s/%s" % (fqdn, path)
190        url = self.url
191        self.display.vvv("Connecting to...%s" % url)
192        results = []
193
194        if not self.update_cache:
195            try:
196                results = self._cache[self.cache_key][self.url]
197            except KeyError:
198                pass
199
200        if not results:
201            if self.cache_key not in self._cache:
202                self._cache[self.cache_key] = {self.url: ''}
203
204            session = requests.Session()
205
206            while url:
207                # perform REST operation, accumulating page results
208                response = session.get(url,
209                                       auth=auth,
210                                       headers=headers,
211                                       proxies={
212                                           'http': proxy,
213                                           'https': proxy
214                                       })
215                if response.status_code == 400 and self.get_option('enhanced'):
216                    raise AnsibleError("http error (%s): %s. Have you installed the enhanced inventory update set on your instance?" %
217                                       (response.status_code, response.text))
218                elif response.status_code != 200:
219                    raise AnsibleError("http error (%s): %s" %
220                                       (response.status_code, response.text))
221                results += response.json()['result']
222                next_link = response.links.get('next', {})
223                url = next_link.get('url', None)
224
225            self._cache[self.cache_key] = {self.url: results}
226
227        results = {'result': results}
228        return results
229
230    def parse(self, inventory, loader, path,
231              cache=True):  # Plugin interface (2)
232        super(InventoryModule, self).parse(inventory, loader, path)
233
234        if not HAS_REQUESTS:
235            raise AnsibleParserError(
236                'Please install "requests" Python module as this is required'
237                ' for ServiceNow dynamic inventory plugin.')
238
239        self._read_config_data(path)
240        self.cache_key = self.get_cache_key(path)
241
242        self.use_cache = self.get_option('cache') and cache
243        self.update_cache = self.get_option('cache') and not cache
244
245        selection = self.get_option('selection_order')
246        fields = self.get_option('fields')
247        table = self.get_option('table')
248        filter_results = self.get_option('filter_results')
249
250        options = "?sysparm_exclude_reference_link=true&sysparm_display_value=true"
251
252        enhanced = self.get_option('enhanced')
253        enhanced_groups = False
254
255        if enhanced:
256            path = '/api/snc/ansible_inventory' + options + \
257                "&sysparm_fields=" + ','.join(fields) + \
258                "&sysparm_query=" + filter_results + \
259                "&table=" + table
260            enhanced_groups = self.get_option('enhanced_groups')
261        else:
262            path = '/api/now/table/' + table + options + \
263                "&sysparm_fields=" + ','.join(fields) + \
264                "&sysparm_query=" + filter_results
265
266        content = self.invoke('GET', path, None)
267        strict = self.get_option('strict')
268
269        for record in content['result']:
270
271            target = None
272
273            # select name for host
274            for k in selection:
275                if k in record:
276                    if record[k] != '':
277                        target = record[k]
278                if target is not None:
279                    break
280
281            if target is None:
282                continue
283
284            # add host to inventory
285            host_name = self.inventory.add_host(target)
286
287            # set variables for host
288            for k in record.keys():
289                self.inventory.set_variable(host_name, 'sn_%s' % k, record[k])
290
291            # add relationship based groups
292            if enhanced and enhanced_groups:
293                for item in record['child_relationships']:
294                    ci = to_safe_group_name(item['ci'])
295                    ci_rel_type = to_safe_group_name(
296                        item['ci_rel_type'].split('__')[0])
297                    ci_type = to_safe_group_name(item['ci_type'])
298                    if ci != '' and ci_rel_type != '' and ci_type != '':
299                        child_group = "%s_%s" % (ci, ci_rel_type)
300                        self.inventory.add_group(child_group)
301                        self.inventory.add_child(child_group, host_name)
302
303                for item in record['parent_relationships']:
304                    ci = to_safe_group_name(item['ci'])
305                    ci_rel_type = to_safe_group_name(
306                        item['ci_rel_type'].split('__')[-1])
307                    ci_type = to_safe_group_name(item['ci_type'])
308
309                    if ci != '' and ci_rel_type != '' and ci_type != '':
310                        child_group = "%s_%s" % (ci, ci_rel_type)
311                        self.inventory.add_group(child_group)
312                        self.inventory.add_child(child_group, host_name)
313
314            self._set_composite_vars(
315                self.get_option('compose'),
316                self.inventory.get_host(host_name).get_vars(), host_name,
317                strict)
318
319            self._add_host_to_composed_groups(self.get_option('groups'),
320                                              dict(), host_name, strict)
321            self._add_host_to_keyed_groups(self.get_option('keyed_groups'),
322                                           dict(), host_name, strict)
323