Support running custom inventory scripts (only) from within proot if enabled

This commit is contained in:
Matthew Jones
2014-12-05 15:24:42 -05:00
parent d93870d828
commit ec887f877f
3 changed files with 89 additions and 67 deletions

View File

@@ -27,7 +27,7 @@ from django.contrib.auth.models import User
# AWX # AWX
from awx.main.models import * 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.signals import disable_activity_stream
from awx.main.task_engine import TaskSerializer as LicenseReader 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. 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 = source
self.source_dir = os.path.dirname(self.source) self.source_dir = os.path.dirname(self.source)
self.all_group = all_group or MemGroup('all', self.source_dir) self.all_group = all_group or MemGroup('all', self.source_dir)
self.group_filter_re = group_filter_re self.group_filter_re = group_filter_re
self.host_filter_re = host_filter_re self.host_filter_re = host_filter_re
self.ipv6_port_re = re.compile(r'^\[([A-Fa-f0-9:]{3,})\]:(\d+?)$') self.ipv6_port_re = re.compile(r'^\[([A-Fa-f0-9:]{3,})\]:(\d+?)$')
self.is_custom = is_custom
def get_host(self, name): def get_host(self, name):
''' '''
@@ -268,7 +269,7 @@ class IniLoader(BaseLoader):
''' '''
Loader to read inventory from an INI-formatted text file. Loader to read inventory from an INI-formatted text file.
''' '''
def load(self): def load(self):
logger.info('Reading INI source: %s', self.source) logger.info('Reading INI source: %s', self.source)
group = self.all_group group = self.all_group
@@ -345,6 +346,11 @@ class ExecutableJsonLoader(BaseLoader):
data = {} data = {}
stdout, stderr = '', '' stdout, stderr = '', ''
try: 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) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
if proc.returncode != 0: if proc.returncode != 0:
@@ -444,7 +450,7 @@ class ExecutableJsonLoader(BaseLoader):
def load_inventory_source(source, all_group=None, group_filter_re=None, 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. 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: else:
all_group = all_group or MemGroup('all', os.path.dirname(source)) all_group = all_group or MemGroup('all', os.path.dirname(source))
if os.access(source, os.X_OK): 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: else:
IniLoader(source, all_group, group_filter_re, host_filter_re).load() 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', make_option('--keep-vars', dest='keep_vars', action='store_true',
metavar="k", default=False, metavar="k", default=False,
help='use database variables if set'), 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, make_option('--source', dest='source', type='str', default=None,
metavar='s', help='inventory directory, file, or script ' metavar='s', help='inventory directory, file, or script '
'to load'), 'to load'),
@@ -1148,6 +1157,7 @@ class Command(NoArgsCommand):
self.overwrite = bool(options.get('overwrite', False)) self.overwrite = bool(options.get('overwrite', False))
self.overwrite_vars = bool(options.get('overwrite_vars', False)) self.overwrite_vars = bool(options.get('overwrite_vars', False))
self.keep_vars = bool(options.get('keep_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.source = options.get('source', None)
self.enabled_var = options.get('enabled_var', None) self.enabled_var = options.get('enabled_var', None)
self.enabled_value = options.get('enabled_value', 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.all_group = load_inventory_source(self.source, None,
self.group_filter_re, self.group_filter_re,
self.host_filter_re, self.host_filter_re,
self.exclude_empty_groups) self.exclude_empty_groups,
self.is_custom)
self.all_group.debug_tree() self.all_group.debug_tree()
# Ensure that this is managed as an atomic SQL transaction, # Ensure that this is managed as an atomic SQL transaction,

View File

@@ -42,7 +42,8 @@ from awx.main.models import * # Job, JobEvent, ProjectUpdate, InventoryUpdate,
# Schedule, UnifiedJobTemplate # Schedule, UnifiedJobTemplate
from awx.main.queue import FifoQueue from awx.main.queue import FifoQueue
from awx.main.utils import (get_ansible_version, decrypt_field, update_scm_url, 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', __all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate',
'handle_work_error', 'update_inventory_computed_fields'] 'handle_work_error', 'update_inventory_computed_fields']
@@ -315,62 +316,6 @@ class BaseTask(Task):
''' '''
return False 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): def build_args(self, instance, **kwargs):
raise NotImplementedError raise NotImplementedError
@@ -483,11 +428,11 @@ class BaseTask(Task):
stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, str(uuid.uuid1()) + ".out") stdout_filename = os.path.join(settings.JOBOUTPUT_ROOT, str(uuid.uuid1()) + ".out")
stdout_handle = codecs.open(stdout_filename, 'w', encoding='utf-8') stdout_handle = codecs.open(stdout_filename, 'w', encoding='utf-8')
if self.should_use_proot(instance, **kwargs): if self.should_use_proot(instance, **kwargs):
if not self.check_proot_installed(): if not check_proot_installed():
raise RuntimeError('proot is not installed') raise RuntimeError('proot is not installed')
kwargs['proot_temp_dir'] = self.build_proot_temp_dir(instance, **kwargs) kwargs['proot_temp_dir'] = build_proot_temp_dir()
args = self.wrap_args_with_proot(args, cwd, **kwargs) args = wrap_args_with_proot(args, cwd, **kwargs)
safe_args = self.wrap_args_with_proot(safe_args, cwd, **kwargs) safe_args = wrap_args_with_proot(safe_args, cwd, **kwargs)
instance = self.update_model(pk, job_args=json.dumps(safe_args), instance = self.update_model(pk, job_args=json.dumps(safe_args),
job_cwd=cwd, job_env=safe_env, result_stdout_file=stdout_filename) job_cwd=cwd, job_env=safe_env, result_stdout_file=stdout_filename)
status = self.run_pexpect(instance, args, cwd, env, kwargs['passwords'], stdout_handle) status = self.run_pexpect(instance, args, cwd, env, kwargs['passwords'], stdout_handle)
@@ -1140,6 +1085,7 @@ class RunInventoryUpdate(BaseTask):
f.close() f.close()
os.chmod(path, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) os.chmod(path, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
args.append(runpath) args.append(runpath)
args.append("--custom")
# try: # try:
# shutil.rmtree(runpath, True) # shutil.rmtree(runpath, True)
# except OSError: # except OSError:

View File

@@ -5,12 +5,15 @@
import base64 import base64
import hashlib import hashlib
import logging import logging
import os
import re import re
import subprocess import subprocess
import stat
import sys import sys
import urlparse import urlparse
import threading import threading
import contextlib import contextlib
import tempfile
# Django REST Framework # Django REST Framework
from rest_framework.exceptions import ParseError, PermissionDenied from rest_framework.exceptions import ParseError, PermissionDenied
@@ -18,6 +21,9 @@ from rest_framework.exceptions import ParseError, PermissionDenied
# PyCrypto # PyCrypto
from Crypto.Cipher import AES from Crypto.Cipher import AES
# Tower
from django.conf import settings
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore',
'get_ansible_version', 'get_awx_version', 'update_scm_url', 'get_ansible_version', 'get_awx_version', 'update_scm_url',
@@ -395,3 +401,62 @@ def ignore_inventory_group_removal():
yield yield
finally: finally:
_inventory_updates.is_removing = previous_value _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