Merge "Add support for bwrap" into feature/zuulv3
This commit is contained in:
commit
ef01295174
|
@ -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
|
|
@ -350,6 +350,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
|
||||
|
@ -366,6 +373,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)
|
||||
|
@ -1130,8 +1138,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
|
||||
|
||||
|
@ -1140,7 +1154,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