mirror of
https://github.com/ansible/awx.git
synced 2026-01-22 23:18:03 -03:30
Support running custom inventory scripts (only) from within proot if enabled
This commit is contained in:
parent
d93870d828
commit
ec887f877f
@ -27,7 +27,7 @@ from django.contrib.auth.models import User
|
||||
|
||||
# AWX
|
||||
from awx.main.models import *
|
||||
from awx.main.utils import ignore_inventory_computed_fields
|
||||
from awx.main.utils import ignore_inventory_computed_fields, check_proot_installed, build_proot_temp_dir, wrap_args_with_proot
|
||||
from awx.main.signals import disable_activity_stream
|
||||
from awx.main.task_engine import TaskSerializer as LicenseReader
|
||||
|
||||
@ -165,13 +165,14 @@ class BaseLoader(object):
|
||||
Common functions for an inventory loader from a given source.
|
||||
'''
|
||||
|
||||
def __init__(self, source, all_group=None, group_filter_re=None, host_filter_re=None):
|
||||
def __init__(self, source, all_group=None, group_filter_re=None, host_filter_re=None, is_custom=False):
|
||||
self.source = source
|
||||
self.source_dir = os.path.dirname(self.source)
|
||||
self.all_group = all_group or MemGroup('all', self.source_dir)
|
||||
self.group_filter_re = group_filter_re
|
||||
self.host_filter_re = host_filter_re
|
||||
self.ipv6_port_re = re.compile(r'^\[([A-Fa-f0-9:]{3,})\]:(\d+?)$')
|
||||
self.is_custom = is_custom
|
||||
|
||||
def get_host(self, name):
|
||||
'''
|
||||
@ -268,7 +269,7 @@ class IniLoader(BaseLoader):
|
||||
'''
|
||||
Loader to read inventory from an INI-formatted text file.
|
||||
'''
|
||||
|
||||
|
||||
def load(self):
|
||||
logger.info('Reading INI source: %s', self.source)
|
||||
group = self.all_group
|
||||
@ -345,6 +346,11 @@ class ExecutableJsonLoader(BaseLoader):
|
||||
data = {}
|
||||
stdout, stderr = '', ''
|
||||
try:
|
||||
if self.is_custom 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.source_dir} # TODO: Remove proot dir
|
||||
cmd = wrap_args_with_proot(cmd, self.source_dir, **kwargs)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
@ -444,7 +450,7 @@ class ExecutableJsonLoader(BaseLoader):
|
||||
|
||||
|
||||
def load_inventory_source(source, all_group=None, group_filter_re=None,
|
||||
host_filter_re=None, exclude_empty_groups=False):
|
||||
host_filter_re=None, exclude_empty_groups=False, is_custom=False):
|
||||
'''
|
||||
Load inventory from given source directory or file.
|
||||
'''
|
||||
@ -470,7 +476,7 @@ def load_inventory_source(source, all_group=None, group_filter_re=None,
|
||||
else:
|
||||
all_group = all_group or MemGroup('all', os.path.dirname(source))
|
||||
if os.access(source, os.X_OK):
|
||||
ExecutableJsonLoader(source, all_group, group_filter_re, host_filter_re).load()
|
||||
ExecutableJsonLoader(source, all_group, group_filter_re, host_filter_re, is_custom).load()
|
||||
else:
|
||||
IniLoader(source, all_group, group_filter_re, host_filter_re).load()
|
||||
|
||||
@ -513,6 +519,9 @@ class Command(NoArgsCommand):
|
||||
make_option('--keep-vars', dest='keep_vars', action='store_true',
|
||||
metavar="k", default=False,
|
||||
help='use database variables if set'),
|
||||
make_option('--custom', dest='custom', action='store_true',
|
||||
metavar="c", default=False,
|
||||
help='this is a custom inventory script'),
|
||||
make_option('--source', dest='source', type='str', default=None,
|
||||
metavar='s', help='inventory directory, file, or script '
|
||||
'to load'),
|
||||
@ -1148,6 +1157,7 @@ class Command(NoArgsCommand):
|
||||
self.overwrite = bool(options.get('overwrite', False))
|
||||
self.overwrite_vars = bool(options.get('overwrite_vars', False))
|
||||
self.keep_vars = bool(options.get('keep_vars', False))
|
||||
self.is_custom = bool(options.get('custom', False))
|
||||
self.source = options.get('source', None)
|
||||
self.enabled_var = options.get('enabled_var', None)
|
||||
self.enabled_value = options.get('enabled_value', None)
|
||||
@ -1195,7 +1205,8 @@ class Command(NoArgsCommand):
|
||||
self.all_group = load_inventory_source(self.source, None,
|
||||
self.group_filter_re,
|
||||
self.host_filter_re,
|
||||
self.exclude_empty_groups)
|
||||
self.exclude_empty_groups,
|
||||
self.is_custom)
|
||||
self.all_group.debug_tree()
|
||||
|
||||
# Ensure that this is managed as an atomic SQL transaction,
|
||||
|
||||
@ -42,7 +42,8 @@ from awx.main.models import * # Job, JobEvent, ProjectUpdate, InventoryUpdate,
|
||||
# Schedule, UnifiedJobTemplate
|
||||
from awx.main.queue import FifoQueue
|
||||
from awx.main.utils import (get_ansible_version, decrypt_field, update_scm_url,
|
||||
ignore_inventory_computed_fields, emit_websocket_notification)
|
||||
ignore_inventory_computed_fields, emit_websocket_notification,
|
||||
check_proot_installed, build_proot_temp_dir, wrap_args_with_proot)
|
||||
|
||||
__all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
|
||||
'handle_work_error', 'update_inventory_computed_fields']
|
||||
@ -315,62 +316,6 @@ class BaseTask(Task):
|
||||
'''
|
||||
return False
|
||||
|
||||
def check_proot_installed(self):
|
||||
'''
|
||||
Check that proot is installed.
|
||||
'''
|
||||
cmd = [getattr(settings, 'AWX_PROOT_CMD', 'proot'), '--version']
|
||||
try:
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
result = proc.communicate()
|
||||
return bool(proc.returncode == 0)
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
|
||||
def build_proot_temp_dir(self, instance, **kwargs):
|
||||
'''
|
||||
Create a temporary directory for proot to use.
|
||||
'''
|
||||
path = tempfile.mkdtemp(prefix='ansible_tower_proot_')
|
||||
os.chmod(path, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
|
||||
return path
|
||||
|
||||
def wrap_args_with_proot(self, args, cwd, **kwargs):
|
||||
'''
|
||||
Wrap existing command line with proot to restrict access to:
|
||||
- /etc/tower (to prevent obtaining db info or secret key)
|
||||
- /var/lib/awx (except for current project)
|
||||
- /var/log/tower
|
||||
- /var/log/supervisor
|
||||
- /tmp (except for own tmp files)
|
||||
'''
|
||||
new_args = [getattr(settings, 'AWX_PROOT_CMD', 'proot'), '-r', '/']
|
||||
hide_paths = ['/etc/tower', '/var/lib/awx', '/var/log',
|
||||
tempfile.gettempdir(), settings.PROJECTS_ROOT,
|
||||
settings.JOBOUTPUT_ROOT]
|
||||
hide_paths.extend(getattr(settings, 'AWX_PROOT_HIDE_PATHS', None) or [])
|
||||
for path in sorted(set(hide_paths)):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
if os.path.isdir(path):
|
||||
new_path = tempfile.mkdtemp(dir=kwargs['proot_temp_dir'])
|
||||
os.chmod(new_path, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
|
||||
else:
|
||||
handle, new_path = tempfile.mkstemp(dir=kwargs['proot_temp_dir'])
|
||||
os.close(handle)
|
||||
os.chmod(new_path, stat.S_IRUSR|stat.S_IWUSR)
|
||||
new_args.extend(['-b', '%s:%s' % (new_path, path)])
|
||||
show_paths = [cwd, kwargs['private_data_dir']]
|
||||
show_paths.extend(getattr(settings, 'AWX_PROOT_SHOW_PATHS', None) or [])
|
||||
for path in sorted(set(show_paths)):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
new_args.extend(['-b', '%s:%s' % (path, path)])
|
||||
new_args.extend(['-w', cwd])
|
||||
new_args.extend(args)
|
||||
return new_args
|
||||
|
||||
def build_args(self, instance, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -483,11 +428,11 @@ class BaseTask(Task):
|
||||
stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, str(uuid.uuid1()) + ".out")
|
||||
stdout_handle = codecs.open(stdout_filename, 'w', encoding='utf-8')
|
||||
if self.should_use_proot(instance, **kwargs):
|
||||
if not self.check_proot_installed():
|
||||
if not check_proot_installed():
|
||||
raise RuntimeError('proot is not installed')
|
||||
kwargs['proot_temp_dir'] = self.build_proot_temp_dir(instance, **kwargs)
|
||||
args = self.wrap_args_with_proot(args, cwd, **kwargs)
|
||||
safe_args = self.wrap_args_with_proot(safe_args, cwd, **kwargs)
|
||||
kwargs['proot_temp_dir'] = build_proot_temp_dir()
|
||||
args = wrap_args_with_proot(args, cwd, **kwargs)
|
||||
safe_args = wrap_args_with_proot(safe_args, cwd, **kwargs)
|
||||
instance = self.update_model(pk, job_args=json.dumps(safe_args),
|
||||
job_cwd=cwd, job_env=safe_env, result_stdout_file=stdout_filename)
|
||||
status = self.run_pexpect(instance, args, cwd, env, kwargs['passwords'], stdout_handle)
|
||||
@ -1140,6 +1085,7 @@ class RunInventoryUpdate(BaseTask):
|
||||
f.close()
|
||||
os.chmod(path, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
|
||||
args.append(runpath)
|
||||
args.append("--custom")
|
||||
# try:
|
||||
# shutil.rmtree(runpath, True)
|
||||
# except OSError:
|
||||
|
||||
@ -5,12 +5,15 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import stat
|
||||
import sys
|
||||
import urlparse
|
||||
import threading
|
||||
import contextlib
|
||||
import tempfile
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
@ -18,6 +21,9 @@ from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
# PyCrypto
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
# Tower
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore',
|
||||
'get_ansible_version', 'get_awx_version', 'update_scm_url',
|
||||
@ -395,3 +401,62 @@ def ignore_inventory_group_removal():
|
||||
yield
|
||||
finally:
|
||||
_inventory_updates.is_removing = previous_value
|
||||
|
||||
def check_proot_installed():
|
||||
'''
|
||||
Check that proot is installed.
|
||||
'''
|
||||
cmd = [getattr(settings, 'AWX_PROOT_CMD', 'proot'), '--version']
|
||||
try:
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
result = proc.communicate()
|
||||
return bool(proc.returncode == 0)
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
|
||||
def build_proot_temp_dir():
|
||||
'''
|
||||
Create a temporary directory for proot to use.
|
||||
'''
|
||||
path = tempfile.mkdtemp(prefix='ansible_tower_proot_')
|
||||
os.chmod(path, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
|
||||
return path
|
||||
|
||||
def wrap_args_with_proot(args, cwd, **kwargs):
|
||||
'''
|
||||
Wrap existing command line with proot to restrict access to:
|
||||
- /etc/tower (to prevent obtaining db info or secret key)
|
||||
- /var/lib/awx (except for current project)
|
||||
- /var/log/tower
|
||||
- /var/log/supervisor
|
||||
- /tmp (except for own tmp files)
|
||||
'''
|
||||
new_args = [getattr(settings, 'AWX_PROOT_CMD', 'proot'), '-r', '/']
|
||||
hide_paths = ['/etc/tower', '/var/lib/awx', '/var/log',
|
||||
tempfile.gettempdir(), settings.PROJECTS_ROOT,
|
||||
settings.JOBOUTPUT_ROOT]
|
||||
hide_paths.extend(getattr(settings, 'AWX_PROOT_HIDE_PATHS', None) or [])
|
||||
for path in sorted(set(hide_paths)):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
if os.path.isdir(path):
|
||||
new_path = tempfile.mkdtemp(dir=kwargs['proot_temp_dir'])
|
||||
os.chmod(new_path, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
|
||||
else:
|
||||
handle, new_path = tempfile.mkstemp(dir=kwargs['proot_temp_dir'])
|
||||
os.close(handle)
|
||||
os.chmod(new_path, stat.S_IRUSR|stat.S_IWUSR)
|
||||
new_args.extend(['-b', '%s:%s' % (new_path, path)])
|
||||
if 'private_data_dir' in kwargs:
|
||||
show_paths = [cwd, kwargs['private_data_dir']]
|
||||
else:
|
||||
show_paths = [cwd]
|
||||
show_paths.extend(getattr(settings, 'AWX_PROOT_SHOW_PATHS', None) or [])
|
||||
for path in sorted(set(show_paths)):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
new_args.extend(['-b', '%s:%s' % (path, path)])
|
||||
new_args.extend(['-w', cwd])
|
||||
new_args.extend(args)
|
||||
return new_args
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user