mirror of
https://github.com/ansible/awx.git
synced 2026-05-17 06:17:36 -02:30
Support running custom inventory scripts (only) from within proot if enabled
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user