Merge "Don't copy the __pycache__ folder" into feature/zuulv3
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index a78a0fa..a7dfb44 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -174,6 +174,10 @@
The zuul-executor process configuration.
+**finger_port**
+ Port to use for finger log streamer.
+ ``finger_port=79``
+
**git_dir**
Directory that Zuul should clone local git repositories to.
``git_dir=/var/lib/zuul/git``
diff --git a/setup.cfg b/setup.cfg
index 9ee64f3..5ae0903 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,6 +26,7 @@
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
diff --git a/tests/base.py b/tests/base.py
index faf9ef3..0aac6bd 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -1844,12 +1844,20 @@
# Make per test copy of Configuration.
self.setup_config()
+ self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
+ if not os.path.exists(self.private_key_file):
+ src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
+ shutil.copy(src_private_key_file, self.private_key_file)
+ shutil.copy('{}.pub'.format(src_private_key_file),
+ '{}.pub'.format(self.private_key_file))
+ os.chmod(self.private_key_file, 0o0600)
self.config.set('zuul', 'tenant_config',
os.path.join(FIXTURE_DIR,
self.config.get('zuul', 'tenant_config')))
self.config.set('merger', 'git_dir', self.merger_src_root)
self.config.set('executor', 'git_dir', self.executor_src_root)
self.config.set('zuul', 'state_dir', self.state_root)
+ self.config.set('executor', 'private_key_file', self.private_key_file)
self.statsd = FakeStatsd()
# note, use 127.0.0.1 rather than localhost to avoid getting ipv6
diff --git a/tests/fixtures/config/ansible/git/common-config/playbooks/hello-post.yaml b/tests/fixtures/config/ansible/git/common-config/playbooks/hello-post.yaml
new file mode 100644
index 0000000..d528be1
--- /dev/null
+++ b/tests/fixtures/config/ansible/git/common-config/playbooks/hello-post.yaml
@@ -0,0 +1,12 @@
+- hosts: all
+ tasks:
+ - name: Register hello-world.txt file.
+ stat:
+ path: "{{zuul.executor.log_root}}/hello-world.txt"
+ register: st
+
+ - name: Assert hello-world.txt file.
+ assert:
+ that:
+ - st.stat.exists
+ - st.stat.isreg
diff --git a/tests/fixtures/config/ansible/git/common-config/zuul.yaml b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
index f9be158..02b87bd 100644
--- a/tests/fixtures/config/ansible/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
@@ -72,3 +72,7 @@
nodes:
- name: ubuntu-xenial
image: ubuntu-xenial
+
+- job:
+ name: hello
+ post-run: hello-post
diff --git a/tests/fixtures/config/ansible/git/org_project/.zuul.yaml b/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
index a2d9c6f..ca734c5 100644
--- a/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
+++ b/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
@@ -2,6 +2,10 @@
parent: python27
name: faillocal
+- job:
+ parent: hello
+ name: hello-world
+
- project:
name: org/project
check:
@@ -10,3 +14,4 @@
- faillocal
- check-vars
- timeout
+ - hello-world
diff --git a/tests/fixtures/config/ansible/git/org_project/playbooks/hello-world.yaml b/tests/fixtures/config/ansible/git/org_project/playbooks/hello-world.yaml
new file mode 100644
index 0000000..373de02
--- /dev/null
+++ b/tests/fixtures/config/ansible/git/org_project/playbooks/hello-world.yaml
@@ -0,0 +1,5 @@
+- hosts: all
+ tasks:
+ - copy:
+ content: "hello world"
+ dest: "{{zuul.executor.log_root}}/hello-world.txt"
diff --git a/tests/fixtures/test_id_rsa b/tests/fixtures/test_id_rsa
new file mode 100644
index 0000000..a793bd0
--- /dev/null
+++ b/tests/fixtures/test_id_rsa
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICWwIBAAKBgQCX10EQhi7hEMk1h7/fQaEj9H2DxWR0s3RXD5UI7j1Bn21tBUus
+Y0tPC5wXES4VfilXg+EuOKsE6z8x8txP1wd1+d6Hq3SWXnOcqxxv2ueAy6Gc31E7
+a2IVDYvqVsAOtxsWddvMGTj98/lexQBX6Bh+wmuba/43lq5UPepwvfgNOQIDAQAB
+AoGADMCHNlwOk9hVDanY82cPoXVnFSn+xc5MdwNYAOgBPQGmrwFC2bd9G6Zd9ZH7
+zNJLpo3s23Tm6ALZy9gZqJrmhWDZBOqeYtmkd0yUf5bCbUzNre8+gHJY8k9PAxVM
+dPr2bq8G4PyN3yC2euTht35KLjb7hD8WiF3exgI/d8oBvgECQQDFKuWmkLtkSkGo
+1KRbeBfRePbfzhGJ1yHRyO72Z1+hVXuRmtcjTfPhMikgx9dxWbpqr/RPgs7D7N8D
+JpFlsiR/AkEAxSX4LOwovklPzCZ8FyfHhkydNgDyBw8y2Xe1OO0LBN51batf9rcl
+rJBYFvulrD+seYNRCWBFpEi4KKZh4YESRwJAKmz+mYbPK9dmpYOMEjqXNXXH+YSH
+9ZcbKd8IvHCl/Ts9qakd3fTqI2z9uJYH39Yk7MwL0Agfob0Yh78GzlE01QJACheu
+g8Y3M76XCjFyKtFLgpGLfsc/nKLnjIB3U4m3BbHJuyqJyByKHjJpgAuz6IR99N6H
+GH7IMefTHame2yd7YwJAUIGRD+iOO0RJvtEHUbsz6IxrQdubNOvzm/78eyBTcbsa
+8996D18fJF6Q0/Gg0Cm65PNOpIthP3qxFkuuduUEUg==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/fixtures/test_id_rsa.pub b/tests/fixtures/test_id_rsa.pub
new file mode 100644
index 0000000..bffc726
--- /dev/null
+++ b/tests/fixtures/test_id_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCX10EQhi7hEMk1h7/fQaEj9H2DxWR0s3RXD5UI7j1Bn21tBUusY0tPC5wXES4VfilXg+EuOKsE6z8x8txP1wd1+d6Hq3SWXnOcqxxv2ueAy6Gc31E7a2IVDYvqVsAOtxsWddvMGTj98/lexQBX6Bh+wmuba/43lq5UPepwvfgNOQ== Private Key For Zuul Tests DO NOT USE
diff --git a/tests/unit/test_bubblewrap.py b/tests/unit/test_bubblewrap.py
new file mode 100644
index 0000000..b274944
--- /dev/null
+++ b/tests/unit/test_bubblewrap.py
@@ -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)
diff --git a/tests/unit/test_ssh_agent.py b/tests/unit/test_ssh_agent.py
new file mode 100644
index 0000000..c9c1ebd
--- /dev/null
+++ b/tests/unit/test_ssh_agent.py
@@ -0,0 +1,56 @@
+# 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 os
+import subprocess
+
+from tests.base import ZuulTestCase
+from zuul.executor.server import SshAgent
+
+
+class TestSshAgent(ZuulTestCase):
+ tenant_config_file = 'config/single-tenant/main.yaml'
+
+ def test_ssh_agent(self):
+ # Need a private key to add
+ env_copy = dict(os.environ)
+ # DISPLAY and SSH_ASKPASS will cause interactive test runners to get a
+ # surprise
+ if 'DISPLAY' in env_copy:
+ del env_copy['DISPLAY']
+ if 'SSH_ASKPASS' in env_copy:
+ del env_copy['SSH_ASKPASS']
+
+ agent = SshAgent()
+ agent.start()
+ env_copy.update(agent.env)
+
+ pub_key_file = '{}.pub'.format(self.private_key_file)
+ pub_key = None
+ with open(pub_key_file) as pub_key_f:
+ pub_key = pub_key_f.read().split('== ')[0]
+
+ agent.add(self.private_key_file)
+ keys = agent.list()
+ self.assertEqual(1, len(keys))
+ self.assertEqual(keys[0].split('== ')[0], pub_key)
+ agent.remove(self.private_key_file)
+ keys = agent.list()
+ self.assertEqual([], keys)
+ agent.stop()
+ # Agent is now dead and thus this should fail
+ with open('/dev/null') as devnull:
+ self.assertRaises(subprocess.CalledProcessError,
+ subprocess.check_call,
+ ['ssh-add', self.private_key_file],
+ env=env_copy,
+ stderr=devnull)
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 2168a7f..21b4729 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -344,6 +344,8 @@
self.assertEqual(build.result, 'FAILURE')
build = self.getJobFromHistory('check-vars')
self.assertEqual(build.result, 'SUCCESS')
+ build = self.getJobFromHistory('hello-world')
+ self.assertEqual(build.result, 'SUCCESS')
build = self.getJobFromHistory('python27')
self.assertEqual(build.result, 'SUCCESS')
flag_path = os.path.join(self.test_root, build.uuid + '.flag')
diff --git a/zuul/ansible/action/copy.py b/zuul/ansible/action/copy.py
index bb54430..d870c24 100644
--- a/zuul/ansible/action/copy.py
+++ b/zuul/ansible/action/copy.py
@@ -25,6 +25,6 @@
source = self._task.args.get('src', None)
remote_src = self._task.args.get('remote_src', False)
- if not remote_src and not paths._is_safe_path(source):
+ if not remote_src and source and not paths._is_safe_path(source):
return paths._fail_dict(source)
return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/cmd/executor.py b/zuul/cmd/executor.py
index ea12b0b..931639f 100755
--- a/zuul/cmd/executor.py
+++ b/zuul/cmd/executor.py
@@ -39,7 +39,7 @@
# Similar situation with gear and statsd.
-FINGER_PORT = 79
+DEFAULT_FINGER_PORT = 79
class Executor(zuul.cmd.ZuulApp):
@@ -86,7 +86,7 @@
self.log.info("Starting log streamer")
streamer = zuul.lib.log_streamer.LogStreamer(
- self.user, '0.0.0.0', FINGER_PORT, self.jobroot_dir)
+ self.user, '0.0.0.0', self.finger_port, self.jobroot_dir)
# Keep running until the parent dies:
pipe_read = os.fdopen(pipe_read)
@@ -127,6 +127,11 @@
self.setup_logging('executor', 'log_config')
self.log = logging.getLogger("zuul.Executor")
+ if self.config.has_option('executor', 'finger_port'):
+ self.finger_port = int(self.config.get('executor', 'finger_port'))
+ else:
+ self.finger_port = DEFAULT_FINGER_PORT
+
self.start_log_streamer()
self.change_privs()
diff --git a/zuul/driver/__init__.py b/zuul/driver/__init__.py
index 671996a..0c3105d 100644
--- a/zuul/driver/__init__.py
+++ b/zuul/driver/__init__.py
@@ -254,3 +254,27 @@
"""
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
diff --git a/zuul/driver/bubblewrap/__init__.py b/zuul/driver/bubblewrap/__init__.py
new file mode 100644
index 0000000..c93e912
--- /dev/null
+++ b/zuul/driver/bubblewrap/__init__.py
@@ -0,0 +1,173 @@
+# 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 six.moves import shlex_quote
+
+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]
+
+ 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
+
+
+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()
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index 27ece54..02c795e 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -479,8 +479,8 @@
change.status = self._get_statuses(project, event.patch_number)
change.reviews = self.getPullReviews(project, change.number)
change.source_event = event
- change.open = self.getPullOpen(project, change.number)
- change.is_current_patchset = self.getIsCurrent(project,
+ change.open = self.getPullOpen(event.project_name, change.number)
+ change.is_current_patchset = self.getIsCurrent(event.project_name,
change.number,
event.patch_number)
elif event.ref:
diff --git a/zuul/driver/nullwrap/__init__.py b/zuul/driver/nullwrap/__init__.py
new file mode 100644
index 0000000..ebcd1da
--- /dev/null
+++ b/zuul/driver/nullwrap/__init__.py
@@ -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
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index 713d7d3..2302412 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -31,10 +31,7 @@
from six.moves import shlex_quote
import zuul.merger.merger
-import zuul.ansible.action
-import zuul.ansible.callback
-import zuul.ansible.library
-import zuul.ansible.lookup
+import zuul.ansible
from zuul.lib import commandsocket
COMMANDS = ['stop', 'pause', 'unpause', 'graceful', 'verbose',
@@ -79,6 +76,78 @@
self.path = None
+class SshAgent(object):
+ log = logging.getLogger("zuul.ExecutorServer")
+
+ def __init__(self):
+ self.env = {}
+ self.ssh_agent = None
+
+ def start(self):
+ if self.ssh_agent:
+ return
+ with open('/dev/null', 'r+') as devnull:
+ ssh_agent = subprocess.Popen(['ssh-agent'], close_fds=True,
+ stdout=subprocess.PIPE,
+ stderr=devnull,
+ stdin=devnull)
+ (output, _) = ssh_agent.communicate()
+ output = output.decode('utf8')
+ for line in output.split("\n"):
+ if '=' in line:
+ line = line.split(";", 1)[0]
+ (key, value) = line.split('=')
+ self.env[key] = value
+ self.log.info('Started SSH Agent, {}'.format(self.env))
+
+ def stop(self):
+ if 'SSH_AGENT_PID' in self.env:
+ try:
+ os.kill(int(self.env['SSH_AGENT_PID']), signal.SIGTERM)
+ except OSError:
+ self.log.exception(
+ 'Problem sending SIGTERM to agent {}'.format(self.env))
+ self.log.info('Sent SIGTERM to SSH Agent, {}'.format(self.env))
+ self.env = {}
+
+ def add(self, key_path):
+ env = os.environ.copy()
+ env.update(self.env)
+ key_path = os.path.expanduser(key_path)
+ self.log.debug('Adding SSH Key {}'.format(key_path))
+ output = ''
+ try:
+ output = subprocess.check_output(['ssh-add', key_path], env=env,
+ stderr=subprocess.PIPE)
+ except subprocess.CalledProcessError:
+ self.log.error('ssh-add failed: {}'.format(output))
+ raise
+ self.log.info('Added SSH Key {}'.format(key_path))
+
+ def remove(self, key_path):
+ env = os.environ.copy()
+ env.update(self.env)
+ key_path = os.path.expanduser(key_path)
+ self.log.debug('Removing SSH Key {}'.format(key_path))
+ subprocess.check_output(['ssh-add', '-d', key_path], env=env,
+ stderr=subprocess.PIPE)
+ self.log.info('Removed SSH Key {}'.format(key_path))
+
+ def list(self):
+ if 'SSH_AUTH_SOCK' not in self.env:
+ return None
+ env = os.environ.copy()
+ env.update(self.env)
+ result = []
+ for line in subprocess.Popen(['ssh-add', '-L'], env=env,
+ stdout=subprocess.PIPE).stdout:
+ line = line.decode('utf8')
+ if line.strip() == 'The agent has no identities.':
+ break
+ result.append(line.strip())
+ return result
+
+
class JobDir(object):
def __init__(self, root, keep, build_uuid):
'''
@@ -181,7 +250,7 @@
self.event = threading.Event()
def __eq__(self, other):
- if (other.connection_name == self.connection_name and
+ if (other and other.connection_name == self.connection_name and
other.project_name == self.project_name):
return True
return False
@@ -280,6 +349,13 @@
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
@@ -296,25 +372,27 @@
path = os.path.join(state_dir, 'executor.socket')
self.command_socket = commandsocket.CommandSocket(path)
ansible_dir = os.path.join(state_dir, 'ansible')
- self.library_dir = os.path.join(ansible_dir, 'library')
- if not os.path.exists(self.library_dir):
- os.makedirs(self.library_dir)
- self.action_dir = os.path.join(ansible_dir, 'action')
- if not os.path.exists(self.action_dir):
- os.makedirs(self.action_dir)
+ self.ansible_dir = ansible_dir
- self.callback_dir = os.path.join(ansible_dir, 'callback')
- if not os.path.exists(self.callback_dir):
- os.makedirs(self.callback_dir)
+ zuul_dir = os.path.join(ansible_dir, 'zuul')
+ plugin_dir = os.path.join(zuul_dir, 'ansible')
- self.lookup_dir = os.path.join(ansible_dir, 'lookup')
- if not os.path.exists(self.lookup_dir):
- os.makedirs(self.lookup_dir)
+ if not os.path.exists(plugin_dir):
+ os.makedirs(plugin_dir)
- _copy_ansible_files(zuul.ansible.library, self.library_dir)
- _copy_ansible_files(zuul.ansible.action, self.action_dir)
- _copy_ansible_files(zuul.ansible.callback, self.callback_dir)
- _copy_ansible_files(zuul.ansible.lookup, self.lookup_dir)
+ self.library_dir = os.path.join(plugin_dir, 'library')
+ self.action_dir = os.path.join(plugin_dir, 'action')
+ self.callback_dir = os.path.join(plugin_dir, 'callback')
+ self.lookup_dir = os.path.join(plugin_dir, 'lookup')
+
+ _copy_ansible_files(zuul.ansible, plugin_dir)
+
+ # We're copying zuul.ansible.* into a directory we are going
+ # to add to pythonpath, so our plugins can "import
+ # zuul.ansible". But we're not installing all of zuul, so
+ # create a __init__.py file for the stub "zuul" module.
+ with open(os.path.join(zuul_dir, '__init__.py'), 'w'):
+ pass
self.job_workers = {}
@@ -526,6 +604,8 @@
self.proc_lock = threading.Lock()
self.running = False
self.aborted = False
+ self.thread = None
+ self.ssh_agent = None
if self.executor_server.config.has_option(
'executor', 'private_key_file'):
@@ -533,8 +613,11 @@
'executor', 'private_key_file')
else:
self.private_key_file = '~/.ssh/id_rsa'
+ self.ssh_agent = SshAgent()
def run(self):
+ self.ssh_agent.start()
+ self.ssh_agent.add(self.private_key_file)
self.running = True
self.thread = threading.Thread(target=self.execute)
self.thread.start()
@@ -542,7 +625,8 @@
def stop(self):
self.aborted = True
self.abortRunningProc()
- self.thread.join()
+ if self.thread:
+ self.thread.join()
def execute(self):
try:
@@ -563,6 +647,11 @@
self.executor_server.finishJob(self.job.unique)
except Exception:
self.log.exception("Error finalizing job thread:")
+ if self.ssh_agent:
+ try:
+ self.ssh_agent.stop()
+ except Exception:
+ self.log.exception("Error stopping SSH agent:")
def _execute(self):
self.log.debug("Job %s: beginning" % (self.job.unique,))
@@ -1044,12 +1133,26 @@
def runAnsible(self, cmd, timeout, trusted=False):
env_copy = os.environ.copy()
+ env_copy.update(self.ssh_agent.env)
env_copy['LOGNAME'] = 'zuul'
+ pythonpath = env_copy.get('PYTHONPATH')
+ if pythonpath:
+ pythonpath = [pythonpath]
+ else:
+ pythonpath = []
+ pythonpath = [self.executor_server.ansible_dir] + pythonpath
+ env_copy['PYTHONPATH'] = os.path.pathsep.join(pythonpath)
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
@@ -1058,7 +1161,7 @@
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,
diff --git a/zuul/lib/connections.py b/zuul/lib/connections.py
index 9908fff..79d78f4 100644
--- a/zuul/lib/connections.py
+++ b/zuul/lib/connections.py
@@ -22,6 +22,8 @@
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 @@
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: