Merge "Fix keyerror with synchronize" into feature/zuulv3
diff --git a/tests/base.py b/tests/base.py
index 3ccc872..b8f9d0c 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -759,7 +759,7 @@
         self.launcher_server.build_history.append(
             BuildHistory(name=build.name, result=result, changes=build.changes,
                          node=build.node, uuid=build.unique,
-                         parameters=build.parameters,
+                         parameters=build.parameters, jobdir=build.jobdir,
                          pipeline=build.parameters['ZUUL_PIPELINE'])
         )
         self.launcher_server.running_builds.remove(build)
diff --git a/tests/fixtures/config/openstack/git/project-config/playbooks/dsvm.yaml b/tests/fixtures/config/openstack/git/project-config/playbooks/dsvm.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/openstack/git/project-config/playbooks/dsvm.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/openstack/git/project-config/zuul.yaml b/tests/fixtures/config/openstack/git/project-config/zuul.yaml
index 9c2231a..420d979 100644
--- a/tests/fixtures/config/openstack/git/project-config/zuul.yaml
+++ b/tests/fixtures/config/openstack/git/project-config/zuul.yaml
@@ -71,12 +71,22 @@
         - python27
         - python35
 
+- job:
+    name: dsvm
+    parent: base
+    repos:
+      - openstack/keystone
+      - openstack/nova
+
 # Project definitions
 
 - project:
     name: openstack/nova
     templates:
       - python-jobs
+    check:
+      jobs:
+        - dsvm
     gate:
       queue: integrated
 
@@ -84,5 +94,8 @@
     name: openstack/keystone
     templates:
       - python-jobs
+    check:
+      jobs:
+        - dsvm
     gate:
       queue: integrated
diff --git a/tests/unit/test_openstack.py b/tests/unit/test_openstack.py
index d0c7ab2..670e578 100644
--- a/tests/unit/test_openstack.py
+++ b/tests/unit/test_openstack.py
@@ -14,6 +14,8 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import os
+
 from tests.base import AnsibleZuulTestCase
 
 
@@ -54,3 +56,45 @@
                          "A should report start and success")
         self.assertEqual(self.getJobFromHistory('python27').node,
                          'ubuntu-trusty')
+
+    def test_dsvm_keystone_repo(self):
+        self.launch_server.keep_jobdir = True
+        A = self.fake_gerrit.addFakeChange('openstack/nova', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        self.assertHistory([
+            dict(name='dsvm', result='SUCCESS', changes='1,1')])
+        build = self.getJobFromHistory('dsvm')
+
+        # Check that a change to nova triggered a keystone clone
+        launcher_git_dir = os.path.join(self.launcher_src_root,
+                                        'openstack', 'keystone', '.git')
+        self.assertTrue(os.path.exists(launcher_git_dir),
+                        msg='openstack/keystone should be cloned.')
+
+        jobdir_git_dir = os.path.join(build.jobdir.src_root,
+                                      'openstack', 'keystone', '.git')
+        self.assertTrue(os.path.exists(jobdir_git_dir),
+                        msg='openstack/keystone should be cloned.')
+
+    def test_dsvm_nova_repo(self):
+        self.launch_server.keep_jobdir = True
+        A = self.fake_gerrit.addFakeChange('openstack/keystone', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        self.assertHistory([
+            dict(name='dsvm', result='SUCCESS', changes='1,1')])
+        build = self.getJobFromHistory('dsvm')
+
+        # Check that a change to keystone triggered a nova clone
+        launcher_git_dir = os.path.join(self.launcher_src_root,
+                                        'openstack', 'nova', '.git')
+        self.assertTrue(os.path.exists(launcher_git_dir),
+                        msg='openstack/nova should be cloned.')
+
+        jobdir_git_dir = os.path.join(build.jobdir.src_root,
+                                      'openstack', 'nova', '.git')
+        self.assertTrue(os.path.exists(jobdir_git_dir),
+                        msg='openstack/nova should be cloned.')
diff --git a/zuul/ansible/action/synchronize.py b/zuul/ansible/action/synchronize.py
index fa3d051..75fd45f 100644
--- a/zuul/ansible/action/synchronize.py
+++ b/zuul/ansible/action/synchronize.py
@@ -24,15 +24,15 @@
 
         source = self._task.args.get('src', None)
         dest = self._task.args.get('dest', None)
-        pull = self._task.args.get('pull', False)
+        mode = self._task.args.get('mode', 'push')
 
         if 'rsync_opts' not in self._task.args:
             self._task.args['rsync_opts'] = []
         if '--safe-links' not in self._task.args['rsync_opts']:
             self._task.args['rsync_opts'].append('--safe-links')
 
-        if not pull and not paths._is_safe_path(source):
+        if mode == 'push' and not paths._is_safe_path(source):
             return paths._fail_dict(source, prefix='Syncing files from')
-        if pull and not paths._is_safe_path(dest):
+        if mode == 'pull' and not paths._is_safe_path(dest):
             return paths._fail_dict(dest, prefix='Syncing files to')
         return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/configloader.py b/zuul/configloader.py
index e4fa620..3e94c37 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -115,6 +115,7 @@
                'run': str,
                '_source_context': model.SourceContext,
                'roles': to_list(role),
+               'repos': to_list(str),
                }
 
         return vs.Schema(job)
@@ -185,6 +186,11 @@
                     ns.addNode(node)
             job.nodeset = ns
 
+        if 'repos' in conf:
+            # Accumulate repos in a set so that job inheritance
+            # is additive.
+            job.repos = job.repos.union(set(conf.get('repos', [])))
+
         tags = conf.get('tags')
         if tags:
             # Tags are merged via a union rather than a
diff --git a/zuul/launcher/client.py b/zuul/launcher/client.py
index f36e2a7..a22f82e 100644
--- a/zuul/launcher/client.py
+++ b/zuul/launcher/client.py
@@ -348,6 +348,13 @@
         params['nodes'] = nodes
         params['zuul'] = zuul_params
         projects = set()
+        if job.repos:
+            for repo in job.repos:
+                project = item.pipeline.source.getProject(repo)
+                params['projects'].append(
+                    dict(name=repo,
+                         url=item.pipeline.source.getGitUrl(project)))
+                projects.add(project)
         for item in all_items:
             if item.change.project not in projects:
                 params['projects'].append(
diff --git a/zuul/launcher/server.py b/zuul/launcher/server.py
index 575d352..df24ff6 100644
--- a/zuul/launcher/server.py
+++ b/zuul/launcher/server.py
@@ -87,7 +87,7 @@
         #     trusted.cfg
         #     untrusted.cfg
         #   work
-        #     git
+        #     src
         #     logs
         self.keep = keep
         self.root = tempfile.mkdtemp(dir=root)
diff --git a/zuul/model.py b/zuul/model.py
index 10d0446..ac3a286 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -699,6 +699,7 @@
             attempts=3,
             final=False,
             roles=frozenset(),
+            repos=frozenset(),
         )
 
         # These are generally internal attributes which are not