executor: run trusted playbook in a bubblewrap

This change renames untrusted_wrapper to execution_wrapper and uses
bubblewrap for both trusted and untrusted playbooks by default.

This change adds new options to the zuul.conf executor section to let
operators define what directories to mount ro or rw for both context:
* trusted_ro_dirs/trusted_rw_dirs, and
* untrusted_ro_dirs/untrusted_rw_dirs

Change-Id: I9a8a74a338a8a837913db5e2effeef1bd949a49c
Story: 2001070
Task: 4687
This commit is contained in:
Tristan Cacqueray 2017-06-15 06:00:12 +00:00
parent 2438860823
commit 44aef15d6e
7 changed files with 45 additions and 15 deletions

View File

@ -26,6 +26,8 @@ zuul_url=http://zuul.example.com/p
[executor]
default_username=zuul
trusted_ro_dirs=/opt/zuul-scripts:/var/cache
trusted_rw_dirs=/opt/zuul-logs
[webapp]
listen_address=0.0.0.0

View File

@ -2024,6 +2024,8 @@ class ZuulTestCase(BaseTestCase):
project = reponame.replace('_', '/')
self.copyDirToRepo(project,
os.path.join(git_path, reponame))
# Make test_root persist after ansible run for .flag test
self.config.set('executor', 'trusted_rw_dirs', self.test_root)
self.setupAllProjectKeys()
def setupSimpleLayout(self):

View File

@ -31,17 +31,14 @@ class TestBubblewrap(testtools.TestCase):
def test_bubblewrap_wraps(self):
bwrap = bubblewrap.BubblewrapDriver()
work_dir = tempfile.mkdtemp()
ansible_dir = tempfile.mkdtemp()
ssh_agent = SshAgent()
self.addCleanup(ssh_agent.stop)
ssh_agent.start()
po = bwrap.getPopen(work_dir=work_dir,
ansible_dir=ansible_dir,
ssh_auth_sock=ssh_agent.env['SSH_AUTH_SOCK'])
self.assertTrue(po.passwd_r > 2)
self.assertTrue(po.group_r > 2)
self.assertTrue(work_dir in po.command)
self.assertTrue(ansible_dir in po.command)
# Now run /usr/bin/id to verify passwd/group entries made it in
true_proc = po(['/usr/bin/id'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

View File

@ -278,3 +278,13 @@ class WrapperInterface(object):
:rtype: Callable
"""
pass
@abc.abstractmethod
def setMountsMap(self, state_dir, ro_dirs=[], rw_dirs=[]):
"""Add additional mount point to the execution environment.
:arg str state_dir: the state directory to be read write
:arg list ro_dirs: read only directories paths
:arg list rw_dirs: read write directories paths
"""
pass

View File

@ -84,7 +84,6 @@ class BubblewrapDriver(Driver, WrapperInterface):
'--ro-bind', '/bin', '/bin',
'--ro-bind', '/sbin', '/sbin',
'--ro-bind', '/etc/resolv.conf', '/etc/resolv.conf',
'--ro-bind', '{ansible_dir}', '{ansible_dir}',
'--ro-bind', '{ssh_auth_sock}', '{ssh_auth_sock}',
'--dir', '{work_dir}',
'--bind', '{work_dir}', '{work_dir}',
@ -99,6 +98,7 @@ class BubblewrapDriver(Driver, WrapperInterface):
'--file', '{uid_fd}', '/etc/passwd',
'--file', '{gid_fd}', '/etc/group',
]
mounts_map = {'rw': [], 'ro': []}
def reconfigure(self, tenant):
pass
@ -106,6 +106,9 @@ class BubblewrapDriver(Driver, WrapperInterface):
def stop(self):
pass
def setMountsMap(self, state_dir, ro_dirs=[], rw_dirs=[]):
self.mounts_map = {'ro': ro_dirs, 'rw': [state_dir] + rw_dirs}
def getPopen(self, **kwargs):
# Set zuul_dir if it was not passed in
if 'zuul_dir' in kwargs:
@ -119,6 +122,11 @@ class BubblewrapDriver(Driver, WrapperInterface):
if not zuul_dir.startswith('/usr'):
bwrap_command.extend(['--ro-bind', zuul_dir, zuul_dir])
for mount_type in ('ro', 'rw'):
bind_arg = '--ro-bind' if mount_type == 'ro' else '--bind'
for bind in self.mounts_map[mount_type]:
bwrap_command.extend([bind_arg, bind, bind])
# Need users and groups
uid = os.getuid()
passwd = pwd.getpwuid(uid)
@ -159,14 +167,12 @@ def main(args=None):
parser = argparse.ArgumentParser()
parser.add_argument('work_dir')
parser.add_argument('ansible_dir')
parser.add_argument('run_args', nargs='+')
cli_args = parser.parse_args()
ssh_auth_sock = os.environ.get('SSH_AUTH_SOCK')
popen = driver.getPopen(work_dir=cli_args.work_dir,
ansible_dir=cli_args.ansible_dir,
ssh_auth_sock=ssh_auth_sock)
x = popen(cli_args.run_args)
x.wait()

View File

@ -26,3 +26,6 @@ class NullwrapDriver(Driver, WrapperInterface):
def getPopen(self, **kwargs):
return subprocess.Popen
def setMountsMap(self, **kwargs):
pass

View File

@ -382,9 +382,9 @@ class ExecutorServer(object):
'default_username', 'zuul')
self.merge_email = get_default(self.config, 'merger', 'git_user_email')
self.merge_name = get_default(self.config, 'merger', 'git_user_name')
untrusted_wrapper_name = get_default(self.config, 'executor',
'untrusted_wrapper', 'bubblewrap')
self.untrusted_wrapper = connections.drivers[untrusted_wrapper_name]
execution_wrapper_name = get_default(self.config, 'executor',
'execution_wrapper', 'bubblewrap')
self.execution_wrapper = connections.drivers[execution_wrapper_name]
self.connections = connections
# This merger and its git repos are used to maintain
@ -1238,14 +1238,24 @@ class AnsibleJob(object):
if trusted:
config_file = self.jobdir.trusted_config
popen = subprocess.Popen
opt_prefix = 'trusted'
else:
config_file = self.jobdir.untrusted_config
driver = self.executor_server.untrusted_wrapper
popen = driver.getPopen(
work_dir=self.jobdir.root,
ansible_dir=self.executor_server.ansible_dir,
ssh_auth_sock=env_copy.get('SSH_AUTH_SOCK'))
opt_prefix = 'untrusted'
ro_dirs = get_default(self.executor_server.config, 'executor',
'%s_ro_dirs' % opt_prefix)
rw_dirs = get_default(self.executor_server.config, 'executor',
'%s_rw_dirs' % opt_prefix)
state_dir = get_default(self.executor_server.config, 'zuul',
'state_dir', '/var/lib/zuul', expand_user=True)
ro_dirs = ro_dirs.split(":") if ro_dirs else []
rw_dirs = rw_dirs.split(":") if rw_dirs else []
self.executor_server.execution_wrapper.setMountsMap(state_dir, ro_dirs,
rw_dirs)
popen = self.executor_server.execution_wrapper.getPopen(
work_dir=self.jobdir.root,
ssh_auth_sock=env_copy.get('SSH_AUTH_SOCK'))
env_copy['ANSIBLE_CONFIG'] = config_file