The Gatekeeper, or a project gating system
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

223 lines
7.2 KiB

# 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 shlex
import subprocess
import sys
import re
from typing import Dict, List # flake8: noqa
from zuul.driver import (Driver, WrapperInterface)
from zuul.execution_context import BaseExecutionContext
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 BubblewrapExecutionContext(BaseExecutionContext):
log = logging.getLogger("zuul.BubblewrapExecutionContext")
def __init__(self, bwrap_command, ro_paths, rw_paths):
self.bwrap_command = bwrap_command
self.mounts_map = {'ro': ro_paths, 'rw': rw_paths}
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])
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 = list(pwd.getpwuid(uid))
# Replace our user's actual home directory with the work dir.
passwd = passwd[:5] + [kwargs['work_dir']] + passwd[6:]
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.write(passwd_w, b'\n')
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.write(group_w, b'\n')
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
command = [x.format(**kwargs) for x in bwrap_command]
self.log.debug("Bubblewrap command: %s",
" ".join(shlex.quote(c) for c in command))
wrapped_popen = WrappedPopen(command, passwd_r, group_r)
return wrapped_popen
class BubblewrapDriver(Driver, WrapperInterface):
log = logging.getLogger("zuul.BubblewrapDriver")
name = 'bubblewrap'
release_file_re = re.compile('^\W+-release$')
def __init__(self):
self.bwrap_command = self._bwrap_command()
def reconfigure(self, tenant):
pass
def stop(self):
pass
def _bwrap_command(self):
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', '/bin', '/bin',
'--ro-bind', '/sbin', '/sbin',
'--ro-bind', '/etc/resolv.conf', '/etc/resolv.conf',
'--ro-bind', '/etc/hosts', '/etc/hosts',
'--ro-bind', '{ssh_auth_sock}', '{ssh_auth_sock}',
'--bind', '{work_dir}', '{work_dir}',
'--dev', '/dev',
'--chdir', '{work_dir}',
'--unshare-all',
'--share-net',
'--die-with-parent',
'--uid', '{uid}',
'--gid', '{gid}',
'--file', '{uid_fd}', '/etc/passwd',
'--file', '{gid_fd}', '/etc/group',
]
for path in ['/lib64',
'/etc/nsswitch.conf',
'/etc/lsb-release.d',
]:
if os.path.exists(path):
bwrap_command.extend(['--ro-bind', path, path])
for fn in os.listdir('/etc'):
if self.release_file_re.match(fn):
path = os.path.join('/etc', fn)
bwrap_command.extend(['--ro-bind', path, path])
return bwrap_command
def getExecutionContext(self, ro_paths=None, rw_paths=None):
if not ro_paths:
ro_paths = []
if not rw_paths:
rw_paths = []
return BubblewrapExecutionContext(
self.bwrap_command,
ro_paths, rw_paths)
def main(args=None):
logging.basicConfig(level=logging.DEBUG)
driver = BubblewrapDriver()
parser = argparse.ArgumentParser()
parser.add_argument('--ro-paths', nargs='+')
parser.add_argument('--rw-paths', nargs='+')
parser.add_argument('work_dir')
parser.add_argument('run_args', nargs='+')
cli_args = parser.parse_args()
ssh_auth_sock = os.environ.get('SSH_AUTH_SOCK')
context = driver.getExecutionContext(
cli_args.ro_paths, cli_args.rw_paths)
popen = context.getPopen(work_dir=cli_args.work_dir,
ssh_auth_sock=ssh_auth_sock)
x = popen(cli_args.run_args)
x.wait()
if __name__ == '__main__':
main()