Merge "Add support for job allowed-projects" into feature/zuulv3
diff --git a/.zuul.yaml b/.zuul.yaml
index e2eea68..98b880d 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,10 +1,12 @@
 - job:
     name: base
-    pre-run: base-pre
-    post-run: base-post
+    pre-run: base/pre
+    post-run: base/post
     success-url: http://zuulv3-dev.openstack.org/logs/{build.uuid}/
     failure-url: http://zuulv3-dev.openstack.org/logs/{build.uuid}/
     timeout: 1800
+    vars:
+      zuul_workspace_root: /home/zuul
     nodes:
       - name: ubuntu-xenial
         image: ubuntu-xenial
@@ -12,25 +14,35 @@
 - job:
     name: tox
     parent: base
-    pre-run: tox-pre
-    post-run: tox-post
+    pre-run: tox/pre
+    post-run: tox/post
 
 - job:
     name: tox-cover
     parent: tox
+    run: tox/cover
     voting: false
 
 - job:
     name: tox-docs
     parent: tox
+    run: tox/docs
 
 - job:
     name: tox-linters
     parent: tox
+    run: tox/docs
 
 - job:
     name: tox-py27
     parent: tox
+    run: tox/py27
+
+- job:
+    name: tox-tarball
+    parent: tox
+    run: tox/tarball
+    post-run: tox/tarball-post
 
 - project:
     name: openstack-infra/zuul
@@ -40,3 +52,4 @@
         - tox-cover
         - tox-linters
         - tox-py27
+        - tox-tarball
diff --git a/README.rst b/README.rst
index 84b9b7a..c55f7b3 100644
--- a/README.rst
+++ b/README.rst
@@ -58,7 +58,7 @@
    Some of the information in the specs may be effectively superceded
    by changes here, which are still undergoing review.
 
-4) Read documentation on the internal data model and testing: http://docs.openstack.org/infra/zuul/feature/zuulv3/internals.html
+4) Read developer documentation on the internal data model and testing: http://docs.openstack.org/infra/zuul/feature/zuulv3/developer.html
 
    The general philosophy for Zuul tests is to perform functional
    testing of either the individual component or the entire end-to-end
@@ -118,7 +118,7 @@
   Construct a test to fully simulate the series of events you want to
   see, then run it in the foreground.  For example::
 
-    .tox/py27/bin/python -m testtools.run tests.test_scheduler.TestScheduler.test_jobs_executed
+    .tox/py27/bin/python -m testtools.run tests.unit.test_scheduler.TestScheduler.test_jobs_executed
 
   See TESTING.rst for more information.
 
diff --git a/etc/zuul.conf-sample b/etc/zuul.conf-sample
index 7207c73..bf19895 100644
--- a/etc/zuul.conf-sample
+++ b/etc/zuul.conf-sample
@@ -18,15 +18,6 @@
 ;git_user_name=zuul
 zuul_url=http://zuul.example.com/p
 
-[swift]
-authurl=https://identity.api.example.org/v2.0/
-user=username
-key=password
-
-default_container=logs
-region_name=EXP
-logserver_prefix=http://logs.example.org/server.app/
-
 [webapp]
 listen_address=0.0.0.0
 port=8001
diff --git a/playbooks/base-post.yaml b/playbooks/base/post.yaml
similarity index 100%
rename from playbooks/base-post.yaml
rename to playbooks/base/post.yaml
diff --git a/playbooks/base-pre.yaml b/playbooks/base/pre.yaml
similarity index 100%
rename from playbooks/base-pre.yaml
rename to playbooks/base/pre.yaml
diff --git a/playbooks/base/roles b/playbooks/base/roles
new file mode 120000
index 0000000..7b9ade8
--- /dev/null
+++ b/playbooks/base/roles
@@ -0,0 +1 @@
+../roles/
\ No newline at end of file
diff --git a/playbooks/roles/extra-test-setup/tasks/main.yaml b/playbooks/roles/extra-test-setup/tasks/main.yaml
new file mode 100644
index 0000000..da4259e
--- /dev/null
+++ b/playbooks/roles/extra-test-setup/tasks/main.yaml
@@ -0,0 +1,13 @@
+---
+- name: Check if projects tools/test-setup.sh exists.
+  stat:
+    path: "{{ zuul_workspace_root }}/src/{{ zuul.project }}/tools/test-setup.sh"
+  register: p
+
+- name: Run tools/test-setup.sh.
+  shell: tools/test-setup.sh
+  args:
+    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
+  when:
+    - p.stat.exists
+    - p.stat.executable
diff --git a/playbooks/roles/prepare-workspace/defaults/main.yaml b/playbooks/roles/prepare-workspace/defaults/main.yaml
deleted file mode 100644
index 9127ad8..0000000
--- a/playbooks/roles/prepare-workspace/defaults/main.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-# tasks/main.yaml
-prepare_workspace_root: /home/zuul/workspace
diff --git a/playbooks/roles/prepare-workspace/tasks/main.yaml b/playbooks/roles/prepare-workspace/tasks/main.yaml
index d8a5316..4d42b2d 100644
--- a/playbooks/roles/prepare-workspace/tasks/main.yaml
+++ b/playbooks/roles/prepare-workspace/tasks/main.yaml
@@ -10,12 +10,13 @@
 
 - name: Create workspace directory.
   file:
-    path: "{{ prepare_workspace_root }}"
+    path: "{{ zuul_workspace_root }}"
     owner: zuul
     group: zuul
     state: directory
 
 - name: Synchronize src repos to workspace directory.
   synchronize:
-    dest: "{{ prepare_workspace_root }}"
+    dest: "{{ zuul_workspace_root }}"
     src: "{{ zuul.executor.src_root }}"
+  no_log: true
diff --git a/playbooks/roles/revoke-sudo/tasks/main.yaml b/playbooks/roles/revoke-sudo/tasks/main.yaml
new file mode 100644
index 0000000..1c18187
--- /dev/null
+++ b/playbooks/roles/revoke-sudo/tasks/main.yaml
@@ -0,0 +1,8 @@
+- name: Remove sudo access for zuul user.
+  become: yes
+  file:
+    path: /etc/sudoers.d/zuul-sudo
+    state: absent
+
+- name: Prove that general sudo access is actually revoked.
+  shell: ! sudo -n true
diff --git a/playbooks/roles/run-bindep/tasks/main.yaml b/playbooks/roles/run-bindep/tasks/main.yaml
index 7717c86..5a9d33e 100644
--- a/playbooks/roles/run-bindep/tasks/main.yaml
+++ b/playbooks/roles/run-bindep/tasks/main.yaml
@@ -2,4 +2,4 @@
 - name: Run install-distro-packages.sh
   shell: /usr/local/jenkins/slave_scripts/install-distro-packages.sh
   args:
-    chdir: "/home/zuul/workspace/src/{{ zuul.project }}"
+    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-cover/tasks/main.yaml b/playbooks/roles/run-cover/tasks/main.yaml
index 5fce7f1..caed13c 100644
--- a/playbooks/roles/run-cover/tasks/main.yaml
+++ b/playbooks/roles/run-cover/tasks/main.yaml
@@ -1,4 +1,4 @@
 - name: Execute run-cover.sh.
   shell: "/usr/local/jenkins/slave_scripts/run-cover.sh {{ run_cover_envlist }}"
   args:
-    chdir: "/home/zuul/workspace/src/{{ zuul.project }}"
+    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-docs/tasks/main.yaml b/playbooks/roles/run-docs/tasks/main.yaml
index 3266f2b..2250593 100644
--- a/playbooks/roles/run-docs/tasks/main.yaml
+++ b/playbooks/roles/run-docs/tasks/main.yaml
@@ -1,4 +1,4 @@
 - name: Execute run-docs.sh.
   shell: "/usr/local/jenkins/slave_scripts/run-docs.sh {{ run_docs_envlist }}"
   args:
-    chdir: "/home/zuul/workspace/src/{{ zuul.project }}"
+    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-tarball/defaults/main.yaml b/playbooks/roles/run-tarball/defaults/main.yaml
new file mode 100644
index 0000000..072828a
--- /dev/null
+++ b/playbooks/roles/run-tarball/defaults/main.yaml
@@ -0,0 +1,2 @@
+---
+run_tarball_envlist: venv
diff --git a/playbooks/roles/run-tarball/tasks/main.yaml b/playbooks/roles/run-tarball/tasks/main.yaml
new file mode 100644
index 0000000..e21c4c8
--- /dev/null
+++ b/playbooks/roles/run-tarball/tasks/main.yaml
@@ -0,0 +1,4 @@
+- name: Execute run-tarball.sh.
+  shell: "/usr/local/jenkins/slave_scripts/run-tarball.sh {{ run_tarball_envlist }}"
+  args:
+    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-tox/tasks/main.yaml b/playbooks/roles/run-tox/tasks/main.yaml
index 1053690..29a4cc4 100644
--- a/playbooks/roles/run-tox/tasks/main.yaml
+++ b/playbooks/roles/run-tox/tasks/main.yaml
@@ -1,4 +1,4 @@
 - name: Run tox
   shell: "/usr/local/jenkins/slave_scripts/run-tox.sh {{ run_tox_envlist }}"
   args:
-    chdir: "/home/zuul/workspace/src/{{ zuul.project }}"
+    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-wheel/defaults/main.yaml b/playbooks/roles/run-wheel/defaults/main.yaml
new file mode 100644
index 0000000..8645d33
--- /dev/null
+++ b/playbooks/roles/run-wheel/defaults/main.yaml
@@ -0,0 +1,2 @@
+---
+run_wheel_envlist: venv
diff --git a/playbooks/roles/run-wheel/tasks/main.yaml b/playbooks/roles/run-wheel/tasks/main.yaml
new file mode 100644
index 0000000..f5aaf54
--- /dev/null
+++ b/playbooks/roles/run-wheel/tasks/main.yaml
@@ -0,0 +1,4 @@
+- name: Execute run-wheel.sh.
+  shell: "/usr/local/jenkins/slave_scripts/run-wheel.sh {{ run_wheel_envlist }}"
+  args:
+    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/tox-cover.yaml b/playbooks/tox-cover.yaml
deleted file mode 100644
index ca391e1..0000000
--- a/playbooks/tox-cover.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-- hosts: all
-  roles:
-    - run-cover
diff --git a/playbooks/tox/cover.yaml b/playbooks/tox/cover.yaml
new file mode 100644
index 0000000..642eb4e
--- /dev/null
+++ b/playbooks/tox/cover.yaml
@@ -0,0 +1,5 @@
+- hosts: all
+  roles:
+    - extra-test-setup
+    - revoke-sudo
+    - run-cover
diff --git a/playbooks/tox-docs.yaml b/playbooks/tox/docs.yaml
similarity index 67%
rename from playbooks/tox-docs.yaml
rename to playbooks/tox/docs.yaml
index 98b3313..028e1c5 100644
--- a/playbooks/tox-docs.yaml
+++ b/playbooks/tox/docs.yaml
@@ -1,3 +1,4 @@
 - hosts: all
   roles:
+    - revoke-sudo
     - run-docs
diff --git a/playbooks/tox-linters.yaml b/playbooks/tox/linters.yaml
similarity index 79%
rename from playbooks/tox-linters.yaml
rename to playbooks/tox/linters.yaml
index 9da2e8a..d1e7f13 100644
--- a/playbooks/tox-linters.yaml
+++ b/playbooks/tox/linters.yaml
@@ -2,4 +2,5 @@
   vars:
     run_tox_envlist: pep8
   roles:
+    - revoke-sudo
     - run-tox
diff --git a/playbooks/tox-post.yaml b/playbooks/tox/post.yaml
similarity index 86%
rename from playbooks/tox-post.yaml
rename to playbooks/tox/post.yaml
index 697b727..3b035f8 100644
--- a/playbooks/tox-post.yaml
+++ b/playbooks/tox/post.yaml
@@ -3,7 +3,7 @@
     - name: Find tox directories to synchrionize.
       find:
         file_type: directory
-        paths: "/home/zuul/workspace/src/{{ zuul.project }}/.tox"
+        paths: "{{ zuul_workspace_root }}/src/{{ zuul.project }}/.tox"
         # NOTE(pabelanger): The .tox/log folder is empty, ignore it.
         patterns: ^(?!log).*$
         use_regex: yes
diff --git a/playbooks/tox-pre.yaml b/playbooks/tox/pre.yaml
similarity index 100%
rename from playbooks/tox-pre.yaml
rename to playbooks/tox/pre.yaml
diff --git a/playbooks/tox-py27.yaml b/playbooks/tox/py27.yaml
similarity index 62%
rename from playbooks/tox-py27.yaml
rename to playbooks/tox/py27.yaml
index 13756b5..fd45f27 100644
--- a/playbooks/tox-py27.yaml
+++ b/playbooks/tox/py27.yaml
@@ -2,4 +2,6 @@
   vars:
     run_tox_envlist: py27
   roles:
+    - extra-test-setup
+    - revoke-sudo
     - run-tox
diff --git a/playbooks/tox/roles b/playbooks/tox/roles
new file mode 120000
index 0000000..7b9ade8
--- /dev/null
+++ b/playbooks/tox/roles
@@ -0,0 +1 @@
+../roles/
\ No newline at end of file
diff --git a/playbooks/tox/tarball-post.yaml b/playbooks/tox/tarball-post.yaml
new file mode 100644
index 0000000..fb41707
--- /dev/null
+++ b/playbooks/tox/tarball-post.yaml
@@ -0,0 +1,10 @@
+- hosts: all
+  tasks:
+    - name: Collect tarball artifacts.
+      synchronize:
+        dest: "{{ zuul.executor.src_root }}/tarballs"
+        mode: pull
+        src: "{{ zuul_workspace_root }}/src/{{ zuul.project }}/dist/{{ item }}"
+      with_items:
+        - "*.tar.gz"
+        - "*.whl"
diff --git a/playbooks/tox/tarball.yaml b/playbooks/tox/tarball.yaml
new file mode 100644
index 0000000..4d5a8f6
--- /dev/null
+++ b/playbooks/tox/tarball.yaml
@@ -0,0 +1,5 @@
+- hosts: all
+  roles:
+    - revoke-sudo
+    - run-tarball
+    - run-wheel
diff --git a/tests/base.py b/tests/base.py
index b039267..2816b9f 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -54,7 +54,6 @@
 
 import zuul.driver.gerrit.gerritsource as gerritsource
 import zuul.driver.gerrit.gerritconnection as gerritconnection
-import zuul.connection.sql
 import zuul.scheduler
 import zuul.webapp
 import zuul.rpclistener
@@ -803,9 +802,13 @@
     def getHostList(self, args):
         self.log.debug("hostlist")
         hosts = super(RecordingAnsibleJob, self).getHostList(args)
-        for name, d in hosts:
-            d['ansible_connection'] = 'local'
-        hosts.append(('localhost', dict(ansible_connection='local')))
+        for host in hosts:
+            host['host_vars']['ansible_connection'] = 'local'
+
+        hosts.append(dict(
+            name='localhost',
+            host_vars=dict(ansible_connection='local'),
+            host_keys=[]))
         return hosts
 
 
@@ -992,6 +995,7 @@
                     created_time=now,
                     updated_time=now,
                     image_id=None,
+                    host_keys=["fake-key1", "fake-key2"],
                     executor='fake-nodepool')
         data = json.dumps(data)
         path = self.client.create(path, data,
@@ -1932,11 +1936,18 @@
 
     def commitLayoutUpdate(self, orig_name, source_name):
         source_path = os.path.join(self.test_root, 'upstream',
-                                   source_name, 'zuul.yaml')
-        with open(source_path, 'r') as nt:
-            before = self.addCommitToRepo(
-                orig_name, 'Pulling content from %s' % source_name,
-                {'zuul.yaml': nt.read()})
+                                   source_name)
+        to_copy = ['zuul.yaml']
+        for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
+            to_copy.append('playbooks/{}'.format(playbook))
+        commit_data = {}
+        for source_file in to_copy:
+            source_file_path = os.path.join(source_path, source_file)
+            with open(source_file_path, 'r') as nt:
+                commit_data[source_file] = nt.read()
+        before = self.addCommitToRepo(
+            orig_name, 'Pulling content from %s' % source_name,
+            commit_data)
         return before
 
     def addEvent(self, connection, event):
@@ -1976,8 +1987,8 @@
 
 
 class ZuulDBTestCase(ZuulTestCase):
-    def setup_config(self, config_file='zuul-connections-same-gerrit.conf'):
-        super(ZuulDBTestCase, self).setup_config(config_file)
+    def setup_config(self):
+        super(ZuulDBTestCase, self).setup_config()
         for section_name in self.config.sections():
             con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
                                  section_name, re.I)
diff --git a/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml b/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml
new file mode 100644
index 0000000..92c66d1
--- /dev/null
+++ b/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml
@@ -0,0 +1,15 @@
+- hosts: ubuntu-xenial
+  tasks:
+    - name: Assert nodepool variables are valid.
+      assert:
+        that:
+          - nodepool_az == 'test-az'
+          - nodepool_region == 'test-region'
+          - nodepool_provider == 'test-provider'
+
+    - name: Assert zuul-executor variables are valid.
+      assert:
+        that:
+          - zuul.executor.hostname is defined
+          - zuul.executor.src_root is defined
+          - zuul.executor.log_root is defined
diff --git a/tests/fixtures/config/ansible/git/common-config/playbooks/nodepool.yaml b/tests/fixtures/config/ansible/git/common-config/playbooks/nodepool.yaml
deleted file mode 100644
index 9970dd7..0000000
--- a/tests/fixtures/config/ansible/git/common-config/playbooks/nodepool.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-- hosts: ubuntu-xenial
-  tasks:
-    - name: Assert nodepool variables are valid.
-      assert:
-        that:
-          - nodepool_az == 'test-az'
-          - nodepool_region == 'test-region'
-          - nodepool_provider == 'test-provider'
diff --git a/tests/fixtures/config/ansible/git/common-config/zuul.yaml b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
index 3678f94..a5eba8c 100644
--- a/tests/fixtures/config/ansible/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
@@ -68,7 +68,7 @@
 
 - job:
     parent: python27
-    name: nodepool
+    name: check-vars
     nodes:
       - name: ubuntu-xenial
         image: ubuntu-xenial
diff --git a/tests/fixtures/config/ansible/git/org_project/.zuul.yaml b/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
index 24ba019..a2d9c6f 100644
--- a/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
+++ b/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
@@ -8,5 +8,5 @@
       jobs:
         - python27
         - faillocal
-        - nodepool
+        - check-vars
         - timeout
diff --git a/tests/fixtures/config/single-tenant/git/layout-no-jobs/playbooks/gate-noop.yaml b/tests/fixtures/config/single-tenant/git/layout-no-jobs/playbooks/gate-noop.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/single-tenant/git/layout-no-jobs/playbooks/gate-noop.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/single-tenant/git/layout-no-jobs/zuul.yaml b/tests/fixtures/config/single-tenant/git/layout-no-jobs/zuul.yaml
new file mode 100644
index 0000000..5894440
--- /dev/null
+++ b/tests/fixtures/config/single-tenant/git/layout-no-jobs/zuul.yaml
@@ -0,0 +1,49 @@
+- pipeline:
+    name: check
+    manager: independent
+    source: gerrit
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        verified: 1
+    failure:
+      gerrit:
+        verified: -1
+
+- pipeline:
+    name: gate
+    manager: dependent
+    success-message: Build succeeded (gate).
+    source:
+      gerrit
+    trigger:
+      gerrit:
+        - event: comment-added
+          approval:
+            - approved: 1
+    success:
+      gerrit:
+        verified: 2
+        submit: true
+    failure:
+      gerrit:
+        verified: -2
+    start:
+      gerrit:
+        verified: 0
+    precedence: high
+
+- job:
+    name: gate-noop
+
+- project:
+    name: org/project
+    merge-mode: cherry-pick
+    check:
+      jobs:
+        - gate-noop
+    gate:
+      jobs:
+        - gate-noop
diff --git a/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-merge.yaml b/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-merge.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-merge.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-test1.yaml b/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-test1.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-test1.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-test2.yaml b/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-test2.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/sql-driver/git/common-config/playbooks/project-test2.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/sql-driver/git/common-config/zuul.yaml b/tests/fixtures/config/sql-driver/git/common-config/zuul.yaml
new file mode 100644
index 0000000..36c7602
--- /dev/null
+++ b/tests/fixtures/config/sql-driver/git/common-config/zuul.yaml
@@ -0,0 +1,38 @@
+- pipeline:
+    name: check
+    manager: independent
+    source: gerrit
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        verified: 1
+      resultsdb:
+        score: 1
+    failure:
+      gerrit:
+        verified: -1
+      resultsdb:
+        score: -1
+      resultsdb_failures:
+        score: -1
+
+- job:
+    name: project-merge
+
+- job:
+    name: project-test1
+
+- job:
+    name: project-test2
+
+- project:
+    name: org/project
+    check:
+      jobs:
+        - project-merge
+        - project-test1:
+            dependencies: project-merge
+        - project-test2:
+            dependencies: project-merge
diff --git a/tests/fixtures/config/sql-driver/git/org_project/README b/tests/fixtures/config/sql-driver/git/org_project/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/sql-driver/git/org_project/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/sql-driver/main.yaml b/tests/fixtures/config/sql-driver/main.yaml
new file mode 100644
index 0000000..d9868fa
--- /dev/null
+++ b/tests/fixtures/config/sql-driver/main.yaml
@@ -0,0 +1,8 @@
+- tenant:
+    name: tenant-one
+    source:
+      gerrit:
+        config-repos:
+          - common-config
+        project-repos:
+          - org/project
diff --git a/tests/fixtures/layout-no-jobs.yaml b/tests/fixtures/layout-no-jobs.yaml
deleted file mode 100644
index e860ad5..0000000
--- a/tests/fixtures/layout-no-jobs.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
-includes:
-  - python-file: custom_functions.py
-
-pipelines:
-  - name: check
-    manager: IndependentPipelineManager
-    trigger:
-      gerrit:
-        - event: patchset-created
-    success:
-      gerrit:
-        verified: 1
-    failure:
-      gerrit:
-        verified: -1
-
-  - name: gate
-    manager: DependentPipelineManager
-    failure-message: Build failed.  For information on how to proceed, see http://wiki.example.org/Test_Failures
-    trigger:
-      gerrit:
-        - event: comment-added
-          approval:
-            - approved: 1
-    success:
-      gerrit:
-        verified: 2
-        submit: true
-    failure:
-      gerrit:
-        verified: -2
-    start:
-      gerrit:
-        verified: 0
-    precedence: high
-
-projects:
-  - name: org/project
-    merge-mode: cherry-pick
-    check:
-      - gate-noop
-    gate:
-      - gate-noop
diff --git a/tests/fixtures/layout-sql-reporter.yaml b/tests/fixtures/layout-sql-reporter.yaml
deleted file mode 100644
index c79a432..0000000
--- a/tests/fixtures/layout-sql-reporter.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-pipelines:
-  - name: check
-    manager: IndependentPipelineManager
-    source:
-        review_gerrit
-    trigger:
-      review_gerrit:
-        - event: patchset-created
-    success:
-      review_gerrit:
-        verified: 1
-      resultsdb:
-        score: 1
-    failure:
-      review_gerrit:
-        verified: -1
-      resultsdb:
-        score: -1
-      resultsdb_failures:
-        score: -1
-
-projects:
-  - name: org/project
-    check:
-      - project-merge:
-        - project-test1
-        - project-test2
diff --git a/tests/fixtures/zuul-connections-bad-sql.conf b/tests/fixtures/zuul-connections-bad-sql.conf
deleted file mode 100644
index 2d1e804..0000000
--- a/tests/fixtures/zuul-connections-bad-sql.conf
+++ /dev/null
@@ -1,40 +0,0 @@
-[gearman]
-server=127.0.0.1
-
-[zuul]
-layout_config=layout-connections-multiple-voters.yaml
-url_pattern=http://logs.example.com/{change.number}/{change.patchset}/{pipeline.name}/{job.name}/{build.number}
-job_name_in_report=true
-
-[merger]
-git_dir=/tmp/zuul-test/git
-git_user_email=zuul@example.com
-git_user_name=zuul
-zuul_url=http://zuul.example.com/p
-
-[connection review_gerrit]
-driver=gerrit
-server=review.example.com
-user=jenkins
-sshkey=none
-
-[connection alt_voting_gerrit]
-driver=gerrit
-server=alt_review.example.com
-user=civoter
-sshkey=none
-
-[connection outgoing_smtp]
-driver=smtp
-server=localhost
-port=25
-default_from=zuul@example.com
-default_to=you@example.com
-
-[connection resultsdb]
-driver=sql
-dburi=mysql+pymysql://bad:creds@host/db
-
-[connection resultsdb_failures]
-driver=sql
-dburi=mysql+pymysql://bad:creds@host/db
diff --git a/tests/fixtures/zuul-connections-same-gerrit.conf b/tests/fixtures/zuul-connections-same-gerrit.conf
index 69f5239..6156df4 100644
--- a/tests/fixtures/zuul-connections-same-gerrit.conf
+++ b/tests/fixtures/zuul-connections-same-gerrit.conf
@@ -33,12 +33,3 @@
 port=25
 default_from=zuul@example.com
 default_to=you@example.com
-
-# TODOv3(jeblair): commented out until sqlalchemy conenction ported to
-# v3 driver syntax
-#[connection resultsdb] driver=sql
-#dburi=$MYSQL_FIXTURE_DBURI$
-
-#[connection resultsdb_failures]
-#driver=sql
-#dburi=$MYSQL_FIXTURE_DBURI$
diff --git a/tests/fixtures/zuul-git-driver.conf b/tests/fixtures/zuul-git-driver.conf
index 936c530..0a4e230 100644
--- a/tests/fixtures/zuul-git-driver.conf
+++ b/tests/fixtures/zuul-git-driver.conf
@@ -15,16 +15,6 @@
 [executor]
 git_dir=/tmp/zuul-test/executor-git
 
-[swift]
-authurl=https://identity.api.example.org/v2.0/
-user=username
-key=password
-tenant_name=" "
-
-default_container=logs
-region_name=EXP
-logserver_prefix=http://logs.example.org/server.app/
-
 [connection gerrit]
 driver=gerrit
 server=review.example.com
diff --git a/tests/fixtures/zuul-sql-driver-bad.conf b/tests/fixtures/zuul-sql-driver-bad.conf
new file mode 100644
index 0000000..d91e2f6
--- /dev/null
+++ b/tests/fixtures/zuul-sql-driver-bad.conf
@@ -0,0 +1,30 @@
+[gearman]
+server=127.0.0.1
+
+[zuul]
+tenant_config=main.yaml
+url_pattern=http://logs.example.com/{change.number}/{change.patchset}/{pipeline.name}/{job.name}/{build.number}
+job_name_in_report=true
+
+[merger]
+git_dir=/tmp/zuul-test/merger-git
+git_user_email=zuul@example.com
+git_user_name=zuul
+zuul_url=http://zuul.example.com/p
+
+[executor]
+git_dir=/tmp/zuul-test/executor-git
+
+[connection gerrit]
+driver=gerrit
+server=review.example.com
+user=jenkins
+sshkey=fake_id_rsa1
+
+[connection resultsdb]
+driver=sql
+dburi=mysql+pymysql://bad:creds@host/db
+
+[connection resultsdb_failures]
+driver=sql
+dburi=mysql+pymysql://bad:creds@host/db
diff --git a/tests/fixtures/zuul-sql-driver.conf b/tests/fixtures/zuul-sql-driver.conf
new file mode 100644
index 0000000..42ab1ba
--- /dev/null
+++ b/tests/fixtures/zuul-sql-driver.conf
@@ -0,0 +1,30 @@
+[gearman]
+server=127.0.0.1
+
+[zuul]
+tenant_config=main.yaml
+url_pattern=http://logs.example.com/{change.number}/{change.patchset}/{pipeline.name}/{job.name}/{build.number}
+job_name_in_report=true
+
+[merger]
+git_dir=/tmp/zuul-test/merger-git
+git_user_email=zuul@example.com
+git_user_name=zuul
+zuul_url=http://zuul.example.com/p
+
+[executor]
+git_dir=/tmp/zuul-test/executor-git
+
+[connection gerrit]
+driver=gerrit
+server=review.example.com
+user=jenkins
+sshkey=fake_id_rsa1
+
+[connection resultsdb]
+driver=sql
+dburi=$MYSQL_FIXTURE_DBURI$
+
+[connection resultsdb_failures]
+driver=sql
+dburi=$MYSQL_FIXTURE_DBURI$
diff --git a/tests/fixtures/zuul.conf b/tests/fixtures/zuul.conf
index 516ce08..ce29310 100644
--- a/tests/fixtures/zuul.conf
+++ b/tests/fixtures/zuul.conf
@@ -15,16 +15,6 @@
 [executor]
 git_dir=/tmp/zuul-test/executor-git
 
-[swift]
-authurl=https://identity.api.example.org/v2.0/
-user=username
-key=password
-tenant_name=" "
-
-default_container=logs
-region_name=EXP
-logserver_prefix=http://logs.example.org/server.app/
-
 [connection gerrit]
 driver=gerrit
 server=review.example.com
diff --git a/tests/nodepool/test_nodepool_integration.py b/tests/nodepool/test_nodepool_integration.py
index 67968a3..2c9a9b3 100644
--- a/tests/nodepool/test_nodepool_integration.py
+++ b/tests/nodepool/test_nodepool_integration.py
@@ -12,7 +12,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-
+import socket
 import time
 from unittest import skip
 
@@ -30,9 +30,9 @@
     def setUp(self):
         super(BaseTestCase, self).setUp()
 
-        self.zk_config = zuul.zk.ZooKeeperConnectionConfig('localhost')
         self.zk = zuul.zk.ZooKeeper()
-        self.zk.connect([self.zk_config])
+        self.zk.connect('localhost:2181')
+        self.hostname = socket.gethostname()
 
         self.provisioned_requests = []
         # This class implements the scheduler methods zuul.nodepool
diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py
index 22cf331..ee9a0b0 100644
--- a/tests/unit/test_connection.py
+++ b/tests/unit/test_connection.py
@@ -13,7 +13,6 @@
 # under the License.
 
 import sqlalchemy as sa
-from unittest import skip
 
 from tests.base import ZuulTestCase, ZuulDBTestCase
 
@@ -57,45 +56,44 @@
         self.assertEqual(B.patchsets[-1]['approvals'][0]['by']['username'],
                          'civoter')
 
-    def _test_sql_tables_created(self, metadata_table=None):
+
+class TestSQLConnection(ZuulDBTestCase):
+    config_file = 'zuul-sql-driver.conf'
+    tenant_config_file = 'config/sql-driver/main.yaml'
+
+    def test_sql_tables_created(self, metadata_table=None):
         "Test the tables for storing results are created properly"
         buildset_table = 'zuul_buildset'
         build_table = 'zuul_build'
 
         insp = sa.engine.reflection.Inspector(
-            self.connections['resultsdb'].engine)
+            self.connections.connections['resultsdb'].engine)
 
         self.assertEqual(9, len(insp.get_columns(buildset_table)))
         self.assertEqual(10, len(insp.get_columns(build_table)))
 
-    @skip("Disabled for early v3 development")
-    def test_sql_tables_created(self):
-        "Test the default table is created"
-        self.config.set('zuul', 'layout_config',
-                        'tests/fixtures/layout-sql-reporter.yaml')
-        self.sched.reconfigure(self.config)
-        self._test_sql_tables_created()
-
-    def _test_sql_results(self):
+    def test_sql_results(self):
         "Test results are entered into an sql table"
         # Grab the sa tables
+        tenant = self.sched.abide.tenants.get('tenant-one')
         reporter = _get_reporter_from_connection_name(
-            self.sched.layout.pipelines['check'].success_actions,
+            tenant.layout.pipelines['check'].success_actions,
             'resultsdb'
         )
 
         # Add a success result
-        A = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'A')
-        self.fake_review_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
         self.waitUntilSettled()
 
         # Add a failed result for a negative score
-        B = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'B')
-        self.worker.addFailTest('project-test1', B)
-        self.fake_review_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+        B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+
+        self.executor_server.failJob('project-test1', B)
+        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
         self.waitUntilSettled()
 
-        conn = self.connections['resultsdb'].engine.connect()
+        conn = self.connections.connections['resultsdb'].engine.connect()
         result = conn.execute(
             sa.sql.select([reporter.connection.zuul_buildset_table]))
 
@@ -122,7 +120,7 @@
         # Check the first result, which should be the project-merge job
         self.assertEqual('project-merge', buildset0_builds[0]['job_name'])
         self.assertEqual("SUCCESS", buildset0_builds[0]['result'])
-        self.assertEqual('http://logs.example.com/1/1/check/project-merge/0',
+        self.assertEqual('https://server/job/project-merge/0/',
                          buildset0_builds[0]['log_url'])
 
         self.assertEqual('check', buildset1['pipeline'])
@@ -144,42 +142,33 @@
         # which failed
         self.assertEqual('project-test1', buildset1_builds[-2]['job_name'])
         self.assertEqual("FAILURE", buildset1_builds[-2]['result'])
-        self.assertEqual('http://logs.example.com/2/1/check/project-test1/4',
+        self.assertEqual('https://server/job/project-test1/0/',
                          buildset1_builds[-2]['log_url'])
 
-    @skip("Disabled for early v3 development")
-    def test_sql_results(self):
-        "Test results are entered into the default sql table"
-        self.config.set('zuul', 'layout_config',
-                        'tests/fixtures/layout-sql-reporter.yaml')
-        self.sched.reconfigure(self.config)
-        self._test_sql_results()
-
-    @skip("Disabled for early v3 development")
     def test_multiple_sql_connections(self):
         "Test putting results in different databases"
-        self.config.set('zuul', 'layout_config',
-                        'tests/fixtures/layout-sql-reporter.yaml')
-        self.sched.reconfigure(self.config)
+        self.updateConfigLayout(
+            'tests/fixtures/layout-sql-reporter.yaml')
 
         # Add a successful result
-        A = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'A')
-        self.fake_review_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
         self.waitUntilSettled()
 
         # Add a failed result
-        B = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'B')
-        self.worker.addFailTest('project-test1', B)
-        self.fake_review_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+        B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+        self.executor_server.failJob('project-test1', B)
+        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
         self.waitUntilSettled()
 
         # Grab the sa tables for resultsdb
+        tenant = self.sched.abide.tenants.get('tenant-one')
         reporter1 = _get_reporter_from_connection_name(
-            self.sched.layout.pipelines['check'].success_actions,
+            tenant.layout.pipelines['check'].success_actions,
             'resultsdb'
         )
 
-        conn = self.connections['resultsdb'].engine.connect()
+        conn = self.connections.connections['resultsdb'].engine.connect()
         buildsets_resultsdb = conn.execute(sa.sql.select(
             [reporter1.connection.zuul_buildset_table])).fetchall()
         # Should have been 2 buildset reported to the resultsdb (both success
@@ -196,11 +185,12 @@
 
         # Grab the sa tables for resultsdb_failures
         reporter2 = _get_reporter_from_connection_name(
-            self.sched.layout.pipelines['check'].failure_actions,
+            tenant.layout.pipelines['check'].failure_actions,
             'resultsdb_failures'
         )
 
-        conn = self.connections['resultsdb_failures'].engine.connect()
+        conn = self.connections.connections['resultsdb_failures'].\
+            engine.connect()
         buildsets_resultsdb_failures = conn.execute(sa.sql.select(
             [reporter2.connection.zuul_buildset_table])).fetchall()
         # The failure db should only have 1 buildset failed
@@ -217,10 +207,9 @@
 
 
 class TestConnectionsBadSQL(ZuulDBTestCase):
-    def setup_config(self, config_file='zuul-connections-bad-sql.conf'):
-        super(TestConnectionsBadSQL, self).setup_config(config_file)
+    config_file = 'zuul-sql-driver-bad.conf'
+    tenant_config_file = 'config/sql-driver/main.yaml'
 
-    @skip("Disabled for early v3 development")
     def test_unable_to_connect(self):
         "Test the SQL reporter fails gracefully when unable to connect"
         self.config.set('zuul', 'layout_config',
@@ -229,8 +218,8 @@
 
         # Trigger a reporter. If no errors are raised, the reporter has been
         # disabled correctly
-        A = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'A')
-        self.fake_review_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
         self.waitUntilSettled()
 
 
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index a923ff1..7de9be0 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -1950,28 +1950,25 @@
         self.assertReportedStat('test-timing', '3|ms')
         self.assertReportedStat('test-gauge', '12|g')
 
-    @skip("Disabled for early v3 development")
     def test_stuck_job_cleanup(self):
         "Test that pending jobs are cleaned up if removed from layout"
-        # This job won't be registered at startup because it is not in
-        # the standard layout, but we need it to already be registerd
-        # for when we reconfigure, as that is when Zuul will attempt
-        # to run the new job.
-        self.worker.registerFunction('build:gate-noop')
+
+        # We want to hold the project-merge job that the fake change enqueues
         self.gearman_server.hold_jobs_in_queue = True
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
         A.addApproval('code-review', 2)
         self.fake_gerrit.addEvent(A.addApproval('approved', 1))
         self.waitUntilSettled()
+        # The assertion is that we have one job in the queue, project-merge
         self.assertEqual(len(self.gearman_server.getQueue()), 1)
 
-        self.updateConfigLayout(
-            'tests/fixtures/layout-no-jobs.yaml')
+        self.commitLayoutUpdate('common-config', 'layout-no-jobs')
         self.sched.reconfigure(self.config)
         self.waitUntilSettled()
 
         self.gearman_server.release('gate-noop')
         self.waitUntilSettled()
+        # asserting that project-merge is removed from queue
         self.assertEqual(len(self.gearman_server.getQueue()), 0)
         self.assertTrue(self.sched._areAllBuildsComplete())
 
@@ -2180,14 +2177,6 @@
         self.assertEqual('https://server/job/project-test2/0/',
                          status_jobs[2]['report_url'])
 
-    @skip("Disabled for early v3 development")
-    def test_merging_queues(self):
-        "Test that transitively-connected change queues are merged"
-        self.updateConfigLayout(
-            'tests/fixtures/layout-merge-queues.yaml')
-        self.sched.reconfigure(self.config)
-        self.assertEqual(len(self.sched.layout.pipelines['gate'].queues), 1)
-
     def test_mutex(self):
         "Test job mutexes"
         self.updateConfigLayout('layout-mutex')
@@ -2814,7 +2803,6 @@
             for q in p['change_queues']:
                 for head in q['heads']:
                     for change in head:
-                        self.assertEqual(change['id'], None)
                         for job in change['jobs']:
                             status_jobs.add(job['name'])
         self.assertIn('project-bitrot-stable-old', status_jobs)
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index efd0b13..678b957 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -267,7 +267,7 @@
         self.assertEqual(build.result, 'ABORTED')
         build = self.getJobFromHistory('faillocal')
         self.assertEqual(build.result, 'FAILURE')
-        build = self.getJobFromHistory('nodepool')
+        build = self.getJobFromHistory('check-vars')
         self.assertEqual(build.result, 'SUCCESS')
         build = self.getJobFromHistory('python27')
         self.assertEqual(build.result, 'SUCCESS')
diff --git a/zuul/ansible/callback/zuul_stream.py b/zuul/ansible/callback/zuul_stream.py
index 9b8bccd..e6b3461 100644
--- a/zuul/ansible/callback/zuul_stream.py
+++ b/zuul/ansible/callback/zuul_stream.py
@@ -83,7 +83,16 @@
             self._print_task_banner(task)
         if task.action == 'command':
             play_vars = self._play._variable_manager._hostvars
-            for host in self._play.hosts:
+
+            hosts = self._play.hosts
+            if 'all' in hosts:
+                # NOTE(jamielennox): play.hosts is purely the list of hosts
+                # that was provided not interpretted by inventory. We don't
+                # have inventory access here but we can assume that 'all' is
+                # everything in hostvars.
+                hosts = play_vars.keys()
+
+            for host in hosts:
                 ip = play_vars[host]['ansible_host']
                 daemon_stamp = self._daemon_stamp % host
                 if not os.path.exists(daemon_stamp):
diff --git a/zuul/change_matcher.py b/zuul/change_matcher.py
index 845ba1c..1da1d2c 100644
--- a/zuul/change_matcher.py
+++ b/zuul/change_matcher.py
@@ -62,7 +62,8 @@
     def matches(self, change):
         return (
             (hasattr(change, 'branch') and self.regex.match(change.branch)) or
-            (hasattr(change, 'ref') and self.regex.match(change.ref))
+            (hasattr(change, 'ref') and
+             change.ref is not None and self.regex.match(change.ref))
         )
 
 
diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py
index 286006f..514aa1f 100644
--- a/zuul/driver/gerrit/gerritconnection.py
+++ b/zuul/driver/gerrit/gerritconnection.py
@@ -26,7 +26,7 @@
 import voluptuous as v
 
 from zuul.connection import BaseConnection
-from zuul.model import TriggerEvent, Project, Change, Ref, NullChange
+from zuul.model import TriggerEvent, Project, Change, Ref
 from zuul import exceptions
 
 
@@ -108,6 +108,7 @@
             'reviewer-added': 'reviewer',  # Gerrit 2.5/2.6
             'ref-replicated': None,
             'ref-replication-done': None,
+            'ref-replication-scheduled': None,
             'topic-changed': 'changer',
         }
         event.account = None
@@ -292,7 +293,13 @@
             change.url = self._getGitwebUrl(project, sha=event.newrev)
         else:
             project = self.getProject(event.project_name)
-            change = NullChange(project)
+            change = Ref(project)
+            branch = event.branch or 'master'
+            change.ref = 'refs/heads/%s' % branch
+            refs = self.getInfoRefs(project)
+            change.oldrev = refs[change.ref]
+            change.newrev = refs[change.ref]
+            change.url = self._getGitwebUrl(project, sha=change.newrev)
         return change
 
     def _getChange(self, number, patchset, refresh=False, history=None):
diff --git a/zuul/driver/sql/__init__.py b/zuul/driver/sql/__init__.py
new file mode 100644
index 0000000..a5f8923
--- /dev/null
+++ b/zuul/driver/sql/__init__.py
@@ -0,0 +1,33 @@
+# 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.
+
+from zuul.driver import Driver, ConnectionInterface, ReporterInterface
+import sqlconnection
+import sqlreporter
+
+
+class SQLDriver(Driver, ConnectionInterface, ReporterInterface):
+    name = 'sql'
+
+    def registerScheduler(self, scheduler):
+        self.sched = scheduler
+
+    def getConnection(self, name, config):
+        return sqlconnection.SQLConnection(self, name, config)
+
+    def getReporter(self, connection, config=None):
+        return sqlreporter.SQLReporter(self, connection, config)
+
+    def getReporterSchema(self):
+        return sqlreporter.getSchema()
diff --git a/zuul/alembic_reporter.ini b/zuul/driver/sql/alembic_reporter.ini
similarity index 97%
rename from zuul/alembic_reporter.ini
rename to zuul/driver/sql/alembic_reporter.ini
index b7f787c..0c59505 100644
--- a/zuul/alembic_reporter.ini
+++ b/zuul/driver/sql/alembic_reporter.ini
@@ -4,7 +4,7 @@
 # path to migration scripts
 # NOTE(jhesketh): We may use alembic for other db components of zuul in the
 # future. Use a sub-folder for the reporters own versions.
-script_location = alembic/sql_reporter
+script_location = alembic_reporter
 
 # template used to generate migration files
 # file_template = %%(rev)s_%%(slug)s
diff --git a/zuul/alembic/sql_reporter/README b/zuul/driver/sql/alembic_reporter/README
similarity index 100%
rename from zuul/alembic/sql_reporter/README
rename to zuul/driver/sql/alembic_reporter/README
diff --git a/zuul/alembic/sql_reporter/env.py b/zuul/driver/sql/alembic_reporter/env.py
similarity index 100%
rename from zuul/alembic/sql_reporter/env.py
rename to zuul/driver/sql/alembic_reporter/env.py
diff --git a/zuul/alembic/sql_reporter/script.py.mako b/zuul/driver/sql/alembic_reporter/script.py.mako
similarity index 100%
rename from zuul/alembic/sql_reporter/script.py.mako
rename to zuul/driver/sql/alembic_reporter/script.py.mako
diff --git a/zuul/alembic/sql_reporter/versions/4d3ebd7f06b9_set_up_initial_reporter_tables.py b/zuul/driver/sql/alembic_reporter/versions/4d3ebd7f06b9_set_up_initial_reporter_tables.py
similarity index 100%
rename from zuul/alembic/sql_reporter/versions/4d3ebd7f06b9_set_up_initial_reporter_tables.py
rename to zuul/driver/sql/alembic_reporter/versions/4d3ebd7f06b9_set_up_initial_reporter_tables.py
diff --git a/zuul/connection/sql.py b/zuul/driver/sql/sqlconnection.py
similarity index 92%
rename from zuul/connection/sql.py
rename to zuul/driver/sql/sqlconnection.py
index 479ee44..69e53df 100644
--- a/zuul/connection/sql.py
+++ b/zuul/driver/sql/sqlconnection.py
@@ -29,9 +29,10 @@
     driver_name = 'sql'
     log = logging.getLogger("connection.sql")
 
-    def __init__(self, connection_name, connection_config):
+    def __init__(self, driver, connection_name, connection_config):
 
-        super(SQLConnection, self).__init__(connection_name, connection_config)
+        super(SQLConnection, self).__init__(driver, connection_name,
+                                            connection_config)
 
         self.dburi = None
         self.engine = None
@@ -61,7 +62,7 @@
 
             config = alembic.config.Config()
             config.set_main_option("script_location",
-                                   "zuul:alembic/sql_reporter")
+                                   "zuul:driver/sql/alembic_reporter")
             config.set_main_option("sqlalchemy.url",
                                    self.connection_config.get('dburi'))
 
diff --git a/zuul/reporter/sql.py b/zuul/driver/sql/sqlreporter.py
similarity index 85%
rename from zuul/reporter/sql.py
rename to zuul/driver/sql/sqlreporter.py
index b663a59..2129f53 100644
--- a/zuul/reporter/sql.py
+++ b/zuul/driver/sql/sqlreporter.py
@@ -25,10 +25,10 @@
     name = 'sql'
     log = logging.getLogger("zuul.reporter.mysql.SQLReporter")
 
-    def __init__(self, reporter_config={}, sched=None, connection=None):
+    def __init__(self, driver, connection, config={}):
         super(SQLReporter, self).__init__(
-            reporter_config, sched, connection)
-        self.result_score = reporter_config.get('score', None)
+            driver, connection, config)
+        self.result_score = config.get('score', None)
 
     def report(self, source, pipeline, item):
         """Create an entry into a database."""
@@ -37,13 +37,12 @@
             self.log.warn("SQL reporter (%s) is disabled " % self)
             return
 
-        if self.sched.config.has_option('zuul', 'url_pattern'):
-            url_pattern = self.sched.config.get('zuul', 'url_pattern')
+        if self.driver.sched.config.has_option('zuul', 'url_pattern'):
+            url_pattern = self.driver.sched.config.get('zuul', 'url_pattern')
         else:
             url_pattern = None
 
-        score = self.reporter_config['score']\
-            if 'score' in self.reporter_config else 0
+        score = self.config.get('score', 0)
 
         with self.connection.engine.begin() as conn:
             buildset_ins = self.connection.zuul_buildset_table.insert().values(
@@ -60,7 +59,7 @@
             buildset_ins_result = conn.execute(buildset_ins)
             build_inserts = []
 
-            for job in pipeline.getJobs(item):
+            for job in item.getJobs():
                 build = item.current_build_set.getBuild(job.name)
                 if not build:
                     # build hasn't began. The sql reporter can only send back
diff --git a/zuul/driver/zuul/__init__.py b/zuul/driver/zuul/__init__.py
index 1bc0ee9..47ccec0 100644
--- a/zuul/driver/zuul/__init__.py
+++ b/zuul/driver/zuul/__init__.py
@@ -87,7 +87,7 @@
     def _createParentChangeEnqueuedEvents(self, change, pipeline):
         self.log.debug("Checking for changes needing %s:" % change)
         if not hasattr(change, 'needed_by_changes'):
-            self.log.debug("  Changeish does not support dependencies")
+            self.log.debug("  %s does not support dependencies" % type(change))
             return
         for needs in change.needed_by_changes:
             self._createParentChangeEnqueuedEvent(needs, pipeline)
diff --git a/zuul/executor/client.py b/zuul/executor/client.py
index 9b39e78..20bc3ef 100644
--- a/zuul/executor/client.py
+++ b/zuul/executor/client.py
@@ -258,7 +258,7 @@
             params['ZUUL_CHANGE_IDS'] = zuul_changes
             params['ZUUL_CHANGE'] = str(item.change.number)
             params['ZUUL_PATCHSET'] = str(item.change.patchset)
-        if hasattr(item.change, 'ref'):
+        if hasattr(item.change, 'ref') and item.change.ref is not None:
             params['ZUUL_REFNAME'] = item.change.ref
             params['ZUUL_OLDREV'] = item.change.oldrev
             params['ZUUL_NEWREV'] = item.change.newrev
@@ -312,6 +312,7 @@
         for node in item.current_build_set.getJobNodeSet(job.name).getNodes():
             nodes.append(dict(name=node.name, image=node.image,
                               az=node.az,
+                              host_keys=node.host_keys,
                               provider=node.provider,
                               region=node.region,
                               public_ipv6=node.public_ipv6,
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index d0741bb..60b30c7 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -641,11 +641,15 @@
             ip = node.get('public_ipv4')
             if not ip:
                 ip = node.get('public_ipv6')
-            hosts.append((node['name'], dict(
+            host_vars = dict(
                 ansible_host=ip,
                 nodepool_az=node.get('az'),
                 nodepool_provider=node.get('provider'),
-                nodepool_region=node.get('region'))))
+                nodepool_region=node.get('region'))
+            hosts.append(dict(
+                name=node['name'],
+                host_vars=host_vars,
+                host_keys=node.get('host_keys')))
         return hosts
 
     def _blockPluginDirs(self, path):
@@ -806,21 +810,26 @@
         self.jobdir.roles_path.append(role_path)
 
     def prepareAnsibleFiles(self, args):
+        keys = []
         with open(self.jobdir.inventory, 'w') as inventory:
-            for host_name, host_vars in self.getHostList(args):
-                inventory.write(host_name)
-                for k, v in host_vars.items():
+            for item in self.getHostList(args):
+                inventory.write(item['name'])
+                for k, v in item['host_vars'].items():
                     inventory.write(' %s=%s' % (k, v))
                 inventory.write('\n')
-                if 'ansible_host' in host_vars:
-                    os.system("ssh-keyscan %s >> %s" % (
-                        host_vars['ansible_host'],
-                        self.jobdir.known_hosts))
+                for key in item['host_keys']:
+                    keys.append(key)
+
+        with open(self.jobdir.known_hosts, 'w') as known_hosts:
+            for key in keys:
+                known_hosts.write('%s\n' % key)
 
         with open(self.jobdir.vars, 'w') as vars_yaml:
             zuul_vars = dict(args['vars'])
-            zuul_vars['zuul']['executor'] = dict(src_root=self.jobdir.src_root,
-                                                 log_root=self.jobdir.log_root)
+            zuul_vars['zuul']['executor'] = dict(
+                hostname=self.executor_server.hostname,
+                src_root=self.jobdir.src_root,
+                log_root=self.jobdir.log_root)
             vars_yaml.write(
                 yaml.safe_dump(zuul_vars, default_flow_style=False))
         self.writeAnsibleConfig(self.jobdir.untrusted_config)
@@ -843,6 +852,7 @@
             if self.jobdir.roles_path:
                 config.write('roles_path = %s\n' %
                              ':'.join(self.jobdir.roles_path))
+            config.write('command_warnings = False\n')
             config.write('callback_plugins = %s\n'
                          % self.executor_server.callback_dir)
             config.write('stdout_callback = zuul_stream\n')
diff --git a/zuul/lib/connections.py b/zuul/lib/connections.py
index 27d8a1b..9964ba9 100644
--- a/zuul/lib/connections.py
+++ b/zuul/lib/connections.py
@@ -20,6 +20,7 @@
 import zuul.driver.git
 import zuul.driver.smtp
 import zuul.driver.timer
+import zuul.driver.sql
 from zuul.connection import BaseConnection
 
 
@@ -41,6 +42,7 @@
         self.registerDriver(zuul.driver.git.GitDriver())
         self.registerDriver(zuul.driver.smtp.SMTPDriver())
         self.registerDriver(zuul.driver.timer.TimerDriver())
+        self.registerDriver(zuul.driver.sql.SQLDriver())
 
     def registerDriver(self, driver):
         if driver.name in self.drivers:
diff --git a/zuul/manager/__init__.py b/zuul/manager/__init__.py
index 58ad607..32f0cbb 100644
--- a/zuul/manager/__init__.py
+++ b/zuul/manager/__init__.py
@@ -13,7 +13,6 @@
 import logging
 
 from zuul import exceptions
-from zuul.model import NullChange
 
 
 class DynamicChangeQueueContextManager(object):
@@ -682,9 +681,8 @@
             build_set.commit = event.commit
             build_set.files.setFiles(event.files)
         elif event.updated:
-            if not isinstance(item.change, NullChange):
-                build_set.commit = item.change.newrev
-        if not build_set.commit and not isinstance(item.change, NullChange):
+            build_set.commit = item.change.newrev
+        if not build_set.commit:
             self.log.info("Unable to merge change %s" % item.change)
             item.setUnableToMerge()
 
diff --git a/zuul/manager/dependent.py b/zuul/manager/dependent.py
index f5fa579..4c48568 100644
--- a/zuul/manager/dependent.py
+++ b/zuul/manager/dependent.py
@@ -89,7 +89,7 @@
         to_enqueue = []
         self.log.debug("Checking for changes needing %s:" % change)
         if not hasattr(change, 'needed_by_changes'):
-            self.log.debug("  Changeish does not support dependencies")
+            self.log.debug("  %s does not support dependencies" % type(change))
             return
         for other_change in change.needed_by_changes:
             with self.getChangeQueue(other_change) as other_change_queue:
@@ -133,7 +133,7 @@
         # Return true if okay to proceed enqueing this change,
         # false if the change should not be enqueued.
         if not hasattr(change, 'needs_changes'):
-            self.log.debug("  Changeish does not support dependencies")
+            self.log.debug("  %s does not support dependencies" % type(change))
             return True
         if not change.needs_changes:
             self.log.debug("  No changes needed")
diff --git a/zuul/manager/independent.py b/zuul/manager/independent.py
index 3d28327..9e2a7d6 100644
--- a/zuul/manager/independent.py
+++ b/zuul/manager/independent.py
@@ -62,7 +62,7 @@
         # Return true if okay to proceed enqueing this change,
         # false if the change should not be enqueued.
         if not hasattr(change, 'needs_changes'):
-            self.log.debug("  Changeish does not support dependencies")
+            self.log.debug("  %s does not support dependencies" % type(change))
             return True
         if not change.needs_changes:
             self.log.debug("  No changes needed")
diff --git a/zuul/model.py b/zuul/model.py
index 846463b..0119fa1 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -1646,103 +1646,22 @@
         return ret
 
 
-class Changeish(object):
-    """Base class for Change and Ref."""
+class Ref(object):
+    """An existing state of a Project."""
 
     def __init__(self, project):
         self.project = project
-
-    def getBasePath(self):
-        base_path = ''
-        if hasattr(self, 'refspec'):
-            base_path = "%s/%s/%s" % (
-                self.number[-2:], self.number, self.patchset)
-        elif hasattr(self, 'ref'):
-            base_path = "%s/%s" % (self.newrev[:2], self.newrev)
-
-        return base_path
-
-    def equals(self, other):
-        raise NotImplementedError()
-
-    def isUpdateOf(self, other):
-        raise NotImplementedError()
-
-    def filterJobs(self, jobs):
-        return filter(lambda job: job.changeMatches(self), jobs)
-
-    def getRelatedChanges(self):
-        return set()
-
-    def updatesConfig(self):
-        return False
-
-
-class Change(Changeish):
-    """A proposed new state for a Project."""
-    def __init__(self, project):
-        super(Change, self).__init__(project)
-        self.branch = None
-        self.number = None
-        self.url = None
-        self.patchset = None
-        self.refspec = None
-
-        self.files = []
-        self.needs_changes = []
-        self.needed_by_changes = []
-        self.is_current_patchset = True
-        self.can_merge = False
-        self.is_merged = False
-        self.failed_to_merge = False
-        self.approvals = []
-        self.open = None
-        self.status = None
-        self.owner = None
-
-    def _id(self):
-        return '%s,%s' % (self.number, self.patchset)
-
-    def __repr__(self):
-        return '<Change 0x%x %s>' % (id(self), self._id())
-
-    def equals(self, other):
-        if self.number == other.number and self.patchset == other.patchset:
-            return True
-        return False
-
-    def isUpdateOf(self, other):
-        if ((hasattr(other, 'number') and self.number == other.number) and
-            (hasattr(other, 'patchset') and
-             self.patchset is not None and
-             other.patchset is not None and
-             int(self.patchset) > int(other.patchset))):
-            return True
-        return False
-
-    def getRelatedChanges(self):
-        related = set()
-        for c in self.needs_changes:
-            related.add(c)
-        for c in self.needed_by_changes:
-            related.add(c)
-            related.update(c.getRelatedChanges())
-        return related
-
-    def updatesConfig(self):
-        if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
-            return True
-        return False
-
-
-class Ref(Changeish):
-    """An existing state of a Project."""
-    def __init__(self, project):
-        super(Ref, self).__init__(project)
         self.ref = None
         self.oldrev = None
         self.newrev = None
 
+    def getBasePath(self):
+        base_path = ''
+        if hasattr(self, 'ref'):
+            base_path = "%s/%s" % (self.newrev[:2], self.newrev)
+
+        return base_path
+
     def _id(self):
         return self.newrev
 
@@ -1771,23 +1690,76 @@
     def isUpdateOf(self, other):
         return False
 
+    def filterJobs(self, jobs):
+        return filter(lambda job: job.changeMatches(self), jobs)
 
-class NullChange(Changeish):
-    # TODOv3(jeblair): remove this in favor of enqueueing Refs (eg
-    # current master) instead.
-    def __repr__(self):
-        return '<NullChange for %s>' % (self.project)
+    def getRelatedChanges(self):
+        return set()
+
+    def updatesConfig(self):
+        return False
+
+
+class Change(Ref):
+    """A proposed new state for a Project."""
+    def __init__(self, project):
+        super(Change, self).__init__(project)
+        self.branch = None
+        self.number = None
+        self.url = None
+        self.patchset = None
+        self.refspec = None
+
+        self.files = []
+        self.needs_changes = []
+        self.needed_by_changes = []
+        self.is_current_patchset = True
+        self.can_merge = False
+        self.is_merged = False
+        self.failed_to_merge = False
+        self.approvals = []
+        self.open = None
+        self.status = None
+        self.owner = None
 
     def _id(self):
-        return None
+        return '%s,%s' % (self.number, self.patchset)
+
+    def __repr__(self):
+        return '<Change 0x%x %s>' % (id(self), self._id())
+
+    def getBasePath(self):
+        if hasattr(self, 'refspec'):
+            return "%s/%s/%s" % (
+                self.number[-2:], self.number, self.patchset)
+        return super(Change, self).getBasePath()
 
     def equals(self, other):
-        if (self.project == other.project
-            and other._id() is None):
+        if self.number == other.number and self.patchset == other.patchset:
             return True
         return False
 
     def isUpdateOf(self, other):
+        if ((hasattr(other, 'number') and self.number == other.number) and
+            (hasattr(other, 'patchset') and
+             self.patchset is not None and
+             other.patchset is not None and
+             int(self.patchset) > int(other.patchset))):
+            return True
+        return False
+
+    def getRelatedChanges(self):
+        related = set()
+        for c in self.needs_changes:
+            related.add(c)
+        for c in self.needed_by_changes:
+            related.add(c)
+            related.update(c.getRelatedChanges())
+        return related
+
+    def updatesConfig(self):
+        if 'zuul.yaml' in self.files or '.zuul.yaml' in self.files:
+            return True
         return False
 
 
diff --git a/zuul/zk.py b/zuul/zk.py
index 2009945..5cd7bee 100644
--- a/zuul/zk.py
+++ b/zuul/zk.py
@@ -34,36 +34,6 @@
     pass
 
 
-class ZooKeeperConnectionConfig(object):
-    '''
-    Represents the connection parameters for a ZooKeeper server.
-    '''
-
-    def __eq__(self, other):
-        if isinstance(other, ZooKeeperConnectionConfig):
-            if other.__dict__ == self.__dict__:
-                return True
-        return False
-
-    def __init__(self, host, port=2181, chroot=None):
-        '''Initialize the ZooKeeperConnectionConfig object.
-
-        :param str host: The hostname of the ZooKeeper server.
-        :param int port: The port on which ZooKeeper is listening.
-            Optional, default: 2181.
-        :param str chroot: A chroot for this connection.  All
-            ZooKeeper nodes will be underneath this root path.
-            Optional, default: None.
-
-        (one per server) defining the ZooKeeper cluster servers. Only
-        the 'host' attribute is required.'.
-
-        '''
-        self.host = host
-        self.port = port
-        self.chroot = chroot or ''
-
-
 class ZooKeeper(object):
     '''
     Class implementing the ZooKeeper interface.
@@ -127,21 +97,20 @@
     def resetLostFlag(self):
         self._became_lost = False
 
-    def connect(self, host_list, read_only=False):
+    def connect(self, hosts, read_only=False):
         '''
         Establish a connection with ZooKeeper cluster.
 
         Convenience method if a pre-existing ZooKeeper connection is not
         supplied to the ZooKeeper object at instantiation time.
 
-        :param list host_list: A list of
-            :py:class:`~nodepool.zk.ZooKeeperConnectionConfig` objects
-            (one per server) defining the ZooKeeper cluster servers.
+        :param str hosts: Comma-separated list of hosts to connect to (e.g.
+            127.0.0.1:2181,127.0.0.1:2182,[::1]:2183).
         :param bool read_only: If True, establishes a read-only connection.
 
         '''
         if self.client is None:
-            self.client = KazooClient(hosts=host_list, read_only=read_only)
+            self.client = KazooClient(hosts=hosts, read_only=read_only)
             self.client.add_listener(self._connection_listener)
             self.client.start()
 
@@ -157,16 +126,15 @@
             self.client.close()
             self.client = None
 
-    def resetHosts(self, host_list):
+    def resetHosts(self, hosts):
         '''
         Reset the ZooKeeper cluster connection host list.
 
-        :param list host_list: A list of
-            :py:class:`~nodepool.zk.ZooKeeperConnectionConfig` objects
-            (one per server) defining the ZooKeeper cluster servers.
+        :param str hosts: Comma-separated list of hosts to connect to (e.g.
+            127.0.0.1:2181,127.0.0.1:2182,[::1]:2183).
         '''
         if self.client is not None:
-            self.client.set_hosts(hosts=host_list)
+            self.client.set_hosts(hosts=hosts)
 
     def submitNodeRequest(self, node_request, watcher):
         '''