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