mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 09:57:35 -02:30
Implements https://trello.com/c/1NJKBOex - Add support for using proot to run jobs in isolated environment.
This commit is contained in:
@@ -14,6 +14,7 @@ import pipes
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -304,6 +305,67 @@ class BaseTask(Task):
|
|||||||
args = ['ssh-agent', 'sh', '-c', cmd]
|
args = ['ssh-agent', 'sh', '-c', cmd]
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
def should_use_proot(self, instance, **kwargs):
|
||||||
|
'''
|
||||||
|
Return whether this task should use proot.
|
||||||
|
'''
|
||||||
|
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
|
||||||
|
- /tmp (except for own tmp files)
|
||||||
|
'''
|
||||||
|
new_args = [getattr(settings, 'AWX_PROOT_CMD', 'proot'), '-r', '/']
|
||||||
|
hide_paths = ['/etc/tower', '/var/lib/awx', '/var/log/tower',
|
||||||
|
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
|
||||||
|
|
||||||
@@ -415,6 +477,12 @@ class BaseTask(Task):
|
|||||||
os.makedirs(settings.JOBOUTPUT_ROOT)
|
os.makedirs(settings.JOBOUTPUT_ROOT)
|
||||||
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 not self.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)
|
||||||
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)
|
||||||
@@ -427,11 +495,16 @@ class BaseTask(Task):
|
|||||||
shutil.rmtree(kwargs['private_data_dir'], True)
|
shutil.rmtree(kwargs['private_data_dir'], True)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
if kwargs.get('proot_temp_dir', ''):
|
||||||
try:
|
try:
|
||||||
stdout_handle.flush()
|
shutil.rmtree(kwargs['proot_temp_dir'], True)
|
||||||
stdout_handle.close()
|
except OSError:
|
||||||
except Exception:
|
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
stdout_handle.flush()
|
||||||
|
stdout_handle.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
instance = self.update_model(pk, status=status, result_traceback=tb,
|
instance = self.update_model(pk, status=status, result_traceback=tb,
|
||||||
output_replacements=output_replacements)
|
output_replacements=output_replacements)
|
||||||
self.post_run_hook(instance, **kwargs)
|
self.post_run_hook(instance, **kwargs)
|
||||||
@@ -658,6 +731,12 @@ class RunJob(BaseTask):
|
|||||||
d[re.compile(r'^Vault password:\s*?$', re.M)] = 'vault_password'
|
d[re.compile(r'^Vault password:\s*?$', re.M)] = 'vault_password'
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def should_use_proot(self, instance, **kwargs):
|
||||||
|
'''
|
||||||
|
Return whether this task should use proot.
|
||||||
|
'''
|
||||||
|
return getattr(settings, 'AWX_PROOT_ENABLED', False)
|
||||||
|
|
||||||
def post_run_hook(self, job, **kwargs):
|
def post_run_hook(self, job, **kwargs):
|
||||||
'''
|
'''
|
||||||
Hook for actions to run after job/task has completed.
|
Hook for actions to run after job/task has completed.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
# Python
|
# Python
|
||||||
from distutils.version import StrictVersion as Version
|
from distutils.version import StrictVersion as Version
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -139,6 +140,51 @@ TEST_ASYNC_NOWAIT_PLAYBOOK = '''
|
|||||||
poll: 0
|
poll: 0
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
TEST_PROOT_PLAYBOOK = '''
|
||||||
|
- name: test proot environment
|
||||||
|
hosts: test-group
|
||||||
|
gather_facts: false
|
||||||
|
connection: local
|
||||||
|
tasks:
|
||||||
|
- name: list projects directory
|
||||||
|
command: ls -1 "{{ projects_root }}"
|
||||||
|
register: projects_ls
|
||||||
|
- name: check that only one project directory is visible
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "projects_ls.stdout_lines|length == 1"
|
||||||
|
- "projects_ls.stdout_lines[0] == '{{ project_path }}'"
|
||||||
|
- name: list job output directory
|
||||||
|
command: ls -1 "{{ joboutput_root }}"
|
||||||
|
register: joboutput_ls
|
||||||
|
- name: check that we see an empty job output directory
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "not joboutput_ls.stdout"
|
||||||
|
- name: check for other project path
|
||||||
|
stat: path={{ other_project_path }}
|
||||||
|
register: other_project_stat
|
||||||
|
- name: check that other project path was not found
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "not other_project_stat.stat.exists"
|
||||||
|
- name: check for temp path
|
||||||
|
stat: path={{ temp_path }}
|
||||||
|
register: temp_stat
|
||||||
|
- name: check that temp path was not found
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "not temp_stat.stat.exists"
|
||||||
|
- name: try to run a tower-manage command
|
||||||
|
command: tower-manage validate
|
||||||
|
ignore_errors: true
|
||||||
|
register: tower_manage_validate
|
||||||
|
- name: check that tower-manage command failed
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "tower_manage_validate|failed"
|
||||||
|
'''
|
||||||
|
|
||||||
TEST_PLAYBOOK_WITH_ROLES = '''
|
TEST_PLAYBOOK_WITH_ROLES = '''
|
||||||
- name: test with roles
|
- name: test with roles
|
||||||
hosts: test-group
|
hosts: test-group
|
||||||
@@ -1283,3 +1329,48 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
job = Job.objects.get(pk=job.pk)
|
job = Job.objects.get(pk=job.pk)
|
||||||
self.check_job_result(job, 'successful')
|
self.check_job_result(job, 'successful')
|
||||||
self.check_job_events(job, 'ok', 1, 3, has_roles=True)
|
self.check_job_events(job, 'ok', 1, 3, has_roles=True)
|
||||||
|
|
||||||
|
def test_run_job_with_proot(self):
|
||||||
|
# Enable proot for this test.
|
||||||
|
settings.AWX_PROOT_ENABLED = True
|
||||||
|
# Hide local settings path.
|
||||||
|
settings.AWX_PROOT_HIDE_PATHS = [os.path.join(settings.BASE_DIR, 'settings')]
|
||||||
|
# Create another project alongside the one we're using to verify it
|
||||||
|
# is hidden.
|
||||||
|
self.create_test_project(TEST_PLAYBOOK)
|
||||||
|
other_project_path = self.project.local_path
|
||||||
|
# Create a temp directory that should not be visible to the playbook.
|
||||||
|
temp_path = tempfile.mkdtemp()
|
||||||
|
self._temp_paths.append(temp_path)
|
||||||
|
# Create our test project and job template.
|
||||||
|
self.create_test_project(TEST_PROOT_PLAYBOOK)
|
||||||
|
project_path = self.project.local_path
|
||||||
|
job_template = self.create_test_job_template()
|
||||||
|
extra_vars = {
|
||||||
|
'projects_root': settings.PROJECTS_ROOT,
|
||||||
|
'joboutput_root': settings.JOBOUTPUT_ROOT,
|
||||||
|
'project_path': project_path,
|
||||||
|
'other_project_path': other_project_path,
|
||||||
|
'temp_path': temp_path,
|
||||||
|
}
|
||||||
|
job = self.create_test_job(job_template=job_template, verbosity=3,
|
||||||
|
extra_vars=json.dumps(extra_vars))
|
||||||
|
self.assertEqual(job.status, 'new')
|
||||||
|
self.assertFalse(job.passwords_needed_to_start)
|
||||||
|
self.assertTrue(job.signal_start())
|
||||||
|
job = Job.objects.get(pk=job.pk)
|
||||||
|
self.check_job_result(job, 'successful')
|
||||||
|
|
||||||
|
def test_run_job_with_proot_not_installed(self):
|
||||||
|
# Enable proot for this test, specify invalid proot cmd.
|
||||||
|
settings.AWX_PROOT_ENABLED = True
|
||||||
|
settings.AWX_PROOT_CMD = 'PR00T'
|
||||||
|
self.create_test_project(TEST_PLAYBOOK)
|
||||||
|
job_template = self.create_test_job_template()
|
||||||
|
job = self.create_test_job(job_template=job_template)
|
||||||
|
self.assertEqual(job.status, 'new')
|
||||||
|
self.assertFalse(job.passwords_needed_to_start)
|
||||||
|
self.assertTrue(job.signal_start())
|
||||||
|
job = Job.objects.get(pk=job.pk)
|
||||||
|
self.assertEqual(job.status, 'error')
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import glob
|
import glob
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import tempfile
|
||||||
|
|
||||||
# Update this module's local settings from the global settings module.
|
# Update this module's local settings from the global settings module.
|
||||||
from django.conf import global_settings
|
from django.conf import global_settings
|
||||||
@@ -313,6 +314,18 @@ AWX_TASK_ENV = {}
|
|||||||
# Flag to enable/disable updating hosts M2M when saving job events.
|
# Flag to enable/disable updating hosts M2M when saving job events.
|
||||||
CAPTURE_JOB_EVENT_HOSTS = False
|
CAPTURE_JOB_EVENT_HOSTS = False
|
||||||
|
|
||||||
|
# Enable proot support for running jobs (playbook runs only).
|
||||||
|
AWX_PROOT_ENABLED = False
|
||||||
|
|
||||||
|
# Command/path to proot.
|
||||||
|
AWX_PROOT_CMD = 'proot'
|
||||||
|
|
||||||
|
# Additional paths to hide from jobs using proot.
|
||||||
|
AWX_PROOT_HIDE_PATHS = []
|
||||||
|
|
||||||
|
# Additional paths to show for jobs using proot.
|
||||||
|
AWX_PROOT_SHOW_PATHS = []
|
||||||
|
|
||||||
# Not possible to get list of regions without authenticating, so use this list
|
# Not possible to get list of regions without authenticating, so use this list
|
||||||
# instead (based on docs from:
|
# instead (based on docs from:
|
||||||
# http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/Service_Access_Endpoints-d1e517.html)
|
# http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/Service_Access_Endpoints-d1e517.html)
|
||||||
|
|||||||
Reference in New Issue
Block a user