Merge "Add support for bwrap" into feature/zuulv3

This commit is contained in:
Jenkins 2017-06-02 19:35:25 +00:00 committed by Gerrit Code Review
commit ef01295174
7 changed files with 294 additions and 1 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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,

View File

@ -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: