From ec887f877f7cf1143d9a9de168f6313b724a11e9 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 5 Dec 2014 15:24:42 -0500 Subject: [PATCH] Support running custom inventory scripts (only) from within proot if enabled --- .../management/commands/inventory_import.py | 23 +++++-- awx/main/tasks.py | 68 ++----------------- awx/main/utils.py | 65 ++++++++++++++++++ 3 files changed, 89 insertions(+), 67 deletions(-) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 9adc43a08a..022d19a6fa 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -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, diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 9e07d94ba8..8ec280a35f 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -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: diff --git a/awx/main/utils.py b/awx/main/utils.py index cac1b30149..5782cfd4f1 100644 --- a/awx/main/utils.py +++ b/awx/main/utils.py @@ -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