community.general/lib/ansible/plugins/inventory/foreman.py
Matt Williams cf00883c9d Return results even when the cache is disabled (#55505)
* Return results even when the cache is disabled

By default the cache is disabled and so the results of the API call
are not placed in there for the return statement to fetch.

* Always update self._cache to return
2019-05-28 13:44:00 -04:00

246 lines
9.1 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>, Daniel Lobato Garcia <dlobatog@redhat.com>
# Copyright (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: foreman
plugin_type: inventory
short_description: foreman inventory source
version_added: "2.6"
requirements:
- requests >= 1.1
description:
- Get inventory hosts from the foreman service.
- "Uses a configuration file as an inventory source, it must end in ``.foreman.yml`` or ``.foreman.yaml`` and has a ``plugin: foreman`` entry."
extends_documentation_fragment:
- inventory_cache
options:
plugin:
description: the name of this plugin, it should always be set to 'foreman' for this plugin to recognize it as it's own.
required: True
choices: ['foreman']
url:
description: url to foreman
default: 'http://localhost:3000'
env:
- name: FOREMAN_SERVER
version_added: "2.8"
user:
description: foreman authentication user
required: True
env:
- name: FOREMAN_USER
version_added: "2.8"
password:
description: foreman authentication password
required: True
env:
- name: FOREMAN_PASSWORD
version_added: "2.8"
validate_certs:
description: verify SSL certificate if using https
type: boolean
default: False
group_prefix:
description: prefix to apply to foreman groups
default: foreman_
vars_prefix:
description: prefix to apply to host variables, does not include facts nor params
default: foreman_
want_facts:
description: Toggle, if True the plugin will retrieve host facts from the server
type: boolean
default: False
want_params:
description: Toggle, if true the inventory will retrieve 'all_parameters' information as host vars
type: boolean
default: False
'''
EXAMPLES = '''
# my.foreman.yml
plugin: foreman
url: http://localhost:2222
user: ansible-tester
password: secure
validate_certs: False
'''
from distutils.version import LooseVersion
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils.common._collections_compat import MutableMapping
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, to_safe_group_name
# 3rd party imports
try:
import requests
if LooseVersion(requests.__version__) < LooseVersion('1.1.0'):
raise ImportError
except ImportError:
raise AnsibleError('This script requires python-requests 1.1 as a minimum version')
from requests.auth import HTTPBasicAuth
class InventoryModule(BaseInventoryPlugin, Cacheable):
''' Host inventory parser for ansible using foreman as source. '''
NAME = 'foreman'
def __init__(self):
super(InventoryModule, self).__init__()
# from config
self.foreman_url = None
self.session = None
self.cache_key = None
self.use_cache = None
def verify_file(self, path):
valid = False
if super(InventoryModule, self).verify_file(path):
if path.endswith(('foreman.yaml', 'foreman.yml')):
valid = True
else:
self.display.vvv('Skipping due to inventory source not ending in "foreman.yaml" nor "foreman.yml"')
return valid
def _get_session(self):
if not self.session:
self.session = requests.session()
self.session.auth = HTTPBasicAuth(self.get_option('user'), to_bytes(self.get_option('password')))
self.session.verify = self.get_option('validate_certs')
return self.session
def _get_json(self, url, ignore_errors=None):
if not self.use_cache or url not in self._cache.get(self.cache_key, {}):
if self.cache_key not in self._cache:
self._cache[self.cache_key] = {url: ''}
results = []
s = self._get_session()
params = {'page': 1, 'per_page': 250}
while True:
ret = s.get(url, params=params)
if ignore_errors and ret.status_code in ignore_errors:
break
ret.raise_for_status()
json = ret.json()
# process results
# FIXME: This assumes 'return type' matches a specific query,
# it will break if we expand the queries and they dont have different types
if 'results' not in json:
# /hosts/:id dos not have a 'results' key
results = json
break
elif isinstance(json['results'], MutableMapping):
# /facts are returned as dict in 'results'
results = json['results']
break
else:
# /hosts 's 'results' is a list of all hosts, returned is paginated
results = results + json['results']
# check for end of paging
if len(results) >= json['subtotal']:
break
if len(json['results']) == 0:
self.display.warning("Did not make any progress during loop. expected %d got %d" % (json['subtotal'], len(results)))
break
# get next page
params['page'] += 1
self._cache[self.cache_key][url] = results
return self._cache[self.cache_key][url]
def _get_hosts(self):
return self._get_json("%s/api/v2/hosts" % self.foreman_url)
def _get_all_params_by_id(self, hid):
url = "%s/api/v2/hosts/%s" % (self.foreman_url, hid)
ret = self._get_json(url, [404])
if not ret or not isinstance(ret, MutableMapping) or not ret.get('all_parameters', False):
return {}
return ret.get('all_parameters')
def _get_facts_by_id(self, hid):
url = "%s/api/v2/hosts/%s/facts" % (self.foreman_url, hid)
return self._get_json(url)
def _get_facts(self, host):
"""Fetch all host facts of the host"""
ret = self._get_facts_by_id(host['id'])
if len(ret.values()) == 0:
facts = {}
elif len(ret.values()) == 1:
facts = list(ret.values())[0]
else:
raise ValueError("More than one set of facts returned for '%s'" % host)
return facts
def _populate(self):
for host in self._get_hosts():
if host.get('name'):
self.inventory.add_host(host['name'])
# create directly mapped groups
group_name = host.get('hostgroup_title', host.get('hostgroup_name'))
if group_name:
group_name = to_safe_group_name('%s%s' % (self.get_option('group_prefix'), group_name.lower().replace(" ", "")))
group_name = self.inventory.add_group(group_name)
self.inventory.add_child(group_name, host['name'])
# set host vars from host info
try:
for k, v in host.items():
if k not in ('name', 'hostgroup_title', 'hostgroup_name'):
try:
self.inventory.set_variable(host['name'], self.get_option('vars_prefix') + k, v)
except ValueError as e:
self.display.warning("Could not set host info hostvar for %s, skipping %s: %s" % (host, k, to_text(e)))
except ValueError as e:
self.display.warning("Could not get host info for %s, skipping: %s" % (host['name'], to_text(e)))
# set host vars from params
if self.get_option('want_params'):
for p in self._get_all_params_by_id(host['id']):
try:
self.inventory.set_variable(host['name'], p['name'], p['value'])
except ValueError as e:
self.display.warning("Could not set hostvar %s to '%s' for the '%s' host, skipping: %s" %
(p['name'], to_native(p['value']), host, to_native(e)))
# set host vars from facts
if self.get_option('want_facts'):
self.inventory.set_variable(host['name'], 'ansible_facts', self._get_facts(host))
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
# read config from file, this sets 'options'
self._read_config_data(path)
# get connection host
self.foreman_url = self.get_option('url')
self.cache_key = self.get_cache_key(path)
self.use_cache = cache and self.get_option('cache')
# actually populate inventory
self._populate()