Add support for bwrap
This will be the minimum "batteries included" bubblwrap driver. It does not do any MAC configuration, since these vary by system. Operators may wish to wrap it further in a MAC wrapper driver. Because we set bubblewrap as the default wrapper, test_playbooks tests it. However, it lacks a negative test, so we won't know if we're not actually containing things. Users who don't have bubblewrap or don't wish to use it can set the untrusted_wrapper to 'nullwrap' which will just execute things as they're done before this change. Change-Id: I84dd7c8cc55d2110b58609784007ffda0d135716 Story: 2000910 Task: 3540 Signed-off-by: Paul Belanger <pabelanger@redhat.com>
This commit is contained in:
parent
50c69d8957
commit
5870ccae62
|
@ -26,6 +26,7 @@ console_scripts =
|
|||
zuul = zuul.cmd.client:main
|
||||
zuul-cloner = zuul.cmd.cloner:main
|
||||
zuul-executor = zuul.cmd.executor:main
|
||||
zuul-bwrap = zuul.driver.bubblewrap:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import fixtures
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
import testtools
|
||||
|
||||
from zuul.driver import bubblewrap
|
||||
from zuul.executor.server import SshAgent
|
||||
|
||||
|
||||
class TestBubblewrap(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestBubblewrap, self).setUp()
|
||||
self.log_fixture = self.useFixture(
|
||||
fixtures.FakeLogger(level=logging.DEBUG))
|
||||
self.useFixture(fixtures.NestedTempfile())
|
||||
|
||||
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)
|
||||
(output, errs) = true_proc.communicate()
|
||||
# Make sure it printed things on stdout
|
||||
self.assertTrue(len(output.strip()))
|
||||
# And that it did not print things on stderr
|
||||
self.assertEqual(0, len(errs.strip()))
|
||||
# Make sure the _r's are closed
|
||||
self.assertIsNone(po.passwd_r)
|
||||
self.assertIsNone(po.group_r)
|
|
@ -254,3 +254,27 @@ class ReporterInterface(object):
|
|||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class WrapperInterface(object):
|
||||
"""The wrapper interface to be implmeneted by a driver.
|
||||
|
||||
A driver which wraps execution of commands executed by Zuul should
|
||||
implement this interface.
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def getPopen(self, **kwargs):
|
||||
"""Create and return a subprocess.Popen factory wrapped however the
|
||||
driver sees fit.
|
||||
|
||||
This method is required by the interface
|
||||
|
||||
:arg dict kwargs: key/values for use by driver as needed
|
||||
|
||||
:returns: a callable that takes the same args as subprocess.Popen
|
||||
:rtype: Callable
|
||||
"""
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2016 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import grp
|
||||
import logging
|
||||
import os
|
||||
import pwd
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from zuul.driver import (Driver, WrapperInterface)
|
||||
|
||||
|
||||
class WrappedPopen(object):
|
||||
def __init__(self, command, passwd_r, group_r):
|
||||
self.command = command
|
||||
self.passwd_r = passwd_r
|
||||
self.group_r = group_r
|
||||
|
||||
def __call__(self, args, *sub_args, **kwargs):
|
||||
try:
|
||||
args = self.command + args
|
||||
if kwargs.get('close_fds') or sys.version_info.major >= 3:
|
||||
# The default in py3 is close_fds=True, so we need to pass
|
||||
# our open fds in. However, this can only work right in
|
||||
# py3.2 or later due to the lack of 'pass_fds' in prior
|
||||
# versions. So until we are py3 only we can only bwrap
|
||||
# things that are close_fds=False
|
||||
pass_fds = list(kwargs.get('pass_fds', []))
|
||||
for fd in (self.passwd_r, self.group_r):
|
||||
if fd not in pass_fds:
|
||||
pass_fds.append(fd)
|
||||
kwargs['pass_fds'] = pass_fds
|
||||
proc = subprocess.Popen(args, *sub_args, **kwargs)
|
||||
finally:
|
||||
self.__del__()
|
||||
return proc
|
||||
|
||||
def __del__(self):
|
||||
if self.passwd_r:
|
||||
try:
|
||||
os.close(self.passwd_r)
|
||||
except OSError:
|
||||
pass
|
||||
self.passwd_r = None
|
||||
if self.group_r:
|
||||
try:
|
||||
os.close(self.group_r)
|
||||
except OSError:
|
||||
pass
|
||||
self.group_r = None
|
||||
|
||||
|
||||
class BubblewrapDriver(Driver, WrapperInterface):
|
||||
name = 'bubblewrap'
|
||||
log = logging.getLogger("zuul.BubblewrapDriver")
|
||||
|
||||
bwrap_command = [
|
||||
'bwrap',
|
||||
'--dir', '/tmp',
|
||||
'--tmpfs', '/tmp',
|
||||
'--dir', '/var',
|
||||
'--dir', '/var/tmp',
|
||||
'--dir', '/run/user/{uid}',
|
||||
'--ro-bind', '/usr', '/usr',
|
||||
'--ro-bind', '/lib', '/lib',
|
||||
'--ro-bind', '/lib64', '/lib64',
|
||||
'--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}',
|
||||
'--dev', '/dev',
|
||||
'--dir', '{user_home}',
|
||||
'--chdir', '/',
|
||||
'--unshare-all',
|
||||
'--share-net',
|
||||
'--uid', '{uid}',
|
||||
'--gid', '{gid}',
|
||||
'--file', '{uid_fd}', '/etc/passwd',
|
||||
'--file', '{gid_fd}', '/etc/group',
|
||||
]
|
||||
|
||||
def reconfigure(self, tenant):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def getPopen(self, **kwargs):
|
||||
# Set zuul_dir if it was not passed in
|
||||
if 'zuul_dir' in kwargs:
|
||||
zuul_dir = kwargs['zuul_dir']
|
||||
else:
|
||||
zuul_python_dir = os.path.dirname(sys.executable)
|
||||
# We want the dir directly above bin to get the whole venv
|
||||
zuul_dir = os.path.normpath(os.path.join(zuul_python_dir, '..'))
|
||||
|
||||
bwrap_command = list(self.bwrap_command)
|
||||
if not zuul_dir.startswith('/usr'):
|
||||
bwrap_command.extend(['--ro-bind', zuul_dir, zuul_dir])
|
||||
|
||||
# Need users and groups
|
||||
uid = os.getuid()
|
||||
passwd = pwd.getpwuid(uid)
|
||||
passwd_bytes = b':'.join(
|
||||
['{}'.format(x).encode('utf8') for x in passwd])
|
||||
(passwd_r, passwd_w) = os.pipe()
|
||||
os.write(passwd_w, passwd_bytes)
|
||||
os.close(passwd_w)
|
||||
|
||||
gid = os.getgid()
|
||||
group = grp.getgrgid(gid)
|
||||
group_bytes = b':'.join(
|
||||
['{}'.format(x).encode('utf8') for x in group])
|
||||
group_r, group_w = os.pipe()
|
||||
os.write(group_w, group_bytes)
|
||||
os.close(group_w)
|
||||
|
||||
kwargs = dict(kwargs) # Don't update passed in dict
|
||||
kwargs['uid'] = uid
|
||||
kwargs['gid'] = gid
|
||||
kwargs['uid_fd'] = passwd_r
|
||||
kwargs['gid_fd'] = group_r
|
||||
kwargs['user_home'] = passwd.pw_dir
|
||||
command = [x.format(**kwargs) for x in bwrap_command]
|
||||
|
||||
wrapped_popen = WrappedPopen(command, passwd_r, group_r)
|
||||
|
||||
return wrapped_popen
|
||||
|
||||
|
||||
def main(args=None):
|
||||
driver = BubblewrapDriver()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2016 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from zuul.driver import (Driver, WrapperInterface)
|
||||
|
||||
|
||||
class NullwrapDriver(Driver, WrapperInterface):
|
||||
name = 'nullwrap'
|
||||
log = logging.getLogger("zuul.NullwrapDriver")
|
||||
|
||||
def getPopen(self, **kwargs):
|
||||
return subprocess.Popen
|
|
@ -337,6 +337,13 @@ class ExecutorServer(object):
|
|||
else:
|
||||
self.merge_name = None
|
||||
|
||||
if self.config.has_option('executor', 'untrusted_wrapper'):
|
||||
untrusted_wrapper_name = self.config.get(
|
||||
'executor', 'untrusted_wrapper').split()
|
||||
else:
|
||||
untrusted_wrapper_name = 'bubblewrap'
|
||||
self.untrusted_wrapper = connections.drivers[untrusted_wrapper_name]
|
||||
|
||||
self.connections = connections
|
||||
# This merger and its git repos are used to maintain
|
||||
# up-to-date copies of all the repos that are used by jobs, as
|
||||
|
@ -353,6 +360,7 @@ class ExecutorServer(object):
|
|||
path = os.path.join(state_dir, 'executor.socket')
|
||||
self.command_socket = commandsocket.CommandSocket(path)
|
||||
ansible_dir = os.path.join(state_dir, 'ansible')
|
||||
self.ansible_dir = ansible_dir
|
||||
self.library_dir = os.path.join(ansible_dir, 'library')
|
||||
if not os.path.exists(self.library_dir):
|
||||
os.makedirs(self.library_dir)
|
||||
|
@ -1120,8 +1128,14 @@ class AnsibleJob(object):
|
|||
|
||||
if trusted:
|
||||
config_file = self.jobdir.trusted_config
|
||||
popen = subprocess.Popen
|
||||
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'))
|
||||
|
||||
env_copy['ANSIBLE_CONFIG'] = config_file
|
||||
|
||||
|
@ -1130,7 +1144,7 @@ class AnsibleJob(object):
|
|||
return (self.RESULT_ABORTED, None)
|
||||
self.log.debug("Ansible command: ANSIBLE_CONFIG=%s %s",
|
||||
config_file, " ".join(shlex_quote(c) for c in cmd))
|
||||
self.proc = subprocess.Popen(
|
||||
self.proc = popen(
|
||||
cmd,
|
||||
cwd=self.jobdir.work_root,
|
||||
stdout=subprocess.PIPE,
|
||||
|
|
|
@ -22,6 +22,8 @@ import zuul.driver.github
|
|||
import zuul.driver.smtp
|
||||
import zuul.driver.timer
|
||||
import zuul.driver.sql
|
||||
import zuul.driver.bubblewrap
|
||||
import zuul.driver.nullwrap
|
||||
from zuul.connection import BaseConnection
|
||||
from zuul.driver import SourceInterface
|
||||
|
||||
|
@ -46,6 +48,8 @@ class ConnectionRegistry(object):
|
|||
self.registerDriver(zuul.driver.smtp.SMTPDriver())
|
||||
self.registerDriver(zuul.driver.timer.TimerDriver())
|
||||
self.registerDriver(zuul.driver.sql.SQLDriver())
|
||||
self.registerDriver(zuul.driver.bubblewrap.BubblewrapDriver())
|
||||
self.registerDriver(zuul.driver.nullwrap.NullwrapDriver())
|
||||
|
||||
def registerDriver(self, driver):
|
||||
if driver.name in self.drivers:
|
||||
|
|
Loading…
Reference in New Issue