mirror of
https://github.com/ansible/awx.git
synced 2026-01-28 00:51:27 -03:30
refactor inventory Loaders for ansible-inventory
* remove support for loading from executable and static files * instead use ansible-inventory with fallback to backport * provide private file dir in task manager for cred injection * durable management of tmp dirs for user scripts * new 'scm' source choice for scm-type * update SCM inventory docs to new reality
This commit is contained in:
parent
a92c7cec7d
commit
2f62182940
@ -2353,7 +2353,7 @@ class InventorySourceUpdateView(RetrieveAPIView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
if obj.source == 'file' and obj.scm_project_id is not None:
|
||||
if obj.source == 'scm':
|
||||
raise PermissionDenied(detail=_(
|
||||
'Update the project `{}` in order to update this inventory source.'.format(
|
||||
obj.scm_project.name)))
|
||||
|
||||
@ -11,10 +11,12 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import shutil
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.core.management.base import NoArgsCommand, CommandError
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import connection, transaction
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
@ -24,7 +26,8 @@ from awx.main.task_engine import TaskEnhancer
|
||||
from awx.main.utils import (
|
||||
ignore_inventory_computed_fields,
|
||||
check_proot_installed,
|
||||
wrap_args_with_proot
|
||||
wrap_args_with_proot,
|
||||
build_proot_temp_dir
|
||||
)
|
||||
from awx.main.utils.mem_inventory import MemInventory, dict_to_mem_data
|
||||
from awx.main.signals import disable_activity_stream
|
||||
@ -48,52 +51,109 @@ Demo mode free license count exceeded, would bring available instances to %(new_
|
||||
See http://www.ansible.com/renew for licensing information.'''
|
||||
|
||||
|
||||
# if called with --list, inventory outputs a JSON representing everything
|
||||
# in the inventory. Supported cases are maintained in tests.
|
||||
# if called with --host <host_record_name> outputs JSON for that host
|
||||
def functioning_dir(path):
|
||||
if os.path.isdir(path):
|
||||
return path
|
||||
return os.path.dirname(path)
|
||||
|
||||
|
||||
class BaseLoader(object):
|
||||
use_proot = True
|
||||
class AnsibleInventoryLoader(object):
|
||||
'''
|
||||
Given executable `source` (directory, executable, or file) this will
|
||||
use the ansible-inventory CLI utility to convert it into in-memory
|
||||
representational objects. Example:
|
||||
/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):
|
||||
def __init__(self, source, group_filter_re=None, host_filter_re=None, is_custom=False):
|
||||
self.source = source
|
||||
self.exe_dir = os.path.dirname(source)
|
||||
self.inventory = MemInventory(
|
||||
group_filter_re=group_filter_re, host_filter_re=host_filter_re)
|
||||
self.is_custom = is_custom
|
||||
self.tmp_private_dir = None
|
||||
self.method = 'ansible-inventory'
|
||||
self.group_filter_re = group_filter_re
|
||||
self.host_filter_re = host_filter_re
|
||||
|
||||
def build_env(self):
|
||||
# Use ansible venv if it's available and setup to use
|
||||
env = dict(os.environ.items())
|
||||
if settings.ANSIBLE_USE_VENV:
|
||||
env['VIRTUAL_ENV'] = settings.ANSIBLE_VENV_PATH
|
||||
# env['VIRTUAL_ENV'] += settings.ANSIBLE_VENV_PATH
|
||||
env['PATH'] = os.path.join(settings.ANSIBLE_VENV_PATH, "bin") + ":" + env['PATH']
|
||||
# env['PATH'] += os.path.join(settings.ANSIBLE_VENV_PATH, "bin") + ":" + env['PATH']
|
||||
venv_libdir = os.path.join(settings.ANSIBLE_VENV_PATH, "lib")
|
||||
env.pop('PYTHONPATH', None) # default to none if no python_ver matches
|
||||
for python_ver in ["python2.7", "python2.6"]:
|
||||
if os.path.isdir(os.path.join(venv_libdir, python_ver)):
|
||||
env['PYTHONPATH'] = os.path.join(venv_libdir, python_ver, "site-packages") + ":"
|
||||
break
|
||||
env['PYTHONPATH'] += os.path.abspath(os.path.join(settings.BASE_DIR, '..')) + ":"
|
||||
return env
|
||||
|
||||
def get_base_args(self):
|
||||
# get ansible-inventory absolute path for running in bubblewrap/proot, in Popen
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
potential_path = os.path.join(path.strip('"'), 'ansible-inventory')
|
||||
if os.path.isfile(potential_path) and os.access(potential_path, os.X_OK):
|
||||
return [potential_path, '-i', self.source]
|
||||
|
||||
# ansible-inventory was not found, look for backported module
|
||||
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('Can not find inventory module')
|
||||
return [abs_module_path, '-i', self.source]
|
||||
|
||||
def get_proot_args(self, cmd, env):
|
||||
source_dir = functioning_dir(self.source)
|
||||
cwd = os.getcwd()
|
||||
if not check_proot_installed():
|
||||
raise RuntimeError("proot is not installed but is configured for use")
|
||||
|
||||
kwargs = {}
|
||||
if self.is_custom:
|
||||
# use source's tmp dir for proot, task manager will delete folder
|
||||
logger.debug("Using provided directory '{}' for isolation.".format(source_dir))
|
||||
kwargs['proot_temp_dir'] = source_dir
|
||||
cwd = source_dir
|
||||
else:
|
||||
# we can not safely store tmp data in source dir or trust script contents
|
||||
if env['AWX_PRIVATE_DATA_DIR']:
|
||||
# If this is non-blank, file credentials are being used and we need access
|
||||
private_data_dir = functioning_dir(env['AWX_PRIVATE_DATA_DIR'])
|
||||
logger.debug("Using private credential data in '{}'.".format(private_data_dir))
|
||||
kwargs['private_data_dir'] = private_data_dir
|
||||
self.tmp_private_dir = build_proot_temp_dir()
|
||||
logger.debug("Using fresh temporary directory '{}' for isolation.".format(self.tmp_private_dir))
|
||||
kwargs['proot_temp_dir'] = self.tmp_private_dir
|
||||
# Run from source's location so that custom script contents are in `show_paths`
|
||||
cwd = functioning_dir(self.source)
|
||||
logger.debug("Running from `{}` working directory.".format(cwd))
|
||||
|
||||
return wrap_args_with_proot(cmd, cwd, **kwargs)
|
||||
|
||||
def command_to_json(self, cmd):
|
||||
data = {}
|
||||
stdout, stderr = '', ''
|
||||
try:
|
||||
if self.use_proot and getattr(settings, 'AWX_PROOT_ENABLED', False):
|
||||
if not check_proot_installed():
|
||||
raise RuntimeError("proot is not installed but is configured for use")
|
||||
kwargs = {'proot_temp_dir': self.exe_dir} # TODO: Remove proot dir
|
||||
cmd = wrap_args_with_proot(cmd, self.exe_dir, **kwargs)
|
||||
env = self.build_env()
|
||||
env = self.build_env()
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
||||
stdout, stderr = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError('%r failed (rc=%d) with output: %s' % (cmd, proc.returncode, stderr))
|
||||
if ((self.is_custom or 'AWX_PRIVATE_DATA_DIR' in env) and
|
||||
getattr(settings, 'AWX_PROOT_ENABLED', False)):
|
||||
cmd = self.get_proot_args(cmd, env)
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
||||
stdout, stderr = proc.communicate()
|
||||
|
||||
if self.tmp_private_dir:
|
||||
shutil.rmtree(self.tmp_private_dir, True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError('%s failed (rc=%d) with output:\n%s' % (self.method, proc.returncode, stderr))
|
||||
elif 'file not found' in stderr:
|
||||
# File not visible to inventory module due proot (exit code 0, Ansible behavior)
|
||||
raise IOError('Inventory module failed to find source {} with output:\n{}.'.format(self.source, stderr))
|
||||
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError('Returned JSON must be a dictionary, got %s instead' % str(type(data)))
|
||||
@ -102,105 +162,22 @@ class BaseLoader(object):
|
||||
raise
|
||||
return data
|
||||
|
||||
def build_base_args(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def load(self):
|
||||
base_args = self.build_base_args()
|
||||
logger.info('Reading executable JSON source: %s', ' '.join(base_args))
|
||||
base_args = self.get_base_args()
|
||||
logger.info('Reading Ansible inventory source: %s', self.source)
|
||||
data = self.command_to_json(base_args + ['--list'])
|
||||
self.has_hostvars = '_meta' in data and 'hostvars' in data['_meta']
|
||||
|
||||
inventory = dict_to_mem_data(data, inventory=self.inventory)
|
||||
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
|
||||
|
||||
|
||||
class AnsibleInventoryLoader(BaseLoader):
|
||||
'''
|
||||
Given executable `source` directory, executable, or file, this will
|
||||
use the ansible-inventory CLI utility to convert it into in-memory
|
||||
representational objects. Example:
|
||||
/usr/bin/ansible/ansible-inventory -i hosts --list
|
||||
'''
|
||||
|
||||
def build_base_args(self):
|
||||
# Need absolute path of anisble-inventory in order to run inside
|
||||
# of bubblewrap, inside of Popen
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
potential_path = os.path.join(path.strip('"'), 'ansible-inventory')
|
||||
if os.path.isfile(potential_path) and os.access(potential_path, os.X_OK):
|
||||
return [potential_path]
|
||||
raise RuntimeError(
|
||||
'ImproperlyConfigured: Called with modern method but '
|
||||
'not detect ansible-inventory on this system. '
|
||||
'Check to see that system Ansible is 2.4 or higher.')
|
||||
|
||||
|
||||
# TODO: delete after Ansible 2.3 is deprecated
|
||||
class InventoryPluginLoader(BaseLoader):
|
||||
'''
|
||||
Implements a different use pattern for loading JSON content from an
|
||||
Ansible inventory module, example:
|
||||
/path/ansible_inventory_module.py -i my_inventory.ini --list
|
||||
'''
|
||||
|
||||
def __init__(self, source, module, *args, **kwargs):
|
||||
super(InventoryPluginLoader, self).__init__(source, *args, **kwargs)
|
||||
assert module in ['legacy', 'backport'], (
|
||||
'Supported modules are `legacy` and `backport`, received {}'.format(module))
|
||||
self.module = module
|
||||
# self.use_proot = False
|
||||
|
||||
def build_env(self):
|
||||
if self.module == 'legacy':
|
||||
# legacy script does not rely on Ansible imports
|
||||
return dict(os.environ.items())
|
||||
return super(InventoryPluginLoader, self).build_env()
|
||||
|
||||
def build_base_args(self):
|
||||
abs_module_path = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), '..', '..', '..', 'plugins',
|
||||
'ansible_inventory', '{}.py'.format(self.module)))
|
||||
return [abs_module_path, '-i', self.source]
|
||||
|
||||
|
||||
# TODO: delete after Ansible 2.3 is deprecated
|
||||
class ExecutableJsonLoader(BaseLoader):
|
||||
'''
|
||||
Directly calls an inventory script, example:
|
||||
/path/ec2.py --list
|
||||
'''
|
||||
|
||||
def __init__(self, source, is_custom=False, **kwargs):
|
||||
super(ExecutableJsonLoader, self).__init__(source, **kwargs)
|
||||
self.use_proot = is_custom
|
||||
|
||||
def build_base_args(self):
|
||||
return [self.source]
|
||||
|
||||
def load(self):
|
||||
inventory = super(ExecutableJsonLoader, self).load()
|
||||
|
||||
# Invoke the executable once for each host name we've built up
|
||||
# to set their variables
|
||||
if not self.has_hostvars:
|
||||
base_args = self.build_base_args()
|
||||
for k,v in self.inventory.all_group.all_hosts.iteritems():
|
||||
host_data = self.command_to_json(
|
||||
base_args + ['--host', k.encode("utf-8")])
|
||||
if isinstance(host_data, dict):
|
||||
v.variables.update(host_data)
|
||||
else:
|
||||
logger.warning('Expected dict of vars for '
|
||||
'host "%s", got %s instead',
|
||||
k, str(type(host_data)))
|
||||
return inventory
|
||||
|
||||
|
||||
def load_inventory_source(source, group_filter_re=None,
|
||||
host_filter_re=None, exclude_empty_groups=False,
|
||||
is_custom=False, method='legacy'):
|
||||
is_custom=False):
|
||||
'''
|
||||
Load inventory from given source directory or file.
|
||||
'''
|
||||
@ -209,35 +186,17 @@ def load_inventory_source(source, group_filter_re=None,
|
||||
source = source.replace('azure.py', 'windows_azure.py')
|
||||
source = source.replace('satellite6.py', 'foreman.py')
|
||||
source = source.replace('vmware.py', 'vmware_inventory.py')
|
||||
logger.debug('Analyzing type of source: %s', source)
|
||||
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))
|
||||
|
||||
# TODO: delete options for 'legacy' and 'backport' after Ansible 2.3 deprecated
|
||||
if method == 'modern':
|
||||
inventory = AnsibleInventoryLoader(
|
||||
source=source,
|
||||
group_filter_re=group_filter_re,
|
||||
host_filter_re=host_filter_re).load()
|
||||
|
||||
elif method == 'legacy' and (os.access(source, os.X_OK) and not os.path.isdir(source)):
|
||||
# Legacy method of loading executable files
|
||||
inventory = ExecutableJsonLoader(
|
||||
source=source,
|
||||
group_filter_re=group_filter_re,
|
||||
host_filter_re=host_filter_re,
|
||||
is_custom=is_custom).load()
|
||||
|
||||
else:
|
||||
# Load using specified ansible-inventory module
|
||||
inventory = InventoryPluginLoader(
|
||||
source=source,
|
||||
module=method,
|
||||
group_filter_re=group_filter_re,
|
||||
host_filter_re=host_filter_re).load()
|
||||
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.
|
||||
@ -299,12 +258,6 @@ class Command(NoArgsCommand):
|
||||
default=None, metavar='v', help='host variable that '
|
||||
'specifies the unique, immutable instance ID, may be '
|
||||
'specified as "foo.bar" to traverse nested dicts.'),
|
||||
# TODO: remove --method option when Ansible 2.3 is deprecated
|
||||
make_option('--method', dest='method', type='choice',
|
||||
choices=['modern', 'backport', 'legacy'],
|
||||
default='legacy', help='Method for importing inventory '
|
||||
'to use, distinguishing whether to use `ansible-inventory`, '
|
||||
'its backport, or the legacy algorithms.'),
|
||||
)
|
||||
|
||||
def set_logging_level(self):
|
||||
@ -975,8 +928,7 @@ class Command(NoArgsCommand):
|
||||
self.group_filter_re,
|
||||
self.host_filter_re,
|
||||
self.exclude_empty_groups,
|
||||
self.is_custom,
|
||||
options.get('method'))
|
||||
self.is_custom)
|
||||
self.all_group.debug_tree()
|
||||
|
||||
with batch_role_ancestor_rebuilding():
|
||||
|
||||
@ -76,12 +76,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='inventorysource',
|
||||
name='source',
|
||||
field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script Locally or in Project'), (b'rax', 'Rackspace Cloud Servers'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure Classic (deprecated)'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'custom', 'Custom Script')]),
|
||||
field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script'), (b'scm', 'Sourced from a project in Tower'), (b'rax', 'Rackspace Cloud Servers'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure Classic (deprecated)'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'custom', 'Custom Script')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryupdate',
|
||||
name='source',
|
||||
field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script Locally or in Project'), (b'rax', 'Rackspace Cloud Servers'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure Classic (deprecated)'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'custom', 'Custom Script')]),
|
||||
field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script'), (b'scm', 'Sourced from a project in Tower'), (b'rax', 'Rackspace Cloud Servers'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure Classic (deprecated)'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'custom', 'Custom Script')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventorysource',
|
||||
|
||||
@ -720,7 +720,8 @@ class InventorySourceOptions(BaseModel):
|
||||
|
||||
SOURCE_CHOICES = [
|
||||
('', _('Manual')),
|
||||
('file', _('File, Directory or Script Locally or in Project')),
|
||||
('file', _('File, Directory or Script')),
|
||||
('scm', _('Sourced from a project in Tower')),
|
||||
('rax', _('Rackspace Cloud Servers')),
|
||||
('ec2', _('Amazon EC2')),
|
||||
('gce', _('Google Compute Engine')),
|
||||
@ -991,7 +992,7 @@ class InventorySourceOptions(BaseModel):
|
||||
if not self.source:
|
||||
return None
|
||||
cred = self.credential
|
||||
if cred and self.source != 'custom':
|
||||
if cred and self.source not in ('custom', 'scm'):
|
||||
# If a credential was provided, it's important that it matches
|
||||
# the actual inventory source being used (Amazon requires Amazon
|
||||
# credentials; Rackspace requires Rackspace credentials; etc...)
|
||||
@ -1135,7 +1136,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
|
||||
# if it hasn't been specified, then we're just doing a normal save.
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
is_new_instance = not bool(self.pk)
|
||||
is_scm_type = self.scm_project_id is not None
|
||||
is_scm_type = self.scm_project_id is not None and self.source == 'scm'
|
||||
|
||||
# Set name automatically. Include PK (or placeholder) to make sure the names are always unique.
|
||||
replace_text = '__replace_%s__' % now()
|
||||
@ -1336,6 +1337,8 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin):
|
||||
if (self.source not in ('custom', 'ec2') and
|
||||
not (self.credential)):
|
||||
return False
|
||||
elif self.source in ('file', 'scm'):
|
||||
return False
|
||||
return True
|
||||
|
||||
'''
|
||||
|
||||
@ -1689,13 +1689,17 @@ class RunInventoryUpdate(BaseTask):
|
||||
env['FOREMAN_INI_PATH'] = cloud_credential
|
||||
elif inventory_update.source == 'cloudforms':
|
||||
env['CLOUDFORMS_INI_PATH'] = cloud_credential
|
||||
elif inventory_update.source == 'file':
|
||||
elif inventory_update.source == 'scm':
|
||||
# Parse source_vars to dict, update env.
|
||||
env.update(parse_yaml_or_json(inventory_update.source_vars))
|
||||
elif inventory_update.source == 'custom':
|
||||
for env_k in inventory_update.source_vars_dict:
|
||||
if str(env_k) not in env and str(env_k) not in settings.INV_ENV_VARIABLE_BLACKLIST:
|
||||
env[str(env_k)] = unicode(inventory_update.source_vars_dict[env_k])
|
||||
elif inventory_update.source == 'file':
|
||||
raise NotImplementedError('Can not update file sources through the task system.')
|
||||
# add private_data_files
|
||||
env['AWX_PRIVATE_DATA_DIR'] = kwargs.get('private_data_dir', '')
|
||||
return env
|
||||
|
||||
def build_args(self, inventory_update, **kwargs):
|
||||
@ -1759,18 +1763,8 @@ class RunInventoryUpdate(BaseTask):
|
||||
getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper()),
|
||||
])
|
||||
|
||||
elif inventory_update.source == 'file':
|
||||
elif inventory_update.source == 'scm':
|
||||
args.append(inventory_update.get_actual_source_path())
|
||||
if hasattr(settings, 'ANSIBLE_INVENTORY_MODULE'):
|
||||
module_name = settings.ANSIBLE_INVENTORY_MODULE
|
||||
else:
|
||||
module_name = 'backport'
|
||||
v = get_ansible_version()
|
||||
if Version(v) > Version('2.4'):
|
||||
module_name = 'modern'
|
||||
elif Version(v) < Version('2.2'):
|
||||
module_name = 'legacy'
|
||||
args.extend(['--method', module_name])
|
||||
elif inventory_update.source == 'custom':
|
||||
runpath = tempfile.mkdtemp(prefix='ansible_tower_launch_')
|
||||
handle, path = tempfile.mkstemp(dir=runpath)
|
||||
|
||||
@ -117,7 +117,7 @@ class TestInvalidOptionsFunctional:
|
||||
@mock.patch.object(inventory_import.Command, 'set_logging_level', mock_logging)
|
||||
class TestINIImports:
|
||||
|
||||
@mock.patch.object(inventory_import.BaseLoader, 'load', mock.MagicMock(return_value=TEST_MEM_OBJECTS))
|
||||
@mock.patch.object(inventory_import.AnsibleInventoryLoader, 'load', mock.MagicMock(return_value=TEST_MEM_OBJECTS))
|
||||
def test_inventory_single_ini_import(self, inventory, capsys):
|
||||
cmd = inventory_import.Command()
|
||||
r = cmd.handle_noargs(
|
||||
|
||||
@ -12,7 +12,8 @@ class TestSCMUpdateFeatures:
|
||||
inv_src = InventorySource(
|
||||
scm_project=project,
|
||||
source_path='inventory_file',
|
||||
inventory=inventory)
|
||||
inventory=inventory,
|
||||
source='scm')
|
||||
with mock.patch.object(inv_src.scm_project, 'update') as mck_update:
|
||||
inv_src.save()
|
||||
mck_update.assert_called_once_with()
|
||||
|
||||
@ -11,6 +11,12 @@ Fields that should be specified on creation of SCM inventory source:
|
||||
- `source_path` - relative path inside of the project indicating a
|
||||
directory or a file, if left blank, "" is still a relative path
|
||||
indicating the root directory of the project
|
||||
- the `source` field should be set to "scm"
|
||||
|
||||
Additionally:
|
||||
|
||||
- `source_vars` - if these are set on a "file" type inventory source
|
||||
then they will be passed to the environment vars when running
|
||||
|
||||
A user should not be able to update this inventory source via through
|
||||
the endpoint `/inventory_sources/N/update/`. Instead, they should update
|
||||
@ -40,18 +46,26 @@ update the project.
|
||||
|
||||
> Any Inventory Ansible supports should be supported by this feature
|
||||
|
||||
This statement is the overall goal and should hold true absolutely for
|
||||
Ansible version 2.4 and beyond due to the use of `ansible-inventory`.
|
||||
Versions of Ansible before that may not support all valid inventory syntax
|
||||
because the internal mechanism is different.
|
||||
This is accomplished by making use of the `ansible-inventory` command.
|
||||
the inventory import tower-manage command will check for the existnce
|
||||
of `ansible-inventory` and if it is not present, it will call a backported
|
||||
version of it. The backport is maintained as its own GPL3 licensed
|
||||
repository.
|
||||
|
||||
Documentation should reflect the limitations of inventory file syntax
|
||||
support in old Ansible versions.
|
||||
https://github.com/ansible/ansible-inventory-backport
|
||||
|
||||
# Acceptance Criteria Notes
|
||||
Because the internal mechanism is different, we need some coverage
|
||||
testing with Ansible versions pre-2.4 and after.
|
||||
|
||||
# Acceptance Criteria Use Cases
|
||||
|
||||
Some test scenarios to look at:
|
||||
- Obviously use a git repo with examples of host patterns, etc.
|
||||
- Test projects that use scripts
|
||||
- Test projects that have multiple inventory files in a directory,
|
||||
group_vars, host_vars, etc.
|
||||
- Test scripts in the project repo
|
||||
- Test scripts that use environment variables provided by a credential
|
||||
in Tower
|
||||
- Test multiple inventories that use the same project, pointing to different
|
||||
files / directories inside of the project
|
||||
- Feature works correctly even if project doesn't have any playbook files
|
||||
@ -61,3 +75,43 @@ Some test scenarios to look at:
|
||||
- If the project SCM update encounters errors, it should not run the
|
||||
inventory updates
|
||||
|
||||
# Notes for Official Documentation
|
||||
|
||||
The API guide should summarize what is in the use details.
|
||||
Once the UI implementation is done, the product docs should cover its
|
||||
standard use.
|
||||
|
||||
## Update-on-launch
|
||||
|
||||
This type of inventory source will not allow the `update_on_launch` field
|
||||
to be set to True. This is because of concerns related to the task
|
||||
manager job dependency tree.
|
||||
|
||||
We should document the alternatives for a user to accomplish the same thing
|
||||
through in a different way.
|
||||
|
||||
### Alternative 1: Use same project for playbook
|
||||
|
||||
You can make a job template that uses a project as well as an inventory
|
||||
that updates from that same project. In this case, you can set the project
|
||||
to `update_on_launch`, in which case it will trigger an inventory update
|
||||
if needed.
|
||||
|
||||
### Alternative 2: Use the project in a workflow
|
||||
|
||||
If you must use a different project for the playbook than for the inventory
|
||||
source, then you can still place the project in a workflow and then have
|
||||
a job template run on success of the project update.
|
||||
|
||||
This is guaranteed to have the inventory update "on time" (by this we mean
|
||||
that the inventory changes are complete before the job template is launched),
|
||||
because the project does not transition to the completed state
|
||||
until the inventory update is finished.
|
||||
|
||||
Note that a failed inventory update does not mark the project as failed.
|
||||
|
||||
## Lazy inventory updates
|
||||
|
||||
It should also be noted that not every project update will trigger a
|
||||
corresponding inventory update. If the project revision has not changed
|
||||
and the inventory has not been edited, the inventory update will not fire.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user