remove unneeded steps in inventory import

Delete some cases that directly loads scripts due
to ansible-inventory group_vars problem (now fixed)

Delete intermediate method that was a go-between the
command and the loader class

Change return type of loader from MemInventory to
a simple python dict

remove backport script and star imports
This commit is contained in:
AlanCoding
2018-03-26 08:24:14 -04:00
parent 584ec9cf75
commit 28822d891c
3 changed files with 88 additions and 482 deletions

View File

@@ -15,12 +15,20 @@ import shutil
# Django # Django
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.core.exceptions import ImproperlyConfigured
from django.db import connection, transaction from django.db import connection, transaction
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
# AWX # AWX inventory imports
from awx.main.models import * # noqa from awx.main.models.inventory import (
Inventory,
InventorySource,
InventoryUpdate,
Host
)
from awx.main.utils.mem_inventory import MemInventory, dict_to_mem_data
# other AWX imports
from awx.main.models.rbac import batch_role_ancestor_rebuilding
from awx.main.utils import ( from awx.main.utils import (
ignore_inventory_computed_fields, ignore_inventory_computed_fields,
check_proot_installed, check_proot_installed,
@@ -28,7 +36,6 @@ from awx.main.utils import (
build_proot_temp_dir, build_proot_temp_dir,
get_licenser get_licenser
) )
from awx.main.utils.mem_inventory import MemInventory, dict_to_mem_data
from awx.main.signals import disable_activity_stream from awx.main.signals import disable_activity_stream
from awx.main.constants import STANDARD_INVENTORY_UPDATE_ENV from awx.main.constants import STANDARD_INVENTORY_UPDATE_ENV
@@ -63,21 +70,14 @@ class AnsibleInventoryLoader(object):
use the ansible-inventory CLI utility to convert it into in-memory use the ansible-inventory CLI utility to convert it into in-memory
representational objects. Example: representational objects. Example:
/usr/bin/ansible/ansible-inventory -i hosts --list /usr/bin/ansible/ansible-inventory -i hosts --list
If it fails to find this, it uses the backported script instead
''' '''
def __init__(self, source, group_filter_re=None, host_filter_re=None, is_custom=False): def __init__(self, source, is_custom=False):
self.source = source self.source = source
self.source_dir = functioning_dir(self.source) self.source_dir = functioning_dir(self.source)
self.is_custom = is_custom self.is_custom = is_custom
self.tmp_private_dir = None self.tmp_private_dir = None
self.method = 'ansible-inventory' self.method = 'ansible-inventory'
self.group_filter_re = group_filter_re
self.host_filter_re = host_filter_re
self.is_vendored_source = False
if self.source_dir == os.path.join(settings.BASE_DIR, 'plugins', 'inventory'):
self.is_vendored_source = True
def build_env(self): def build_env(self):
env = dict(os.environ.items()) env = dict(os.environ.items())
@@ -95,28 +95,10 @@ class AnsibleInventoryLoader(object):
def get_base_args(self): def get_base_args(self):
# get ansible-inventory absolute path for running in bubblewrap/proot, in Popen # get ansible-inventory absolute path for running in bubblewrap/proot, in Popen
for path in os.environ["PATH"].split(os.pathsep): abs_ansible_inventory = shutil.which('ansible-inventory')
potential_path = os.path.join(path.strip('"'), 'ansible-inventory') bargs= [abs_ansible_inventory, '-i', self.source]
if os.path.isfile(potential_path) and os.access(potential_path, os.X_OK): logger.debug('Using base command: {}'.format(' '.join(bargs)))
logger.debug('Using system install of ansible-inventory CLI: {}'.format(potential_path)) return bargs
return [potential_path, '-i', self.source]
# Stopgap solution for group_vars, do not use backported module for official
# vendored cloud modules or custom scripts TODO: remove after Ansible 2.3 deprecation
if self.is_vendored_source or self.is_custom:
self.method = 'inventory script invocation'
return [self.source]
# ansible-inventory was not found, look for backported module TODO: remove after Ansible 2.3 deprecation
abs_module_path = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', '..', '..', 'plugins',
'ansible_inventory', 'backport.py'))
self.method = 'ansible-inventory backport'
if not os.path.exists(abs_module_path):
raise ImproperlyConfigured('Cannot find inventory module')
logger.debug('Using backported ansible-inventory module: {}'.format(abs_module_path))
return [abs_module_path, '-i', self.source]
def get_proot_args(self, cmd, env): def get_proot_args(self, cmd, env):
cwd = os.getcwd() cwd = os.getcwd()
@@ -179,80 +161,7 @@ class AnsibleInventoryLoader(object):
base_args = self.get_base_args() base_args = self.get_base_args()
logger.info('Reading Ansible inventory source: %s', self.source) logger.info('Reading Ansible inventory source: %s', self.source)
data = self.command_to_json(base_args + ['--list']) return self.command_to_json(base_args + ['--list'])
# TODO: remove after we run custom scripts through ansible-inventory
if self.is_custom and '_meta' not in data or 'hostvars' not in data['_meta']:
# Invoke the executable once for each host name we've built up
# to set their variables
data.setdefault('_meta', {})
data['_meta'].setdefault('hostvars', {})
logger.warning('Re-calling script for hostvars individually.')
for group_name, group_data in list(data.items()):
if group_name == '_meta':
continue
if isinstance(group_data, dict):
group_host_list = group_data.get('hosts', [])
elif isinstance(group_data, list):
group_host_list = group_data
else:
logger.warning('Group data for "%s" is not a dict or list',
group_name)
group_host_list = []
for hostname in group_host_list:
logger.debug('Obtaining hostvars for %s' % hostname.encode('utf-8'))
hostdata = self.command_to_json(
base_args + ['--host', hostname.encode("utf-8")]
)
if isinstance(hostdata, dict):
data['_meta']['hostvars'][hostname] = hostdata
else:
logger.warning(
'Expected dict of vars for host "%s" when '
'calling with `--host`, got %s instead',
k, str(type(data))
)
logger.info('Processing JSON output...')
inventory = MemInventory(
group_filter_re=self.group_filter_re, host_filter_re=self.host_filter_re)
inventory = dict_to_mem_data(data, inventory=inventory)
return inventory
def load_inventory_source(source, group_filter_re=None,
host_filter_re=None, exclude_empty_groups=False,
is_custom=False):
'''
Load inventory from given source directory or file.
'''
# Sanity check: We sanitize these module names for our API but Ansible proper doesn't follow
# good naming conventions
source = source.replace('rhv.py', 'ovirt4.py')
source = source.replace('satellite6.py', 'foreman.py')
source = source.replace('vmware.py', 'vmware_inventory.py')
if not os.path.exists(source):
raise IOError('Source does not exist: %s' % source)
source = os.path.join(os.getcwd(), os.path.dirname(source),
os.path.basename(source))
source = os.path.normpath(os.path.abspath(source))
inventory = AnsibleInventoryLoader(
source=source,
group_filter_re=group_filter_re,
host_filter_re=host_filter_re,
is_custom=is_custom).load()
logger.debug('Finished loading from source: %s', source)
# Exclude groups that are completely empty.
if exclude_empty_groups:
inventory.delete_empty_groups()
logger.info('Loaded %d groups, %d hosts', len(inventory.all_group.all_groups),
len(inventory.all_group.all_hosts))
return inventory.all_group
class Command(BaseCommand): class Command(BaseCommand):
@@ -359,6 +268,19 @@ class Command(BaseCommand):
else: else:
raise NotImplementedError('Value of enabled {} not understood.'.format(enabled)) raise NotImplementedError('Value of enabled {} not understood.'.format(enabled))
def get_source_absolute_path(self, source):
# Sanity check: We sanitize these module names for our API but Ansible proper doesn't follow
# good naming conventions
source = source.replace('rhv.py', 'ovirt4.py')
source = source.replace('satellite6.py', 'foreman.py')
source = source.replace('vmware.py', 'vmware_inventory.py')
if not os.path.exists(source):
raise IOError('Source does not exist: %s' % source)
source = os.path.join(os.getcwd(), os.path.dirname(source),
os.path.basename(source))
source = os.path.normpath(os.path.abspath(source))
return source
def load_inventory_from_database(self): def load_inventory_from_database(self):
''' '''
Load inventory and related objects from the database. Load inventory and related objects from the database.
@@ -988,12 +910,26 @@ class Command(BaseCommand):
self.inventory_update.status = 'running' self.inventory_update.status = 'running'
self.inventory_update.save() self.inventory_update.save()
# Load inventory from source. source = self.get_source_absolute_path(self.source)
self.all_group = load_inventory_source(self.source,
self.group_filter_re, data = AnsibleInventoryLoader(source=source, is_custom=self.is_custom).load()
self.host_filter_re,
self.exclude_empty_groups, logger.debug('Finished loading from source: %s', source)
self.is_custom) logger.info('Processing JSON output...')
inventory = MemInventory(
group_filter_re=self.group_filter_re, host_filter_re=self.host_filter_re)
inventory = dict_to_mem_data(data, inventory=inventory)
del data # forget dict from import, could be large
logger.info('Loaded %d groups, %d hosts', len(inventory.all_group.all_groups),
len(inventory.all_group.all_hosts))
if self.exclude_empty_groups:
inventory.delete_empty_groups()
self.all_group = inventory.all_group
if settings.DEBUG: if settings.DEBUG:
# depending on inventory source, this output can be # depending on inventory source, this output can be
# *exceedingly* verbose - crawling a deeply nested # *exceedingly* verbose - crawling a deeply nested

View File

@@ -11,7 +11,6 @@ from django.core.management.base import CommandError
# AWX # AWX
from awx.main.management.commands import inventory_import from awx.main.management.commands import inventory_import
from awx.main.models import Inventory, Host, Group from awx.main.models import Inventory, Host, Group
from awx.main.utils.mem_inventory import dict_to_mem_data
TEST_INVENTORY_CONTENT = { TEST_INVENTORY_CONTENT = {
@@ -73,7 +72,13 @@ TEST_INVENTORY_CONTENT = {
} }
TEST_MEM_OBJECTS = dict_to_mem_data(TEST_INVENTORY_CONTENT) class MockLoader:
def __init__(self, *args, **kwargs):
pass
def load(self):
return self._data
def mock_logging(self): def mock_logging(self):
@@ -86,7 +91,6 @@ def mock_logging(self):
@mock.patch.object(inventory_import.Command, 'set_logging_level', mock_logging) @mock.patch.object(inventory_import.Command, 'set_logging_level', mock_logging)
class TestInvalidOptionsFunctional: class TestInvalidOptionsFunctional:
@mock.patch.object(inventory_import.InstanceGroup.objects, 'get', new=mock.MagicMock(return_value=None))
def test_invalid_options_invalid_source(self, inventory): def test_invalid_options_invalid_source(self, inventory):
# Give invalid file to the command # Give invalid file to the command
cmd = inventory_import.Command() cmd = inventory_import.Command()
@@ -114,13 +118,13 @@ class TestInvalidOptionsFunctional:
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.inventory_import @pytest.mark.inventory_import
@mock.patch.object(inventory_import.InstanceGroup.objects, 'get', new=mock.MagicMock(return_value=None))
@mock.patch.object(inventory_import.Command, 'check_license', new=mock.MagicMock()) @mock.patch.object(inventory_import.Command, 'check_license', new=mock.MagicMock())
@mock.patch.object(inventory_import.Command, 'set_logging_level', new=mock_logging) @mock.patch.object(inventory_import.Command, 'set_logging_level', new=mock_logging)
class TestINIImports: class TestINIImports:
@mock.patch.object(inventory_import.AnsibleInventoryLoader, 'load', mock.MagicMock(return_value=TEST_MEM_OBJECTS)) @mock.patch.object(inventory_import, 'AnsibleInventoryLoader', MockLoader)
def test_inventory_single_ini_import(self, inventory, capsys): def test_inventory_single_ini_import(self, inventory, capsys):
inventory_import.AnsibleInventoryLoader._data = TEST_INVENTORY_CONTENT
cmd = inventory_import.Command() cmd = inventory_import.Command()
r = cmd.handle( r = cmd.handle(
inventory_id=inventory.pk, source=__file__, inventory_id=inventory.pk, source=__file__,
@@ -174,52 +178,44 @@ class TestINIImports:
assert reloaded_inv.inventory_sources.count() == 1 assert reloaded_inv.inventory_sources.count() == 1
assert reloaded_inv.inventory_sources.all()[0].source == 'file' assert reloaded_inv.inventory_sources.all()[0].source == 'file'
@mock.patch.object( @mock.patch.object(inventory_import, 'AnsibleInventoryLoader', MockLoader)
inventory_import, 'load_inventory_source', mock.MagicMock(
return_value=dict_to_mem_data(
{
"_meta": {
"hostvars": {"foo": {"some_hostvar": "foobar"}}
},
"all": {
"children": ["ungrouped"]
},
"ungrouped": {
"hosts": ["foo"]
}
}).all_group
)
)
def test_hostvars_are_saved(self, inventory): def test_hostvars_are_saved(self, inventory):
inventory_import.AnsibleInventoryLoader._data = {
"_meta": {
"hostvars": {"foo": {"some_hostvar": "foobar"}}
},
"all": {
"children": ["ungrouped"]
},
"ungrouped": {
"hosts": ["foo"]
}
}
cmd = inventory_import.Command() cmd = inventory_import.Command()
cmd.handle(inventory_id=inventory.pk, source='doesnt matter') cmd.handle(inventory_id=inventory.pk, source=__file__)
assert inventory.hosts.count() == 1 assert inventory.hosts.count() == 1
h = inventory.hosts.all()[0] h = inventory.hosts.all()[0]
assert h.name == 'foo' assert h.name == 'foo'
assert h.variables_dict == {"some_hostvar": "foobar"} assert h.variables_dict == {"some_hostvar": "foobar"}
@mock.patch.object( @mock.patch.object(inventory_import, 'AnsibleInventoryLoader', MockLoader)
inventory_import, 'load_inventory_source', mock.MagicMock(
return_value=dict_to_mem_data(
{
"_meta": {
"hostvars": {}
},
"all": {
"children": ["fooland", "barland"]
},
"fooland": {
"children": ["barland"]
},
"barland": {
"children": ["fooland"]
}
}).all_group
)
)
def test_recursive_group_error(self, inventory): def test_recursive_group_error(self, inventory):
inventory_import.AnsibleInventoryLoader._data = {
"_meta": {
"hostvars": {}
},
"all": {
"children": ["fooland", "barland"]
},
"fooland": {
"children": ["barland"]
},
"barland": {
"children": ["fooland"]
}
}
cmd = inventory_import.Command() cmd = inventory_import.Command()
cmd.handle(inventory_id=inventory.pk, source='doesnt matter') cmd.handle(inventory_id=inventory.pk, source=__file__)
@pytest.mark.django_db @pytest.mark.django_db

View File

@@ -1,326 +0,0 @@
#!/usr/bin/env python
# (c) 2017, Brian Coca <bcoca@ansible.com>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import distutils.spawn
import optparse
from operator import attrgetter
from ansible.cli import CLI
from ansible.errors import AnsibleOptionsError
from ansible.parsing.dataloader import DataLoader
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
INTERNAL_VARS = frozenset([ 'ansible_facts',
'ansible_version',
'ansible_playbook_python',
'inventory_dir',
'inventory_file',
'inventory_hostname',
'inventory_hostname_short',
'groups',
'group_names',
'omit',
'playbook_dir',
])
class InventoryCLI(CLI):
''' used to display or dump the configured inventory as Ansible sees it '''
ARGUMENTS = { 'host': 'The name of a host to match in the inventory, relevant when using --list',
'group': 'The name of a group in the inventory, relevant when using --graph',
}
def __init__(self, args):
super(InventoryCLI, self).__init__(args)
self.args = args
self.vm = None
self.loader = None
self.inventory = None
self._new_api = True
def parse(self):
self.parser = CLI.base_parser(
usage='usage: %prog [options] [host|group]',
epilog='Show Ansible inventory information, by default it uses the inventory script JSON format',
inventory_opts=True,
vault_opts=True
)
self.parser.add_option("--optimize", action="store_true", default=False, dest='optimize',
help='Output variables on the group or host where they are defined')
# Actions
action_group = optparse.OptionGroup(self.parser, "Actions", "One of following must be used on invocation, ONLY ONE!")
action_group.add_option("--list", action="store_true", default=False, dest='list', help='Output all hosts info, works as inventory script')
action_group.add_option("--host", action="store", default=None, dest='host', help='Output specific host info, works as inventory script')
action_group.add_option("--graph", action="store_true", default=False, dest='graph',
help='create inventory graph, if supplying pattern it must be a valid group name')
self.parser.add_option_group(action_group)
# Options
self.parser.add_option("-y", "--yaml", action="store_true", default=False, dest='yaml',
help='Use YAML format instead of default JSON, ignored for --graph')
self.parser.add_option("--vars", action="store_true", default=False, dest='show_vars',
help='Add vars to graph display, ignored unless used with --graph')
try:
super(InventoryCLI, self).parse()
except Exception as e:
if 'Need to implement!' not in e.args[0]:
raise
# --- Start of 2.3+ super(InventoryCLI, self).parse() ---
self.options, self.args = self.parser.parse_args(self.args[1:])
# --- End of 2.3+ super(InventoryCLI, self).parse() ---
display.verbosity = self.options.verbosity
self.validate_conflicts(vault_opts=True)
# there can be only one! and, at least, one!
used = 0
for opt in (self.options.list, self.options.host, self.options.graph):
if opt:
used += 1
if used == 0:
raise AnsibleOptionsError("No action selected, at least one of --host, --graph or --list needs to be specified.")
elif used > 1:
raise AnsibleOptionsError("Conflicting options used, only one of --host, --graph or --list can be used at the same time.")
# set host pattern to default if not supplied
if len(self.args) > 0:
self.options.pattern = self.args[0]
else:
self.options.pattern = 'all'
def run(self):
results = None
super(InventoryCLI, self).run()
# Initialize needed objects
if getattr(self, '_play_prereqs', False):
self.loader, self.inventory, self.vm = self._play_prereqs(self.options)
else:
# fallback to pre 2.4 way of initialzing
from ansible.vars import VariableManager
from ansible.inventory import Inventory
self._new_api = False
self.loader = DataLoader()
self.vm = VariableManager()
# use vault if needed
if self.options.vault_password_file:
vault_pass = CLI.read_vault_password_file(self.options.vault_password_file, loader=self.loader)
elif self.options.ask_vault_pass:
vault_pass = self.ask_vault_passwords()
else:
vault_pass = None
if vault_pass:
self.loader.set_vault_password(vault_pass)
# actually get inventory and vars
self.inventory = Inventory(loader=self.loader, variable_manager=self.vm, host_list=self.options.inventory)
self.vm.set_inventory(self.inventory)
if self.options.host:
hosts = self.inventory.get_hosts(self.options.host)
if len(hosts) != 1:
raise AnsibleOptionsError("You must pass a single valid host to --hosts parameter")
myvars = self._get_host_variables(host=hosts[0])
self._remove_internal(myvars)
# FIXME: should we template first?
results = self.dump(myvars)
elif self.options.graph:
results = self.inventory_graph()
elif self.options.list:
top = self._get_group('all')
if self.options.yaml:
results = self.yaml_inventory(top)
else:
results = self.json_inventory(top)
results = self.dump(results)
if results:
# FIXME: pager?
display.display(results)
exit(0)
exit(1)
def dump(self, stuff):
if self.options.yaml:
import yaml
from ansible.parsing.yaml.dumper import AnsibleDumper
results = yaml.dump(stuff, Dumper=AnsibleDumper, default_flow_style=False)
else:
import json
results = json.dumps(stuff, sort_keys=True, indent=4)
return results
def _get_host_variables(self, host):
if self._new_api:
hostvars = self.vm.get_vars(host=host)
else:
hostvars = self.vm.get_vars(self.loader, host=host)
return hostvars
def _get_group(self, gname):
if self._new_api:
group = self.inventory.groups.get(gname)
else:
group = self.inventory.get_group(gname)
return group
def _remove_internal(self, dump):
for internal in INTERNAL_VARS:
if internal in dump:
del dump[internal]
def _remove_empty(self, dump):
# remove empty keys
for x in ('hosts', 'vars', 'children'):
if x in dump and not dump[x]:
del dump[x]
def _show_vars(self, dump, depth):
result = []
self._remove_internal(dump)
if self.options.show_vars:
for (name, val) in sorted(dump.items()):
result.append(self._graph_name('{%s = %s}' % (name, val), depth + 1))
return result
def _graph_name(self, name, depth=0):
if depth:
name = " |" * (depth) + "--%s" % name
return name
def _graph_group(self, group, depth=0):
result = [self._graph_name('@%s:' % group.name, depth)]
depth = depth + 1
for kid in sorted(group.child_groups, key=attrgetter('name')):
result.extend(self._graph_group(kid, depth))
if group.name != 'all':
for host in sorted(group.hosts, key=attrgetter('name')):
result.append(self._graph_name(host.name, depth))
result.extend(self._show_vars(host.get_vars(), depth))
result.extend(self._show_vars(group.get_vars(), depth))
return result
def inventory_graph(self):
start_at = self._get_group(self.options.pattern)
if start_at:
return '\n'.join(self._graph_group(start_at))
else:
raise AnsibleOptionsError("Pattern must be valid group name when using --graph")
def json_inventory(self, top):
def format_group(group):
results = {}
results[group.name] = {}
if group.name != 'all':
results[group.name]['hosts'] = [h.name for h in sorted(group.hosts, key=attrgetter('name'))]
results[group.name]['vars'] = group.get_vars()
results[group.name]['children'] = []
for subgroup in sorted(group.child_groups, key=attrgetter('name')):
results[group.name]['children'].append(subgroup.name)
results.update(format_group(subgroup))
self._remove_empty(results[group.name])
return results
results = format_group(top)
# populate meta
results['_meta'] = {'hostvars': {}}
hosts = self.inventory.get_hosts()
for host in hosts:
results['_meta']['hostvars'][host.name] = self._get_host_variables(host=host)
self._remove_internal(results['_meta']['hostvars'][host.name])
return results
def yaml_inventory(self, top):
seen = []
def format_group(group):
results = {}
# initialize group + vars
results[group.name] = {}
results[group.name]['vars'] = group.get_vars()
# subgroups
results[group.name]['children'] = {}
for subgroup in sorted(group.child_groups, key=attrgetter('name')):
if subgroup.name != 'all':
results[group.name]['children'].update(format_group(subgroup))
# hosts for group
results[group.name]['hosts'] = {}
if group.name != 'all':
for h in sorted(group.hosts, key=attrgetter('name')):
myvars = {}
if h.name not in seen: # avoid defining host vars more than once
seen.append(h.name)
myvars = self._get_host_variables(host=h)
self._remove_internal(myvars)
results[group.name]['hosts'][h.name] = myvars
self._remove_empty(results[group.name])
return results
return format_group(top)
if __name__ == '__main__':
import imp
import sys
with open(__file__) as f:
imp.load_source('ansible.cli.inventory', __file__ + '.py', f)
ansible_path = distutils.spawn.find_executable('ansible')
sys.argv[0] = 'ansible-inventory'
with open(ansible_path) as in_file:
exec(in_file.read())