diff --git a/.zuul.yaml b/.zuul.yaml
index 7f39baa7d0..9c8e3a07fe 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1011,6 +1011,32 @@
       - playbooks/roles/zuul-preview/
       - testinfra/test_zuul_preview.py
 
+- job:
+    name: system-config-run-review-dev
+    parent: system-config-run
+    description: |
+      Run the playbook for gerrit (in a container).
+    nodeset:
+      nodes:
+        - name: bridge.openstack.org
+          label: ubuntu-bionic
+        - name: review-dev01.openstack.org
+          label: ubuntu-bionic
+    vars:
+      run_playbooks:
+        - playbooks/service-review-dev.yaml
+    host-vars:
+      review-dev01.openstack.org:
+        host_copy_output:
+          '/home/gerrit2/review_site/etc': logs
+          '/home/gerrit2/review_site/logs': logs
+    files:
+      - playbooks/group_vars/review-dev.yaml
+      - ^playbooks/host_vars/review-dev\d+.opendev.org.yaml
+      - playbooks/zuul/templates/group_vars/review.yaml.j2
+      - playbooks/roles/gerrit/
+      - testinfra/test_gerrit.py
+
 - job:
     name: infra-prod-playbook
     description: |
@@ -1068,6 +1094,7 @@
                 soft: true
               - name: system-config-build-image-haproxy-statsd
                 soft: true
+        - system-config-run-review-dev
         - system-config-run-zuul-preview
         - system-config-run-letsencrypt
         - system-config-build-image-bazel
@@ -1127,6 +1154,7 @@
                 soft: true
               - name: system-config-upload-image-haproxy-statsd
                 soft: true
+        - system-config-run-review-dev
         - system-config-run-zuul-preview
         - system-config-run-letsencrypt
         - system-config-upload-image-bazel
diff --git a/modules/openstack_project/files/gerrit/web_server.py b/modules/openstack_project/files/gerrit/web_server.py
deleted file mode 100755
index 8c61a122b6..0000000000
--- a/modules/openstack_project/files/gerrit/web_server.py
+++ /dev/null
@@ -1,178 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright 2015 Hewlett-Packard Development Company, L.P.
-#
-# 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.
-
-"""
-This is a simple test server that serves up the web content locally
-as if it was a working remote server. It also proxies all the live
-date/*.json files into the local test server, so that the Ajax async
-loading works without hitting Cross Site Scripting violations.
-"""
-
-import argparse
-import BaseHTTPServer
-import os.path
-import urllib2
-
-import requests
-
-# Values for these set via cli defaults
-GERRIT_UPSTREAM = ""
-ZUUL_UPSTREAM = ""
-
-
-def replace_urls(line, port):
-    line = line.replace(
-        GERRIT_UPSTREAM,
-        "http://localhost:%s" % port)
-    line = line.replace(
-        ZUUL_UPSTREAM,
-        "http://localhost:%s" % port)
-    return line
-
-
-class GerritHandler(BaseHTTPServer.BaseHTTPRequestHandler):
-    """A request handler to create a magic local Gerrit server"""
-
-    def do_POST(self):
-        data = self.rfile.read(int(self.headers['content-length']))
-        headers = {}
-        # we need to trim some of the local headers in order for this
-        # request to remain valid.
-        for header in self.headers:
-            if header not in ("host", "origin", "connection"):
-                headers[header] = self.headers[header]
-        resp = requests.post("%s%s" %
-                             (GERRIT_UPSTREAM, self.path),
-                             headers=headers,
-                             data=data)
-
-        # Process request back to client
-        self.send_response(resp.status_code)
-        for header in resp.headers:
-            # Requests has now decoded the response so it's no longer
-            # a gzip stream, which also means content-length is
-            # wrong. So we remove content-encoding, then drop
-            # content-length because if provided Gerrit strictly uses
-            # it for reads. We also drop all the keep-alive related
-            # headers, our server doesn't do that.
-            if header not in ("connection", "content-length",
-                              "keep-alive", "content-encoding"):
-                self.send_header(header, resp.headers[header])
-        self.end_headers()
-        self.wfile.write(resp.text)
-
-    def do_GET(self):
-        # possible local file path
-        local_path = self.path.replace('/static/', '').split('?')[0]
-
-        # if the file exists locally, we'll serve it up directly
-        if os.path.isfile(local_path):
-            self.send_response(200, "Success")
-            self.end_headers()
-            with open(local_path) as f:
-                for line in f.readlines():
-                    line = replace_urls(line, self.server.server_port)
-                    self.wfile.write(line)
-            print "Loaded from local override"
-            return
-
-        # First we'll look for a zuul status call, /status doesn't map
-        # to gerrit so we can overload the localhost server for this.
-        if self.path.startswith("/status"):
-            try:
-                zuul_url = "%s%s" % (ZUUL_UPSTREAM, self.path)
-                # BUG(sdague): for some reason SSL connections to zuul
-                # from python 2.7 blow up with an SSL exception
-                zuul_url = zuul_url.replace('https', 'http')
-                response = urllib2.urlopen(zuul_url)
-                self.send_response(200, "Success")
-                for header in response.info():
-                    # need to reset content-length otherwise jquery complains
-                    if header not in ("connection", "content-length",
-                                      "keep-alive", "content-encoding"):
-                        self.send_header(header, response.info()[header])
-                self.end_headers()
-
-                for line in response.readlines():
-                    line = replace_urls(line, self.server.server_port)
-                self.wfile.write(line)
-                return
-            except urllib2.HTTPError as e:
-                self.send_response(e.code)
-                self.end_headers()
-                self.wfile.write(e.read())
-                return
-            except urllib2.URLError as e:
-                print "URLError on %s" % (zuul_url)
-                print e
-
-        # If you've not built local data to test with, instead grab
-        # the data off the production server on the fly and serve it
-        # up from our server.
-        try:
-            response = urllib2.urlopen("%s%s" %
-                                       (GERRIT_UPSTREAM, self.path))
-            self.send_response(200, "Success")
-            for header in response.info():
-                self.send_header(header, response.info()[header])
-            self.end_headers()
-
-            for line in response.readlines():
-                line = replace_urls(line, self.server.server_port)
-                self.wfile.write(line)
-        except urllib2.HTTPError as e:
-            self.send_response(e.code)
-            self.end_headers()
-            self.wfile.write(e.read())
-
-
-def parse_opts():
-    parser = argparse.ArgumentParser(description=__doc__)
-    parser.add_argument('-p', '--port',
-                        help='port to bind to [default: 8001]',
-                        type=int,
-                        default=8001)
-    parser.add_argument('-z', '--zuul-url',
-                        help='url for zuul server',
-                        default="https://zuul.openstack.org")
-    parser.add_argument('-g', '--gerrit-url',
-                        help='url for gerrit server',
-                        default="https://review.opendev.org")
-    return parser.parse_args()
-
-
-def main():
-    global ZUUL_UPSTREAM
-    global GERRIT_UPSTREAM
-    opts = parse_opts()
-    ZUUL_UPSTREAM = opts.zuul_url
-    GERRIT_UPSTREAM = opts.gerrit_url
-    server_address = ('', opts.port)
-    httpd = BaseHTTPServer.HTTPServer(server_address, GerritHandler)
-
-    print "Test Server is running at http://localhost:%s" % opts.port
-    print "Ctrl-C to exit"
-    print
-
-    while True:
-        httpd.handle_request()
-
-if __name__ == '__main__':
-    try:
-        main()
-    except KeyboardInterrupt:
-        print "\n"
-        print "Thanks for testing! Please come again."
diff --git a/playbooks/roles/gerrit/README.rst b/playbooks/roles/gerrit/README.rst
new file mode 100644
index 0000000000..984e3c5c95
--- /dev/null
+++ b/playbooks/roles/gerrit/README.rst
@@ -0,0 +1 @@
+Run Gerrit.
diff --git a/playbooks/roles/gerrit/defaults/main.yaml b/playbooks/roles/gerrit/defaults/main.yaml
new file mode 100644
index 0000000000..2136969cf7
--- /dev/null
+++ b/playbooks/roles/gerrit/defaults/main.yaml
@@ -0,0 +1,5 @@
+gerrit_id: 3000
+gerrit_user_name: gerrit2
+gerrit_home_dir: /home/gerrit2
+gerrit_site_dir: "{{ gerrit_home_dir }}/review_site"
+gerrit_run_init: false
diff --git a/playbooks/roles/gerrit/files/cla.html b/playbooks/roles/gerrit/files/cla.html
new file mode 100644
index 0000000000..83f8688a7a
--- /dev/null
+++ b/playbooks/roles/gerrit/files/cla.html
@@ -0,0 +1,259 @@
+<html><body><div>
+
+<h1>
+OpenStack Project Individual Contributor License Agreement
+</h1>
+
+<!--
+This is the current OpenStack Project Individual Contributor License Agreement
+reformatted for HTML from the original "RevisedCLA.doc" with SHA1 sum
+0467dd893d276cefde614e063a363b995d67e5ee provided by Jonathan Bryce, Executive
+Director on behalf of The OpenStack Foundation on Monday, January 7, 2013. No
+textual content was changed except to replace quote marks with their strict
+ASCII equivalents, add the original LLC CLA text as a block quote, and restore
+a previously inapplicable statement (sentence #2 of paragraph #2) adapted from
+the Apache Software Foundation ICLA at his direction.
+-->
+
+<p><em>
+In order to clarify the intellectual property license granted with
+Contributions from any person or entity, the OpenStack Project (the "Project")
+must have a Contributor License Agreement ("Agreement") on file that has been
+signed by each Contributor, indicating agreement to the license terms below.
+This license is for your protection as a Contributor as well as the protection
+of OpenStack Foundation as Project manager (the "Project Manager") and the
+Project users; it does not change your rights to use your own Contributions for
+any other purpose.
+</em></p>
+
+<p><em>
+You accept and agree to the following terms and conditions for Your present and
+future Contributions submitted to the Project Manager. In return, the Project
+Manager shall not use Your Contributions in a way that is contrary to the
+public benefit or inconsistent with its nonprofit status and bylaws in effect
+at the time of the Contribution. Except for the license granted herein to the
+Project Manager and recipients of software distributed by the Project Manager,
+You reserve all right, title, and interest in and to Your Contributions.
+</em></p>
+
+<ol>
+
+<li><p><strong>
+Definitions.
+</strong>
+"You" (or "Your") shall mean the copyright owner or legal entity authorized by
+the copyright owner that is making this Agreement with the Project Manager. For
+legal entities, the entity making a Contribution and all other entities that
+control, are controlled by, or are under common control with that entity are
+considered to be a single Contributor. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the direction or
+management of such entity, whether by contract or otherwise, or (ii) ownership
+of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
+ownership of such entity. "Contribution" shall mean any original work of
+authorship, including any modifications or additions to an existing work, that
+is intentionally submitted by You to the Project Manager for inclusion in, or
+documentation of, any of the projects owned or managed by the Project Manager
+(the "Work"). For the purposes of this definition, "submitted" means any form of
+electronic, verbal, or written communication sent to the Project Manager or its
+representatives, including but not limited to communication on electronic
+mailing lists, source code control systems, and issue tracking systems that are
+managed by, or on behalf of, the Project Manager for the purpose of discussing
+and improving the Work, but excluding communication that is conspicuously marked
+or otherwise designated in writing by You as "Not a Contribution."
+</p></li>
+
+<li><p><strong>
+Grant of Copyright License.
+</strong>
+Subject to the terms and conditions of this Agreement, You hereby grant to the
+Project Manager and to recipients of software distributed by the Project
+Manager a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare derivative works of,
+publicly display, publicly perform, sublicense, and distribute Your
+Contributions and such derivative works.
+</p></li>
+
+<li><p><strong>
+Grant of Patent License.
+</strong>
+Subject to the terms and conditions of this Agreement, You hereby grant to the
+Project Manager and to recipients of software distributed by the Project
+Manager a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by You that are
+necessarily infringed by Your Contribution(s) alone or by combination of Your
+Contribution(s) with the Work to which such Contribution(s) was submitted. If
+any entity institutes patent litigation against You or any other entity
+(including a cross-claim or counterclaim in a lawsuit) alleging that Your
+Contribution, or the Work to which You have contributed, constitutes direct or
+contributory patent infringement, then any patent licenses granted to that
+entity under this Agreement for that Contribution or Work shall terminate as of
+the date such litigation is filed.
+</p></li>
+
+<li><p>
+You represent that you are legally entitled to grant the above license. If your
+employer(s) has rights to intellectual property that you create that includes
+your Contributions, You represent that you have received permission to make
+Contributions on behalf of that employer, that your employer has waived such
+rights for your Contributions to the Project Manager, or that your employer has
+executed a separate Corporate Contributor License Agreement with the Project
+Manager.
+</p></li>
+
+<li><p>
+You represent that each of Your Contributions is Your original creation (see
+Section 7 for submissions on behalf of others). You represent that Your
+Contribution submissions include complete details of any third-party license or
+other restriction (including, but not limited to, related patents and
+trademarks) of which you are personally aware and which are associated with any
+part of Your Contributions.
+</p></li>
+
+<li><p>
+You are not expected to provide support for Your Contributions, except to the
+extent You desire to provide support. You may provide support for free, for a
+fee, or not at all. Unless required by applicable law or agreed to in writing,
+You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied, including, without
+limitation, any warranties or conditions of TITLE, NONINFRINGEMENT,
+MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
+</p></li>
+
+<li><p>
+Should You wish to submit work that is not Your original creation, You may
+submit it to the Project Manager separately from any Contribution, identifying
+the complete details of its source and of any license or other restriction
+(including, but not limited to, related patents, trademarks, and license
+agreements) of which you are personally aware, and conspicuously marking the
+work as "Submitted on behalf of a third-party: [named here]".
+</p></li>
+
+<li><p>
+You agree to notify the Project Manager of any facts or circumstances of which
+you become aware that would make these representations inaccurate in any
+respect.
+</p></li>
+
+<li><p>
+In addition, if you have provided a Contribution (as defined in the LLC
+Contribution License Agreement below) to the Project under the Contribution
+License Agreement to OpenStack, LLC ("LLC Contribution Agreement"), you agree
+that OpenStack, LLC may assign the LLC Contribution Agreement along with all
+its rights and obligations under the LLC Contribution License Agreement to the
+Project Manager.
+</p></li>
+
+</ol>
+
+<blockquote>
+<p><em>In order to clarify the intellectual property license granted with
+Contributions from any person or entity, the OpenStack Project (the "Project")
+must have a Contributor License Agreement ("Agreement") on file that has been
+signed by each Contributor, indicating agreement to the license terms below.
+This license is for your protection as a Contributor as well as the protection
+of OpenStack, LLC as Project manager (the "Project Manager") and the Project
+users; it does not change your rights to use your own Contributions for any
+other purpose. If you have not already done so, please complete and sign this
+Individual License Agreement by following the instructions embedded below.
+After you fill in the required information and apply your digital signature to
+the Agreement, the signature service will generate an email to you. You must
+confirm your digital signature as instructed in this email to complete the
+signing process. The signature service will then send you a signed copy of this
+Agreement for your records.</em></p>
+
+<p><em>You accept and agree to the following terms and conditions for Your
+present and future Contributions submitted to the Project Manager.  Except for
+the license granted herein to the Project Manager and recipients of software
+distributed by the Project Manager, You reserve all right, title, and interest
+in and to Your Contributions.</em></p>
+
+<ol>
+
+<li><p><strong>Definitions</strong>"You" (or "Your") shall mean the copyright
+owner or legal entity authorized by the copyright owner that is making this
+Agreement with the Project Manager. For legal entities, the entity making a
+Contribution and all other entities that control, are controlled by, or are
+under common control with that entity are considered to be a single
+Contributor. For the purposes of this definition, "control" means (i) the
+power, direct or indirect, to cause the direction or management of such entity,
+whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or
+more of the outstanding shares, or (iii) beneficial ownership of such
+entity. "Contribution" shall mean any original work of authorship, including
+any modifications or additions to an existing work, that is intentionally
+submitted by You to the Project Manager for inclusion in, or documentation of,
+any of the projects owned or managed by the Project Manager (the "Work"). For
+the purposes of this definition, "submitted" means any form of electronic,
+verbal, or written communication sent to the Project Manager or its
+representatives, including but not limited to communication on electronic
+mailing lists, source code control systems, and issue tracking systems that are
+managed by, or on behalf of, the Project Manager for the purpose of discussing
+and improving the Work, but excluding communication that is conspicuously
+marked or otherwise designated in writing by You as "Not a
+Contribution."</p></li>
+
+<li><p><strong>Grant of Copyright License.</strong> Subject to the terms and
+conditions of this Agreement, You hereby grant to the Project Manager and to
+recipients of software distributed by the Project Manager a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright
+license to reproduce, prepare derivative works of, publicly display, publicly
+perform, sublicense, and distribute Your Contributions and such derivative
+works.</p></li>
+
+<li><p><strong>Grant of Patent License.</strong> Subject to the terms and
+conditions of this Agreement, You hereby grant to the Project Manager and to
+recipients of software distributed by the Project Manager a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as
+stated in this section) patent license to make, have made, use, offer to sell,
+sell, import, and otherwise transfer the Work, where such license applies only
+to those patent claims licensable by You that are necessarily infringed by Your
+Contribution(s) alone or by combination of Your Contribution(s) with the Work
+to which such Contribution(s) was submitted. If any entity institutes patent
+litigation against You or any other entity (including a cross-claim or
+counterclaim in a lawsuit) alleging that Your Contribution, or the Work to
+which You have contributed, constitutes direct or contributory patent
+infringement, then any patent licenses granted to that entity under this
+Agreement for that Contribution or Work shall terminate as of the date such
+litigation is filed.</p></li>
+
+<li><p>You represent that you are legally entitled to grant the above license.
+If your employer(s) has rights to intellectual property that you create that
+includes your Contributions, You represent that you have received permission to
+make Contributions on behalf of that employer, that your employer has waived
+such rights for your Contributions to the Project Manager, or that your
+employer has executed a separate Corporate Contributor License Agreement with
+the Project Manager.</p></li>
+
+<li><p>You represent that each of Your Contributions is Your original creation
+(see Section 7 for submissions on behalf other others). You represent that Your
+Contribution submissions include complete details of any third-party license or
+other restriction (including, but not limited to, related patents and
+trademarks) of which you are personally aware and which are associated with any
+part of Your Contributions.</p></li>
+
+<li><p>You are not expected to provide support for Your Contributions, except
+to the extent You desire to provide support. You may provide support for free,
+for a fee, or not at all. Unless required by applicable law or agreed to in
+writing, You provide Your Contributions on as "AS IS" BASIS, WITHOUT WARRANTIES
+OR CONDITIONS OR ANY KIND, either express or implied, including, without
+limitation, any warranties or conditions of TITLE, NONINFRINGEMENT,
+MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.</p></li>
+
+<li><p>Should You wish to submit work that is not Your original creation, You
+may submit it to the Project Manager separately from any Contribution,
+identifying the complete details of its source and of any license or other
+restriction (including, but not limited to, related patents, trademarks, and
+license agreements) of which you are personally aware, and conspicuously
+marking the work as "Submitted on behalf of a third-party: [named
+here]".</p></li>
+
+<li><p>You agree to notify the Project Manager of any facts or circumstances of
+which you become aware that would make these representations inaccurate in any
+respect.</p></li>
+
+</ol>
+
+</blockquote>
+
+</div></body></html>
diff --git a/playbooks/roles/gerrit/files/etc/GerritSite.css b/playbooks/roles/gerrit/files/etc/GerritSite.css
new file mode 100644
index 0000000000..c1c9d47575
--- /dev/null
+++ b/playbooks/roles/gerrit/files/etc/GerritSite.css
@@ -0,0 +1,179 @@
+body {color: #000 !important;}
+a,a:visited {color: #264D69 !important; text-decoration: none;}
+a:hover {color: #000 !important; text-decoration: underline}
+
+a.gwt-InlineHyperlink {background: none !important}
+
+#openstack-logo img { height: 40px; }
+#openstack-logo h1 { margin: 20px; }
+#gerrit_header #openstack-logo h1 { margin: 20px 0 0 0; }
+
+#gerrit_header {display: block !important; position: relative; top: -60px; margin-bottom: -60px; width: 200px; padding-left: 17px}
+#gerrit_header h1 {font-family: 'PT Sans', sans-serif; font-weight: normal; letter-spacing: -1px}
+
+#gerrit_topmenu {background: none; position:relative; top: 0px; left: 220px; margin-right: 220px}
+
+#gerrit_topmenu tbody tr td table {border: 0}
+
+#gerrit_topmenu tbody tr td table.gwt-TabBar {color: #353535; border-bottom: 1px solid #C5E2EA;}
+#gerrit_topmenu .gwt-Button {padding: 3px 6px}
+.gwt-TabBarItem-selected {color: #CF2F19 !important; border-bottom: 3px solid #CF2F19;}
+.gwt-TabBarItem {color: #353535; border-right: 0 !important}
+.gwt-TabBar .gwt-TabBarItem, .gwt-TabBar .gwt-TabBarRest, .gwt-TabPanelBottom {background: 0 !important;}
+
+#gerrit_topmenu .searchTextBox {width: 250px}
+
+#change_infoTable {
+  border-collapse: collapse;
+}
+
+#change_infoTable th {
+  padding: 2px 4px 2px 6px;
+  background-color: #eef3f5;
+  font-style: italic;
+  text-align: left;
+}
+
+#change_infoTable td {
+  padding: 2px 4px 2px 6px;
+  border-bottom: 1px solid #eef3f5;
+  border-right: 1px solid #eef3f5;
+}
+
+#change_infoTable tr:last-child td {
+  border: none;
+}
+
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2 {
+  padding-left: 10px;
+  padding-right: 10px;
+}
+
+/* Section headers */
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2
+    .com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-headerLine,
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2
+    .com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-sectionHeader {
+  margin-top: 10px !important;
+  margin-bottom: 10px !important;
+}
+
+/* Commit message */
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2
+    .com-google-gerrit-client-change-CommitBox_BinderImpl_GenCss_style-text::first-line {
+  font-weight: bold !important;
+}
+
+/* Commit metadata */
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2
+    .com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-commitColumn
+    .com-google-gerrit-client-change-CommitBox_BinderImpl_GenCss_style-header th {
+  padding: 2px 4px 2px 6px;
+  background-color: #eef3f5;
+  font-style: italic;
+  text-align: left;
+}
+
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2
+    .com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-commitColumn
+    .com-google-gerrit-client-change-CommitBox_BinderImpl_GenCss_style-header td {
+  border-bottom: 1px solid #eef3f5;
+  padding: 2px 4px 2px 6px;
+}
+
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2
+    .com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-commitColumn
+    .com-google-gerrit-client-change-CommitBox_BinderImpl_GenCss_style-header td:last-child {
+  border-right: 1px solid #eef3f5;
+}
+
+/* increase the middle info column to fill empty space (for wide
+monitors), but ensure there is a sufficient lower bound to kick in
+horiz scroll bar. This will relieve the preasure on the hideci test
+results. */
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-infoColumn {
+    width: 100% !important;
+    min-width: 400px;
+}
+
+/* Review history */
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2
+    .com-google-gerrit-client-change-Message_BinderImpl_GenCss_style-name {
+  font-weight: bold !important;
+}
+.com-google-gerrit-client-change-ChangeScreen_BinderImpl_GenCss_style-cs2
+    .com-google-gerrit-client-change-Message_BinderImpl_GenCss_style-messageBox {
+  width: inherit;
+  max-width: 1168px;
+}
+
+.comment_test_name {
+  display: inline-block;
+  *display: inline;
+  *zoom: 1;
+  width: auto !important;
+  width: 25em;
+  min-width: 20em;
+  padding-bottom: 2pt;
+}
+
+.comment_test_result {
+}
+
+.result_SUCCESS {
+    color: #007f00;
+}
+.result_FAILURE, .result_POST_FAILURE, .result_TIMED_OUT, .result_RETRY_LIMIT, .result_DISK_FULL {
+    color: #cf2f19;
+}
+.result_UNSTABLE, .result_WARNING {
+    color: #e39f00;
+}
+.result_LOST {
+    color: #e39f00;
+}
+li.comment_test {list-style-type: none; }
+
+/* this is for support of 'Display Person Name In Review Category' */
+.cAPPROVAL {
+    max-width: 100px;
+    overflow: hidden;
+}
+/* fixes to make this like old gerrit */
+.changeTable td.dataCell {
+    height: 1em;
+}
+
+/* don't make the non voting stand out more than the voting */
+table.infoTable td.notVotable,
+.changeTable td.dataCell.labelNotApplicable {
+    background: inherit;
+}
+
+.test_result_table {
+  border-collapse: collapse;
+}
+
+.test_result_table tr {
+  border-left: 1px solid #eef3f5;
+  border-right: 1px solid #eef3f5;
+}
+
+.test_result_table td.header {
+  background-color: #eef3f5;
+}
+
+.test_result_table td {
+  padding: 2px 4px 2px 6px;
+  border: 1px solid #eef3f5;
+}
+
+.addMemberTextBox {
+    width: 20em;
+}
+
+/* css attribute selector to make -1s show up red in new screen */
+[title="Doesn't seem to work"],
+[title="This patch needs further work before it can be merged"] {
+    color: red;
+}
diff --git a/playbooks/roles/gerrit/files/etc/GerritSiteHeader.html b/playbooks/roles/gerrit/files/etc/GerritSiteHeader.html
new file mode 100644
index 0000000000..0e2f060fa8
--- /dev/null
+++ b/playbooks/roles/gerrit/files/etc/GerritSiteHeader.html
@@ -0,0 +1,6 @@
+<div id="openstack-logo">
+  <script type="text/javascript" src="static/jquery.js" />
+  <script type="text/javascript" src="static/jquery-visibility.js"></script>
+  <script type="text/javascript" src="static/hideci.js" />
+  <a href="/"><h1><img src="static/title.svg" style="vertical-align:middle;" /></h1></a>
+</div>
diff --git a/playbooks/roles/gerrit/files/gerrit-podman/docker-compose.yaml b/playbooks/roles/gerrit/files/gerrit-podman/docker-compose.yaml
new file mode 100644
index 0000000000..3ec71e443e
--- /dev/null
+++ b/playbooks/roles/gerrit/files/gerrit-podman/docker-compose.yaml
@@ -0,0 +1,16 @@
+version: '3'
+
+services:
+  gerrit:
+    image: docker.io/opendevorg/gerrit:2.13
+    network_mode: host
+    user: gerrit
+    volumes:
+      - /home/gerrit2/review_site/cache:/var/gerrit/cache
+      - /home/gerrit2/review_site/etc:/var/gerrit/etc
+      - /home/gerrit2/review_site/git:/var/gerrit/git
+      - /home/gerrit2/review_site/tmp:/var/gerrit/tmp
+      - /home/gerrit2/review_site/hooks:/var/gerrit/hooks
+      - /home/gerrit2/review_site/index:/var/gerrit/index
+      - /home/gerrit2/review_site/logs:/var/log/gerrit
+      - /home/gerrit2/review_site/static:/var/gerrit/static
diff --git a/playbooks/roles/gerrit/files/hooks/change-abandoned b/playbooks/roles/gerrit/files/hooks/change-abandoned
new file mode 100755
index 0000000000..5761951eca
--- /dev/null
+++ b/playbooks/roles/gerrit/files/hooks/change-abandoned
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+# Use timeout to kill any process running longer than 10 minutes.
+timeout -k 2m 10m /usr/local/bin/update-bug change-abandoned "$@"
diff --git a/playbooks/roles/gerrit/files/hooks/change-merged b/playbooks/roles/gerrit/files/hooks/change-merged
new file mode 100755
index 0000000000..a35424f302
--- /dev/null
+++ b/playbooks/roles/gerrit/files/hooks/change-merged
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+# Use timeout to kill any process running longer than 10 minutes.
+timeout -k 2m 10m /usr/local/bin/update-bug change-merged "$@"
diff --git a/playbooks/roles/gerrit/files/robots.txt b/playbooks/roles/gerrit/files/robots.txt
new file mode 100644
index 0000000000..05cf74abae
--- /dev/null
+++ b/playbooks/roles/gerrit/files/robots.txt
@@ -0,0 +1,11 @@
+# Directions for web crawlers.
+# See http://www.robotstxt.org/wc/norobots.html.
+
+User-agent: HTTrack
+User-agent: puf
+User-agent: MSIECrawler
+User-agent: Nutch
+Disallow: /
+
+User-agent: msnbot
+Crawl-delay: 1
diff --git a/playbooks/roles/gerrit/files/static/hideci.js b/playbooks/roles/gerrit/files/static/hideci.js
new file mode 100644
index 0000000000..012b2c9512
--- /dev/null
+++ b/playbooks/roles/gerrit/files/static/hideci.js
@@ -0,0 +1,575 @@
+// Copyright (c) 2014 VMware, Inc.
+// Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+//
+// 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.
+
+// this regex matches the hash part of review pages
+var hashRegex = /^\#\/c\/([\d]+)((\/\d+)([.][.](\d+))?)?\/?$/;
+// this regex matches CI comments
+var ciRegex = /^(.* CI|Jenkins|Zuul)$/;
+// this regex matches "Patch set #"
+var psRegex = /^(Uploaded patch set|Patch Set) (\d+)(:|\.)/;
+// this regex matches merge failure messages
+var mergeFailedRegex = /Merge Failed\./;
+// this regex matches the name of CI systems we trust to report merge failures
+var trustedCIRegex = /^(OpenStack CI|Jenkins|Zuul)$/;
+// this regex matches the name+pipeline that we want at the top of the CI list
+var firstPartyCI = /^(Jenkins|Zuul)/;
+// this regex matches the pipeline markup
+var pipelineNameRegex = /Build \w+ \(([-\w]+) pipeline\)/;
+// The url to full status information on running jobs
+var zuulStatusURL = 'https://zuul.openstack.org';
+// The json URL to check for running jobs
+var zuulStatusJSON = 'https://zuul.openstack.org/status/change/';
+
+// This is a variable to determine if we're in debugging mode, which
+// lets you globally set it to see what's going on in the flow.
+var hideci_debug = false;
+// This is a variable to enable zuul integration, we default it off so
+// that it creates no additional load, and that it's easy to turn off
+// the feature.
+var zuul_inline = false;
+
+/**
+ dbg(...) - prints a list of items out to the javascript
+ console.log. This allows us to leave tracing in this file which is a
+ no-op by default, but can be triggered if you enter a javascript
+ console and set hideci_debug = true.
+*/
+function dbg () {
+    if (hideci_debug == true) {
+        for (var i = 0; i < arguments.length; i++) {
+            console.log(arguments[i]);
+        }
+    }
+}
+
+
+function format_time(ms, words) {
+    if (ms == null) {
+        return "unknown";
+    }
+    var seconds = (+ms)/1000;
+    var minutes = Math.floor(seconds/60);
+    var hours = Math.floor(minutes/60);
+    seconds = Math.floor(seconds % 60);
+    minutes = Math.floor(minutes % 60);
+    r = '';
+    if (words) {
+        if (hours) {
+            r += hours;
+            r += ' hr ';
+        }
+        r += minutes + ' min';
+    } else {
+        if (hours < 10) r += '0';
+        r += hours + ':';
+        if (minutes < 10) r += '0';
+        r += minutes + ':';
+        if (seconds < 10) r += '0';
+        r += seconds;
+    }
+    return r;
+}
+
+var ci_parse_psnum = function($panel) {
+    var match = psRegex.exec($panel.html());
+    if (match !== null) {
+        return parseInt(match[2]);
+    }
+    return 0;
+};
+
+var ci_parse_is_merge_conflict = function($panel) {
+    return (mergeFailedRegex.exec($panel.html()) !== null);
+};
+
+var ci_find_pipeline = function($panel) {
+    var match = pipelineNameRegex.exec($panel.html());
+    if (match !== null) {
+        return match[1];
+    } else {
+        return null;
+    }
+};
+
+var ci_parse_results = function($panel) {
+    var result_list = [];
+    var test_results = $panel.find("li.comment_test");
+    var pipeline = null;
+    if (test_results !== null) {
+        test_results.each(function(i, li) {
+            var result = {};
+            if ($(li).find("a").length > 0) {
+                result["name"] = $(li).find("span.comment_test_name").find("a")[0].innerHTML;
+                result["link"] = $(li).find("span.comment_test_name").find("a")[0];
+            }
+            else {
+                result["name"] = $(li).find("span.comment_test_name")[0].innerHTML;
+            }
+            result["result"] = $(li).find("span.comment_test_result")[0];
+            result_list.push(result);
+        });
+    }
+    return result_list;
+};
+
+/***
+ * function ci_group_by_ci_pipeline - create a group by structure for
+ * iterating on CI's pipelines
+ *
+ * This function takes the full list of comments, the current patch
+ * number, and builds an array of (ci_name_pipelinename, comments array)
+ * tuples. That makes it very easy to process during the display
+ * phase to ensure we only display the latest result for every CI
+ * pipeline.
+ *
+ * Comments that do not have a parsable pipeline (3rd party ci
+ * systems) get collapsed by name, and we specify 'check' for their
+ * pipeline.
+ *
+ **/
+
+var ci_group_by_ci_pipeline = function(current, comments) {
+    var ci_pipelines = [];
+    var ci_pipeline_comments = [];
+    for (var i = 0; i < comments.length; i++) {
+        var comment = comments[i];
+        if ((comment.psnum != current) || !comment.is_ci || (comment.results.length == 0)) {
+            continue;
+        }
+        var name_pipeline = comment.name;
+        if (comment.pipeline !== null) {
+            name_pipeline += ' ' + comment.pipeline;
+        }
+
+        var index = ci_pipelines.indexOf(name_pipeline);
+        if (index == -1) {
+            // not found, so create new entries
+            ci_pipelines.push(name_pipeline);
+            ci_pipeline_comments.push([comment]);
+        } else {
+            ci_pipeline_comments[index].push(comment);
+        }
+    }
+
+    function sort_by_name(a,b) {
+        if (a[0] < b[0])
+            return -1;
+        else if (a[0] > b[0])
+            return 1;
+        else
+            return 0;
+    }
+
+    var results = [];
+    var notfirstparty = [];
+    // we want to separate out first party CI results to always be the
+    // top of the list, and third party CI to come after, so that
+    // hunting for first party CI isn't tough.
+    for (i = 0; i < ci_pipelines.length; i++) {
+        if (firstPartyCI.test(ci_pipelines[i])) {
+            results.push([ci_pipelines[i], ci_pipeline_comments[i]]);
+        } else {
+            notfirstparty.push([ci_pipelines[i], ci_pipeline_comments[i]]);
+        }
+    }
+
+    notfirstparty.sort(sort_by_name);
+
+    for (i = 0; i < notfirstparty.length; i++) {
+        results.push(notfirstparty[i]);
+    }
+    return results;
+};
+
+var ci_parse_comments = function() {
+    var comments = [];
+    $("p").each(function() {
+        var match = psRegex.exec($(this).html());
+        if (match !== null) {
+            var psnum = parseInt(match[2]);
+            var top = $(this).parent().parent().parent();
+            // old change screen
+            var name = top.attr("name");
+            if (!name) {
+                // new change screen
+                name = $(this).parent().parent().parent().children().children()[0].innerHTML;
+                top = $(this).parent().parent().parent().parent();
+            }
+            var comment = {};
+            comment.name = name;
+
+            var date_cell = top.find(".commentPanelDateCell");
+            if (date_cell.attr("title")) {
+                // old change screen
+                comment.date = date_cell.attr("title");
+            } else {
+                // new change screen
+                comment.date = $(this).parent().parent().parent().children().children()[2].innerHTML
+            }
+            var comment_panel = $(this).parent();
+            comment.psnum = psnum;
+            comment.merge_conflict = ci_parse_is_merge_conflict(comment_panel);
+            comment.pipeline = ci_find_pipeline(comment_panel);
+            comment.results = ci_parse_results(comment_panel);
+            comment.is_ci = (ciRegex.exec(comment.name) !== null);
+            comment.is_trusted_ci = (trustedCIRegex.exec(comment.name) !== null);
+            comment.ref = top;
+            dbg("Found comment", comment);
+            comments.push(comment);
+        }
+    });
+    return comments;
+};
+
+var ci_latest_patchset = function(comments) {
+    var psnum = 0;
+    for (var i = 0; i < comments.length; i++) {
+        psnum = Math.max(psnum, comments[i].psnum);
+    }
+    return psnum;
+};
+
+var ci_is_merge_conflict = function(comments) {
+    var latest = ci_latest_patchset(comments);
+    var conflict = false;
+    for (var i = 0; i < comments.length; i++) {
+        var comment = comments[i];
+        // only if we are actually talking about the latest patch set
+        if (comment.psnum == latest) {
+            if (comment.is_trusted_ci) {
+                conflict = comment.merge_conflict;
+            }
+        }
+    }
+    return conflict;
+};
+
+var ci_prepare_results_table = function() {
+    // Create a table and insert it after the approval table
+    var table = $("table.test_result_table")[0];
+    if (!table) {
+        table = document.createElement("table");
+        $(table).addClass("test_result_table");
+        $(table).addClass("infoTable").css({"margin-top":"1em", "margin-bottom":"1em"});
+
+        var approval_table = $("div.approvalTable");
+        if (approval_table.length) {
+            var outer_table = document.createElement("table");
+            $(outer_table).insertBefore(approval_table);
+            var outer_table_row = document.createElement("tr");
+            $(outer_table).append(outer_table_row);
+            var td = document.createElement("td");
+            $(outer_table_row).append(td);
+            $(td).css({"vertical-align":"top"});
+            $(td).append(approval_table);
+            td = document.createElement("td");
+            $(outer_table_row).append(td);
+            $(td).css({"vertical-align":"top"});
+            $(td).append(table);
+        } else {
+            var big_table_row = $("div.screen>div>div>table>tbody>tr");
+            var td = $(big_table_row).children()[1];
+            $(td).append(table);
+        }
+    } else {
+        $(table).empty();
+    }
+    return table;
+};
+
+var ci_display_results = function(comments) {
+    var table = ci_prepare_results_table();
+    if (ci_is_merge_conflict(comments)) {
+        var mc_header = $("<tr>").append($('<td class="merge_conflict" colpsan="2">Patch in Merge Conflict</td>'));
+        mc_header.css('width', '400');
+        mc_header.css('font-weight', 'bold');
+        mc_header.css('color', 'red');
+        mc_header.css('padding-left', '2em');
+        $(table).append(mc_header);
+
+        return;
+    }
+    var current = ci_latest_patchset(comments);
+    var ci_pipelines = ci_group_by_ci_pipeline(current, comments);
+    for (var i = 0; i < ci_pipelines.length; i++) {
+        var ci_pipeline_name = ci_pipelines[i][0];
+        var ci_pipeline_comments = ci_pipelines[i][1];
+        // the most recent comment on a pipeline
+        var last = ci_pipeline_comments.length - 1;
+        var comment = ci_pipeline_comments[last];
+        var rechecks = "";
+        if (last > 0) {
+            rechecks = " (" + last + " rechecks)";
+        }
+
+        var header = $("<tr>").append($('<td class="header">' + ci_pipeline_name + rechecks + '</td>'));
+        header.append('<td class="header ci_date">' + comment.date + '</td>');
+        $(table).append(header);
+        for (var j = 0; j < comment.results.length; j++) {
+            var result = comment.results[j];
+            var tr = $("<tr>");
+            if ("link" in result) {
+                tr.append($("<td>").append($(result["link"]).clone()));
+            }
+            else {
+                tr.append($("<td>").text(result["name"]));
+            }
+            tr.append($("<td>").append($(result["result"]).clone()));
+            $(table).append(tr);
+        }
+    }
+};
+
+var set_cookie = function (name, value) {
+    document.cookie = name + "=" + value + "; path=/";
+};
+
+var read_cookie = function (name) {
+    var nameEQ = name + "=";
+    var ca = document.cookie.split(';');
+    for (var i = 0; i < ca.length; i++) {
+        var c = ca[i];
+        while (c.charAt(0) == ' ') {
+            c = c.substring(1, c.length);
+        }
+        if (c.indexOf(nameEQ) == 0) {
+            return c.substring(nameEQ.length, c.length);
+        }
+    }
+    return null;
+};
+
+var ci_toggle_visibility = function(comments, showOrHide) {
+    if (!comments) {
+        comments = ci_parse_comments();
+    }
+    $.each(comments, function(i, comment) {
+        if (comment.is_ci && !comment.is_trusted_ci) {
+            $(comment.ref).toggle(showOrHide);
+        }
+    });
+};
+
+var ci_hide_ci_comments = function(comments) {
+    if (!comments) {
+        comments = ci_parse_comments();
+    }
+    $.each(comments, function(i, comment) {
+        if (comment.is_ci && !comment.is_trusted_ci) {
+            $(comment.ref).hide();
+        }
+    });
+};
+
+var ci_page_loaded = function() {
+    if (hashRegex.test(window.location.hash)) {
+        dbg("Searching for ci results on " + window.location.hash);
+        $("#toggleci").show();
+        var comments = ci_parse_comments();
+        ci_display_results(comments);
+        var showOrHide = 'true' == read_cookie('show-ci-comments');
+        if (!showOrHide) {
+            ci_hide_ci_comments(comments);
+        }
+        if (zuul_inline === true) {
+            ci_zuul_for_change(comments);
+        }
+    } else {
+        $("#toggleci").hide();
+    }
+};
+
+var ci_current_change = function() {
+    var change = hashRegex.exec(window.location.hash);
+    if (change.length > 1) {
+        return change[1];
+    }
+    return null;
+};
+
+// recursively find the zuul status change, will be much more
+// efficient once zuul supports since json status.
+var ci_find_zuul_status = function (data, change_psnum) {
+    var objects = [];
+    for (var i in data) {
+        if (!data.hasOwnProperty(i)) continue;
+        if (typeof data[i] == 'object') {
+            objects = objects.concat(ci_find_zuul_status(data[i],
+                                                         change_psnum));
+        } else if (i == 'id' && data.id == change_psnum) {
+            objects.push(data);
+        }
+    }
+    return objects;
+};
+
+var ci_zuul_all_status = function(jobs) {
+    var status = "passing";
+    for (var i = 0; i < jobs.length; i++) {
+        if (jobs[i].result && jobs[i].result != "SUCCESS") {
+            status = "failing";
+            break;
+        }
+    }
+    return status;
+};
+
+var ci_zuul_display_status = function(status) {
+    var zuul_table = $("table.zuul_result_table")[0];
+    if (!zuul_table) {
+        var test_results = $("table.test_result_table")[0];
+        zuul_table = document.createElement("table");
+        $(zuul_table).addClass("zuul_result_table");
+        $(zuul_table).addClass("infoTable").css({"margin-bottom":"1em"});
+        if (test_results) {
+            $(test_results).prepend(zuul_table);
+        }
+    }
+    $(zuul_table).empty();
+    $(zuul_table).show();
+    $(zuul_table).append("<tr><td class='header'>Change currently being tested (<a href='" + zuulStatusURL + "'>full status</a>)</td></tr>");
+    for (var i = 0; i < status.length; i++) {
+        var item = status[i];
+        var pipeline = item.jobs[0].pipeline;
+        var passing = (item.failing_reasons && item.failing_reasons.length > 0) ? "failing" : "passing";
+        var timeleft = item.remaining_time;
+        var row = "<tr><td>";
+        if (pipeline != null) {
+            row += pipeline + " pipeline: " + passing;
+            row += " (" + format_time(timeleft, false) + " left)";
+        } else {
+            row += "in between pipelines, status should update shortly";
+        }
+        row += "</td></tr>";
+
+        $(zuul_table).append(row);
+    }
+};
+
+var ci_zuul_clear_status = function () {
+    var zuul_table = $("table.zuul_result_table")[0];
+    if (zuul_table) {
+        $(zuul_table).hide();
+    }
+};
+
+var ci_zuul_process_changes = function(data, change_psnum) {
+    var zuul_status = ci_find_zuul_status(data, change_psnum);
+    if (zuul_status.length) {
+        ci_zuul_display_status(zuul_status);
+    } else {
+        ci_zuul_clear_status();
+    }
+};
+
+var ci_zuul_for_change = function(comments) {
+    if (!comments) {
+        comments = ci_parse_comments();
+    }
+    var change = ci_current_change();
+    var psnum = ci_latest_patchset(comments);
+    var change_psnum = change + "," + psnum;
+
+    // do the loop recursively in ajax
+    (function poll() {
+        $.ajax({
+            url: zuulStatusJSON + change_psnum,
+            type: "GET",
+            success: function(data) {
+                dbg("Found zuul data for " + change_psnum, data);
+                ci_zuul_process_changes(data, change_psnum);
+            },
+            dataType: "json",
+            complete: setTimeout(function() {
+                // once we are done with this cycle in the loop we
+                // schedule ourselves again in the future with
+                // setTimeout. However, by the time the function
+                // actually gets called, other things might have
+                // happened, and we may want to just dump the data
+                // instead.
+                //
+                // the UI might have gone hidden (user was bored,
+                // switched to another tab / window).
+                //
+                // the user may have navigated to another review url,
+                // so the data returned is not relevant.
+                //
+                // both cases are recoverable when the user navigates
+                // around, because a new "thread" gets started on
+                // ci_page_load.
+                //
+                // BUG(sdague): there is the possibility that the user
+                // navigates away from a page and back fast enough
+                // that the first "thread" is not dead, and a second
+                // one is started. greghaynes totally said he'd come
+                // up with a way to fix that.
+                if (window.zuul_enable_status_updates == false) {
+                    return;
+                }
+                var current = ci_current_change();
+                if (current && change_psnum.indexOf(current) != 0) {
+                    // window url is dead, so don't schedule any more future
+                    // updates for this url.
+                    return;
+                }
+                poll();
+            }, 15000),
+            timeout: 5000
+        });
+    })();
+};
+
+
+window.onload = function() {
+    var input = document.createElement("input");
+    input.id = "toggleci";
+    input.type = "button";
+    input.className = "gwt-Button";
+    input.value = "Toggle Extra CI";
+    input.onclick = function () {
+        // Flip the cookie
+        var showOrHide = 'true' == read_cookie('show-ci-comments');
+        set_cookie('show-ci-comments', showOrHide ? 'false' : 'true');
+        // Hide or Show existing comments based on cookie
+        ci_toggle_visibility(null, !showOrHide);
+    };
+    document.body.appendChild(input);
+
+    MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
+    var observer = new MutationObserver(function(mutations, observer) {
+        var span = $("span.rpcStatus");
+        $.each(mutations, function(i, mutation) {
+            if (mutation.target === span[0] &&
+                mutation.attributeName === "style" &&
+                (!(span.is(":visible")))) {
+                ci_page_loaded();
+            }
+        });
+    });
+    observer.observe(document, {
+        subtree: true,
+        attributes: true
+    });
+
+    $(document).on({
+        'show.visibility': function() {
+            window.zuul_enable_status_updates = true;
+            ci_page_loaded();
+        },
+        'hide.visibility': function() {
+            window.zuul_enable_status_updates = false;
+        }
+    });
+};
diff --git a/playbooks/roles/gerrit/files/static/system-cla.html b/playbooks/roles/gerrit/files/static/system-cla.html
new file mode 100644
index 0000000000..e970a56a77
--- /dev/null
+++ b/playbooks/roles/gerrit/files/static/system-cla.html
@@ -0,0 +1,14 @@
+<html><body><div>
+
+<h1>
+Gerrit System Role Accounts Proxy CLA
+</h1>
+
+<p>
+This is not a real CLA and cannot be signed. See <a
+href="https://docs.openstack.org/infra/manual/developers.html#account-setup">the
+account setup instructions</a> for more information on OpenStack Contributor
+License Agreements.
+</p>
+
+</div></body></html>
diff --git a/playbooks/roles/gerrit/files/static/usg-cla.html b/playbooks/roles/gerrit/files/static/usg-cla.html
new file mode 100644
index 0000000000..832fe152c9
--- /dev/null
+++ b/playbooks/roles/gerrit/files/static/usg-cla.html
@@ -0,0 +1,16 @@
+<html><body><div>
+
+<h1>
+OpenStack Project U.S. Government Contributor License Agreement
+</h1>
+
+<p>
+This agreement is not managed through Gerrit. If you need to sign the U.S.
+Government Contributor License Agreement, please contact the OpenStack
+Foundation to initiate the process. See <a
+href="https://docs.openstack.org/infra/manual/developers.html#account-setup">the
+account setup instructions</a> for more information on OpenStack Contributor
+License Agreements.
+</p>
+
+</div></body></html>
diff --git a/playbooks/roles/gerrit/tasks/main.yaml b/playbooks/roles/gerrit/tasks/main.yaml
new file mode 100644
index 0000000000..b8614d2f00
--- /dev/null
+++ b/playbooks/roles/gerrit/tasks/main.yaml
@@ -0,0 +1,170 @@
+# TODO(mordred) We should do *something* where this could use a zuul cloned
+# copy of project-config instead. This is needed not just for things like
+# manage-projects (which could be run completely differently and non-locally)
+# but also for things like notify-impact, which is currently run by a gerrit
+# hook inside of the container via jeepyb.
+- name: Clone project-config repo
+  git:
+    repo: https://opendev.org/openstack/project-config
+    dest: /opt/project-config
+    force: yes
+
+- name: Synchronize podman-compose directory
+  synchronize:
+    src: gerrit-podman/
+    dest: /etc/gerrit-podman/
+
+- name: Create Gerrit Group
+  group:
+    name: "{{ gerrit_user_name }}"
+    gid: "{{ gerrit_id }}"
+    system: yes
+
+- name: Create Gerrit User
+  user:
+    name: "{{ gerrit_user_name }}"
+    uid: "{{ gerrit_id }}"
+    comment: Gerit User
+    shell: /bin/bash
+    home: "{{ gerrit_home_dir }}"
+    group: "{{ gerrit_user_name }}"
+    create_home: yes
+    system: yes
+
+- name: Ensure review_site directory exists
+  file:
+    state: directory
+    path: "{{ gerrit_site_dir }}"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0755
+
+- name: Ensure Gerrit volume directories exists
+  file:
+    state: directory
+    path: "{{ gerrit_site_dir }}/{{ item }}"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0755
+  loop:
+    - etc
+    - git
+    - index
+    - cache
+    - static
+    - hooks
+    - tmp
+    - logs
+
+- name: Write Gerrit config file
+  template:
+    src: gerrit.config
+    dest: "{{ gerrit_site_dir }}/etc/gerrit.config"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0644
+
+- name: Write Gerrit SSH private key
+  copy:
+    content: "{{ gerrit_ssh_rsa_key_contents }}"
+    dest: "{{ gerrit_site_dir }}/etc/ssh_host_rsa_key"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0600
+
+- name: Write Gerrit SSH public key
+  copy:
+    content: "{{ gerrit_ssh_rsa_pubkey_contents }}"
+    dest: "{{ gerrit_site_dir }}/etc/ssh_host_rsa_key.pub"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0644
+
+- name: Write Welcome SSH private key
+  copy:
+    content: "{{ welcome_message_gerrit_ssh_private_key }}"
+    dest: "{{ gerrit_site_dir }}/etc/ssh_welcome_rsa_key"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0600
+  when: welcome_message_gerrit_ssh_private_key is defined
+
+- name: Write Welcome SSH public key
+  copy:
+    content: "{{ welcome_message_gerrit_ssh_public_key }}"
+    dest: "{{ gerrit_site_dir }}/etc/ssh_welcome_rsa_key.pub"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0644
+  when: welcome_message_gerrit_ssh_public_key is defined
+
+- name: Copy static hooks
+  copy:
+    src: "hooks/{{ item }}"
+    dest: "{{ gerrit_site_dir }}/hooks/{{ item }}"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0555
+  loop:
+    - change-merged
+    - change-abandoned
+
+- name: Copy notify-impact yaml file
+  copy:
+    src: "/opt/project-config/gerrit/notify_impact.yaml"
+    dest: "{{ gerrit_site_dir }}/hooks/notify_impact.yaml"
+    remote_src: yes
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0444
+
+- name: Install patchset-created hook
+  template:
+    src: patchset-created.j2
+    dest: "{{ gerrit_site_dir }}/hooks/patchset-created"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: 0555
+
+# TODO(mordred) These things should really go into the image instead.
+- name: Copy static and etc
+  copy:
+    src: "{{ item }}"
+    dest: "{{ gerrit_site_dir }}/{{ item }}"
+    owner: "{{ gerrit_user_name }}"
+    group: "{{ gerrit_user_name }}"
+    mode: preserve
+  loop:
+    - static
+    - etc
+
+- name: Install podman-compose
+  pip:
+    name: podman-compose
+    state: latest
+    # NOTE(mordred) Cannot use full path to pip3 here because it is
+    # different on zuul test nodes and in production. This is, of
+    # course, not stellar.
+    executable: pip3
+
+# TODO(mordred) Make this suck less, like if we could do an init container
+# or something just generally less gross.
+- name: Run gerrit init
+  when: gerrit_run_init | bool
+  command: >
+    podman run -it --rm --net=host -u gerrit
+    -v /home/gerrit2/review_site/cache:/var/gerrit/cache
+    -v /home/gerrit2/review_site/etc:/var/gerrit/etc
+    -v /home/gerrit2/review_site/git:/var/gerrit/git
+    -v /home/gerrit2/review_site/tmp:/var/gerrit/tmp
+    -v /home/gerrit2/review_site/hooks:/var/gerrit/hooks
+    -v /home/gerrit2/review_site/index:/var/gerrit/index
+    -v /home/gerrit2/review_site/logs:/var/log/gerrit
+    -v /home/gerrit2/review_site/static:/var/gerrit/static
+    docker.io/opendevorg/gerrit:2.13
+    /usr/local/openjdk-8/bin/java -jar /var/gerrit/bin/gerrit.war init -d /var/gerrit -b --no-auto-start --install-all-plugins
+
+- name: Run podman-compose up
+  shell:
+    cmd: podman-compose up -d
+    chdir: /etc/gerrit-podman/
diff --git a/playbooks/roles/gerrit/templates/gerrit.config b/playbooks/roles/gerrit/templates/gerrit.config
new file mode 100644
index 0000000000..5f6c82f2d7
--- /dev/null
+++ b/playbooks/roles/gerrit/templates/gerrit.config
@@ -0,0 +1,180 @@
+# This file is managed by ansible.
+# https://opendev.org/opendev/system-config
+
+[gerrit]
+	basePath = git
+	canonicalWebUrl = https://review.opendev.org/
+	changeScreen = OLD_UI
+	reportBugText = Get Help
+	reportBugUrl = https://docs.openstack.org/infra/system-config/project.html#contributing
+	gitHttpUrl = https://review.opendev.org/
+{{ gerrit_database_config_section }}
+[auth]
+	contributorAgreements = true
+	type = OPENID_SSO
+	cookieSecure = true
+	enableRunAs = true
+	openIdSsoUrl = https://login.ubuntu.com/+openid
+
+[sendemail]
+	smtpServer = localhost
+	from = MIXED
+	includeDiff = false
+[container]
+	user = gerrit2
+	startupTimeout = 300
+	heapLimit = 48g
+[gc]
+[core]
+	packedGitOpenFiles = 4096
+	packedGitLimit = 400m
+	packedGitWindowSize = 16k
+[sshd]
+	listenAddress = *:29418
+	threads = 100
+	maxConnectionsPerUser = 96
+[httpd]
+	listenUrl = proxy-https://*:8081/
+	minThreads = 20
+	maxThreads = 100
+	maxQueued = 200
+[cache]
+	directory = cache
+[cache "web_sessions"]
+	maxAge = 7days
+[cache "accounts"]
+    memoryLimit = 32768
+[cache "accounts_byemail"]
+    memoryLimit = 32768
+[cache "accounts_byname"]
+    memoryLimit = 32768
+[cache "groups_byuuid"]
+    memoryLimit = 32768
+[user]
+	email = review@openstack.org
+[change]
+	allowDrafts = false
+[receive]
+	maxObjectSizeLimit = 100 m
+[commentlink "bugheader"]
+	match = "([Cc]loses|[Pp]artial|[Rr]elated)-[Bb]ug:\\s*#?(\\d+)"
+	link = "https://launchpad.net/bugs/$2"
+	html = ""
+[commentlink "bug"]
+	match = "\\b[Bb]ug:? #?(\\d+)"
+	link = "https://launchpad.net/bugs/$1"
+	html = ""
+[commentlink "story"]
+	match = "\\b[Ss]tory:? #?(\\d+)"
+	link = "https://storyboard.openstack.org/#!/story/$1"
+	html = ""
+[commentlink "task"]
+	match = "\\b[Tt]ask:? #?(\\d+)"
+	link = "https://storyboard.openstack.org/#!/task/$1"
+	html = ""
+[commentlink "its-storyboard"]
+	match = "\\b[Tt]ask:? #?(\\d+)"
+	link = "task: $1"
+	html = ""
+[commentlink "blueprint"]
+	match = "(\\b[Bb]lue[Pp]rint\\b|\\b[Bb][Pp]\\b)[ \\t#:]*([A-Za-z0-9\\-]+)"
+	link = "https://blueprints.launchpad.net/openstack/?searchtext=$2"
+	html = ""
+[commentlink "testresult"]
+	match = "<li>([^ ]+) <a href=\"[^\"]+\" target=\"_blank\" rel=\"nofollow\">([^<]+)</a> : ([^ ]+)([^<]*)</li>"
+	link = ""
+	html = "<li class=\"comment_test\"><span class=\"comment_test_name\"><a href=\"$2\" rel=\"nofollow\">$1</a></span> <span class=\"comment_test_result\"><span class=\"result_$3\">$3</span>$4</span></li>"
+[commentlink "testresultnoop"]
+	match = "<li>noop noop : SUCCESS([^<]*)</li>"
+	link = ""
+	html = "<li class=\"comment_test\"><span class=\"comment_test_name\">noop</span> <span class=\"comment_test_result\"><span class=\"result_SUCCESS\">SUCCESS</span>$1</span></li>"
+[commentlink "launchpadbug"]
+	match = "<a href=\"(https://bugs\\.launchpad\\.net/[a-zA-Z0-9\\-]+/\\+bug/(\\d+))[^\"]*\">[^<]+</a>"
+	link = ""
+	html = "<a href=\"$1\">$1</a>"
+[commentlink "changeid"]
+	match = "(I[0-9a-f]{8,40})"
+	link = "/#/q/$1"
+	html = ""
+[commentlink "gitsha"]
+	match = "(<p>|[\\s(])([0-9a-f]{40})(</p>|[\\s.,;:)])"
+	link = ""
+	html = "$1<a href=\"/#/q/$2\">$2</a>$3"
+[its-storyboard]
+	url = https://storyboard.openstack.org
+[trackingid "launchpad-bug"]
+	match = "\\#?(\\d+)"
+	footer = "closes-bug:"
+	footer = "partial-bug:"
+	footer = "related-bug:"
+	system = "Launchpad"
+[trackingid "storyboard-story"]
+	match = "\\#?(\\d+)"
+	footer = "story:"
+	system = "Storyboard"
+[trackingid "storyboard-task"]
+	match = "\\#?(\\d+)"
+	footer = "task:"
+	system = "Storyboard"
+[theme]
+	backgroundColor = ffffff
+	topMenuColor = ffffff
+	textColor = 264d69
+	trimColor = eef3f5
+	selectionColor = d1e6ea
+	changeTableOutdatedColor = f5cccc
+	tableOddRowColor = ffffff
+	tableEvenRowColor = f5f5ff
+[melody]
+	monitoring = true
+	session = true
+[plugin "javamelody"]
+        allowTopMenu = false
+# Gerrit upstream hardcodes a .git extension for cgit.
+# The cgit settings below are the same just without the
+# .git extension.
+[gitweb]
+	type = gitweb
+	cgi = /usr/share/gitweb/gitweb.cgi
+	revision = "?p=${project}.git;a=commitdiff;h=${commit}"
+[index]
+	type = LUCENE
+	threads = 4
+[download]
+        command = checkout
+        command = cherry_pick
+        command = pull
+        command = format_patch
+        scheme = ssh
+        scheme = anon_http
+        scheme = anon_git
+        archive = tar
+        archive = tbz2
+        archive = tgz
+        archive = txz
+[commitmessage]
+        maxLineLength = 72
+[groups]
+        newGroupsVisibleToAll = true
+[mimetype "image/*"]
+        safe = true
+[mimetype "text/x-yaml"]
+        safe = true
+[mimetype "text/xml"]
+        safe = true
+[mimetype "application/xml"]
+        safe = true
+[mimetype "text/x-rst"]
+        safe = true
+[mimetype "text/plain"]
+        safe = true
+[mimetype "text/x-puppet"]
+        safe = true
+[mimetype "text/x-ini"]
+        safe = true
+[mimetype "text/x-properties"]
+        safe = true
+[mimetype "text/x-markdown"]
+        safe = true
+[mimetype "text/css"]
+        safe = true
diff --git a/playbooks/roles/gerrit/templates/patchset-created.j2 b/playbooks/roles/gerrit/templates/patchset-created.j2
new file mode 100755
index 0000000000..59147b0810
--- /dev/null
+++ b/playbooks/roles/gerrit/templates/patchset-created.j2
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+# Use timeout to kill any process running longer than 10 minutes.
+timeout -k 2m 10m /usr/local/bin/update-blueprint patchset-created "$@"
+timeout -k 2m 10m /usr/local/bin/update-bug patchset-created "$@"
+timeout -k 2m 10m /usr/local/bin/notify-impact patchset-created "$@" --impact SecurityImpact --dest-address 'openstack-security@lists.openstack.org'
+{% if welcome_message_gerrit_ssh_private_key is defined %}
+timeout -k 2m 10m /usr/local/bin/welcome-message patchset-created \
+    --verbose --ssh-user=welcome-message \
+    --ssh-key=/var/gerrit/etc/ssh_welcome_rsa_key "$@"
+{% endif %}
diff --git a/playbooks/roles/install-podman/README.rst b/playbooks/roles/install-podman/README.rst
new file mode 100644
index 0000000000..7afdaaa052
--- /dev/null
+++ b/playbooks/roles/install-podman/README.rst
@@ -0,0 +1 @@
+An ansible role to install podman in the OpenDev production environment
diff --git a/playbooks/roles/install-podman/defaults/main.yaml b/playbooks/roles/install-podman/defaults/main.yaml
new file mode 100644
index 0000000000..b2555554ac
--- /dev/null
+++ b/playbooks/roles/install-podman/defaults/main.yaml
@@ -0,0 +1,29 @@
+projectatomic_gpg_key: |
+  -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+  xsFNBFlRJjABEADuE3ZLY/2W++bPsxtcaoi7VaNnkvsXuVYbbHalEh/YwKFVsDTo
+  PQpuw1UlPpmVTwT3ufWfv2v42eZiiWMZaKG9/aWF/TeIdH5+3anfVi+X+tuIW9sv
+  GKTHZdtDqd7fIhtY6AuNQ/D629TJxLvafZ5MoGeyxjsebt5dOvOrl0SHpwR75uPP
+  aCXTWrokhH7W2BbJQUB+47k62BMd03EKe8stz9FzUxptROFJJ2bITijJlDXNfSbV
+  bwCiyREIkzXS6ZdWliJAqencOIZ4UbUax+5BT8SRbSLtr/c4YxvARilpSVCkxo8/
+  EkPHBGygmgfw0kRPSGtLL7IqfWip9mFObji2geoU3A8gV/i3s9Ccc9GPKApX8r7b
+  QFs1tIlgUJKPqVwB2FAh+Xrqlsy/+8r95jL2gfRptSw7u8OP4AySj5WVm7cCEQ69
+  aLyemCsf+v72bFOUXuYQ22Kr3yqz2O/1IsG/0Usr4riTdG65Aq6gnq4KRHMNgXu8
+  7fC9omoy3sKHvzeAJsw/eC9chYNwO8pv8KRIvpDSGL5L7Ems8mq2C5xMyzSVegTr
+  AvXu7nJoZWVBFRluh42bZa9QesX9MzzfOQ+G3085aW8BE++lhtX5QOkfRd74E49H
+  1I2piAq/aE8P9jUHr60Po1C1Tw9iXeEaULLKut8eTMLkQ/02DXhBfq0I5QARAQAB
+  zSBMYXVuY2hwYWQgUFBBIGZvciBQcm9qZWN0IEF0b21pY8LBeAQTAQIAIgUCWVEm
+  MAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQi+zxY3rYx50HLw/5Ad6k
+  EHf2uT4owvzu393S/bUR6VVwCWYMbg14XgphxnoOfrHZWUjbrETTURyd1UexoHt7
+  ZDtMCVmzeY0jpvMb1W3WDebFVo+wR4CI15sPjyycsOxWTviD743wxaPCL1s009co
+  CzWg5AgP88B0D353Y39meC07BBgOJgIfk1OkFdeRjqHfAtucT99NrCuKr/bbBwDn
+  0E+wWaJoIbQvBzsPIFzMWWQ6RcnrZtyQv35epo+VBmW3VEIkorv1VoStF0RjvJM+
+  cMW/ogZsIEZk0IUREOtrtTKUXVrMw1hZ9IGYZRpbJ2g670UGuNjW/vo3rRCRSDaF
+  6Txp5Pn6ZLTgQWsWMw/6M6ooFIEpz3rhYmQSJLNmUN6SgKeWGVmOrQlg4f7YM75o
+  UEw56GKQWl9FAthO0qH0qF1OMfUKp/Tv2OSV/FNZsokf6alWXOB6Bzj6gYmmGXIv
+  MfFW5fZ1cuu5/0ULDckxWhVQ1ywLHREEoBQ6oKYONwUjSdWcM+VsKCEFeCqsNwak
+  qweP8C0fooycfiEZuncc/9ZujgkQ2p7xXTlv3t2SPF9h43xHs3515VS/OTJPGW59
+  98AqllpfqGxggYs5cwi2LO3xwvHyPoTqj3hcl1dRMspZINRsIo4VC8bSrCOqbjDc
+  CD2WFOo2c4mwTDmJpz0PLK87ev/WZ8K0OEflTfc=
+  =DzDk
+  -----END PGP PUBLIC KEY BLOCK-----
diff --git a/playbooks/roles/install-podman/tasks/main.yaml b/playbooks/roles/install-podman/tasks/main.yaml
new file mode 100644
index 0000000000..8318382b4a
--- /dev/null
+++ b/playbooks/roles/install-podman/tasks/main.yaml
@@ -0,0 +1,20 @@
+- name: Add PPA GPG key
+  become: yes
+  apt_key:
+    data: "{{ projectatomic_gpg_key }}"
+
+- name: Add projectatomic apt repo
+  become: yes
+  template:
+    dest: /etc/apt/sources.list.d/projectatomic.list
+    group: root
+    mode: 0644
+    owner: root
+    src: sources.list.j2
+
+- name: Install podman
+  become: yes
+  apt:
+    name: podman
+    state: present
+    update_cache: yes
diff --git a/playbooks/roles/install-podman/templates/sources.list.j2 b/playbooks/roles/install-podman/templates/sources.list.j2
new file mode 100644
index 0000000000..cc249acda6
--- /dev/null
+++ b/playbooks/roles/install-podman/templates/sources.list.j2
@@ -0,0 +1 @@
+deb http://ppa.launchpad.net/projectatomic/ppa/ubuntu {{ ansible_lsb.codename }} main 
diff --git a/playbooks/service-review-dev.yaml b/playbooks/service-review-dev.yaml
new file mode 100644
index 0000000000..eb274834a8
--- /dev/null
+++ b/playbooks/service-review-dev.yaml
@@ -0,0 +1,9 @@
+- hosts: "review-dev01.openstack.org:!disabled"
+  name: "Configure gerrit on review-dev01.openstack.org"
+  roles:
+    - pip3
+    - install-podman
+    - role: gerrit
+      gerrit_ssh_rsa_key_contents: "{{ gerrit_dev_ssh_rsa_key_contents }}"
+      gerrit_ssh_rsa_pubkey_contents: "{{ gerrit_dev_ssh_rsa_pubkey_contents }}"
+      gerrit_database_config_section: "{{ gerrit_dev_database_config_section }}"
diff --git a/playbooks/zuul/run-base-post.yaml b/playbooks/zuul/run-base-post.yaml
index 24f8b464df..57380ac970 100644
--- a/playbooks/zuul/run-base-post.yaml
+++ b/playbooks/zuul/run-base-post.yaml
@@ -16,19 +16,32 @@
 
 - hosts: all
   tasks:
-    - name: List containers
-      command: "docker ps -a --format '{{ '{{ .Names }}' }}'"
-      register: docker_containers
-      ignore_errors: true
-      become: true
-
     - name: Create container log dir
       file:
         path: "/var/log/docker"
         state: directory
       become: true
 
-    - name: Save container logs
+    - name: List podman containers
+      command: "podman ps -a --format '{{ '{{ .Names }}' }}'"
+      register: podman_containers
+      ignore_errors: true
+      become: true
+
+    - name: Save podman container logs
+      loop: "{{ podman_containers.stdout_lines | default([]) }}"
+      shell: "podman logs {{ item }} &> /var/log/docker/{{ item }}.txt"
+      args:
+        executable: /bin/bash
+      become: true
+
+    - name: List docker containers
+      command: "docker ps -a --format '{{ '{{ .Names }}' }}'"
+      register: docker_containers
+      ignore_errors: true
+      become: true
+
+    - name: Save docker container logs
       loop: "{{ docker_containers.stdout_lines | default([]) }}"
       shell: "docker logs {{ item }} &> /var/log/docker/{{ item }}.txt"
       args:
diff --git a/playbooks/zuul/run-base.yaml b/playbooks/zuul/run-base.yaml
index fe42556df8..54158067a9 100644
--- a/playbooks/zuul/run-base.yaml
+++ b/playbooks/zuul/run-base.yaml
@@ -80,6 +80,8 @@
         - group_vars/gitea-lb.yaml
         - group_vars/letsencrypt.yaml
         - group_vars/registry.yaml
+        - group_vars/review.yaml
+        - group_vars/review-dev.yaml
         - group_vars/control-plane-clouds.yaml
         - group_vars/mirror_opendev.yaml
         - host_vars/bridge.openstack.org.yaml
diff --git a/playbooks/zuul/templates/group_vars/review-dev.yaml.j2 b/playbooks/zuul/templates/group_vars/review-dev.yaml.j2
new file mode 100644
index 0000000000..4b9a212adf
--- /dev/null
+++ b/playbooks/zuul/templates/group_vars/review-dev.yaml.j2
@@ -0,0 +1,34 @@
+gerrit_dev_ssh_rsa_key_contents: |
+  -----BEGIN RSA PRIVATE KEY-----
+  MIIEpQIBAAKCAQEAvqQkJUwEGJgqzmOmj2728ikA3Dgl4mzzRiI5zzzLhCLQktL7
+  UZ6hAc+851mUuQ66vciqUToerhRdNWqNlZzY8On/jXUTVdfUVlXupmDfqmtlax9n
+  Gi8Aayj4h9F3P3iuNQ+eXWVOyFsCmAMPzaYgWgXx2rxcIqSPq5hG+vDB9oXIpg3a
+  /244vtIZId1/uNnZDb5jHbZLglupynQBIZ12h2XOUiUfxL84EQnwfL2zC9d6eOjv
+  cYaCYJ+2VrzG972keY8SJEMLLVEd+q2hFYbbQQxLlmrFpx2G6zNc3d6KLnW0xEH+
+  b1yEmShcoB8iRRam+IkO+ovoQ5W9FUCmQnWqJQIDAQABAoIBAQCABKeFKDPD9EMi
+  j0ZlIUXRPfmm6EmAcFs46Hjbkl60H9DiF63OlHBYUBDxZnV5g8ug8CR3IUlC6sfg
+  u+nR4x7HQMtsSYcvaRzc0R3OOlVYEoBrXA4XRiLI0x15zw593+NUHGXjH8m0v3DR
+  dZTiK0GkUkOj+EMCvHEB8OMRViVaDjel+TXI6lM2UuexpYLag/u+GotDUQ8sjq4v
+  kT2FWzrZAGHih98oZ7AflinasGjMBr21lCRq/u3w0ieU4ZPmxpLxz9EUbWS06kQe
+  W2pafIhDq+4mVtDuhmb87yMMpda+TCBcCRCzA3MGDMGVzQgQvPe49R4EPRWzBWXU
+  vso8JSzBAoGBAPrDobXukDJ0j2zD3Cw7xCSJy3foU4sdQF5yxeIWox1gfsgkAhn2
+  zxmuewdfdh0hZAnZWZ41sJzFrEhrvTV/wFIfIZ2qlMf3q3ZdG8c0CQ6E3blsAF8g
+  jExgck8mV2kvZItowS8JCdWvNw1zpwNJ8Ae+mMUSMBKbs5hzO8X0V1FxAoGBAMKf
+  JNjqN/LegvlnfFXMPM+vo39MX2RsukwmTZT6msMi3rNgN3oekXSpOK1Mkftrd/wL
+  B5Vp9Nl3d3kFu3bTKA+N8bjtokXfN+fF9zPFeLwOfEOEtQlnvzb/XF4LhSGiq7Kg
+  OP6/4udaa9cr9yLE7jTDoLTiyRP796k4ADGRo8n1AoGBAPad0XsAbPYCJk/ca7tg
+  5+zbS6zYYtlM4lJA5BYPF0o77VPd/ecVEOZ772j33EyX2y/If1eWC+SSj3YF52Yn
+  Brh8/211Jq9nB0z4EuqdZU7PItRJwC/N7czriWAIIC1FO8fTNTKR3Ac4M5daJ2WX
+  oKWtL8eYtIiIcl867Q0o2XsxAoGAHtJrWHsHb0wz++GavE/DFbUgNInY1aOugV7T
+  jJN/rQW5tTi4sHtk18/w7OkieWnUr7LjeZM1NreH0T/KJ5BbWNNHA92GhIBMb0VX
+  1/gfHdFBdsPuk9W9X61jaHV0mRYVEQ302Mt02OTmDUamNfhwLSUKL3t3EtYlq8P1
+  q+P/x30CgYEAiLaGdCtlPy5e35ULD/Ep73kOB1uRVtF4JlVpqZ4CygVoCguSZ3Sd
+  pHMmNylg7j2NyL/9aLKs1NzdGBxpxVa5A4vgcr1DjoS1cuRVEiQoSkI6D6DCmENA
+  Pb95AevPUxqqAKNZYsj4yDsXnmbFSHARijPWcpfkCDJmVhMFPObr4OE=
+  -----END RSA PRIVATE KEY-----
+gerrit_dev_ssh_rsa_pubkey_contents: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+pCQlTAQYmCrOY6aPbvbyKQDcOCXibPNGIjnPPMuEItCS0vtRnqEBz7znWZS5Drq9yKpROh6uFF01ao2VnNjw6f+NdRNV19RWVe6mYN+qa2VrH2caLwBrKPiH0Xc/eK41D55dZU7IWwKYAw/NpiBaBfHavFwipI+rmEb68MH2hcimDdr/bji+0hkh3X+42dkNvmMdtkuCW6nKdAEhnXaHZc5SJR/EvzgRCfB8vbML13p46O9xhoJgn7ZWvMb3vaR5jxIkQwstUR36raEVhttBDEuWasWnHYbrM1zd3ooudbTEQf5vXISZKFygHyJFFqb4iQ76i+hDlb0VQKZCdaol gerrit-code-review@829f141b0fa5
+gerrit_dev_database_config_section: |
+  [database]
+  	type = h2
+  	database = tmp/ReviewDB
+gerrit_run_init: true
diff --git a/playbooks/zuul/templates/group_vars/review.yaml.j2 b/playbooks/zuul/templates/group_vars/review.yaml.j2
new file mode 100644
index 0000000000..0d742b678f
--- /dev/null
+++ b/playbooks/zuul/templates/group_vars/review.yaml.j2
@@ -0,0 +1,29 @@
+gerrit_ssh_rsa_key_contents: |
+  -----BEGIN RSA PRIVATE KEY-----
+  MIIEpQIBAAKCAQEAvqQkJUwEGJgqzmOmj2728ikA3Dgl4mzzRiI5zzzLhCLQktL7
+  UZ6hAc+851mUuQ66vciqUToerhRdNWqNlZzY8On/jXUTVdfUVlXupmDfqmtlax9n
+  Gi8Aayj4h9F3P3iuNQ+eXWVOyFsCmAMPzaYgWgXx2rxcIqSPq5hG+vDB9oXIpg3a
+  /244vtIZId1/uNnZDb5jHbZLglupynQBIZ12h2XOUiUfxL84EQnwfL2zC9d6eOjv
+  cYaCYJ+2VrzG972keY8SJEMLLVEd+q2hFYbbQQxLlmrFpx2G6zNc3d6KLnW0xEH+
+  b1yEmShcoB8iRRam+IkO+ovoQ5W9FUCmQnWqJQIDAQABAoIBAQCABKeFKDPD9EMi
+  j0ZlIUXRPfmm6EmAcFs46Hjbkl60H9DiF63OlHBYUBDxZnV5g8ug8CR3IUlC6sfg
+  u+nR4x7HQMtsSYcvaRzc0R3OOlVYEoBrXA4XRiLI0x15zw593+NUHGXjH8m0v3DR
+  dZTiK0GkUkOj+EMCvHEB8OMRViVaDjel+TXI6lM2UuexpYLag/u+GotDUQ8sjq4v
+  kT2FWzrZAGHih98oZ7AflinasGjMBr21lCRq/u3w0ieU4ZPmxpLxz9EUbWS06kQe
+  W2pafIhDq+4mVtDuhmb87yMMpda+TCBcCRCzA3MGDMGVzQgQvPe49R4EPRWzBWXU
+  vso8JSzBAoGBAPrDobXukDJ0j2zD3Cw7xCSJy3foU4sdQF5yxeIWox1gfsgkAhn2
+  zxmuewdfdh0hZAnZWZ41sJzFrEhrvTV/wFIfIZ2qlMf3q3ZdG8c0CQ6E3blsAF8g
+  jExgck8mV2kvZItowS8JCdWvNw1zpwNJ8Ae+mMUSMBKbs5hzO8X0V1FxAoGBAMKf
+  JNjqN/LegvlnfFXMPM+vo39MX2RsukwmTZT6msMi3rNgN3oekXSpOK1Mkftrd/wL
+  B5Vp9Nl3d3kFu3bTKA+N8bjtokXfN+fF9zPFeLwOfEOEtQlnvzb/XF4LhSGiq7Kg
+  OP6/4udaa9cr9yLE7jTDoLTiyRP796k4ADGRo8n1AoGBAPad0XsAbPYCJk/ca7tg
+  5+zbS6zYYtlM4lJA5BYPF0o77VPd/ecVEOZ772j33EyX2y/If1eWC+SSj3YF52Yn
+  Brh8/211Jq9nB0z4EuqdZU7PItRJwC/N7czriWAIIC1FO8fTNTKR3Ac4M5daJ2WX
+  oKWtL8eYtIiIcl867Q0o2XsxAoGAHtJrWHsHb0wz++GavE/DFbUgNInY1aOugV7T
+  jJN/rQW5tTi4sHtk18/w7OkieWnUr7LjeZM1NreH0T/KJ5BbWNNHA92GhIBMb0VX
+  1/gfHdFBdsPuk9W9X61jaHV0mRYVEQ302Mt02OTmDUamNfhwLSUKL3t3EtYlq8P1
+  q+P/x30CgYEAiLaGdCtlPy5e35ULD/Ep73kOB1uRVtF4JlVpqZ4CygVoCguSZ3Sd
+  pHMmNylg7j2NyL/9aLKs1NzdGBxpxVa5A4vgcr1DjoS1cuRVEiQoSkI6D6DCmENA
+  Pb95AevPUxqqAKNZYsj4yDsXnmbFSHARijPWcpfkCDJmVhMFPObr4OE=
+  -----END RSA PRIVATE KEY-----
+gerrit_ssh_rsa_pubkey_contents: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+pCQlTAQYmCrOY6aPbvbyKQDcOCXibPNGIjnPPMuEItCS0vtRnqEBz7znWZS5Drq9yKpROh6uFF01ao2VnNjw6f+NdRNV19RWVe6mYN+qa2VrH2caLwBrKPiH0Xc/eK41D55dZU7IWwKYAw/NpiBaBfHavFwipI+rmEb68MH2hcimDdr/bji+0hkh3X+42dkNvmMdtkuCW6nKdAEhnXaHZc5SJR/EvzgRCfB8vbML13p46O9xhoJgn7ZWvMb3vaR5jxIkQwstUR36raEVhttBDEuWasWnHYbrM1zd3ooudbTEQf5vXISZKFygHyJFFqb4iQ76i+hDlb0VQKZCdaol gerrit-code-review@829f141b0fa5
diff --git a/testinfra/test_gerrit.py b/testinfra/test_gerrit.py
new file mode 100644
index 0000000000..7fb3d1a1cf
--- /dev/null
+++ b/testinfra/test_gerrit.py
@@ -0,0 +1,21 @@
+# Copyright 2018 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.
+
+
+testinfra_hosts = ['review-dev01.openstack.org']
+
+
+def test_gerrit_listening(host):
+    gerrit_web = host.socket("tcp://:::8081")
+    assert gerrit_web.is_listening