Merge "Fix running with SERVICE_IP_VERSION=6"
diff --git a/.gitignore b/.gitignore
index d2c127d..8553b3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 *.log
 *.log.[1-9]
 *.pem
+*.pyc
 .localrc.auto
 .localrc.password
 .prereqs
diff --git a/.zuul.yaml b/.zuul.yaml
index 25bd757..c8bb337 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -29,6 +29,16 @@
           - controller
 
 - nodeset:
+    name: devstack-single-node-opensuse-tumbleweed
+    nodes:
+      - name: controller
+        label: opensuse-tumbleweed
+    groups:
+      - name: tempest
+        nodes:
+          - controller
+
+- nodeset:
     name: devstack-single-node-fedora-27
     nodes:
       - name: controller
@@ -39,6 +49,16 @@
           - controller
 
 - nodeset:
+    name: devstack-single-node-fedora-latest
+    nodes:
+      - name: controller
+        label: fedora-28
+    groups:
+      - name: tempest
+        nodes:
+          - controller
+
+- nodeset:
     name: openstack-two-node
     nodes:
       - name: controller
@@ -46,19 +66,24 @@
       - name: compute1
         label: ubuntu-xenial
     groups:
+      # Node where tests are executed and test results collected
       - name: tempest
         nodes:
           - controller
+      # Nodes running the compute service
       - name: compute
         nodes:
           - controller
           - compute1
+      # Nodes that are not the controller
       - name: subnode
         nodes:
           - compute1
+      # Switch node for multinode networking setup
       - name: switch
         nodes:
           - controller
+      # Peer nodes for multinode networking setup
       - name: peers
         nodes:
           - compute1
@@ -75,7 +100,7 @@
       all single Devstack jobs, single or multinode.
       Variables are defined in job.vars, which is what is then used by single
       node jobs and by multi node jobs for the controller, as well as in
-      job.group-vars.peers, which is what is used by multi node jobs for peer
+      job.group-vars.peers, which is what is used by multi node jobs for subnode
       nodes (everything but the controller).
     required-projects:
       - openstack-dev/devstack
@@ -101,11 +126,8 @@
         ERROR_ON_CLONE: true
         # Gate jobs can't deal with nested virt. Disable it.
         LIBVIRT_TYPE: qemu
-        # NOTE(dims): etcd 3.x is not available in debian/ubuntu
-        # etc. As a stop gap measure, devstack uses wget to download
-        # from the location below for all the CI jobs.
-        ETCD_DOWNLOAD_URL: http://tarballs.openstack.org/etcd/
       devstack_services:
+        # Ignore any default set by devstack. Emit a "disable_all_services".
         base: false
       zuul_copy_output:
         '{{ devstack_conf_dir }}/local.conf': 'logs'
@@ -149,7 +171,7 @@
         stackenv: True
         auto: True
     group-vars:
-      peers:
+      subnode:
         devstack_localrc:
           DATABASE_PASSWORD: secretdatabase
           RABBIT_PASSWORD: secretrabbit
@@ -167,7 +189,6 @@
           NOVNC_FROM_PACKAGE: true
           ERROR_ON_CLONE: true
           LIBVIRT_TYPE: qemu
-          ETCD_DOWNLOAD_URL: http://tarballs.openstack.org/etcd/
         devstack_services:
           base: false
     pre-run: playbooks/pre.yaml
@@ -183,10 +204,70 @@
       - ^.*/locale/.*po$
 
 - job:
-    name: devstack
+    name: devstack-minimal
     parent: devstack-base
     description: |
-      Single node devstack job for integration gate.
+      Minimal devstack base job, intended for use by jobs that need
+      less than the normal minimum set of required-projects.
+    nodeset: openstack-single-node
+    required-projects:
+      - openstack/requirements
+    vars:
+      devstack_localrc:
+        # Multinode specific settings
+        SERVICE_HOST: "{{ hostvars['controller']['nodepool']['private_ipv4'] }}"
+        HOST_IP: "{{ hostvars['controller']['nodepool']['private_ipv4'] }}"
+        PUBLIC_BRIDGE_MTU: "{{ external_bridge_mtu }}"
+      devstack_services:
+        # Shared services
+        dstat: true
+        etcd3: true
+        mysql: true
+        peakmem_tracker: true
+        rabbit: true
+    group-vars:
+      subnode:
+        devstack_services:
+          # Shared services
+          dstat: true
+          peakmem_tracker: true
+        devstack_localrc:
+          # Multinode specific settings
+          HOST_IP: "{{ hostvars[inventory_hostname]['nodepool']['private_ipv4'] }}"
+          SERVICE_HOST: "{{ hostvars['controller']['nodepool']['private_ipv4'] }}"
+          PUBLIC_BRIDGE_MTU: "{{ external_bridge_mtu }}"
+          # Subnode specific settings
+          DATABASE_TYPE: mysql
+          RABBIT_HOST: "{{ hostvars['controller']['nodepool']['private_ipv4'] }}"
+          DATABASE_HOST: "{{ hostvars['controller']['nodepool']['private_ipv4'] }}"
+
+
+- job:
+    name: devstack
+    parent: devstack-minimal
+    description: |
+      Base devstack job for integration gate.
+
+      This base job can be used for single node and multinode devstack jobs.
+
+      With a single node nodeset, this job sets up an "all-in-one" (aio)
+      devstack with the six OpenStack services included in the devstack tree:
+      keystone, glance, cinder, neutron, nova and swift.
+
+      With a two node nodeset, this job sets up an aio + compute node.
+      The controller can be customised using host-vars.controller, the
+      sub-nodes can be customised using group-vars.subnode.
+
+      Descendent jobs can enable / disable services, add devstack configuration
+      options, enable devstack plugins, configure log files or directories to be
+      transferred to the log server.
+
+      The job assumes that there is only one controller node. The number of
+      subnodes can be scaled up seamlessly by setting a custom nodeset in
+      job.nodeset.
+
+      The run playbook consists of a single role, so it can be easily rewritten
+      and extended.
     nodeset: openstack-single-node
     required-projects:
       - openstack/cinder
@@ -194,11 +275,9 @@
       - openstack/keystone
       - openstack/neutron
       - openstack/nova
-      - openstack/requirements
       - openstack/swift
     timeout: 7200
     vars:
-      test_matrix_configs: [neutron, tlsproxy]
       devstack_localrc:
         # Common OpenStack services settings
         SWIFT_REPLICAS: 1
@@ -208,21 +287,111 @@
         DEBUG_LIBVIRT_COREDUMPS: True
         NOVA_VNC_ENABLED: true
         VNCSERVER_LISTEN: 0.0.0.0
-        VNCSERVER_PROXYCLIENT_ADDRESS: "{{ hostvars[inventory_hostname]['nodepool']['private_ipv4'] }}"
+        VNCSERVER_PROXYCLIENT_ADDRESS: $HOST_IP
+      devstack_local_conf:
+        post-config:
+          $NEUTRON_CONF:
+            DEFAULT:
+              global_physnet_mtu: "{{ external_bridge_mtu }}"
       devstack_services:
-        base: true
+        # Core services enabled for this branch.
+        # This list replaces the test-matrix.
+        # Shared services
+        dstat: true
+        etcd3: true
+        mysql: true
+        peakmem_tracker: true
+        rabbit: true
+        tls-proxy: true
+        # Keystone services
+        key: true
+        # Glance services
+        g-api: true
+        g-reg: true
+        # Nova services
+        n-api: true
+        n-api-meta: true
+        n-cauth: true
+        n-cond: true
+        n-cpu: true
+        n-novnc: true
+        n-obj: true
+        n-sch: true
+        placement-api: true
+        # Neutron services
+        # We need to keep using the neutron-legacy based services for
+        # now until all issues with the new lib/neutron code are solved
+        q-agt: true
+        q-dhcp: true
+        q-l3: true
+        q-meta: true
+        q-metering: true
+        q-svc: true
+        # neutron-api: true
+        # neutron-agent: true
+        # neutron-dhcp: true
+        # neutron-l3: true
+        # neutron-metadata-agent: true
+        # neutron-metering: true
+        # Swift services
+        s-account: true
+        s-container: true
+        s-object: true
+        s-proxy: true
+        # Cinder services
+        c-api: true
+        c-bak: true
+        c-sch: true
+        c-vol: true
+        cinder: true
+        # Services we don't need.
+        # This section is not really needed, it's for readability.
         horizon: false
         tempest: false
+        # Test matrix emits ceilometer but ceilomenter is not installed in the
+        # integrated gate, so specifying the services has not effect.
+        # ceilometer-*: false
+    group-vars:
+      subnode:
+        devstack_services:
+          # Core services enabled for this branch.
+          # This list replaces the test-matrix.
+          # Shared services
+          dstat: true
+          peakmem_tracker: true
+          tls-proxy: true
+          # Nova services
+          n-cpu: true
+          placement-client: true
+          # Neutron services
+          neutron-agent: true
+          # Cinder services
+          c-bak: true
+          c-vol: true
+          # Services we don't run at all on subnode.
+          # This section is not really needed, it's for readability.
+          # keystone: false
+          # s-*: false
+          horizon: false
+          tempest: false
+          # Test matrix emits ceilometer but ceilomenter is not installed in the
+          # integrated gate, so specifying the services has not effect.
+          # ceilometer-*: false
+        devstack_localrc:
+          # Subnode specific settings
+          GLANCE_HOSTPORT: "{{ hostvars['controller']['nodepool']['private_ipv4'] }}:9292"
+          Q_HOST: "{{ hostvars['controller']['nodepool']['private_ipv4'] }}"
+          NOVA_VNC_ENABLED: true
+          VNCSERVER_LISTEN: 0.0.0.0
+          VNCSERVER_PROXYCLIENT_ADDRESS: $HOST_IP
 
 - job:
     name: devstack-multinode
-    parent: devstack-base
-    description: Base devstack multinode job
+    parent: devstack
     nodeset: openstack-two-node
-    # NOTE(andreaf) The multinode job is useful to see the setup of different
-    # services on different nodes, however the subnode configuration is not
-    # ready yet. Until then this job should stay non-voting.
-    voting: false
+    description: |
+      Simple multinode test to verify multinode functionality on devstack side.
+      This is not meant to be used as a parent job.
 
 # NOTE(ianw) Platform tests have traditionally been non-voting because
 # we often have to rush things through devstack to stabilise the gate,
@@ -238,15 +407,22 @@
 - job:
     name: devstack-platform-opensuse-423
     parent: tempest-full
-    description: OpenSuSE 43.2 platform test
+    description: openSUSE 43.2 platform test
     nodeset: devstack-single-node-opensuse-423
     voting: false
 
 - job:
-    name: devstack-platform-fedora-27
+    name: devstack-platform-opensuse-tumbleweed
     parent: tempest-full
-    description: Fedora 27 platform test
-    nodeset: devstack-single-node-fedora-27
+    description: openSUSE Tumbleweed platform test
+    nodeset: devstack-single-node-opensuse-tumbleweed
+    voting: false
+
+- job:
+    name: devstack-platform-fedora-latest
+    parent: tempest-full
+    description: Fedora latest platform test
+    nodeset: devstack-single-node-fedora-latest
     voting: false
 
 - job:
@@ -318,7 +494,8 @@
         - devstack
         - devstack-platform-centos-7
         - devstack-platform-opensuse-423
-        - devstack-platform-fedora-27
+        - devstack-platform-opensuse-tumbleweed
+        - devstack-platform-fedora-latest
         - devstack-multinode
         - devstack-unit-tests
     gate:
@@ -329,8 +506,15 @@
     # being experimental any more, so we can keep this list somewhat
     # pruned.
     #
+    # * nova-cells-v1: maintained by nova for cells v1 (nova-cells service);
+    #    nova gates on this job, it's in experimental for testing cells v1
+    #    changes to devstack w/o gating on it for all devstack changes.
     # * nova-next: maintained by nova for unreleased/undefaulted
     #    things like cellsv2 and placement-api
     experimental:
       jobs:
+        - nova-cells-v1:
+            irrelevant-files:
+              - ^.*\.rst$
+              - ^doc/.*$
         - nova-next
diff --git a/clean.sh b/clean.sh
index 2333596..a29ebd9 100755
--- a/clean.sh
+++ b/clean.sh
@@ -122,9 +122,6 @@
 if [[ -n "$LOGDIR" ]] && [[ -d "$LOGDIR" ]]; then
     sudo rm -rf $LOGDIR
 fi
-if [[ -n "$SCREEN_LOGDIR" ]] && [[ -d "$SCREEN_LOGDIR" ]]; then
-    sudo rm -rf $SCREEN_LOGDIR
-fi
 
 # Clean out the sytemd user unit files if systemd was used.
 if [[ "$USE_SYSTEMD" = "True" ]]; then
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 1d02395..7efe4d6 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -41,6 +41,7 @@
 -  **extra** - runs after services are started and before any files in
    ``extra.d`` are executed
 -  **post-extra** - runs after files in ``extra.d`` are executed
+-  **test-config** - runs after tempest (and plugins) are configured
 
 The file is processed strictly in sequence; meta-sections may be
 specified more than once but if any settings are duplicated the last to
@@ -655,7 +656,7 @@
 Cells
 ~~~~~
 
-`Cells <http://wiki.openstack.org/blueprint-nova-compute-cells>`__ is
+`Cells <https://wiki.openstack.org/wiki/Blueprint-nova-compute-cells>`__ is
 an alternative scaling option.  To setup a cells environment add the
 following to your ``localrc`` section:
 
diff --git a/doc/source/faq.rst b/doc/source/faq.rst
index ed9b4da..efb315c 100644
--- a/doc/source/faq.rst
+++ b/doc/source/faq.rst
@@ -18,6 +18,57 @@
 Your best choice is probably to choose a `distribution of OpenStack
 <https://www.openstack.org/marketplace/distros/>`__.
 
+Can I use DevStack as a development environment?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sure, you can. That said, there are a couple of things you should note before
+doing so:
+
+- DevStack makes a lot of configuration changes to your system and should not
+  be run in your main development environment.
+
+- All the repositories that DevStack clones when deploying are considered
+  volatile by default and thus are subject to hard resets. This is necessary to
+  keep you in sync with the latest upstream, which is what you want in a CI
+  situation, but it can result in branches being overwritten and files being
+  removed.
+
+  The corollary of this is that if you are working on a specific project, using
+  the DevStack project repository (defaulted to ``/opt/stack/<project>``) as
+  the single master repository for storing all your work is not recommended.
+  This behavior can be overridden by setting the ``RECLONE`` config option to
+  ``no``.  Alternatively, you can avoid running ``stack.sh`` to redeploy by
+  restarting services manually. In any case, you should generally ensure work
+  in progress is pushed to Gerrit or otherwise backed up before running
+  ``stack.sh``.
+
+- If you use DevStack within a VM, you may wish to mount a local OpenStack
+  directory, such as ``~/src/openstack``, inside the VM and configure DevStack
+  to use this as the clone location using the ``{PROJECT}_REPO`` config
+  variables. For example, assuming you're using Vagrant and sharing your home
+  directory, you should place the following in ``local.conf``:
+
+  .. code-block:: shell
+
+     NEUTRON_REPO=/home/vagrant/src/neutron
+     NOVA_REPO=/home/vagrant/src/nova
+     KEYSTONE_REPO=/home/vagrant/src/keystone
+     GLANCE_REPO=/home/vagrant/src/glance
+     SWIFT_REPO=/home/vagrant/src/swift
+     HORIZON_REPO=/home/vagrant/src/horizon
+     CINDER_REPO=/home/vagrant/src/cinder
+     HEAT_REPO=/home/vagrant/src/heat
+     TEMPEST_REPO=/home/vagrant/src/tempest
+     HEATCLIENT_REPO=/home/vagrant/src/python-heatclient
+     GLANCECLIENT_REPO=/home/vagrant/src/python-glanceclient
+     NOVACLIENT_REPO=/home/vagrant/src/python-novaclient
+     NEUTRONCLIENT_REPO=/home/vagrant/src/python-neutronclient
+     OPENSTACKCLIENT_REPO=/home/vagrant/src/python-openstackclient
+     HEAT_CFNTOOLS_REPO=/home/vagrant/src/heat-cfntools
+     HEAT_TEMPLATES_REPO=/home/vagrant/src/heat-templates
+     NEUTRON_FWAAS_REPO=/home/vagrant/src/neutron-fwaas
+     # ...
+
 Why a shell script, why not chef/puppet/...
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/doc/source/guides.rst b/doc/source/guides.rst
index c2c7b91..82e0dd6 100644
--- a/doc/source/guides.rst
+++ b/doc/source/guides.rst
@@ -20,6 +20,7 @@
    guides/devstack-with-nested-kvm
    guides/nova
    guides/devstack-with-lbaas-v2
+   guides/devstack-with-ldap
 
 All-In-One Single VM
 --------------------
@@ -66,3 +67,8 @@
 --------------------------------
 
 Guide to working with nova features :doc:`Nova and devstack <guides/nova>`.
+
+Deploying DevStack with LDAP
+----------------------------
+
+Guide to setting up :doc:`DevStack with LDAP <guides/devstack-with-ldap>`.
diff --git a/doc/source/guides/devstack-with-lbaas-v2.rst b/doc/source/guides/devstack-with-lbaas-v2.rst
index 3592844..df3c7ce 100644
--- a/doc/source/guides/devstack-with-lbaas-v2.rst
+++ b/doc/source/guides/devstack-with-lbaas-v2.rst
@@ -2,7 +2,7 @@
 =================================
 
 Starting in the OpenStack Liberty release, the
-`neutron LBaaS v2 API <http://developer.openstack.org/api-ref-networking-v2-ext.html>`_
+`neutron LBaaS v2 API <https://developer.openstack.org/api-ref/network/v2/index.html>`_
 is now stable while the LBaaS v1 API has been deprecated.  The LBaaS v2 reference
 driver is based on Octavia.
 
@@ -15,7 +15,7 @@
 
 Install devstack
 
-  ::
+::
 
     git clone https://git.openstack.org/openstack-dev/devstack
     cd devstack
@@ -23,7 +23,7 @@
 
 Edit your ``local.conf`` to look like
 
-  ::
+::
 
     [[local|localrc]]
     # Load the external LBaaS plugin.
@@ -60,7 +60,7 @@
 
 Run stack.sh and do some sanity checks
 
-  ::
+::
 
     ./stack.sh
     . ./openrc
@@ -69,7 +69,7 @@
 
 Create two nova instances that we can use as test http servers:
 
-  ::
+::
 
     #create nova instances on private network
     nova boot --image $(nova image-list | awk '/ cirros-.*-x86_64-uec / {print $2}') --flavor 1 --nic net-id=$(openstack network list | awk '/ private / {print $2}') node1
@@ -83,7 +83,7 @@
 
 Set up a simple web server on each of these instances. ssh into each instance (username 'cirros', password 'cubswin:)') and run
 
- ::
+::
 
     MYIP=$(ifconfig eth0|grep 'inet addr'|awk -F: '{print $2}'| awk '{print $1}')
     while true; do echo -e "HTTP/1.0 200 OK\r\n\r\nWelcome to $MYIP" | sudo nc -l -p 80 ; done&
@@ -91,7 +91,7 @@
 Phase 2: Create your load balancers
 ------------------------------------
 
- ::
+::
 
     neutron lbaas-loadbalancer-create --name lb1 private-subnet
     neutron lbaas-loadbalancer-show lb1  # Wait for the provisioning_status to be ACTIVE.
diff --git a/doc/source/guides/devstack-with-ldap.rst b/doc/source/guides/devstack-with-ldap.rst
new file mode 100644
index 0000000..ec41141
--- /dev/null
+++ b/doc/source/guides/devstack-with-ldap.rst
@@ -0,0 +1,174 @@
+============================
+Deploying DevStack with LDAP
+============================
+
+The OpenStack Identity service has the ability to integrate with LDAP. The goal
+of this guide is to walk you through setting up an LDAP-backed OpenStack
+development environment.
+
+Introduction
+============
+
+LDAP support in keystone is read-only. You can use it to back an entire
+OpenStack deployment to a single LDAP server, or you can use it to back
+separate LDAP servers to specific keystone domains. Users within those domains
+will can authenticate against keystone, assume role assignments, and interact
+with other OpenStack services.
+
+Configuration
+=============
+
+To deploy an OpenLDAP server, make sure ``ldap`` is added to the list of
+``ENABLED_SERVICES``::
+
+    enable_service ldap
+
+Devstack will require a password to set up an LDAP administrator. This
+administrative user is also the bind user specified in keystone's configuration
+files, similar to a ``keystone`` user for MySQL databases.
+
+Devstack will prompt you for a password when running ``stack.sh`` if
+``LDAP_PASSWORD`` is not set. You can add the following to your
+``local.conf``::
+
+    LDAP_PASSWORD=super_secret_password
+
+At this point, devstack should have everything it needs to deploy OpenLDAP,
+bootstrap it with a minimal set of users, and configure it to back to a domain
+in keystone::
+
+    ./stack.sh
+
+Once ``stack.sh`` completes, you should have a running keystone deployment with
+a basic set of users. It is important to note that not all users will live
+within LDAP. Instead, keystone will back different domains to different
+identity sources. For example, the ``default`` domain will be backed by MySQL.
+This is usually where you'll find your administrative and services users. If
+you query keystone for a list of domains, you should see a domain called
+``Users``. This domain is set up by devstack and points to OpenLDAP.
+
+User Management
+===============
+
+Initially, there will only be two users in the LDAP server. The ``Manager``
+user is used by keystone to talk to OpenLDAP. The ``demo`` user is a generic
+user that you should be able to see if you query keystone for users within the
+``Users`` domain. Both of these users were added to LDAP using basic LDAP
+utilities installed by devstack (e.g. ``ldap-utils``) and LDIFs. The LDIFs used
+to create these users can be found in ``devstack/files/ldap/``.
+
+Listing Users
+-------------
+
+To list all users in LDAP directly, you can use ``ldapsearch`` with the LDAP
+user bootstrapped by devstack::
+
+    ldapsearch -x -w LDAP_PASSWORD -D cn=Manager,dc=openstack,dc=org \
+        -H ldap://localhost -b dc=openstack,dc=org
+
+As you can see, devstack creates an OpenStack domain called ``openstack.org``
+as a container for the ``Manager`` and ``demo`` users.
+
+Creating Users
+--------------
+
+Since keystone's LDAP integration is read-only, users must be added directly to
+LDAP. Users added directly to OpenLDAP will automatically be placed into the
+``Users`` domain.
+
+LDIFs can be used to add users via the command line. The following is an
+example LDIF that can be used to create a new LDAP user, let's call it
+``peter.ldif.in``::
+
+    dn: cn=peter,ou=Users,dc=openstack,dc=org
+    cn: peter
+    displayName: Peter Quill
+    givenName: Peter Quill
+    mail: starlord@openstack.org
+    objectClass: inetOrgPerson
+    objectClass: top
+    sn: peter
+    uid: peter
+    userPassword: im-a-better-pilot-than-rocket
+
+Now, we use the ``Manager`` user to create a user for Peter in LDAP::
+
+    ldapadd -x -w LDAP_PASSWORD -D cn=Manager,dc=openstack,dc=org \
+        -H ldap://localhost -c -f peter.ldif.in
+
+We should be able to assign Peter roles on projects. After Peter has some level
+of authorization, he should be able to login to Horizon by specifying the
+``Users`` domain and using his ``peter`` username and password. Authorization
+can be given to Peter by creating a project within the ``Users`` domain and
+giving him a role assignment on that project::
+
+    $ openstack project create --domain Users awesome-mix-vol-1
+    +-------------+----------------------------------+
+    | Field       | Value                            |
+    +-------------+----------------------------------+
+    | description |                                  |
+    | domain_id   | 61a2de23107c46bea2d758167af707b9 |
+    | enabled     | True                             |
+    | id          | 7d422396d54945cdac8fe1e8e32baec4 |
+    | is_domain   | False                            |
+    | name        | awesome-mix-vol-1                |
+    | parent_id   | 61a2de23107c46bea2d758167af707b9 |
+    | tags        | []                               |
+    +-------------+----------------------------------+
+    $ openstack role add --user peter --user-domain Users \
+          --project awesome-mix-vol-1 --project-domain Users admin
+
+
+Deleting Users
+--------------
+
+We can use the same basic steps to remove users from LDAP, but instead of using
+LDIFs, we can just pass the ``dn`` of the user we want to delete::
+
+    ldapdelete -x -w LDAP_PASSWORD -D cn=Manager,dc=openstack,dc=org \
+        -H ldap://localhost cn=peter,ou=Users,dc=openstack,dc=org
+
+Group Management
+================
+
+Like users, groups are considered specific identities. This means that groups
+also fall under the same read-only constraints as users and they can be managed
+directly with LDAP in the same way users are with LDIFs.
+
+Adding Groups
+-------------
+
+Let's define a specific group with the following LDIF::
+
+    dn: cn=guardians,ou=UserGroups,dc=openstack,dc=org
+    objectClass: groupOfNames
+    cn: guardians
+    description: Guardians of the Galaxy
+    member: cn=peter,dc=openstack,dc=org
+    member: cn=gamora,dc=openstack,dc=org
+    member: cn=drax,dc=openstack,dc=org
+    member: cn=rocket,dc=openstack,dc=org
+    member: cn=groot,dc=openstack,dc=org
+
+We can create the group using the same ``ldapadd`` command as we did with
+users::
+
+    ldapadd -x -w LDAP_PASSWORD -D cn=Manager,dc=openstack,dc=org \
+        -H ldap://localhost -c -f guardian-group.ldif.in
+
+If we check the group membership in Horizon, we'll see that only Peter is a
+member of the ``guardians`` group, despite the whole crew being specified in
+the LDIF. Once those accounts are created in LDAP, they will automatically be
+added to the ``guardians`` group. They will also assume any role assignments
+given to the ``guardians`` group.
+
+Deleting Groups
+---------------
+
+Just like users, groups can be deleted using the ``dn``::
+
+    ldapdelete -x -w LDAP_PASSWORD -D cn=Manager,dc=openstack,dc=org \
+        -H ldap://localhost cn=guardians,ou=UserGroups,dc=openstack,dc=org
+
+Note that this operation will not remove users within that group. It will only
+remove the group itself and the memberships any users had with that group.
diff --git a/doc/source/guides/neutron.rst b/doc/source/guides/neutron.rst
index 092809a..7f360c6 100644
--- a/doc/source/guides/neutron.rst
+++ b/doc/source/guides/neutron.rst
@@ -244,7 +244,7 @@
 
     ## Neutron options
     PUBLIC_INTERFACE=eth0
-    ENABLED_SERVICES=n-cpu,rabbit,q-agt
+    ENABLED_SERVICES=n-cpu,rabbit,q-agt,placement-client
 
 Network traffic from `eth0` on the compute nodes is then NAT'd by the
 controller node that runs Neutron's `neutron-l3-agent` and provides L3
@@ -396,7 +396,7 @@
 
 In this configuration we are defining IPV4_ADDRS_SAFE_TO_USE to be a
 publicly routed IPv4 subnet. In this specific instance we are using
-the special TEST-NET-3 subnet defined in `RFC 5737 <http://tools.ietf.org/html/rfc5737>`_,
+the special TEST-NET-3 subnet defined in `RFC 5737 <https://tools.ietf.org/html/rfc5737>`_,
 which is used for documentation.  In your DevStack setup, IPV4_ADDRS_SAFE_TO_USE
 would be a public IP address range that you or your organization has
 allocated to you, so that you could access your instances from the
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 9254c23..2ff4ff0 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -139,7 +139,9 @@
 Enable :doc:`devstack plugins <plugins>` to support additional
 services, features, and configuration not present in base devstack.
 
-Use devstack in your CI with :doc:`Ansible roles <roles>` for Zuul V3.
+Use devstack in your CI with :doc:`Ansible roles <zuul_roles>` and
+:doc:`Jobs <zuul_jobs>` for Zuul V3. Migrate your devstack Zuul V2 jobs to Zuul
+V3 with this full migration :doc:`how-to <zuul_ci_jobs_migration>`.
 
 Get :doc:`the big picture <overview>` of what we are trying to do
 with devstack, and help us by :doc:`contributing to the project
diff --git a/doc/source/plugin-registry.rst b/doc/source/plugin-registry.rst
index 591e226..9b2cb7e 100644
--- a/doc/source/plugin-registry.rst
+++ b/doc/source/plugin-registry.rst
@@ -26,17 +26,18 @@
 ====================================== ===
 almanach                               `git://git.openstack.org/openstack/almanach <https://git.openstack.org/cgit/openstack/almanach>`__
 aodh                                   `git://git.openstack.org/openstack/aodh <https://git.openstack.org/cgit/openstack/aodh>`__
+apmec                                  `git://git.openstack.org/openstack/apmec <https://git.openstack.org/cgit/openstack/apmec>`__
 astara                                 `git://git.openstack.org/openstack/astara <https://git.openstack.org/cgit/openstack/astara>`__
 barbican                               `git://git.openstack.org/openstack/barbican <https://git.openstack.org/cgit/openstack/barbican>`__
 bilean                                 `git://git.openstack.org/openstack/bilean <https://git.openstack.org/cgit/openstack/bilean>`__
 blazar                                 `git://git.openstack.org/openstack/blazar <https://git.openstack.org/cgit/openstack/blazar>`__
 broadview-collector                    `git://git.openstack.org/openstack/broadview-collector <https://git.openstack.org/cgit/openstack/broadview-collector>`__
+castellan-ui                           `git://git.openstack.org/openstack/castellan-ui <https://git.openstack.org/cgit/openstack/castellan-ui>`__
 ceilometer                             `git://git.openstack.org/openstack/ceilometer <https://git.openstack.org/cgit/openstack/ceilometer>`__
 ceilometer-powervm                     `git://git.openstack.org/openstack/ceilometer-powervm <https://git.openstack.org/cgit/openstack/ceilometer-powervm>`__
 cloudkitty                             `git://git.openstack.org/openstack/cloudkitty <https://git.openstack.org/cgit/openstack/cloudkitty>`__
-collectd-ceilometer-plugin             `git://git.openstack.org/openstack/collectd-ceilometer-plugin <https://git.openstack.org/cgit/openstack/collectd-ceilometer-plugin>`__
+collectd-openstack-plugins             `git://git.openstack.org/openstack/collectd-openstack-plugins <https://git.openstack.org/cgit/openstack/collectd-openstack-plugins>`__
 congress                               `git://git.openstack.org/openstack/congress <https://git.openstack.org/cgit/openstack/congress>`__
-cue                                    `git://git.openstack.org/openstack/cue <https://git.openstack.org/cgit/openstack/cue>`__
 cyborg                                 `git://git.openstack.org/openstack/cyborg <https://git.openstack.org/cgit/openstack/cyborg>`__
 designate                              `git://git.openstack.org/openstack/designate <https://git.openstack.org/cgit/openstack/designate>`__
 devstack-plugin-additional-pkg-repos   `git://git.openstack.org/openstack/devstack-plugin-additional-pkg-repos <https://git.openstack.org/cgit/openstack/devstack-plugin-additional-pkg-repos>`__
@@ -71,7 +72,6 @@
 ironic-inspector                       `git://git.openstack.org/openstack/ironic-inspector <https://git.openstack.org/cgit/openstack/ironic-inspector>`__
 ironic-staging-drivers                 `git://git.openstack.org/openstack/ironic-staging-drivers <https://git.openstack.org/cgit/openstack/ironic-staging-drivers>`__
 ironic-ui                              `git://git.openstack.org/openstack/ironic-ui <https://git.openstack.org/cgit/openstack/ironic-ui>`__
-k8s-cloud-provider                     `git://git.openstack.org/openstack/k8s-cloud-provider <https://git.openstack.org/cgit/openstack/k8s-cloud-provider>`__
 karbor                                 `git://git.openstack.org/openstack/karbor <https://git.openstack.org/cgit/openstack/karbor>`__
 karbor-dashboard                       `git://git.openstack.org/openstack/karbor-dashboard <https://git.openstack.org/cgit/openstack/karbor-dashboard>`__
 keystone                               `git://git.openstack.org/openstack/keystone <https://git.openstack.org/cgit/openstack/keystone>`__
@@ -148,13 +148,13 @@
 octavia                                `git://git.openstack.org/openstack/octavia <https://git.openstack.org/cgit/openstack/octavia>`__
 octavia-dashboard                      `git://git.openstack.org/openstack/octavia-dashboard <https://git.openstack.org/cgit/openstack/octavia-dashboard>`__
 omni                                   `git://git.openstack.org/openstack/omni <https://git.openstack.org/cgit/openstack/omni>`__
+openstacksdk                           `git://git.openstack.org/openstack/openstacksdk <https://git.openstack.org/cgit/openstack/openstacksdk>`__
 os-xenapi                              `git://git.openstack.org/openstack/os-xenapi <https://git.openstack.org/cgit/openstack/os-xenapi>`__
 osprofiler                             `git://git.openstack.org/openstack/osprofiler <https://git.openstack.org/cgit/openstack/osprofiler>`__
 oswin-tempest-plugin                   `git://git.openstack.org/openstack/oswin-tempest-plugin <https://git.openstack.org/cgit/openstack/oswin-tempest-plugin>`__
 panko                                  `git://git.openstack.org/openstack/panko <https://git.openstack.org/cgit/openstack/panko>`__
 patrole                                `git://git.openstack.org/openstack/patrole <https://git.openstack.org/cgit/openstack/patrole>`__
 picasso                                `git://git.openstack.org/openstack/picasso <https://git.openstack.org/cgit/openstack/picasso>`__
-python-openstacksdk                    `git://git.openstack.org/openstack/python-openstacksdk <https://git.openstack.org/cgit/openstack/python-openstacksdk>`__
 qinling                                `git://git.openstack.org/openstack/qinling <https://git.openstack.org/cgit/openstack/qinling>`__
 rally                                  `git://git.openstack.org/openstack/rally <https://git.openstack.org/cgit/openstack/rally>`__
 rally-openstack                        `git://git.openstack.org/openstack/rally-openstack <https://git.openstack.org/cgit/openstack/rally-openstack>`__
@@ -164,6 +164,7 @@
 searchlight                            `git://git.openstack.org/openstack/searchlight <https://git.openstack.org/cgit/openstack/searchlight>`__
 searchlight-ui                         `git://git.openstack.org/openstack/searchlight-ui <https://git.openstack.org/cgit/openstack/searchlight-ui>`__
 senlin                                 `git://git.openstack.org/openstack/senlin <https://git.openstack.org/cgit/openstack/senlin>`__
+slogging                               `git://git.openstack.org/openstack/slogging <https://git.openstack.org/cgit/openstack/slogging>`__
 solum                                  `git://git.openstack.org/openstack/solum <https://git.openstack.org/cgit/openstack/solum>`__
 stackube                               `git://git.openstack.org/openstack/stackube <https://git.openstack.org/cgit/openstack/stackube>`__
 storlets                               `git://git.openstack.org/openstack/storlets <https://git.openstack.org/cgit/openstack/storlets>`__
diff --git a/doc/source/roles.rst b/doc/source/roles.rst
deleted file mode 100644
index 5baa1e4..0000000
--- a/doc/source/roles.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Roles
-=====
-
-.. zuul:autoroles::
diff --git a/doc/source/systemd.rst b/doc/source/systemd.rst
index 9cc4017..1bc9911 100644
--- a/doc/source/systemd.rst
+++ b/doc/source/systemd.rst
@@ -152,6 +152,19 @@
 
   /usr/local/bin/nova-scheduler --config-file /etc/nova/nova.conf
 
+Some executables, such as :program:`nova-compute`, will need to be executed
+with a particular group. This will be shown in the systemd unit file::
+
+  sudo systemctl cat devstack@n-cpu.service | grep Group
+
+::
+
+  Group = libvirt
+
+Use the :program:`sg` tool to execute the command as this group::
+
+  sg libvirt -c '/usr/local/bin/nova-compute --config-file /etc/nova/nova-cpu.conf'
+
 Using remote-pdb
 ----------------
 
diff --git a/doc/source/zuul_ci_jobs_migration.rst b/doc/source/zuul_ci_jobs_migration.rst
new file mode 100644
index 0000000..c00f06e
--- /dev/null
+++ b/doc/source/zuul_ci_jobs_migration.rst
@@ -0,0 +1,301 @@
+===============================
+Migrating Zuul V2 CI jobs to V3
+===============================
+
+The OpenStack CI system moved from Zuul v2 to Zuul v3, and all CI jobs moved to
+the new CI system. All jobs have been migrated automatically to a format
+compatible with Zuul v3; the jobs produced in this way however are suboptimal
+and do not use the capabilities introduced by Zuul v3, which allow for re-use of
+job parts, in the form of Ansible roles, as well as inheritance between jobs.
+
+DevStack hosts a set of roles, plays and jobs that can be used by other
+repositories to define their DevStack based jobs. To benefit from them, jobs
+must be migrated from the legacy v2 ones into v3 native format.
+
+This document provides guidance and examples to make the migration process as
+painless and smooth as possible.
+
+Where to host the job definitions.
+==================================
+
+In Zuul V3 jobs can be defined in the repository that contains the code they
+excercise. If you are writing CI jobs for an OpenStack service you can define
+your DevStack based CI jobs in one of the repositories that host the code for
+your service. If you have a branchless repo, like a Tempest plugin, that is
+a convenient choice to host the job definitions since job changes do not have
+to be backported. For example, see the beginning of the ``.zuul.yaml`` from the
+sahara Tempest plugin repo:
+
+.. code:: yaml
+
+  # In http://git.openstack.org/cgit/openstack/sahara-tests/tree/.zuul.yaml:
+  - job:
+      name: sahara-tests-tempest
+      description: |
+        Run Tempest tests from the Sahara plugin.
+      parent: devstack-tempest
+
+Which base job to start from
+============================
+
+If your job needs an OpenStack cloud deployed via DevStack, but you don't plan
+on running Tempest tests, you can start from one of the base
+:doc:`jobs <zuul_jobs>` defined in the DevStack repo.
+
+The ``devstack`` job can be used for both single-node jobs and multi-node jobs,
+and it includes the list of services used in the integrated gate (keystone,
+glance, nova, cinder, neutron and swift). Different topologies can be achieved
+by switching the nodeset used in the child job.
+
+The ``devstack-base`` job is similar to ``devstack`` but it does not specify any
+required repo or service to be run in DevStack. It can be useful to setup
+children jobs that use a very narrow DevStack setup.
+
+If your job needs an OpenStack cloud deployed via DevStack, and you do plan
+on running Tempest tests, you can start from one of the base jobs defined in the
+Tempest repo.
+
+The ``devstack-tempest`` job can be used for both single-node jobs and
+multi-node jobs. Different topologies can be achieved by switching the nodeset
+used in the child job.
+
+Jobs can be customized as follows without writing any Ansible code:
+
+- add and/or remove DevStack services
+- add or modify DevStack and services configuration
+- install DevStack plugins
+- extend the number of sub-nodes (multinode only)
+- define extra log files and/or directories to be uploaded on logs.o.o
+- define extra log file extensions to be rewritten to .txt for ease of access
+
+Tempest jobs can be further customized as follows:
+
+- define the Tempest tox environment to be used
+- define the test concurrency
+- define the test regular expression
+
+Writing Ansible code, or importing existing custom roles, jobs can be further
+extended by:
+
+- adding pre and/or post playbooks
+- overriding the run playbook, add custom roles
+
+The (partial) example below extends a Tempest single node base job
+"devstack-tempest" in the Kuryr repository. The parent job name is defined in
+job.parent.
+
+.. code:: yaml
+
+  # https://git.openstack.org/cgit/openstack/kuryr-kubernetes/tree/.zuul.yaml:
+  - job:
+      name: kuryr-kubernetes-tempest-base
+      parent: devstack-tempest
+      description: Base kuryr-kubernetes-job
+      required-projects:
+        - openstack/devstack-plugin-container
+        - openstack/kuryr
+        - openstack/kuryr-kubernetes
+        - openstack/kuryr-tempest-plugin
+        - openstack/neutron-lbaas
+      vars:
+        tempest_test_regex: '^(kuryr_tempest_plugin.tests.)'
+        tox_envlist: 'all'
+        devstack_localrc:
+          KURYR_K8S_API_PORT: 8080
+          TEMPEST_PLUGINS: '/opt/stack/kuryr-tempest-plugin'
+        devstack_services:
+          kubernetes-api: true
+          kubernetes-controller-manager: true
+          kubernetes-scheduler: true
+          kubelet: true
+          kuryr-kubernetes: true
+          (...)
+        devstack_plugins:
+          kuryr-kubernetes: https://git.openstack.org/openstack/kuryr
+          devstack-plugin-container: https://git.openstack.org/openstack/devstack-plugin-container
+          neutron-lbaas: https://git.openstack.org/openstack/neutron-lbaas
+        (...)
+
+Job variables
+=============
+
+Variables can be added to the job in three different places:
+
+- job.vars: these are global variables available to all node in the nodeset
+- job.host-vars.[HOST]: these are variables available only to the specified HOST
+- job.group-vars.[GROUP]: these are variables available only to the specified
+  GROUP
+
+Zuul merges dict variables through job inheritance. Host and group variables
+override variables with the same name defined as global variables.
+
+In the example below, for the sundaes job, hosts that are not part of the
+subnode group will run vanilla and chocolate. Hosts in the subnode group will
+run stracciatella and strawberry.
+
+.. code:: yaml
+
+  - job:
+      name: ice-creams
+      vars:
+        devstack_service:
+          vanilla: true
+          chocolate: false
+      group-vars:
+        subnode:
+          devstack_service:
+            pistacchio: true
+            stracciatella: true
+
+  - job:
+      name: sundaes
+      parent: ice-creams
+      vars:
+        devstack_service:
+          chocolate: true
+      group-vars:
+        subnode:
+          devstack_service:
+            strawberry: true
+            pistacchio: false
+
+
+DevStack Gate Flags
+===================
+
+The old CI system worked using a combination of DevStack, Tempest and
+devstack-gate to setup a test environment and run tests against it. With Zuul
+V3, the logic that used to live in devstack-gate is moved into different repos,
+including DevStack, Tempest and grenade.
+
+DevStack-gate exposes an interface for job definition based on a number of
+DEVSTACK_GATE_* environment variables, or flags. This guide shows how to map
+DEVSTACK_GATE flags into the new
+system.
+
+The repo column indicates in which repository is hosted the code that replaces
+the devstack-gate flag. The new implementation column explains how to reproduce
+the same or a similar behaviour in Zuul v3 jobs. For localrc settings,
+devstack-gate defined a default value. In ansible jobs the default is either the
+value defined in the parent job, or the default from DevStack, if any.
+
+==============================================  ============= ==================
+DevStack gate flag                              Repo          New implementation
+==============================================  ============= ==================
+OVERRIDE_ZUUL_BRANCH                            zuul          override-checkout:
+                                                              [branch]
+                                                              in the job definition.
+DEVSTACK_GATE_NET_OVERLAY                       zuul-jobs     A bridge called
+                                                              br-infra is set up for
+                                                              all jobs that inherit
+                                                              from multinode with
+                                                              a dedicated `bridge role <https://docs.openstack.org/infra/zuul-jobs/roles.html#role-multi-node-bridge>`_.
+DEVSTACK_GATE_FEATURE_MATRIX                    devstack-gate ``test_matrix_features``
+                                                              variable of the
+                                                              test-matrix role in
+                                                              devstack-gate. This
+                                                              is a temporary
+                                                              solution, feature
+                                                              matrix will go away.
+                                                              In the future services
+                                                              will be defined in
+                                                              jobs only.
+DEVSTACK_CINDER_VOLUME_CLEAR                    devstack      *CINDER_VOLUME_CLEAR: true/false*
+                                                              in devstack_localrc
+                                                              in the job vars.
+DEVSTACK_GATE_NEUTRON                           devstack      True by default. To
+                                                              disable, disable all
+                                                              neutron services in
+                                                              devstack_services in
+                                                              the job definition.
+DEVSTACK_GATE_CONFIGDRIVE                       devstack      *FORCE_CONFIG_DRIVE: true/false*
+                                                              in devstack_localrc
+                                                              in the job vars.
+DEVSTACK_GATE_INSTALL_TESTONLY                  devstack      *INSTALL_TESTONLY_PACKAGES: true/false*
+                                                              in devstack_localrc
+                                                              in the job vars.
+DEVSTACK_GATE_VIRT_DRIVER                       devstack      *VIRT_DRIVER: [virt driver]*
+                                                              in devstack_localrc
+                                                              in the job vars.
+DEVSTACK_GATE_LIBVIRT_TYPE                      devstack      *LIBVIRT_TYPE: [libvirt type]*
+                                                              in devstack_localrc
+                                                              in the job vars.
+DEVSTACK_GATE_TEMPEST                           devstack      Defined by the job
+                                                tempest       that is used. The
+                                                              ``devstack`` job only
+                                                              runs devstack.
+                                                              The ``devstack-tempest``
+                                                              one triggers a Tempest
+                                                              run as well.
+DEVSTACK_GATE_TEMPEST_FULL                      tempest       *tox_envlist: full*
+                                                              in the job vars.
+DEVSTACK_GATE_TEMPEST_ALL                       tempest       *tox_envlist: all*
+                                                              in the job vars.
+DEVSTACK_GATE_TEMPEST_ALL_PLUGINS               tempest       *tox_envlist: all-plugin*
+                                                              in the job vars.
+DEVSTACK_GATE_TEMPEST_SCENARIOS                 tempest       *tox_envlist: scenario*
+                                                              in the job vars.
+TEMPEST_CONCURRENCY                             tempest       *tempest_concurrency: [value]*
+                                                              in the job vars. This
+                                                              is available only on
+                                                              jobs that inherit from
+                                                              ``devstack-tempest``
+                                                              down.
+DEVSTACK_GATE_TEMPEST_NOTESTS                   tempest       *tox_envlist: venv-tempest*
+                                                              in the job vars. This
+                                                              will create Tempest
+                                                              virtual environment
+                                                              but run no tests.
+DEVSTACK_GATE_SMOKE_SERIAL                      tempest       *tox_envlist: smoke-serial*
+                                                              in the job vars.
+DEVSTACK_GATE_TEMPEST_DISABLE_TENANT_ISOLATION  tempest       *tox_envlist: full-serial*
+                                                              in the job vars.
+                                                              *TEMPEST_ALLOW_TENANT_ISOLATION: false*
+                                                              in devstack_localrc in
+                                                              the job vars.
+==============================================  ============= ==================
+
+The following flags have not been migrated yet or are legacy and won't be
+migrated at all.
+
+=====================================  ======  ==========================
+DevStack gate flag                     Status  Details
+=====================================  ======  ==========================
+DEVSTACK_GATE_TOPOLOGY                 WIP     The topology depends on the base
+                                               job that is used and more
+                                               specifically on the nodeset
+                                               attached to it. The new job
+                                               format allows project to define
+                                               the variables to be passed to
+                                               every node/node-group that exists
+                                               in the topology. Named topologies
+                                               that include the nodeset and the
+                                               matching variables can be defined
+                                               in the form of base jobs.
+DEVSTACK_GATE_GRENADE                  TBD     Grenade Zuul V3 jobs will be
+                                               hosted in the grenade repo.
+GRENADE_BASE_BRANCH                    TBD     Grenade Zuul V3 jobs will be
+                                               hosted in the grenade repo.
+DEVSTACK_GATE_NEUTRON_DVR              TBD     Depends on multinode support.
+DEVSTACK_GATE_EXERCISES                TBD     Can be done on request.
+DEVSTACK_GATE_IRONIC                   TBD     This will probably be implemented
+                                               on ironic side.
+DEVSTACK_GATE_IRONIC_DRIVER            TBD     This will probably be implemented
+                                               on ironic side.
+DEVSTACK_GATE_IRONIC_BUILD_RAMDISK     TBD     This will probably be implemented
+                                               on ironic side.
+DEVSTACK_GATE_POSTGRES                 Legacy  This flag exists in d-g but the
+                                               only thing that it does is
+                                               capture postgres logs. This is
+                                               already supported by the roles in
+                                               post, so the flag is useless in
+                                               the new jobs. postgres itself can
+                                               be enabled via the
+                                               devstack_service job variable.
+DEVSTACK_GATE_ZEROMQ                   Legacy  This has no effect in d-g.
+DEVSTACK_GATE_MQ_DRIVER                Legacy  This has no effect in d-g.
+DEVSTACK_GATE_TEMPEST_STRESS_ARGS      Legacy  Stress is not in Tempest anymore.
+DEVSTACK_GATE_TEMPEST_HEAT_SLOW        Legacy  This is not used anywhere.
+DEVSTACK_GATE_CELLS                    Legacy  This has no effect in d-g.
+DEVSTACK_GATE_NOVA_API_METADATA_SPLIT  Legacy  This has no effect in d-g.
+=====================================  ======  ==========================
diff --git a/doc/source/zuul_jobs.rst b/doc/source/zuul_jobs.rst
new file mode 100644
index 0000000..cf203a8
--- /dev/null
+++ b/doc/source/zuul_jobs.rst
@@ -0,0 +1,4 @@
+Zuul CI Jobs
+============
+
+.. zuul:autojobs::
diff --git a/doc/source/zuul_roles.rst b/doc/source/zuul_roles.rst
new file mode 100644
index 0000000..4939281
--- /dev/null
+++ b/doc/source/zuul_roles.rst
@@ -0,0 +1,4 @@
+Zuul CI Roles
+=============
+
+.. zuul:autoroles::
diff --git a/files/debs/n-cpu b/files/debs/n-cpu
index d8bbf59..636644f 100644
--- a/files/debs/n-cpu
+++ b/files/debs/n-cpu
@@ -1,4 +1,5 @@
 cryptsetup
+dosfstools
 genisoimage
 gir1.2-libosinfo-1.0
 lvm2 # NOPRIME
diff --git a/files/rpms-suse/general b/files/rpms-suse/general
index 0b69cb1..b870d72 100644
--- a/files/rpms-suse/general
+++ b/files/rpms-suse/general
@@ -11,7 +11,6 @@
 iputils
 libffi-devel  # pyOpenSSL
 libjpeg8-devel # Pillow 3.0.0
-libmysqlclient-devel # MySQL-python
 libopenssl-devel # to rebuild pyOpenSSL if needed
 libxslt-devel  # lxml
 lsof # useful when debugging
diff --git a/files/rpms-suse/n-cpu b/files/rpms-suse/n-cpu
index 9ece115..c11e9f0 100644
--- a/files/rpms-suse/n-cpu
+++ b/files/rpms-suse/n-cpu
@@ -1,7 +1,8 @@
 cryptsetup
-genisoimage
+dosfstools
 libosinfo
 lvm2
+mkisofs
 open-iscsi
 sg3_utils
 # Stuff for diablo volumes
diff --git a/files/rpms-suse/nova b/files/rpms-suse/nova
index ae115d2..4103a40 100644
--- a/files/rpms-suse/nova
+++ b/files/rpms-suse/nova
@@ -4,7 +4,6 @@
 dnsmasq-utils # dist:opensuse-12.3,opensuse-13.1
 ebtables
 gawk
-genisoimage # required for config_drive
 iptables
 iputils
 kpartx
@@ -12,6 +11,7 @@
 libvirt # NOPRIME
 libvirt-python # NOPRIME
 mariadb # NOPRIME
+mkisofs # required for config_drive
 parted
 polkit
 # qemu as fallback if kvm cannot be used
diff --git a/files/rpms/cinder b/files/rpms/cinder
index e6addc6..058c235 100644
--- a/files/rpms/cinder
+++ b/files/rpms/cinder
@@ -1,5 +1,5 @@
 iscsi-initiator-utils
 lvm2
 qemu-img
-scsi-target-utils # not:rhel7,f25,f26,f27 NOPRIME
-targetcli # dist:rhel7,f25,f26,f27 NOPRIME
+scsi-target-utils # not:rhel7,f25,f26,f27,f28 NOPRIME
+targetcli # dist:rhel7,f25,f26,f27,f28 NOPRIME
diff --git a/files/rpms/general b/files/rpms/general
index 5d9a4ad..c7863e4 100644
--- a/files/rpms/general
+++ b/files/rpms/general
@@ -9,9 +9,9 @@
 graphviz # needed only for docs
 httpd
 httpd-devel
-iptables-services  # NOPRIME f25,f26,f27
+iptables-services  # NOPRIME f25,f26,f27,f28
 java-1.7.0-openjdk-headless  # NOPRIME rhel7
-java-1.8.0-openjdk-headless  # NOPRIME f25,f26,f27
+java-1.8.0-openjdk-headless  # NOPRIME f25,f26,f27,f28
 libffi-devel
 libjpeg-turbo-devel # Pillow 3.0.0
 libxml2-devel # lxml
diff --git a/files/rpms/horizon b/files/rpms/horizon
index fa5601a..a88552b 100644
--- a/files/rpms/horizon
+++ b/files/rpms/horizon
@@ -1,4 +1,2 @@
-Django
 httpd # NOPRIME
 mod_wsgi  # NOPRIME
-pyxattr
diff --git a/files/rpms/n-cpu b/files/rpms/n-cpu
index 26c5ced..68e5472 100644
--- a/files/rpms/n-cpu
+++ b/files/rpms/n-cpu
@@ -1,4 +1,5 @@
 cryptsetup
+dosfstools
 genisoimage
 iscsi-initiator-utils
 libosinfo
diff --git a/files/rpms/nova b/files/rpms/nova
index 9fb7282..4140cd7 100644
--- a/files/rpms/nova
+++ b/files/rpms/nova
@@ -7,7 +7,7 @@
 genisoimage # required for config_drive
 iptables
 iputils
-kernel-modules # dist:f25,f26,f27
+kernel-modules # dist:f25,f26,f27,f28
 kpartx
 libxml2-python
 m2crypto
diff --git a/files/rpms/swift b/files/rpms/swift
index be0db14..f2f5de6 100644
--- a/files/rpms/swift
+++ b/files/rpms/swift
@@ -2,7 +2,7 @@
 liberasurecode-devel
 memcached
 pyxattr
-rsync-daemon # dist:f25,f26,f27
+rsync-daemon # dist:f25,f26,f27,f28
 sqlite
 xfsprogs
 xinetd
diff --git a/functions b/functions
index 24994c0..f63595d 100644
--- a/functions
+++ b/functions
@@ -441,7 +441,12 @@
 function wait_for_compute {
     local timeout=$1
     local rval=0
+    local compute_hostname
     time_start "wait_for_service"
+    compute_hostname=$(iniget $NOVA_CONF DEFAULT host)
+    if [[ -z $compute_hostname ]]; then
+        compute_hostname=$(hostname)
+    fi
     timeout $timeout bash -x <<EOF || rval=$?
         ID=""
         while [[ "\$ID" == "" ]]; do
@@ -450,7 +455,7 @@
                 # When using the fake driver the compute hostnames have a suffix of 1 to NUMBER_FAKE_NOVA_COMPUTE
                 ID=\$(openstack --os-cloud devstack-admin --os-region "$REGION_NAME" compute service list --host `hostname`1 --service nova-compute -c ID -f value)
             else
-                ID=\$(openstack --os-cloud devstack-admin --os-region "$REGION_NAME" compute service list --host `hostname` --service nova-compute -c ID -f value)
+                ID=\$(openstack --os-cloud devstack-admin --os-region "$REGION_NAME" compute service list --host "$compute_hostname" --service nova-compute -c ID -f value)
             fi
         done
 EOF
diff --git a/functions-common b/functions-common
index 5f933d6..fae936a 100644
--- a/functions-common
+++ b/functions-common
@@ -373,6 +373,9 @@
         DISTRO="f$os_RELEASE"
     elif [[ "$os_VENDOR" =~ (openSUSE) ]]; then
         DISTRO="opensuse-$os_RELEASE"
+        # Tumbleweed uses "n/a" as a codename, and the release is a datestring
+        # like 20180218, so not very useful.
+        [ "$os_CODENAME" = "n/a" ] && DISTRO="opensuse-tumbleweed"
     elif [[ "$os_VENDOR" =~ (SUSE LINUX) ]]; then
         # just use major release
         DISTRO="sle${os_RELEASE%.*}"
@@ -2130,7 +2133,7 @@
 function python3_version {
     local python3_version
     python3_version=$(_get_python_version python3)
-    echo "python${python_version}"
+    echo "python${python3_version}"
 }
 
 
@@ -2306,12 +2309,7 @@
 
 function cleanup_oscwrap {
     local total=0
-    if python3_enabled ; then
-        local python=python3
-    else
-        local python=python
-    fi
-    total=$(cat $OSCWRAP_TIMER_FILE | $python -c "import sys; print(sum(int(l) for l in sys.stdin))")
+    total=$(cat $OSCWRAP_TIMER_FILE | $PYTHON -c "import sys; print(sum(int(l) for l in sys.stdin))")
     _TIME_TOTAL["osc"]=$total
     rm $OSCWRAP_TIMER_FILE
 }
diff --git a/inc/ini-config b/inc/ini-config
index 68d48d1..6fe7788 100644
--- a/inc/ini-config
+++ b/inc/ini-config
@@ -200,7 +200,7 @@
         local sep
         sep=$(echo -ne "\x01")
         # Replace it
-        $sudo sed -i -e '/^\['${section}'\]/,/^\[.*\]/ s'${sep}'^\('${option}'[ \t]*=[ \t]*\).*$'${sep}'\1'"${value}"${sep} "$file"
+        $sudo sed -i -e '/^\['${section}'\]/,/^\[.*\]/ s'${sep}'^\('"${option}"'[ \t]*=[ \t]*\).*$'${sep}'\1'"${value}"${sep} "$file"
     fi
     $xtrace
 }
diff --git a/inc/python b/inc/python
index e074ea4..96be107 100644
--- a/inc/python
+++ b/inc/python
@@ -411,12 +411,6 @@
 function lib_installed_from_git {
     local name=$1
     local safe_name
-    # TODO(mordred) This is a special case for python-openstacksdk, where the
-    # repo name and the pip name do not match. We should either add systemic
-    # support for providing aliases, or we should rename the git repo.
-    if [[ $name == 'python-openstacksdk' ]] ; then
-        name=openstacksdk
-    fi
     safe_name=$(python -c "from pkg_resources import safe_name; \
         print(safe_name('${name}'))")
     # Note "pip freeze" doesn't always work here, because it tries to
@@ -435,22 +429,6 @@
     [[ -n $(pip list --format=columns 2>/dev/null | awk "/^$safe_name/ {print \$3}") ]]
 }
 
-# check that everything that's in LIBS_FROM_GIT was actually installed
-# correctly, this helps double check issues with library fat fingering.
-function check_libs_from_git {
-    local lib=""
-    local not_installed=""
-    for lib in $(echo ${LIBS_FROM_GIT} | tr "," " "); do
-        if ! lib_installed_from_git "$lib"; then
-            not_installed+=" $lib"
-        fi
-    done
-    # if anything is not installed, say what it is.
-    if [[ -n "$not_installed" ]]; then
-        die $LINENO "The following LIBS_FROM_GIT were not installed correct: $not_installed"
-    fi
-}
-
 # setup a library by name. If we are trying to use the library from
 # git, we'll do a git based install, otherwise we'll punt and the
 # library should be installed by a requirements pull from another
@@ -561,6 +539,13 @@
 
     setup_package $project_dir "$flags" $extras
 
+    # If this project is in LIBS_FROM_GIT, verify it was actually installed
+    # correctly.  This helps catch errors caused by constraints mismatches.
+    if use_library_from_git "$project_dir"; then
+        if ! lib_installed_from_git "$project_dir"; then
+            die $LINENO "The following LIBS_FROM_GIT was not installed correctly: $project_dir"
+        fi
+    fi
 }
 
 # ``pip install -e`` the package, which processes the dependencies
diff --git a/lib/cinder b/lib/cinder
index 4d6085f..92d0295 100644
--- a/lib/cinder
+++ b/lib/cinder
@@ -227,7 +227,6 @@
 
     configure_auth_token_middleware $CINDER_CONF cinder $CINDER_AUTH_CACHE_DIR
 
-    iniset $CINDER_CONF DEFAULT auth_strategy keystone
     iniset $CINDER_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
 
     iniset $CINDER_CONF DEFAULT target_helper "$CINDER_ISCSI_HELPER"
@@ -350,7 +349,7 @@
             get_or_create_endpoint \
                 "block-storage" \
                 "$REGION_NAME" \
-                "$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST:$CINDER_SERVICE_PORT/"
+                "$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST:$CINDER_SERVICE_PORT/v3/\$(project_id)s"
 
             get_or_create_endpoint \
                 "volume" \
@@ -372,7 +371,7 @@
             get_or_create_endpoint \
                 "block-storage" \
                 "$REGION_NAME" \
-                "$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST/volume/"
+                "$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST/volume/v3/\$(project_id)s"
 
             get_or_create_endpoint \
                 "volume" \
@@ -541,7 +540,17 @@
         local be be_name
         for be in ${CINDER_ENABLED_BACKENDS//,/ }; do
             be_name=${be##*:}
-            openstack --os-region-name="$REGION_NAME" volume type create --property volume_backend_name="${be_name}" ${be_name}
+            # NOTE (e0ne): openstack client doesn't work with cinder in noauth mode
+            if is_service_enabled keystone; then
+                openstack --os-region-name="$REGION_NAME" volume type create --property volume_backend_name="${be_name}" ${be_name}
+            else
+                # TODO (e0ne): use openstack client once it will support cinder in noauth mode:
+                # https://bugs.launchpad.net/python-cinderclient/+bug/1755279
+                local cinder_url
+                cinder_url=$CINDER_SERVICE_PROTOCOL://$SERVICE_HOST:$CINDER_SERVICE_PORT/v3
+                OS_USER_ID=$OS_USERNAME OS_PROJECT_ID=$OS_PROJECT_NAME cinder --os-auth-type noauth --os-endpoint=$cinder_url type-create ${be_name}
+                OS_USER_ID=$OS_USERNAME OS_PROJECT_ID=$OS_PROJECT_NAME cinder --os-auth-type noauth --os-endpoint=$cinder_url type-key ${be_name} set volume_backend_name=${be_name}
+            fi
         done
     fi
 }
diff --git a/lib/glance b/lib/glance
index 4a3e25e..94f6a22 100644
--- a/lib/glance
+++ b/lib/glance
@@ -111,11 +111,10 @@
     # Server is configured through this function and not init_glance.
     create_glance_cache_dir
 
-    # Copy over our glance configurations and update them
-    cp $GLANCE_DIR/etc/glance-registry.conf $GLANCE_REGISTRY_CONF
+    # Set non-default configuration options for registry
     iniset $GLANCE_REGISTRY_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
     iniset $GLANCE_REGISTRY_CONF DEFAULT bind_host $GLANCE_SERVICE_LISTEN_ADDRESS
-    inicomment $GLANCE_REGISTRY_CONF DEFAULT log_file
+    iniset $GLANCE_REGISTRY_CONF DEFAULT workers $API_WORKERS
     local dburl
     dburl=`database_connection_url glance`
     iniset $GLANCE_REGISTRY_CONF database connection $dburl
@@ -126,8 +125,8 @@
     iniset_rpc_backend glance $GLANCE_REGISTRY_CONF
     iniset $GLANCE_REGISTRY_CONF DEFAULT graceful_shutdown_timeout "$SERVICE_GRACEFUL_SHUTDOWN_TIMEOUT"
 
+    # Set non-default configuration options for the API server
     iniset $GLANCE_API_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
-    inicomment $GLANCE_API_CONF DEFAULT log_file
     iniset $GLANCE_API_CONF database connection $dburl
     iniset $GLANCE_API_CONF DEFAULT use_syslog $SYSLOG
     iniset $GLANCE_API_CONF DEFAULT image_cache_dir $GLANCE_CACHE_DIR/
@@ -185,11 +184,6 @@
             iniset $GLANCE_SWIFT_STORE_CONF ref1 auth_address $KEYSTONE_SERVICE_URI/v3
         fi
         iniset $GLANCE_SWIFT_STORE_CONF ref1 auth_version 3
-
-        # commenting is not strictly necessary but it's confusing to have bad values in conf
-        inicomment $GLANCE_API_CONF glance_store swift_store_user
-        inicomment $GLANCE_API_CONF glance_store swift_store_key
-        inicomment $GLANCE_API_CONF glance_store swift_store_auth_address
     fi
 
     # We need to tell glance what it's public endpoint is so that the version
@@ -215,18 +209,13 @@
     cp -p $GLANCE_DIR/etc/glance-registry-paste.ini $GLANCE_REGISTRY_PASTE_INI
     cp -p $GLANCE_DIR/etc/glance-api-paste.ini $GLANCE_API_PASTE_INI
 
-    cp $GLANCE_DIR/etc/glance-cache.conf $GLANCE_CACHE_CONF
+    # Set non-default configuration options for the glance-cache
     iniset $GLANCE_CACHE_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
-    inicomment $GLANCE_CACHE_CONF DEFAULT log_file
     iniset $GLANCE_CACHE_CONF DEFAULT use_syslog $SYSLOG
     iniset $GLANCE_CACHE_CONF DEFAULT image_cache_dir $GLANCE_CACHE_DIR/
-    iniuncomment $GLANCE_CACHE_CONF DEFAULT auth_url
     iniset $GLANCE_CACHE_CONF DEFAULT auth_url $KEYSTONE_AUTH_URI
-    iniuncomment $GLANCE_CACHE_CONF DEFAULT auth_tenant_name
     iniset $GLANCE_CACHE_CONF DEFAULT admin_tenant_name $SERVICE_PROJECT_NAME
-    iniuncomment $GLANCE_CACHE_CONF DEFAULT auth_user
     iniset $GLANCE_CACHE_CONF DEFAULT admin_user glance
-    iniuncomment $GLANCE_CACHE_CONF DEFAULT auth_password
     iniset $GLANCE_CACHE_CONF DEFAULT admin_password $SERVICE_PASSWORD
     iniset $GLANCE_CACHE_CONF DEFAULT registry_host $(ipv6_unquote $GLANCE_SERVICE_HOST)
 
diff --git a/lib/horizon b/lib/horizon
index fab41bb..293a627 100644
--- a/lib/horizon
+++ b/lib/horizon
@@ -87,7 +87,7 @@
     _horizon_config_set $local_settings "" WEBROOT \"$HORIZON_APACHE_ROOT/\"
 
     _horizon_config_set $local_settings "" COMPRESS_OFFLINE True
-    _horizon_config_set $local_settings "" OPENSTACK_KEYSTONE_DEFAULT_ROLE \"Member\"
+    _horizon_config_set $local_settings "" OPENSTACK_KEYSTONE_DEFAULT_ROLE \"member\"
 
     _horizon_config_set $local_settings "" OPENSTACK_HOST \"${KEYSTONE_SERVICE_HOST}\"
 
diff --git a/lib/keystone b/lib/keystone
index 714f089..2860325 100644
--- a/lib/keystone
+++ b/lib/keystone
@@ -49,7 +49,6 @@
 
 KEYSTONE_CONF_DIR=${KEYSTONE_CONF_DIR:-/etc/keystone}
 KEYSTONE_CONF=$KEYSTONE_CONF_DIR/keystone.conf
-KEYSTONE_PASTE_INI=${KEYSTONE_PASTE_INI:-$KEYSTONE_CONF_DIR/keystone-paste.ini}
 KEYSTONE_PUBLIC_UWSGI_CONF=$KEYSTONE_CONF_DIR/keystone-uwsgi-public.ini
 KEYSTONE_ADMIN_UWSGI_CONF=$KEYSTONE_CONF_DIR/keystone-uwsgi-admin.ini
 KEYSTONE_PUBLIC_UWSGI=$KEYSTONE_BIN_DIR/keystone-wsgi-public
@@ -202,25 +201,9 @@
     sudo install -d -o $STACK_USER $KEYSTONE_CONF_DIR
 
     if [[ "$KEYSTONE_CONF_DIR" != "$KEYSTONE_DIR/etc" ]]; then
-        install -m 600 $KEYSTONE_DIR/etc/keystone.conf.sample $KEYSTONE_CONF
-        if [[ -f "$KEYSTONE_DIR/etc/keystone-paste.ini" ]]; then
-            cp -p "$KEYSTONE_DIR/etc/keystone-paste.ini" "$KEYSTONE_PASTE_INI"
-        fi
+        install -m 600 /dev/null $KEYSTONE_CONF
     fi
-    if [[ -f "$KEYSTONE_PASTE_INI" ]]; then
-        iniset "$KEYSTONE_CONF" paste_deploy config_file "$KEYSTONE_PASTE_INI"
-    else
-        # compatibility with mixed cfg and paste.deploy configuration
-        KEYSTONE_PASTE_INI="$KEYSTONE_CONF"
-    fi
-
-    if [ "$ENABLE_IDENTITY_V2" == "False" ]; then
-        # Only Identity v3 API should be available; then disable v2 pipelines
-        inidelete $KEYSTONE_PASTE_INI composite:main \\/v2.0
-        inidelete $KEYSTONE_PASTE_INI composite:admin \\/v2.0
-    fi
-
-    # Rewrite stock ``keystone.conf``
+    # Populate ``keystone.conf``
     if is_service_enabled ldap; then
         iniset $KEYSTONE_CONF identity domain_config_dir "$KEYSTONE_CONF_DIR/domains"
         iniset $KEYSTONE_CONF identity domain_specific_drivers_enabled "True"
@@ -309,30 +292,32 @@
 # service              --         --
 # --                   --         service
 # --                   --         ResellerAdmin
-# --                   --         Member
+# --                   --         member
 # demo                 admin      admin
-# demo                 demo       Member, anotherrole
+# demo                 demo       member, anotherrole
 # alt_demo             admin      admin
-# alt_demo             alt_demo   Member, anotherrole
-# invisible_to_admin   demo       Member
+# alt_demo             alt_demo   member, anotherrole
+# invisible_to_admin   demo       member
 
 # Group                Users            Roles                 Project
 # ------------------------------------------------------------------
 # admins               admin            admin                 admin
-# nonadmins            demo, alt_demo   Member, anotherrole   demo, alt_demo
+# nonadmins            demo, alt_demo   member, anotherrole   demo, alt_demo
 
 
 # Migrated from keystone_data.sh
 function create_keystone_accounts {
 
-    # The keystone bootstrapping process (performed via keystone-manage bootstrap)
-    # creates an admin user, admin role and admin project. As a sanity check
-    # we exercise the CLI to retrieve the IDs for these values.
+    # The keystone bootstrapping process (performed via keystone-manage
+    # bootstrap) creates an admin user, admin role, member role, and admin
+    # project. As a sanity check we exercise the CLI to retrieve the IDs for
+    # these values.
     local admin_project
     admin_project=$(openstack project show "admin" -f value -c id)
     local admin_user
     admin_user=$(openstack user show "admin" -f value -c id)
     local admin_role="admin"
+    local member_role="member"
 
     get_or_add_user_domain_role $admin_role $admin_user default
 
@@ -349,17 +334,6 @@
     # role is also configurable in swift-proxy.conf
     get_or_create_role ResellerAdmin
 
-    # The Member role is used by Horizon and Swift so we need to keep it:
-    local member_role="member"
-
-    # Capital Member role is legacy hard coded in Horizon / Swift
-    # configs. Keep it around.
-    get_or_create_role "Member"
-
-    # The reality is that the rest of the roles listed below honestly
-    # should work by symbolic names.
-    get_or_create_role $member_role
-
     # another_role demonstrates that an arbitrary role may be created and used
     # TODO(sleepsonthefloor): show how this can be used for rbac in the future!
     local another_role="anotherrole"
diff --git a/lib/libraries b/lib/libraries
index 6d52f64..b4f3c31 100644
--- a/lib/libraries
+++ b/lib/libraries
@@ -28,6 +28,7 @@
 GITDIR["cursive"]=$DEST/cursive
 GITDIR["debtcollector"]=$DEST/debtcollector
 GITDIR["futurist"]=$DEST/futurist
+GITDIR["openstacksdk"]=$DEST/openstacksdk
 GITDIR["os-client-config"]=$DEST/os-client-config
 GITDIR["osc-lib"]=$DEST/osc-lib
 GITDIR["osc-placement"]=$DEST/osc-placement
@@ -51,7 +52,6 @@
 GITDIR["oslo.vmware"]=$DEST/oslo.vmware
 GITDIR["osprofiler"]=$DEST/osprofiler
 GITDIR["pycadf"]=$DEST/pycadf
-GITDIR["python-openstacksdk"]=$DEST/python-openstacksdk
 GITDIR["stevedore"]=$DEST/stevedore
 GITDIR["taskflow"]=$DEST/taskflow
 GITDIR["tooz"]=$DEST/tooz
@@ -91,6 +91,7 @@
     _install_lib_from_source "cursive"
     _install_lib_from_source "debtcollector"
     _install_lib_from_source "futurist"
+    _install_lib_from_source "openstacksdk"
     _install_lib_from_source "osc-lib"
     _install_lib_from_source "osc-placement"
     _install_lib_from_source "os-client-config"
@@ -114,7 +115,6 @@
     _install_lib_from_source "oslo.vmware"
     _install_lib_from_source "osprofiler"
     _install_lib_from_source "pycadf"
-    _install_lib_from_source "python-openstacksdk"
     _install_lib_from_source "stevedore"
     _install_lib_from_source "taskflow"
     _install_lib_from_source "tooz"
diff --git a/lib/neutron b/lib/neutron
index 0834792..9f9b132 100644
--- a/lib/neutron
+++ b/lib/neutron
@@ -32,6 +32,17 @@
 NEUTRON_DIR=$DEST/neutron
 NEUTRON_AUTH_CACHE_DIR=${NEUTRON_AUTH_CACHE_DIR:-/var/cache/neutron}
 
+NEUTRON_DISTRIBUTED_ROUTING=$(trueorfalse False NEUTRON_DISTRIBUTED_ROUTING)
+# Distributed Virtual Router (DVR) configuration
+# Can be:
+# - ``legacy``          - No DVR functionality
+# - ``dvr_snat``        - Controller or single node DVR
+# - ``dvr``             - Compute node in multi-node DVR
+# - ``dvr_no_external`` - Compute node in multi-node DVR, no external network
+#
+# Default is 'dvr_snat' since it can handle both DVR and legacy routers
+NEUTRON_DVR_MODE=${NEUTRON_DVR_MODE:-dvr_snat}
+
 NEUTRON_BIN_DIR=$(get_python_exec_prefix)
 NEUTRON_DHCP_BINARY="neutron-dhcp-agent"
 
@@ -174,6 +185,7 @@
 
         iniset $NEUTRON_CONF DEFAULT policy_file $policy_file
         iniset $NEUTRON_CONF DEFAULT allow_overlapping_ips True
+        iniset $NEUTRON_CONF DEFAULT router_distributed $NEUTRON_DISTRIBUTED_ROUTING
 
         iniset $NEUTRON_CONF DEFAULT auth_strategy $NEUTRON_AUTH_STRATEGY
         configure_auth_token_middleware $NEUTRON_CONF neutron $NEUTRON_AUTH_CACHE_DIR keystone_authtoken
@@ -182,7 +194,15 @@
         # Configure VXLAN
         # TODO(sc68cal) not hardcode?
         iniset $NEUTRON_CORE_PLUGIN_CONF ml2 tenant_network_types vxlan
-        iniset $NEUTRON_CORE_PLUGIN_CONF ml2 mechanism_drivers openvswitch,linuxbridge
+
+        local mech_drivers="openvswitch"
+        if [[ "$NEUTRON_DISTRIBUTED_ROUTING" = "True" ]]; then
+            mech_drivers+=",l2population"
+        else
+            mech_drivers+=",linuxbridge"
+        fi
+        iniset $NEUTRON_CORE_PLUGIN_CONF ml2 mechanism_drivers $mech_drivers
+
         iniset $NEUTRON_CORE_PLUGIN_CONF ml2_type_vxlan vni_ranges 1001:2000
         iniset $NEUTRON_CORE_PLUGIN_CONF ml2_type_flat flat_networks public
         if [[ "$NEUTRON_PORT_SECURITY" = "True" ]]; then
@@ -200,9 +220,14 @@
         if [[ $NEUTRON_AGENT == "linuxbridge" ]]; then
             iniset $NEUTRON_CORE_PLUGIN_CONF securitygroup firewall_driver iptables
             iniset $NEUTRON_CORE_PLUGIN_CONF vxlan local_ip $HOST_IP
-        else
-            iniset $NEUTRON_CORE_PLUGIN_CONF securitygroup firewall_driver iptables_hybrid
+        elif [[ $NEUTRON_AGENT == "openvswitch" ]]; then
+            iniset $NEUTRON_CORE_PLUGIN_CONF securitygroup firewall_driver openvswitch
             iniset $NEUTRON_CORE_PLUGIN_CONF ovs local_ip $HOST_IP
+
+            if [[ "$NEUTRON_DISTRIBUTED_ROUTING" = "True" ]]; then
+                iniset $NEUTRON_CORE_PLUGIN_CONF agent l2_population True
+                iniset $NEUTRON_CORE_PLUGIN_CONF agent enable_distributed_routing True
+            fi
         fi
 
         if ! running_in_container; then
@@ -237,6 +262,10 @@
         else
             iniset $NEUTRON_CORE_PLUGIN_CONF ovs bridge_mappings "$PUBLIC_NETWORK_NAME:$PUBLIC_BRIDGE"
         fi
+
+        if [[ "$NEUTRON_DISTRIBUTED_ROUTING" = "True" ]]; then
+            iniset $NEUTRON_L3_CONF DEFAULT agent_mode $NEUTRON_DVR_MODE
+        fi
     fi
 
     # Metadata
@@ -296,26 +325,28 @@
 }
 
 # Make Neutron-required changes to nova.conf
+# Takes a single optional argument which is the config file to update,
+# if not passed $NOVA_CONF is used.
 function configure_neutron_nova_new {
-    iniset $NOVA_CONF DEFAULT use_neutron True
-    iniset $NOVA_CONF neutron auth_type "password"
-    iniset $NOVA_CONF neutron auth_url "$KEYSTONE_SERVICE_URI"
-    iniset $NOVA_CONF neutron username neutron
-    iniset $NOVA_CONF neutron password "$SERVICE_PASSWORD"
-    iniset $NOVA_CONF neutron user_domain_name "Default"
-    iniset $NOVA_CONF neutron project_name "$SERVICE_TENANT_NAME"
-    iniset $NOVA_CONF neutron project_domain_name "Default"
-    iniset $NOVA_CONF neutron auth_strategy $NEUTRON_AUTH_STRATEGY
-    iniset $NOVA_CONF neutron region_name "$REGION_NAME"
-    iniset $NOVA_CONF neutron url $NEUTRON_SERVICE_PROTOCOL://$NEUTRON_SERVICE_HOST:$NEUTRON_SERVICE_PORT
+    local conf=${1:-$NOVA_CONF}
+    iniset $conf DEFAULT use_neutron True
+    iniset $conf neutron auth_type "password"
+    iniset $conf neutron auth_url "$KEYSTONE_SERVICE_URI"
+    iniset $conf neutron username neutron
+    iniset $conf neutron password "$SERVICE_PASSWORD"
+    iniset $conf neutron user_domain_name "Default"
+    iniset $conf neutron project_name "$SERVICE_TENANT_NAME"
+    iniset $conf neutron project_domain_name "Default"
+    iniset $conf neutron auth_strategy $NEUTRON_AUTH_STRATEGY
+    iniset $conf neutron region_name "$REGION_NAME"
 
-    iniset $NOVA_CONF DEFAULT firewall_driver nova.virt.firewall.NoopFirewallDriver
+    iniset $conf DEFAULT firewall_driver nova.virt.firewall.NoopFirewallDriver
 
     # optionally set options in nova_conf
-    neutron_plugin_create_nova_conf
+    neutron_plugin_create_nova_conf $conf
 
     if is_service_enabled neutron-metadata-agent; then
-        iniset $NOVA_CONF neutron service_metadata_proxy "True"
+        iniset $conf neutron service_metadata_proxy "True"
     fi
 
 }
@@ -540,9 +571,23 @@
 function configure_neutron_nova {
     if is_neutron_legacy_enabled; then
         # Call back to old function
-        create_nova_conf_neutron "$@"
+        create_nova_conf_neutron $NOVA_CONF
+        if [[ "${CELLSV2_SETUP}" == "superconductor" ]]; then
+            for i in $(seq 1 $NOVA_NUM_CELLS); do
+                local conf
+                conf=$(conductor_conf $i)
+                create_nova_conf_neutron $conf
+            done
+        fi
     else
-        configure_neutron_nova_new "$@"
+        configure_neutron_nova_new $NOVA_CONF
+        if [[ "${CELLSV2_SETUP}" == "superconductor" ]]; then
+            for i in $(seq 1 $NOVA_NUM_CELLS); do
+                local conf
+                conf=$(conductor_conf $i)
+                configure_neutron_nova_new $conf
+            done
+        fi
     fi
 }
 
diff --git a/lib/neutron-legacy b/lib/neutron-legacy
index 3c6ec68..15bcfe3 100644
--- a/lib/neutron-legacy
+++ b/lib/neutron-legacy
@@ -366,32 +366,32 @@
 }
 
 function create_nova_conf_neutron {
-    iniset $NOVA_CONF DEFAULT use_neutron True
-    iniset $NOVA_CONF neutron auth_type "password"
-    iniset $NOVA_CONF neutron auth_url "$KEYSTONE_AUTH_URI"
-    iniset $NOVA_CONF neutron username "$Q_ADMIN_USERNAME"
-    iniset $NOVA_CONF neutron password "$SERVICE_PASSWORD"
-    iniset $NOVA_CONF neutron user_domain_name "$SERVICE_DOMAIN_NAME"
-    iniset $NOVA_CONF neutron project_name "$SERVICE_PROJECT_NAME"
-    iniset $NOVA_CONF neutron project_domain_name "$SERVICE_DOMAIN_NAME"
-    iniset $NOVA_CONF neutron auth_strategy "$Q_AUTH_STRATEGY"
-    iniset $NOVA_CONF neutron region_name "$REGION_NAME"
-    iniset $NOVA_CONF neutron url "${Q_PROTOCOL}://$Q_HOST:$Q_PORT"
+    local conf=${1:-$NOVA_CONF}
+    iniset $conf DEFAULT use_neutron True
+    iniset $conf neutron auth_type "password"
+    iniset $conf neutron auth_url "$KEYSTONE_AUTH_URI"
+    iniset $conf neutron username "$Q_ADMIN_USERNAME"
+    iniset $conf neutron password "$SERVICE_PASSWORD"
+    iniset $conf neutron user_domain_name "$SERVICE_DOMAIN_NAME"
+    iniset $conf neutron project_name "$SERVICE_PROJECT_NAME"
+    iniset $conf neutron project_domain_name "$SERVICE_DOMAIN_NAME"
+    iniset $conf neutron auth_strategy "$Q_AUTH_STRATEGY"
+    iniset $conf neutron region_name "$REGION_NAME"
 
     if [[ "$Q_USE_SECGROUP" == "True" ]]; then
         LIBVIRT_FIREWALL_DRIVER=nova.virt.firewall.NoopFirewallDriver
-        iniset $NOVA_CONF DEFAULT firewall_driver $LIBVIRT_FIREWALL_DRIVER
+        iniset $conf DEFAULT firewall_driver $LIBVIRT_FIREWALL_DRIVER
     fi
 
     # optionally set options in nova_conf
-    neutron_plugin_create_nova_conf
+    neutron_plugin_create_nova_conf $conf
 
     if is_service_enabled q-meta; then
-        iniset $NOVA_CONF neutron service_metadata_proxy "True"
+        iniset $conf neutron service_metadata_proxy "True"
     fi
 
-    iniset $NOVA_CONF DEFAULT vif_plugging_is_fatal "$VIF_PLUGGING_IS_FATAL"
-    iniset $NOVA_CONF DEFAULT vif_plugging_timeout "$VIF_PLUGGING_TIMEOUT"
+    iniset $conf DEFAULT vif_plugging_is_fatal "$VIF_PLUGGING_IS_FATAL"
+    iniset $conf DEFAULT vif_plugging_timeout "$VIF_PLUGGING_TIMEOUT"
 }
 
 # create_mutnauq_accounts() - Set up common required neutron accounts
diff --git a/lib/neutron_plugins/nuage b/lib/neutron_plugins/nuage
index 1c04aaa..f39c7c4 100644
--- a/lib/neutron_plugins/nuage
+++ b/lib/neutron_plugins/nuage
@@ -8,10 +8,11 @@
 set +o xtrace
 
 function neutron_plugin_create_nova_conf {
+    local conf="$1"
     NOVA_OVS_BRIDGE=${NOVA_OVS_BRIDGE:-"br-int"}
-    iniset $NOVA_CONF neutron ovs_bridge $NOVA_OVS_BRIDGE
+    iniset $conf neutron ovs_bridge $NOVA_OVS_BRIDGE
     LIBVIRT_FIREWALL_DRIVER=nova.virt.firewall.NoopFirewallDriver
-    iniset $NOVA_CONF DEFAULT firewall_driver $LIBVIRT_FIREWALL_DRIVER
+    iniset $conf DEFAULT firewall_driver $LIBVIRT_FIREWALL_DRIVER
 }
 
 function neutron_plugin_install_agent_packages {
diff --git a/lib/neutron_plugins/ovs_base b/lib/neutron_plugins/ovs_base
index 50b9ae5..523024e 100644
--- a/lib/neutron_plugins/ovs_base
+++ b/lib/neutron_plugins/ovs_base
@@ -72,14 +72,21 @@
         if [[ $DISTRO == "sle12" ]] && [[ $os_RELEASE -lt 12.2 ]]; then
             restart_service openvswitch-switch
         else
-            restart_service openvswitch
+            # workaround for https://bugzilla.suse.com/show_bug.cgi?id=1085971
+            if [[ $DISTRO =~ "tumbleweed" ]]; then
+                sudo sed -i -e "s,^OVS_USER_ID=.*,OVS_USER_ID='root:root'," /etc/sysconfig/openvswitch
+            fi
+            restart_service openvswitch || {
+                journalctl -xe || :
+                systemctl status openvswitch
+            }
         fi
     fi
 }
 
 function _neutron_ovs_base_configure_firewall_driver {
     if [[ "$Q_USE_SECGROUP" == "True" ]]; then
-        iniset /$Q_PLUGIN_CONF_FILE securitygroup firewall_driver iptables_hybrid
+        iniset /$Q_PLUGIN_CONF_FILE securitygroup firewall_driver openvswitch
         if ! running_in_container; then
             enable_kernel_bridge_firewall
         fi
diff --git a/lib/neutron_plugins/services/l3 b/lib/neutron_plugins/services/l3
index 41a467d..9be32b7 100644
--- a/lib/neutron_plugins/services/l3
+++ b/lib/neutron_plugins/services/l3
@@ -39,9 +39,9 @@
 Q_L3_ROUTER_PER_TENANT=${Q_L3_ROUTER_PER_TENANT:-True}
 
 
-# Use flat providernet for public network
+# Use providernet for public network
 #
-# If Q_USE_PROVIDERNET_FOR_PUBLIC=True, use a flat provider network
+# If Q_USE_PROVIDERNET_FOR_PUBLIC=True, use a provider network
 # for external interface of neutron l3-agent.  In that case,
 # PUBLIC_PHYSICAL_NETWORK specifies provider:physical_network value
 # used for the network.  In case of ofagent, you should add the
@@ -59,6 +59,10 @@
 #    Q_USE_PROVIDERNET_FOR_PUBLIC=True
 #    PUBLIC_PHYSICAL_NETWORK=public
 #    OVS_BRIDGE_MAPPINGS=public:br-ex
+#
+# The provider-network-type defaults to flat, however, the values
+# PUBLIC_PROVIDERNET_TYPE and PUBLIC_PROVIDERNET_SEGMENTATION_ID could
+# be set to specify the parameters for an alternate network type.
 Q_USE_PROVIDERNET_FOR_PUBLIC=${Q_USE_PROVIDERNET_FOR_PUBLIC:-True}
 PUBLIC_PHYSICAL_NETWORK=${PUBLIC_PHYSICAL_NETWORK:-public}
 
@@ -240,7 +244,7 @@
         fi
         # Create an external network, and a subnet. Configure the external network as router gw
         if [ "$Q_USE_PROVIDERNET_FOR_PUBLIC" = "True" ]; then
-            EXT_NET_ID=$(openstack --os-cloud devstack-admin --os-region "$REGION_NAME" network create "$PUBLIC_NETWORK_NAME" $EXTERNAL_NETWORK_FLAGS --provider-network-type flat --provider-physical-network ${PUBLIC_PHYSICAL_NETWORK} | grep ' id ' | get_field 2)
+            EXT_NET_ID=$(openstack --os-cloud devstack-admin --os-region "$REGION_NAME" network create "$PUBLIC_NETWORK_NAME" $EXTERNAL_NETWORK_FLAGS --provider-network-type ${PUBLIC_PROVIDERNET_TYPE:-flat} ${PUBLIC_PROVIDERNET_SEGMENTATION_ID:+--provider-segment $PUBLIC_PROVIDERNET_SEGMENTATION_ID} --provider-physical-network ${PUBLIC_PHYSICAL_NETWORK} | grep ' id ' | get_field 2)
         else
             EXT_NET_ID=$(openstack --os-cloud devstack-admin --os-region "$REGION_NAME" network create "$PUBLIC_NETWORK_NAME" $EXTERNAL_NETWORK_FLAGS | grep ' id ' | get_field 2)
         fi
diff --git a/lib/nova b/lib/nova
index da07579..5e157c5 100644
--- a/lib/nova
+++ b/lib/nova
@@ -424,6 +424,9 @@
     iniset $NOVA_CONF DEFAULT rootwrap_config "$NOVA_CONF_DIR/rootwrap.conf"
     iniset $NOVA_CONF scheduler driver "$SCHEDULER"
     iniset $NOVA_CONF filter_scheduler enabled_filters "$FILTERS"
+    if [[ $SCHEDULER == "filter_scheduler" ]]; then
+        iniset $NOVA_CONF scheduler workers "$API_WORKERS"
+    fi
     iniset $NOVA_CONF DEFAULT default_floating_pool "$PUBLIC_NETWORK_NAME"
     if [[ $SERVICE_IP_VERSION == 6 ]]; then
         iniset $NOVA_CONF DEFAULT my_ip "$HOST_IPV6"
@@ -503,9 +506,17 @@
     if [ "$FORCE_CONFIG_DRIVE" != "False" ]; then
         iniset $NOVA_CONF DEFAULT force_config_drive "$FORCE_CONFIG_DRIVE"
     fi
+
+    # nova defaults to genisoimage but only mkisofs is available for 15.0+
+    if is_suse; then
+        iniset $NOVA_CONF DEFAULT mkisofs_cmd /usr/bin/mkisofs
+    fi
+
     # Format logging
     setup_logging $NOVA_CONF
 
+    iniset $NOVA_CONF upgrade_levels compute "auto"
+
     write_uwsgi_config "$NOVA_UWSGI_CONF" "$NOVA_UWSGI" "/compute"
     write_uwsgi_config "$NOVA_METADATA_UWSGI_CONF" "$NOVA_METADATA_UWSGI" "" "$SERVICE_LISTEN_ADDRESS:${METADATA_SERVICE_PORT}"
 
@@ -515,52 +526,6 @@
         iniset $NOVA_CONF DEFAULT notify_on_state_change "vm_and_task_state"
     fi
 
-    # All nova-compute workers need to know the vnc configuration options
-    # These settings don't hurt anything if n-xvnc and n-novnc are disabled
-    if is_service_enabled n-cpu; then
-        NOVNCPROXY_URL=${NOVNCPROXY_URL:-"http://$SERVICE_HOST:6080/vnc_auto.html"}
-        iniset $NOVA_CONF vnc novncproxy_base_url "$NOVNCPROXY_URL"
-        XVPVNCPROXY_URL=${XVPVNCPROXY_URL:-"http://$SERVICE_HOST:6081/console"}
-        iniset $NOVA_CONF vnc xvpvncproxy_base_url "$XVPVNCPROXY_URL"
-        SPICEHTML5PROXY_URL=${SPICEHTML5PROXY_URL:-"http://$SERVICE_HOST:6082/spice_auto.html"}
-        iniset $NOVA_CONF spice html5proxy_base_url "$SPICEHTML5PROXY_URL"
-    fi
-
-    if is_service_enabled n-novnc || is_service_enabled n-xvnc || [ "$NOVA_VNC_ENABLED" != False ]; then
-        # Address on which instance vncservers will listen on compute hosts.
-        # For multi-host, this should be the management ip of the compute host.
-        VNCSERVER_LISTEN=${VNCSERVER_LISTEN=$NOVA_SERVICE_LOCAL_HOST}
-        VNCSERVER_PROXYCLIENT_ADDRESS=${VNCSERVER_PROXYCLIENT_ADDRESS=$NOVA_SERVICE_LOCAL_HOST}
-        iniset $NOVA_CONF vnc server_listen "$VNCSERVER_LISTEN"
-        iniset $NOVA_CONF vnc server_proxyclient_address "$VNCSERVER_PROXYCLIENT_ADDRESS"
-        iniset $NOVA_CONF vnc novncproxy_host "$NOVA_SERVICE_LISTEN_ADDRESS"
-        iniset $NOVA_CONF vnc xvpvncproxy_host "$NOVA_SERVICE_LISTEN_ADDRESS"
-
-        if is_nova_console_proxy_compute_tls_enabled ; then
-            iniset $NOVA_CONF vnc auth_schemes "vencrypt"
-            iniset $NOVA_CONF vnc vencrypt_client_key "/etc/pki/nova-novnc/client-key.pem"
-            iniset $NOVA_CONF vnc vencrypt_client_cert "/etc/pki/nova-novnc/client-cert.pem"
-            iniset $NOVA_CONF vnc vencrypt_ca_certs "/etc/pki/nova-novnc/ca-cert.pem"
-
-            sudo mkdir -p /etc/pki/nova-novnc
-            deploy_int_CA /etc/pki/nova-novnc/ca-cert.pem
-            deploy_int_cert /etc/pki/nova-novnc/client-cert.pem /etc/pki/nova-novnc/client-key.pem
-        fi
-    else
-        iniset $NOVA_CONF vnc enabled false
-    fi
-
-    if is_service_enabled n-spice; then
-        # Address on which instance spiceservers will listen on compute hosts.
-        # For multi-host, this should be the management ip of the compute host.
-        SPICESERVER_PROXYCLIENT_ADDRESS=${SPICESERVER_PROXYCLIENT_ADDRESS=$NOVA_SERVICE_LOCAL_HOST}
-        SPICESERVER_LISTEN=${SPICESERVER_LISTEN=$NOVA_SERVICE_LOCAL_HOST}
-        iniset $NOVA_CONF spice enabled true
-        iniset $NOVA_CONF spice server_listen "$SPICESERVER_LISTEN"
-        iniset $NOVA_CONF spice server_proxyclient_address "$SPICESERVER_PROXYCLIENT_ADDRESS"
-        iniset $NOVA_CONF spice html5proxy_host "$NOVA_SERVICE_LISTEN_ADDRESS"
-    fi
-
     # Set the oslo messaging driver to the typical default. This does not
     # enable notifications, but it will allow them to function when enabled.
     iniset $NOVA_CONF oslo_messaging_notifications driver "messagingv2"
@@ -579,10 +544,6 @@
         iniset $NOVA_CONF oslo_middleware enable_proxy_headers_parsing True
     fi
 
-    if is_service_enabled n-sproxy; then
-        iniset $NOVA_CONF serial_console serialproxy_host "$NOVA_SERVICE_LISTEN_ADDRESS"
-        iniset $NOVA_CONF serial_console enabled True
-    fi
     iniset $NOVA_CONF DEFAULT graceful_shutdown_timeout "$SERVICE_GRACEFUL_SHUTDOWN_TIMEOUT"
 
     # Setup logging for nova-dhcpbridge command line
@@ -632,6 +593,86 @@
             setup_logging $conf
         done
     fi
+
+    # Console proxy configuration has to go after conductor configuration
+    # because the per cell config file nova_cellN.conf is cleared out as part
+    # of conductor configuration.
+    if [[ "${CELLSV2_SETUP}" == "singleconductor" ]]; then
+        configure_console_proxies
+    else
+        for i in $(seq 1 $NOVA_NUM_CELLS); do
+            local conf
+            conf=$(conductor_conf $i)
+            configure_console_proxies $conf
+        done
+    fi
+}
+
+function configure_console_compute {
+    # All nova-compute workers need to know the vnc configuration options
+    # These settings don't hurt anything if n-xvnc and n-novnc are disabled
+    if is_service_enabled n-cpu; then
+        NOVNCPROXY_URL=${NOVNCPROXY_URL:-"http://$SERVICE_HOST:6080/vnc_auto.html"}
+        iniset $NOVA_CPU_CONF vnc novncproxy_base_url "$NOVNCPROXY_URL"
+        XVPVNCPROXY_URL=${XVPVNCPROXY_URL:-"http://$SERVICE_HOST:6081/console"}
+        iniset $NOVA_CPU_CONF vnc xvpvncproxy_base_url "$XVPVNCPROXY_URL"
+        SPICEHTML5PROXY_URL=${SPICEHTML5PROXY_URL:-"http://$SERVICE_HOST:6082/spice_auto.html"}
+        iniset $NOVA_CPU_CONF spice html5proxy_base_url "$SPICEHTML5PROXY_URL"
+    fi
+
+    if is_service_enabled n-novnc || is_service_enabled n-xvnc || [ "$NOVA_VNC_ENABLED" != False ]; then
+        # Address on which instance vncservers will listen on compute hosts.
+        # For multi-host, this should be the management ip of the compute host.
+        VNCSERVER_LISTEN=${VNCSERVER_LISTEN=$NOVA_SERVICE_LOCAL_HOST}
+        VNCSERVER_PROXYCLIENT_ADDRESS=${VNCSERVER_PROXYCLIENT_ADDRESS=$NOVA_SERVICE_LOCAL_HOST}
+        iniset $NOVA_CPU_CONF vnc server_listen "$VNCSERVER_LISTEN"
+        iniset $NOVA_CPU_CONF vnc server_proxyclient_address "$VNCSERVER_PROXYCLIENT_ADDRESS"
+    else
+        iniset $NOVA_CPU_CONF vnc enabled false
+    fi
+
+    if is_service_enabled n-spice; then
+        # Address on which instance spiceservers will listen on compute hosts.
+        # For multi-host, this should be the management ip of the compute host.
+        SPICESERVER_PROXYCLIENT_ADDRESS=${SPICESERVER_PROXYCLIENT_ADDRESS=$NOVA_SERVICE_LOCAL_HOST}
+        SPICESERVER_LISTEN=${SPICESERVER_LISTEN=$NOVA_SERVICE_LOCAL_HOST}
+        iniset $NOVA_CPU_CONF spice enabled true
+        iniset $NOVA_CPU_CONF spice server_listen "$SPICESERVER_LISTEN"
+        iniset $NOVA_CPU_CONF spice server_proxyclient_address "$SPICESERVER_PROXYCLIENT_ADDRESS"
+    fi
+
+    if is_service_enabled n-sproxy; then
+        iniset $NOVA_CPU_CONF serial_console enabled True
+    fi
+}
+
+function configure_console_proxies {
+    # Use the provided config file path or default to $NOVA_CONF.
+    local conf=${1:-$NOVA_CONF}
+
+    if is_service_enabled n-novnc || is_service_enabled n-xvnc || [ "$NOVA_VNC_ENABLED" != False ]; then
+        iniset $conf vnc novncproxy_host "$NOVA_SERVICE_LISTEN_ADDRESS"
+        iniset $conf vnc xvpvncproxy_host "$NOVA_SERVICE_LISTEN_ADDRESS"
+
+        if is_nova_console_proxy_compute_tls_enabled ; then
+            iniset $conf vnc auth_schemes "vencrypt"
+            iniset $conf vnc vencrypt_client_key "/etc/pki/nova-novnc/client-key.pem"
+            iniset $conf vnc vencrypt_client_cert "/etc/pki/nova-novnc/client-cert.pem"
+            iniset $conf vnc vencrypt_ca_certs "/etc/pki/nova-novnc/ca-cert.pem"
+
+            sudo mkdir -p /etc/pki/nova-novnc
+            deploy_int_CA /etc/pki/nova-novnc/ca-cert.pem
+            deploy_int_cert /etc/pki/nova-novnc/client-cert.pem /etc/pki/nova-novnc/client-key.pem
+        fi
+    fi
+
+    if is_service_enabled n-spice; then
+        iniset $conf spice html5proxy_host "$NOVA_SERVICE_LISTEN_ADDRESS"
+    fi
+
+    if is_service_enabled n-sproxy; then
+        iniset $conf serial_console serialproxy_host "$NOVA_SERVICE_LISTEN_ADDRESS"
+    fi
 }
 
 function init_nova_service_user_conf {
@@ -682,7 +723,7 @@
         $NOVA_BIN_DIR/nova-manage cell create --name=child --cell_type=child --username=$RABBIT_USERID --hostname=$RABBIT_HOST --port=5672 --password=$RABBIT_PASSWORD --virtual_host=child_cell --woffset=0 --wscale=1
 
         # Creates the single cells v2 cell for the child cell (v1) nova db.
-        nova-manage --config-file $NOVA_CELLS_CONF cell_v2 create_cell \
+        $NOVA_BIN_DIR/nova-manage --config-file $NOVA_CELLS_CONF cell_v2 create_cell \
             --transport-url $(get_transport_url child_cell) --name 'cell1'
     fi
 }
@@ -726,12 +767,12 @@
         # this needs to come after the api_db sync happens. We also want to run
         # this before the db sync below since that will migrate both the nova
         # and nova_cell0 databases.
-        nova-manage cell_v2 map_cell0 --database_connection `database_connection_url nova_cell0`
+        $NOVA_BIN_DIR/nova-manage cell_v2 map_cell0 --database_connection `database_connection_url nova_cell0`
 
         # (Re)create nova databases
         for i in $(seq 1 $NOVA_NUM_CELLS); do
             recreate_database nova_cell${i}
-            $NOVA_BIN_DIR/nova-manage --config-file $(conductor_conf $i) db sync
+            $NOVA_BIN_DIR/nova-manage --config-file $(conductor_conf $i) db sync --local_cell
         done
 
         # Migrate nova and nova_cell0 databases.
@@ -747,7 +788,7 @@
 
         # create the cell1 cell for the main nova db where the hosts live
         for i in $(seq 1 $NOVA_NUM_CELLS); do
-            nova-manage --config-file $NOVA_CONF --config-file $(conductor_conf $i) cell_v2 create_cell --name "cell$i"
+            $NOVA_BIN_DIR/nova-manage --config-file $NOVA_CONF --config-file $(conductor_conf $i) cell_v2 create_cell --name "cell$i"
         done
     fi
 
@@ -883,6 +924,11 @@
         iniset_rpc_backend nova $NOVA_CPU_CONF DEFAULT "nova_cell${NOVA_CPU_CELL}"
     fi
 
+    # Console proxies were configured earlier in create_nova_conf. Now that the
+    # nova-cpu.conf has been created, configure the console settings required
+    # by the compute process.
+    configure_console_compute
+
     if [[ "$VIRT_DRIVER" = 'libvirt' ]]; then
         # The group **$LIBVIRT_GROUP** is added to the current user in this script.
         # ``sg`` is used in run_process to execute nova-compute as a member of the
@@ -943,11 +989,46 @@
         run_process n-api-meta "$NOVA_BIN_DIR/uwsgi --procname-prefix nova-api-meta --ini $NOVA_METADATA_UWSGI_CONF"
     fi
 
-    run_process n-novnc "$NOVA_BIN_DIR/nova-novncproxy --config-file $api_cell_conf --web $NOVNC_WEB_DIR"
-    run_process n-xvnc "$NOVA_BIN_DIR/nova-xvpvncproxy --config-file $api_cell_conf"
-    run_process n-spice "$NOVA_BIN_DIR/nova-spicehtml5proxy --config-file $api_cell_conf --web $SPICE_WEB_DIR"
+    # nova-consoleauth always runs globally
     run_process n-cauth "$NOVA_BIN_DIR/nova-consoleauth --config-file $api_cell_conf"
-    run_process n-sproxy "$NOVA_BIN_DIR/nova-serialproxy --config-file $api_cell_conf"
+
+    export PATH=$old_path
+}
+
+function enable_nova_console_proxies {
+    for i in $(seq 1 $NOVA_NUM_CELLS); do
+        for srv in n-novnc n-xvnc n-spice n-sproxy; do
+            if is_service_enabled $srv; then
+                enable_service ${srv}-cell${i}
+            fi
+        done
+    done
+}
+
+function start_nova_console_proxies {
+    # Hack to set the path for rootwrap
+    local old_path=$PATH
+    # This is needed to find the nova conf
+    export PATH=$NOVA_BIN_DIR:$PATH
+
+    local api_cell_conf=$NOVA_CONF
+    # console proxies run globally for singleconductor, else they run per cell
+    if [[ "${CELLSV2_SETUP}" == "singleconductor" ]]; then
+        run_process n-novnc "$NOVA_BIN_DIR/nova-novncproxy --config-file $api_cell_conf --web $NOVNC_WEB_DIR"
+        run_process n-xvnc "$NOVA_BIN_DIR/nova-xvpvncproxy --config-file $api_cell_conf"
+        run_process n-spice "$NOVA_BIN_DIR/nova-spicehtml5proxy --config-file $api_cell_conf --web $SPICE_WEB_DIR"
+        run_process n-sproxy "$NOVA_BIN_DIR/nova-serialproxy --config-file $api_cell_conf"
+    else
+        enable_nova_console_proxies
+        for i in $(seq 1 $NOVA_NUM_CELLS); do
+            local conf
+            conf=$(conductor_conf $i)
+            run_process n-novnc-cell${i} "$NOVA_BIN_DIR/nova-novncproxy --config-file $conf --web $NOVNC_WEB_DIR"
+            run_process n-xvnc-cell${i} "$NOVA_BIN_DIR/nova-xvpvncproxy --config-file $conf"
+            run_process n-spice-cell${i} "$NOVA_BIN_DIR/nova-spicehtml5proxy --config-file $conf --web $SPICE_WEB_DIR"
+            run_process n-sproxy-cell${i} "$NOVA_BIN_DIR/nova-serialproxy --config-file $conf"
+        done
+    fi
 
     export PATH=$old_path
 }
@@ -1007,12 +1088,13 @@
     # this catches the cells v1 case early
     _set_singleconductor
     start_nova_rest
+    start_nova_console_proxies
     start_nova_conductor
     start_nova_compute
     if is_service_enabled n-api; then
         # dump the cell mapping to ensure life is good
         echo "Dumping cells_v2 mapping"
-        nova-manage cell_v2 list_cells --verbose
+        $NOVA_BIN_DIR/nova-manage cell_v2 list_cells --verbose
     fi
 }
 
@@ -1032,11 +1114,26 @@
 
 function stop_nova_rest {
     # Kill the non-compute nova processes
-    for serv in n-api n-api-meta n-net n-sch n-novnc n-xvnc n-cauth n-spice n-cell n-cell n-sproxy; do
+    for serv in n-api n-api-meta n-net n-sch n-cauth n-cell n-cell; do
         stop_process $serv
     done
 }
 
+function stop_nova_console_proxies {
+    if [[ "${CELLSV2_SETUP}" == "singleconductor" ]]; then
+        for srv in n-novnc n-xvnc n-spice n-sproxy; do
+            stop_process $srv
+        done
+    else
+        enable_nova_console_proxies
+        for i in $(seq 1 $NOVA_NUM_CELLS); do
+            for srv in n-novnc n-xvnc n-spice n-sproxy; do
+                stop_process ${srv}-cell${i}
+            done
+        done
+    fi
+}
+
 function stop_nova_conductor {
     if [[ "${CELLSV2_SETUP}" == "singleconductor" ]]; then
         stop_process n-cond
@@ -1054,6 +1151,7 @@
 # stop_nova() - Stop running processes
 function stop_nova {
     stop_nova_rest
+    stop_nova_console_proxies
     stop_nova_conductor
     stop_nova_compute
 }
diff --git a/lib/nova_plugins/hypervisor-fake b/lib/nova_plugins/hypervisor-fake
index 49c8dee..87ee49f 100644
--- a/lib/nova_plugins/hypervisor-fake
+++ b/lib/nova_plugins/hypervisor-fake
@@ -38,18 +38,7 @@
 function configure_nova_hypervisor {
     iniset $NOVA_CONF DEFAULT compute_driver "fake.FakeDriver"
     # Disable arbitrary limits
-    iniset $NOVA_CONF DEFAULT quota_instances -1
-    iniset $NOVA_CONF DEFAULT quota_cores -1
-    iniset $NOVA_CONF DEFAULT quota_ram -1
-    iniset $NOVA_CONF DEFAULT quota_floating_ips -1
-    iniset $NOVA_CONF DEFAULT quota_fixed_ips -1
-    iniset $NOVA_CONF DEFAULT quota_metadata_items -1
-    iniset $NOVA_CONF DEFAULT quota_injected_files -1
-    iniset $NOVA_CONF DEFAULT quota_injected_file_path_length -1
-    iniset $NOVA_CONF DEFAULT quota_security_groups -1
-    iniset $NOVA_CONF DEFAULT quota_security_group_rules -1
-    iniset $NOVA_CONF DEFAULT quota_key_pairs -1
-    iniset $NOVA_CONF filter_scheduler enabled_filters "RetryFilter,AvailabilityZoneFilter,ComputeFilter,ComputeCapabilitiesFilter,ImagePropertiesFilter,CoreFilter,RamFilter,DiskFilter"
+    iniset $NOVA_CONF quota driver nova.quota.NoopQuotaDriver
 }
 
 # install_nova_hypervisor() - Install external components
diff --git a/lib/nova_plugins/hypervisor-ironic b/lib/nova_plugins/hypervisor-ironic
index c91f70b..49110a8 100644
--- a/lib/nova_plugins/hypervisor-ironic
+++ b/lib/nova_plugins/hypervisor-ironic
@@ -42,14 +42,6 @@
     iniset $NOVA_CONF DEFAULT compute_driver ironic.IronicDriver
     iniset $NOVA_CONF DEFAULT firewall_driver $LIBVIRT_FIREWALL_DRIVER
 
-    if [[ "$IRONIC_USE_RESOURCE_CLASSES" == "False" ]]; then
-        iniset $NOVA_CONF DEFAULT scheduler_host_manager ironic_host_manager
-        iniset $NOVA_CONF filter_scheduler use_baremetal_filters True
-        iniset $NOVA_CONF filter_scheduler host_subset_size 999
-        iniset $NOVA_CONF DEFAULT ram_allocation_ratio 1.0
-        iniset $NOVA_CONF DEFAULT reserved_host_memory_mb 0
-    fi
-
     # ironic section
     iniset $NOVA_CONF ironic auth_type password
     iniset $NOVA_CONF ironic username admin
diff --git a/lib/placement b/lib/placement
index 1d68f8a..a1602ba 100644
--- a/lib/placement
+++ b/lib/placement
@@ -44,8 +44,6 @@
 
 # The placement service can optionally use a separate database
 # connection. Set PLACEMENT_DB_ENABLED to True to use it.
-# NOTE(cdent): This functionality depends on some code that is not
-# yet merged in nova but is coming soon.
 PLACEMENT_DB_ENABLED=$(trueorfalse False PLACEMENT_DB_ENABLED)
 
 if is_service_enabled tls-proxy; then
@@ -112,7 +110,6 @@
     iniset $conf placement user_domain_name "$SERVICE_DOMAIN_NAME"
     iniset $conf placement project_name "$SERVICE_TENANT_NAME"
     iniset $conf placement project_domain_name "$SERVICE_DOMAIN_NAME"
-    iniset $conf placement os_region_name "$REGION_NAME"
     # TODO(cdent): auth_strategy, which is common to see in these
     # blocks is not currently used here. For the time being the
     # placement api uses the auth_strategy configuration setting
@@ -152,9 +149,9 @@
 function init_placement {
     if [ "$PLACEMENT_DB_ENABLED" != False ]; then
         recreate_database placement
-        time_start "dbsync"
-        $NOVA_BIN_DIR/nova-manage --config-file $NOVA_CONF api_db sync
-        time_stop "dbsync"
+        # Database migration will be handled when nova does an api_db sync
+        # TODO(cdent): When placement is extracted we'll do our own sync
+        # here.
     fi
     create_placement_accounts
 }
diff --git a/lib/rpc_backend b/lib/rpc_backend
index 44d0717..1c7c82f 100644
--- a/lib/rpc_backend
+++ b/lib/rpc_backend
@@ -52,7 +52,20 @@
     if is_service_enabled rabbit; then
         # Install rabbitmq-server
         install_package rabbitmq-server
-        if is_fedora; then
+        if is_suse; then
+            install_package rabbitmq-server-plugins
+            # the default systemd socket activation only listens on the loopback interface
+            # which causes rabbitmq to try to start its own epmd
+            sudo mkdir -p /etc/systemd/system/epmd.socket.d
+            cat <<EOF | sudo tee /etc/systemd/system/epmd.socket.d/ports.conf >/dev/null
+[Socket]
+ListenStream=
+ListenStream=[::]:4369
+EOF
+            sudo systemctl daemon-reload
+            sudo systemctl restart epmd.socket epmd.service
+        fi
+        if is_fedora || is_suse; then
             sudo systemctl enable rabbitmq-server
         fi
     fi
diff --git a/lib/swift b/lib/swift
index 62b8a32..3b3e608 100644
--- a/lib/swift
+++ b/lib/swift
@@ -37,6 +37,7 @@
 
 # Set up default directories
 GITDIR["python-swiftclient"]=$DEST/python-swiftclient
+SWIFT_DIR=$DEST/swift
 
 # Swift virtual environment
 if [[ ${USE_VENV} = True ]]; then
@@ -46,8 +47,6 @@
     SWIFT_BIN_DIR=$(get_python_exec_prefix)
 fi
 
-
-SWIFT_DIR=$DEST/swift
 SWIFT_AUTH_CACHE_DIR=${SWIFT_AUTH_CACHE_DIR:-/var/cache/swift}
 SWIFT_APACHE_WSGI_DIR=${SWIFT_APACHE_WSGI_DIR:-/var/www/swift}
 SWIFT3_DIR=$DEST/swift3
@@ -341,7 +340,7 @@
     local user_group
 
     # Make sure to kill all swift processes first
-    swift-init --run-dir=${SWIFT_DATA_DIR}/run all stop || true
+    $SWIFT_BIN_DIR/swift-init --run-dir=${SWIFT_DATA_DIR}/run all stop || true
 
     sudo install -d -o ${STACK_USER} ${SWIFT_CONF_DIR}
     sudo install -d -o ${STACK_USER} ${SWIFT_CONF_DIR}/{object,container,account}-server
@@ -369,6 +368,7 @@
 
     SWIFT_CONFIG_PROXY_SERVER=${SWIFT_CONF_DIR}/proxy-server.conf
     cp ${SWIFT_DIR}/etc/proxy-server.conf-sample ${SWIFT_CONFIG_PROXY_SERVER}
+    cp ${SWIFT_DIR}/etc/internal-client.conf-sample ${SWIFT_CONF_DIR}/internal-client.conf
 
     # To run container sync feature introduced in Swift ver 1.12.0,
     # container sync "realm" is added in container-sync-realms.conf
@@ -557,7 +557,11 @@
 
     local swift_log_dir=${SWIFT_DATA_DIR}/logs
     sudo rm -rf ${swift_log_dir}
-    sudo install -d -o ${STACK_USER} -g adm ${swift_log_dir}/hourly
+    local swift_log_group=adm
+    if is_suse; then
+        swift_log_group=root
+    fi
+    sudo install -d -o ${STACK_USER} -g ${swift_log_group} ${swift_log_dir}/hourly
 
     if [[ $SYSLOG != "False" ]]; then
         sed "s,%SWIFT_LOGDIR%,${swift_log_dir}," $FILES/swift/rsyslog.conf | sudo \
@@ -700,7 +704,7 @@
 function init_swift {
     local node_number
     # Make sure to kill all swift processes first
-    swift-init --run-dir=${SWIFT_DATA_DIR}/run all stop || true
+    $SWIFT_BIN_DIR/swift-init --run-dir=${SWIFT_DATA_DIR}/run all stop || true
 
     # Forcibly re-create the backing filesystem
     create_swift_disk
@@ -711,9 +715,9 @@
 
         rm -f *.builder *.ring.gz backups/*.builder backups/*.ring.gz
 
-        swift-ring-builder object.builder create ${SWIFT_PARTITION_POWER_SIZE} ${SWIFT_REPLICAS} 1
-        swift-ring-builder container.builder create ${SWIFT_PARTITION_POWER_SIZE} ${SWIFT_REPLICAS} 1
-        swift-ring-builder account.builder create ${SWIFT_PARTITION_POWER_SIZE} ${SWIFT_REPLICAS} 1
+        $SWIFT_BIN_DIR/swift-ring-builder object.builder create ${SWIFT_PARTITION_POWER_SIZE} ${SWIFT_REPLICAS} 1
+        $SWIFT_BIN_DIR/swift-ring-builder container.builder create ${SWIFT_PARTITION_POWER_SIZE} ${SWIFT_REPLICAS} 1
+        $SWIFT_BIN_DIR/swift-ring-builder account.builder create ${SWIFT_PARTITION_POWER_SIZE} ${SWIFT_REPLICAS} 1
 
         # The ring will be created on each node, and because the order of
         # nodes is identical we can use a seed for rebalancing, making it
@@ -724,26 +728,26 @@
             node_number=1
 
             for node in ${SWIFT_STORAGE_IPS}; do
-                swift-ring-builder object.builder add z${node_number}-${node}:${OBJECT_PORT_BASE}/sdb1 1
-                swift-ring-builder container.builder add z${node_number}-${node}:${CONTAINER_PORT_BASE}/sdb1 1
-                swift-ring-builder account.builder add z${node_number}-${node}:${ACCOUNT_PORT_BASE}/sdb1 1
+                $SWIFT_BIN_DIR/swift-ring-builder object.builder add z${node_number}-${node}:${OBJECT_PORT_BASE}/sdb1 1
+                $SWIFT_BIN_DIR/swift-ring-builder container.builder add z${node_number}-${node}:${CONTAINER_PORT_BASE}/sdb1 1
+                $SWIFT_BIN_DIR/swift-ring-builder account.builder add z${node_number}-${node}:${ACCOUNT_PORT_BASE}/sdb1 1
                 let "node_number=node_number+1"
             done
 
         else
 
             for node_number in ${SWIFT_REPLICAS_SEQ}; do
-                swift-ring-builder object.builder add z${node_number}-${SWIFT_SERVICE_LOCAL_HOST}:$(( OBJECT_PORT_BASE + 10 * (node_number - 1) ))/sdb1 1
-                swift-ring-builder container.builder add z${node_number}-${SWIFT_SERVICE_LOCAL_HOST}:$(( CONTAINER_PORT_BASE + 10 * (node_number - 1) ))/sdb1 1
-                swift-ring-builder account.builder add z${node_number}-${SWIFT_SERVICE_LOCAL_HOST}:$(( ACCOUNT_PORT_BASE + 10 * (node_number - 1) ))/sdb1 1
+                $SWIFT_BIN_DIR/swift-ring-builder object.builder add z${node_number}-${SWIFT_SERVICE_LOCAL_HOST}:$(( OBJECT_PORT_BASE + 10 * (node_number - 1) ))/sdb1 1
+                $SWIFT_BIN_DIR/swift-ring-builder container.builder add z${node_number}-${SWIFT_SERVICE_LOCAL_HOST}:$(( CONTAINER_PORT_BASE + 10 * (node_number - 1) ))/sdb1 1
+                $SWIFT_BIN_DIR/swift-ring-builder account.builder add z${node_number}-${SWIFT_SERVICE_LOCAL_HOST}:$(( ACCOUNT_PORT_BASE + 10 * (node_number - 1) ))/sdb1 1
             done
         fi
 
         # We use a seed for rebalancing. Doing this allows us to create
         # identical rings on multiple nodes if SWIFT_STORAGE_IPS is the same
-        swift-ring-builder object.builder rebalance 42
-        swift-ring-builder container.builder rebalance 42
-        swift-ring-builder account.builder rebalance 42
+        $SWIFT_BIN_DIR/swift-ring-builder object.builder rebalance 42
+        $SWIFT_BIN_DIR/swift-ring-builder container.builder rebalance 42
+        $SWIFT_BIN_DIR/swift-ring-builder account.builder rebalance 42
     } && popd >/dev/null
 
     # Create cache dir
@@ -799,7 +803,7 @@
         # Apache should serve the "PACO" a.k.a "main" services
         restart_apache_server
         # The rest of the services should be started in backgroud
-        swift-init --run-dir=${SWIFT_DATA_DIR}/run rest start
+        $SWIFT_BIN_DIR/swift-init --run-dir=${SWIFT_DATA_DIR}/run rest start
         return 0
     fi
 
@@ -823,7 +827,7 @@
         done
 
         if [[ "$SWIFT_START_ALL_SERVICES" == "True" ]]; then
-            swift-init --run-dir=${SWIFT_DATA_DIR}/run rest start
+            $SWIFT_BIN_DIR/swift-init --run-dir=${SWIFT_DATA_DIR}/run rest start
         else
             # The container-sync daemon is strictly needed to pass the container
             # sync Tempest tests.
@@ -831,8 +835,8 @@
             run_process s-container-sync "$SWIFT_BIN_DIR/swift-container-sync ${SWIFT_CONF_DIR}/container-server/1.conf"
         fi
     else
-        swift-init --run-dir=${SWIFT_DATA_DIR}/run all restart || true
-        swift-init --run-dir=${SWIFT_DATA_DIR}/run proxy stop || true
+        $SWIFT_BIN_DIR/swift-init --run-dir=${SWIFT_DATA_DIR}/run all restart || true
+        $SWIFT_BIN_DIR/swift-init --run-dir=${SWIFT_DATA_DIR}/run proxy stop || true
     fi
 
     if is_service_enabled tls-proxy; then
@@ -859,12 +863,12 @@
     local type
 
     if [ "$SWIFT_USE_MOD_WSGI" == "True" ]; then
-        swift-init --run-dir=${SWIFT_DATA_DIR}/run rest stop && return 0
+        $SWIFT_BIN_DIR/swift-init --run-dir=${SWIFT_DATA_DIR}/run rest stop && return 0
     fi
 
     # screen normally killed by ``unstack.sh``
-    if type -p swift-init >/dev/null; then
-        swift-init --run-dir=${SWIFT_DATA_DIR}/run all stop || true
+    if type -p $SWIFT_BIN_DIR/swift-init >/dev/null; then
+        $SWIFT_BIN_DIR/swift-init --run-dir=${SWIFT_DATA_DIR}/run all stop || true
     fi
     # Dump all of the servers
     # Maintain the iteration as stop_process() has some desirable side-effects
diff --git a/lib/tempest b/lib/tempest
index 3b39dae..60f571c 100644
--- a/lib/tempest
+++ b/lib/tempest
@@ -299,6 +299,14 @@
         iniset $TEMPEST_CONFIG identity-feature-enabled domain_specific_drivers True
     fi
 
+    # TODO(felipemonteiro): Remove this once Tempest no longer supports Pike
+    # as this is supported in Queens and beyond.
+    iniset $TEMPEST_CONFIG identity-feature-enabled project_tags True
+
+    # In Queens and later, application credentials are enabled by default
+    # so remove this once Tempest no longer supports Pike.
+    iniset $TEMPEST_CONFIG identity-feature-enabled application_credentials True
+
     # Image
     # We want to be able to override this variable in the gate to avoid
     # doing an external HTTP fetch for this test.
@@ -425,6 +433,13 @@
     iniset $TEMPEST_CONFIG validation network_for_ssh $TEMPEST_SSH_NETWORK_NAME
 
     # Volume
+    # Set the service catalog entry for Tempest to run on. Typically
+    # used to try different Volume API version targets. The tempest
+    # default it to 'volumev3'(v3 APIs endpoint) , so only set this
+    # if you want to change it.
+    if [[ -n "$TEMPEST_VOLUME_TYPE" ]]; then
+        iniset $TEMPEST_CONFIG volume catalog_type $TEMPEST_VOLUME_TYPE
+    fi
     # Only turn on TEMPEST_VOLUME_MANAGE_SNAPSHOT by default for "lvm" backends
     if [[ "$CINDER_ENABLED_BACKENDS" == *"lvm"* ]]; then
         TEMPEST_VOLUME_MANAGE_SNAPSHOT=${TEMPEST_VOLUME_MANAGE_SNAPSHOT:-True}
@@ -446,6 +461,12 @@
     iniset $TEMPEST_CONFIG volume-feature-enabled api_v1 $(trueorfalse False TEMPEST_VOLUME_API_V1)
     local tempest_volume_min_microversion=${TEMPEST_VOLUME_MIN_MICROVERSION:-None}
     local tempest_volume_max_microversion=${TEMPEST_VOLUME_MAX_MICROVERSION:-"latest"}
+    # Reset microversions to None where v2 is running which does not support microversion.
+    # Both "None" means no microversion testing.
+    if [[ "$TEMPEST_VOLUME_TYPE" == "volumev2" ]]; then
+        tempest_volume_min_microversion=None
+        tempest_volume_max_microversion=None
+    fi
     if [ "$tempest_volume_min_microversion" == "None" ]; then
         inicomment $TEMPEST_CONFIG volume min_microversion
     else
@@ -554,7 +575,10 @@
     if [[ "$OFFLINE" != "True" ]]; then
         tox -revenv-tempest --notest
     fi
-    tox -evenv-tempest -- pip install -c $REQUIREMENTS_DIR/upper-constraints.txt -r requirements.txt
+
+    # The requirements might be on a different branch, while tempest needs master requirements.
+    (cd $REQUIREMENTS_DIR && git show master:upper-constraints.txt) > u-c-m.txt
+    tox -evenv-tempest -- pip install -c u-c-m.txt -r requirements.txt
 
     # Auth:
     iniset $TEMPEST_CONFIG auth tempest_roles "Member"
@@ -639,7 +663,7 @@
 function install_tempest_plugins {
     pushd $TEMPEST_DIR
     if [[ $TEMPEST_PLUGINS != 0 ]] ; then
-        tox -evenv-tempest -- pip install $TEMPEST_PLUGINS
+        tox -evenv-tempest -- pip install -c $REQUIREMENTS_DIR/upper-constraints.txt $TEMPEST_PLUGINS
         echo "Checking installed Tempest plugins:"
         tox -evenv-tempest -- tempest list-plugins
     fi
diff --git a/openrc b/openrc
index 37724c5..99d3351 100644
--- a/openrc
+++ b/openrc
@@ -29,6 +29,7 @@
 # Load the last env variables if available
 if [[ -r $RC_DIR/.stackenv ]]; then
     source $RC_DIR/.stackenv
+    export OS_CACERT
 fi
 
 # Get some necessary configuration
@@ -108,5 +109,5 @@
 
 # Currently cinderclient needs you to specify the *volume api* version. This
 # needs to match the config of your catalog returned by Keystone.
-export CINDER_VERSION=${CINDER_VERSION:-2}
+export CINDER_VERSION=${CINDER_VERSION:-3}
 export OS_VOLUME_API_VERSION=${OS_VOLUME_API_VERSION:-$CINDER_VERSION}
diff --git a/pkg/elasticsearch.sh b/pkg/elasticsearch.sh
index fefd454..afbf11d 100755
--- a/pkg/elasticsearch.sh
+++ b/pkg/elasticsearch.sh
@@ -37,7 +37,7 @@
 function download_elasticsearch {
     if is_ubuntu; then
         wget_elasticsearch elasticsearch-${ELASTICSEARCH_VERSION}.deb
-    elif is_fedora; then
+    elif is_fedora || is_suse; then
         wget_elasticsearch elasticsearch-${ELASTICSEARCH_VERSION}.noarch.rpm
     fi
 }
@@ -61,6 +61,9 @@
     elif is_fedora; then
         sudo /bin/systemctl start elasticsearch.service
         _check_elasticsearch_ready
+    elif is_suse; then
+        sudo /usr/bin/systemctl start elasticsearch.service
+        _check_elasticsearch_ready
     else
         echo "Unsupported architecture...can not start elasticsearch."
     fi
@@ -71,6 +74,8 @@
         sudo /etc/init.d/elasticsearch stop
     elif is_fedora; then
         sudo /bin/systemctl stop elasticsearch.service
+    elif is_suse ; then
+        sudo /usr/bin/systemctl stop elasticsearch.service
     else
         echo "Unsupported architecture...can not stop elasticsearch."
     fi
@@ -92,6 +97,11 @@
         yum_install ${FILES}/elasticsearch-${ELASTICSEARCH_VERSION}.noarch.rpm
         sudo /bin/systemctl daemon-reload
         sudo /bin/systemctl enable elasticsearch.service
+    elif is_suse; then
+        is_package_installed java-1_8_0-openjdk-headless || install_package java-1_8_0-openjdk-headless
+        zypper_install --no-gpg-checks ${FILES}/elasticsearch-${ELASTICSEARCH_VERSION}.noarch.rpm
+        sudo /usr/bin/systemctl daemon-reload
+        sudo /usr/bin/systemctl enable elasticsearch.service
     else
         echo "Unsupported install of elasticsearch on this architecture."
     fi
@@ -103,6 +113,8 @@
             sudo apt-get purge elasticsearch
         elif is_fedora; then
             sudo yum remove elasticsearch
+        elif is_suse; then
+            sudo zypper rm elasticsearch
         else
             echo "Unsupported install of elasticsearch on this architecture."
         fi
diff --git a/playbooks/devstack.yaml b/playbooks/devstack.yaml
index ede8382..d090638 100644
--- a/playbooks/devstack.yaml
+++ b/playbooks/devstack.yaml
@@ -1,3 +1,7 @@
 - hosts: all
+  # This is the default strategy, however since orchestrate-devstack requires
+  # "linear", it is safer to enforce it in case this is running in an
+  # environment configured with a different default strategy.
+  strategy: linear
   roles:
-    - run-devstack
+    - orchestrate-devstack
diff --git a/playbooks/pre.yaml b/playbooks/pre.yaml
index 6681fb2..4689a63 100644
--- a/playbooks/pre.yaml
+++ b/playbooks/pre.yaml
@@ -1,15 +1,25 @@
-- hosts: controller
-  roles:
-    - role: test-matrix
-      test_matrix_role: primary 
-
-- hosts: subnode
-  roles:
-    - role: test-matrix
-      test_matrix_role: subnode
-
 - hosts: all
+  pre_tasks:
+    - name: Gather minimum local MTU
+      set_fact:
+        local_mtu: >
+          {% set mtus = [] -%}
+          {% for interface in ansible_interfaces -%}
+            {% set interface_variable = 'ansible_' + interface -%}
+            {% if interface_variable in hostvars[inventory_hostname] -%}
+              {% set _ = mtus.append(hostvars[inventory_hostname][interface_variable]['mtu']|int) -%}
+            {% endif -%}
+          {% endfor -%}
+          {{- mtus|min -}}
+    - name: Calculate external_bridge_mtu
+      # 50 bytes is overhead for vxlan (which is greater than GRE
+      # allowing us to use either overlay option with this MTU.
+      # TODO(andreaf) This should work, but it may have to be reconcilied with
+      # the MTU setting used by the multinode setup roles in multinode pre.yaml
+      set_fact:
+        external_bridge_mtu: "{{ local_mtu | int - 50 }}"
   roles:
+    - test-matrix
     - configure-swap
     - setup-stack-user
     - setup-tempest-user
diff --git a/roles/orchestrate-devstack/README.rst b/roles/orchestrate-devstack/README.rst
new file mode 100644
index 0000000..097dcea
--- /dev/null
+++ b/roles/orchestrate-devstack/README.rst
@@ -0,0 +1,25 @@
+Orchestrate a devstack
+
+Runs devstack in a multinode scenario, with one controller node
+and a group of subnodes.
+
+The reason for this role is so that jobs in other repository may
+run devstack in their plays with no need for re-implementing the
+orchestration logic.
+
+The "run-devstack" role is available to run devstack with no
+orchestration.
+
+This role sets up the controller and CA first, it then pushes CA
+data to sub-nodes and run devstack there. The only requirement for
+this role is for the controller inventory_hostname to be "controller"
+and for all sub-nodes to be defined in a group called "subnode".
+
+This role needs to be invoked from a playbook that uses a "linear" strategy.
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_base_dir
+   :default: /opt/stack
+
+   The devstack base directory.
diff --git a/roles/orchestrate-devstack/defaults/main.yaml b/roles/orchestrate-devstack/defaults/main.yaml
new file mode 100644
index 0000000..fea05c8
--- /dev/null
+++ b/roles/orchestrate-devstack/defaults/main.yaml
@@ -0,0 +1 @@
+devstack_base_dir: /opt/stack
diff --git a/roles/orchestrate-devstack/tasks/main.yaml b/roles/orchestrate-devstack/tasks/main.yaml
new file mode 100644
index 0000000..f747943
--- /dev/null
+++ b/roles/orchestrate-devstack/tasks/main.yaml
@@ -0,0 +1,44 @@
+- name: Run devstack on the controller
+  include_role:
+    name: run-devstack
+  when: inventory_hostname == 'controller'
+
+- name: Setup devstack on sub-nodes
+  block:
+
+  - name: Distribute the build sshkey for the user "stack"
+    include_role:
+      name: copy-build-sshkey
+    vars:
+      copy_sshkey_target_user: 'stack'
+
+  - name: Sync CA data to subnodes (when any)
+    # Only do this if the tls-proxy service is defined and enabled
+    include_role:
+      name: sync-devstack-data
+    when: devstack_services['tls-proxy']|default(false)
+
+  - name: Run devstack on the sub-nodes
+    include_role:
+      name: run-devstack
+    when: inventory_hostname in groups['subnode']
+
+  - name: Discover hosts
+    # Discovers compute nodes (subnodes) and maps them to cells. Only run
+    # on the controller node.
+    # NOTE(mriedem): We want to remove this if/when nova supports
+    # auto-registration of computes with cells, but that's not happening in
+    # Ocata.
+    # NOTE(andreaf) This is taken (NOTE included) from the discover_hosts
+    # function in devstack gate. Since this is now in devstack, which is
+    # branched, we know that the discover_hosts tool exists.
+    become: true
+    become_user: stack
+    shell: ./tools/discover_hosts.sh
+    args:
+      chdir: "{{ devstack_base_dir }}/devstack"
+    when: inventory_hostname == 'controller'
+
+  when:
+    - '"controller" in hostvars'
+    - '"subnode" in groups'
diff --git a/roles/run-devstack/tasks/main.yaml b/roles/run-devstack/tasks/main.yaml
index 1ff8295..f58b31d 100644
--- a/roles/run-devstack/tasks/main.yaml
+++ b/roles/run-devstack/tasks/main.yaml
@@ -2,7 +2,9 @@
   shell:
     cmd: |
       ./stack.sh 2>&1
+      rc=$?
       echo "*** FINISHED ***"
+      exit $rc
   args:
     chdir: "{{devstack_base_dir}}/devstack"
   become: true
diff --git a/roles/sync-devstack-data/README.rst b/roles/sync-devstack-data/README.rst
new file mode 100644
index 0000000..500e8cc
--- /dev/null
+++ b/roles/sync-devstack-data/README.rst
@@ -0,0 +1,12 @@
+Sync devstack data for multinode configurations
+
+Sync any data files which include certificates to be used if TLS is enabled.
+This role must be executed on the controller and it pushes data to all
+subnodes.
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_base_dir
+   :default: /opt/stack
+
+   The devstack base directory.
diff --git a/roles/sync-devstack-data/defaults/main.yaml b/roles/sync-devstack-data/defaults/main.yaml
new file mode 100644
index 0000000..fea05c8
--- /dev/null
+++ b/roles/sync-devstack-data/defaults/main.yaml
@@ -0,0 +1 @@
+devstack_base_dir: /opt/stack
diff --git a/roles/sync-devstack-data/tasks/main.yaml b/roles/sync-devstack-data/tasks/main.yaml
new file mode 100644
index 0000000..4600015
--- /dev/null
+++ b/roles/sync-devstack-data/tasks/main.yaml
@@ -0,0 +1,48 @@
+- name: Ensure the data folder exists
+  become: true
+  file:
+    path: "{{ devstack_base_dir }}/data"
+    state: directory
+    owner: stack
+    group: stack
+    mode: 0755
+  when: 'inventory_hostname in groups["subnode"]|default([])'
+
+- name: Ensure the CA folder exists
+  become: true
+  file:
+    path: "{{ devstack_base_dir }}/data/CA"
+    state: directory
+    owner: stack
+    group: stack
+    mode: 0755
+  when: 'inventory_hostname in groups["subnode"]|default([])'
+
+- name: Pull the CA certificate and folder
+  become: true
+  synchronize:
+    src: "{{ item }}"
+    dest: "{{ zuul.executor.work_root }}/{{ item | basename }}"
+    mode: pull
+  with_items:
+    - "{{ devstack_base_dir }}/data/ca-bundle.pem"
+    - "{{ devstack_base_dir }}/data/CA"
+  when: inventory_hostname == 'controller'
+
+- name: Push the CA certificate
+  become: true
+  become_user: stack
+  synchronize:
+    src: "{{ zuul.executor.work_root }}/ca-bundle.pem"
+    dest: "{{ devstack_base_dir }}/data/ca-bundle.pem"
+    mode: push
+  when: 'inventory_hostname in groups["subnode"]|default([])'
+
+- name: Push the CA folder
+  become: true
+  become_user: stack
+  synchronize:
+    src: "{{ zuul.executor.work_root }}/CA/"
+    dest: "{{ devstack_base_dir }}/data/"
+    mode: push
+  when: 'inventory_hostname in groups["subnode"]|default([])'
diff --git a/roles/write-devstack-local-conf/README.rst b/roles/write-devstack-local-conf/README.rst
index 73f9f0d..e9739cd 100644
--- a/roles/write-devstack-local-conf/README.rst
+++ b/roles/write-devstack-local-conf/README.rst
@@ -20,6 +20,15 @@
    bash shell variables, and will be ordered so that variables used by
    later entries appear first.
 
+   As a special case, the variable ``LIBS_FROM_GIT`` will be
+   constructed automatically from the projects which appear in the
+   ``required-projects`` list defined by the job plus the project of
+   the change under test.  To instruct devstack to install a library
+   from source rather than pypi, simply add that library to the job's
+   ``required-projects`` list.  To override the
+   automatically-generated value, set ``LIBS_FROM_GIT`` in
+   ``devstack_localrc`` to the desired value.
+
 .. zuul:rolevar:: devstack_local_conf
    :type: dict
 
@@ -75,3 +84,7 @@
    A dictionary mapping a plugin name to a git repo location.  If the
    location is a non-empty string, then an ``enable_plugin`` line will
    be emmitted for the plugin name.
+
+   If a plugin declares a dependency on another plugin (via
+   ``plugin_requires`` in the plugin's settings file), this role will
+   automatically emit ``enable_plugin`` lines in the correct order.
diff --git a/roles/write-devstack-local-conf/library/devstack_local_conf.py b/roles/write-devstack-local-conf/library/devstack_local_conf.py
index 55ba4af..bba7e31 100644
--- a/roles/write-devstack-local-conf/library/devstack_local_conf.py
+++ b/roles/write-devstack-local-conf/library/devstack_local_conf.py
@@ -14,16 +14,69 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import os
 import re
 
 
-class VarGraph(object):
+class DependencyGraph(object):
     # This is based on the JobGraph from Zuul.
 
+    def __init__(self):
+        self._names = set()
+        self._dependencies = {}  # dependent_name -> set(parent_names)
+
+    def add(self, name, dependencies):
+        # Append the dependency information
+        self._dependencies.setdefault(name, set())
+        try:
+            for dependency in dependencies:
+                # Make sure a circular dependency is never created
+                ancestors = self._getParentNamesRecursively(
+                    dependency, soft=True)
+                ancestors.add(dependency)
+                if name in ancestors:
+                    raise Exception("Dependency cycle detected in {}".
+                                    format(name))
+                self._dependencies[name].add(dependency)
+        except Exception:
+            del self._dependencies[name]
+            raise
+
+    def getDependenciesRecursively(self, parent):
+        dependencies = []
+
+        current_dependencies = self._dependencies[parent]
+        for current in current_dependencies:
+            if current not in dependencies:
+                dependencies.append(current)
+            for dep in self.getDependenciesRecursively(current):
+                if dep not in dependencies:
+                    dependencies.append(dep)
+        return dependencies
+
+    def _getParentNamesRecursively(self, dependent, soft=False):
+        all_parent_items = set()
+        items_to_iterate = set([dependent])
+        while len(items_to_iterate) > 0:
+            current_item = items_to_iterate.pop()
+            current_parent_items = self._dependencies.get(current_item)
+            if current_parent_items is None:
+                if soft:
+                    current_parent_items = set()
+                else:
+                    raise Exception("Dependent item {} not found: ".format(
+                                    dependent))
+            new_parent_items = current_parent_items - all_parent_items
+            items_to_iterate |= new_parent_items
+            all_parent_items |= new_parent_items
+        return all_parent_items
+
+
+class VarGraph(DependencyGraph):
     def __init__(self, vars):
+        super(VarGraph, self).__init__()
         self.vars = {}
         self._varnames = set()
-        self._dependencies = {}  # dependent_var_name -> set(parent_var_names)
         for k, v in vars.items():
             self._varnames.add(k)
         for k, v in vars.items():
@@ -38,28 +91,21 @@
             raise Exception("Variable {} already added".format(key))
         self.vars[key] = value
         # Append the dependency information
-        self._dependencies.setdefault(key, set())
+        dependencies = set()
+        for dependency in self.getDependencies(value):
+            if dependency == key:
+                # A variable is allowed to reference itself; no
+                # dependency link needed in that case.
+                continue
+            if dependency not in self._varnames:
+                # It's not necessary to create a link for an
+                # external variable.
+                continue
+            dependencies.add(dependency)
         try:
-            for dependency in self.getDependencies(value):
-                if dependency == key:
-                    # A variable is allowed to reference itself; no
-                    # dependency link needed in that case.
-                    continue
-                if dependency not in self._varnames:
-                    # It's not necessary to create a link for an
-                    # external variable.
-                    continue
-                # Make sure a circular dependency is never created
-                ancestor_vars = self._getParentVarNamesRecursively(
-                    dependency, soft=True)
-                ancestor_vars.add(dependency)
-                if any((key == anc_var) for anc_var in ancestor_vars):
-                    raise Exception("Dependency cycle detected in var {}".
-                                    format(key))
-                self._dependencies[key].add(dependency)
+            self.add(key, dependencies)
         except Exception:
             del self.vars[key]
-            del self._dependencies[key]
             raise
 
     def getVars(self):
@@ -67,59 +113,118 @@
         keys = sorted(self.vars.keys())
         seen = set()
         for key in keys:
-            dependencies = self.getDependentVarsRecursively(key)
+            dependencies = self.getDependenciesRecursively(key)
             for var in dependencies + [key]:
                 if var not in seen:
                     ret.append((var, self.vars[var]))
                     seen.add(var)
         return ret
 
-    def getDependentVarsRecursively(self, parent_var):
-        dependent_vars = []
 
-        current_dependent_vars = self._dependencies[parent_var]
-        for current_var in current_dependent_vars:
-            if current_var not in dependent_vars:
-                dependent_vars.append(current_var)
-            for dep in self.getDependentVarsRecursively(current_var):
-                if dep not in dependent_vars:
-                    dependent_vars.append(dep)
-        return dependent_vars
+class PluginGraph(DependencyGraph):
+    def __init__(self, base_dir, plugins):
+        super(PluginGraph, self).__init__()
+        # The dependency trees expressed by all the plugins we found
+        # (which may be more than those the job is using).
+        self._plugin_dependencies = {}
+        self.loadPluginNames(base_dir)
 
-    def _getParentVarNamesRecursively(self, dependent_var, soft=False):
-        all_parent_vars = set()
-        vars_to_iterate = set([dependent_var])
-        while len(vars_to_iterate) > 0:
-            current_var = vars_to_iterate.pop()
-            current_parent_vars = self._dependencies.get(current_var)
-            if current_parent_vars is None:
-                if soft:
-                    current_parent_vars = set()
-                else:
-                    raise Exception("Dependent var {} not found: ".format(
-                                    dependent_var))
-            new_parent_vars = current_parent_vars - all_parent_vars
-            vars_to_iterate |= new_parent_vars
-            all_parent_vars |= new_parent_vars
-        return all_parent_vars
+        self.plugins = {}
+        self._pluginnames = set()
+        for k, v in plugins.items():
+            self._pluginnames.add(k)
+        for k, v in plugins.items():
+            self._addPlugin(k, str(v))
+
+    def loadPluginNames(self, base_dir):
+        if base_dir is None:
+            return
+        git_roots = []
+        for root, dirs, files in os.walk(base_dir):
+            if '.git' not in dirs:
+                continue
+            # Don't go deeper than git roots
+            dirs[:] = []
+            git_roots.append(root)
+        for root in git_roots:
+            devstack = os.path.join(root, 'devstack')
+            if not (os.path.exists(devstack) and os.path.isdir(devstack)):
+                continue
+            settings = os.path.join(devstack, 'settings')
+            if not (os.path.exists(settings) and os.path.isfile(settings)):
+                continue
+            self.loadDevstackPluginInfo(settings)
+
+    define_re = re.compile(r'^define_plugin\s+(\w+).*')
+    require_re = re.compile(r'^plugin_requires\s+(\w+)\s+(\w+).*')
+    def loadDevstackPluginInfo(self, fn):
+        name = None
+        reqs = set()
+        with open(fn) as f:
+            for line in f:
+                m = self.define_re.match(line)
+                if m:
+                    name = m.group(1)
+                m = self.require_re.match(line)
+                if m:
+                    if name == m.group(1):
+                        reqs.add(m.group(2))
+        if name and reqs:
+            self._plugin_dependencies[name] = reqs
+
+    def getDependencies(self, value):
+        return self._plugin_dependencies.get(value, [])
+
+    def _addPlugin(self, key, value):
+        if key in self.plugins:
+            raise Exception("Plugin {} already added".format(key))
+        self.plugins[key] = value
+        # Append the dependency information
+        dependencies = set()
+        for dependency in self.getDependencies(key):
+            if dependency == key:
+                continue
+            dependencies.add(dependency)
+        try:
+            self.add(key, dependencies)
+        except Exception:
+            del self.plugins[key]
+            raise
+
+    def getPlugins(self):
+        ret = []
+        keys = sorted(self.plugins.keys())
+        seen = set()
+        for key in keys:
+            dependencies = self.getDependenciesRecursively(key)
+            for plugin in dependencies + [key]:
+                if plugin not in seen:
+                    ret.append((plugin, self.plugins[plugin]))
+                    seen.add(plugin)
+        return ret
 
 
 class LocalConf(object):
 
-    def __init__(self, localrc, localconf, base_services, services, plugins):
+    def __init__(self, localrc, localconf, base_services, services, plugins,
+                 base_dir, projects, project):
         self.localrc = []
         self.meta_sections = {}
+        self.plugin_deps = {}
+        self.base_dir = base_dir
+        self.projects = projects
+        self.project = project
         if plugins:
             self.handle_plugins(plugins)
         if services or base_services:
             self.handle_services(base_services, services or {})
-        if localrc:
-            self.handle_localrc(localrc)
+        self.handle_localrc(localrc)
         if localconf:
             self.handle_localconf(localconf)
 
     def handle_plugins(self, plugins):
-        for k, v in plugins.items():
+        pg = PluginGraph(self.base_dir, plugins)
+        for k, v in pg.getPlugins():
             if v:
                 self.localrc.append('enable_plugin {} {}'.format(k, v))
 
@@ -137,9 +242,26 @@
                 self.localrc.append('enable_service {}'.format(k))
 
     def handle_localrc(self, localrc):
-        vg = VarGraph(localrc)
-        for k, v in vg.getVars():
-            self.localrc.append('{}={}'.format(k, v))
+        lfg = False
+        if localrc:
+            vg = VarGraph(localrc)
+            for k, v in vg.getVars():
+                self.localrc.append('{}={}'.format(k, v))
+                if k == 'LIBS_FROM_GIT':
+                    lfg = True
+
+        if not lfg and (self.projects or self.project):
+            required_projects = []
+            if self.projects:
+                for project_name, project_info in self.projects.items():
+                    if project_info.get('required'):
+                        required_projects.append(project_info['short_name'])
+            if self.project:
+                if self.project['short_name'] not in required_projects:
+                    required_projects.append(self.project['short_name'])
+            if required_projects:
+                self.localrc.append('LIBS_FROM_GIT={}'.format(
+                    ','.join(required_projects)))
 
     def handle_localconf(self, localconf):
         for phase, phase_data in localconf.items():
@@ -171,7 +293,10 @@
             services=dict(type='dict'),
             localrc=dict(type='dict'),
             local_conf=dict(type='dict'),
+            base_dir=dict(type='path'),
             path=dict(type='str'),
+            projects=dict(type='dict'),
+            project=dict(type='dict'),
         )
     )
 
@@ -180,14 +305,20 @@
                    p.get('local_conf'),
                    p.get('base_services'),
                    p.get('services'),
-                   p.get('plugins'))
+                   p.get('plugins'),
+                   p.get('base_dir'),
+                   p.get('projects'),
+                   p.get('project'))
     lc.write(p['path'])
 
     module.exit_json()
 
 
-from ansible.module_utils.basic import *  # noqa
-from ansible.module_utils.basic import AnsibleModule
+try:
+    from ansible.module_utils.basic import *  # noqa
+    from ansible.module_utils.basic import AnsibleModule
+except ImportError:
+    pass
 
 if __name__ == '__main__':
     main()
diff --git a/roles/write-devstack-local-conf/library/test.py b/roles/write-devstack-local-conf/library/test.py
new file mode 100644
index 0000000..791552d
--- /dev/null
+++ b/roles/write-devstack-local-conf/library/test.py
@@ -0,0 +1,235 @@
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+#
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import shutil
+import tempfile
+import unittest
+
+from devstack_local_conf import LocalConf
+from collections import OrderedDict
+
+class TestDevstackLocalConf(unittest.TestCase):
+    def setUp(self):
+        self.tmpdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.tmpdir)
+
+    def test_plugins(self):
+        "Test that plugins without dependencies work"
+        localrc = {'test_localrc': '1'}
+        local_conf = {'install':
+                      {'nova.conf':
+                       {'main':
+                        {'test_conf': '2'}}}}
+        services = {'cinder': True}
+        # We use ordereddict here to make sure the plugins are in the
+        # *wrong* order for testing.
+        plugins = OrderedDict([
+            ('bar', 'git://git.openstack.org/openstack/bar-plugin'),
+            ('foo', 'git://git.openstack.org/openstack/foo-plugin'),
+            ('baz', 'git://git.openstack.org/openstack/baz-plugin'),
+            ])
+        p = dict(localrc=localrc,
+                 local_conf=local_conf,
+                 base_services=[],
+                 services=services,
+                 plugins=plugins,
+                 base_dir='./test',
+                 path=os.path.join(self.tmpdir, 'test.local.conf'))
+        lc = LocalConf(p.get('localrc'),
+                       p.get('local_conf'),
+                       p.get('base_services'),
+                       p.get('services'),
+                       p.get('plugins'),
+                       p.get('base_dir'),
+                       p.get('projects'),
+                       p.get('project'))
+        lc.write(p['path'])
+
+        plugins = []
+        with open(p['path']) as f:
+            for line in f:
+                if line.startswith('enable_plugin'):
+                    plugins.append(line.split()[1])
+        self.assertEqual(['bar', 'baz', 'foo'], plugins)
+
+
+    def test_plugin_deps(self):
+        "Test that plugins with dependencies work"
+        os.makedirs(os.path.join(self.tmpdir, 'foo-plugin', 'devstack'))
+        os.makedirs(os.path.join(self.tmpdir, 'foo-plugin', '.git'))
+        os.makedirs(os.path.join(self.tmpdir, 'bar-plugin', 'devstack'))
+        os.makedirs(os.path.join(self.tmpdir, 'bar-plugin', '.git'))
+        with open(os.path.join(
+                self.tmpdir,
+                'foo-plugin', 'devstack', 'settings'), 'w') as f:
+            f.write('define_plugin foo\n')
+        with open(os.path.join(
+                self.tmpdir,
+                'bar-plugin', 'devstack', 'settings'), 'w') as f:
+            f.write('define_plugin bar\n')
+            f.write('plugin_requires bar foo\n')
+
+        localrc = {'test_localrc': '1'}
+        local_conf = {'install':
+                      {'nova.conf':
+                       {'main':
+                        {'test_conf': '2'}}}}
+        services = {'cinder': True}
+        # We use ordereddict here to make sure the plugins are in the
+        # *wrong* order for testing.
+        plugins = OrderedDict([
+            ('bar', 'git://git.openstack.org/openstack/bar-plugin'),
+            ('foo', 'git://git.openstack.org/openstack/foo-plugin'),
+            ])
+        p = dict(localrc=localrc,
+                 local_conf=local_conf,
+                 base_services=[],
+                 services=services,
+                 plugins=plugins,
+                 base_dir=self.tmpdir,
+                 path=os.path.join(self.tmpdir, 'test.local.conf'))
+
+    def test_libs_from_git(self):
+        "Test that LIBS_FROM_GIT is auto-generated"
+        projects = {
+            'git.openstack.org/openstack/nova': {
+                'required': True,
+                'short_name': 'nova',
+            },
+            'git.openstack.org/openstack/oslo.messaging': {
+                'required': True,
+                'short_name': 'oslo.messaging',
+            },
+            'git.openstack.org/openstack/devstack-plugin': {
+                'required': False,
+                'short_name': 'devstack-plugin',
+            },
+        }
+        project = {
+            'short_name': 'glance',
+        }
+        p = dict(base_services=[],
+                 base_dir='./test',
+                 path=os.path.join(self.tmpdir, 'test.local.conf'),
+                 projects=projects,
+                 project=project)
+        lc = LocalConf(p.get('localrc'),
+                       p.get('local_conf'),
+                       p.get('base_services'),
+                       p.get('services'),
+                       p.get('plugins'),
+                       p.get('base_dir'),
+                       p.get('projects'),
+                       p.get('project'))
+        lc.write(p['path'])
+
+        lfg = None
+        with open(p['path']) as f:
+            for line in f:
+                if line.startswith('LIBS_FROM_GIT'):
+                    lfg = line.strip().split('=')[1]
+        self.assertEqual('nova,oslo.messaging,glance', lfg)
+
+    def test_overridelibs_from_git(self):
+        "Test that LIBS_FROM_GIT can be overridden"
+        localrc = {'LIBS_FROM_GIT': 'oslo.db'}
+        projects = {
+            'git.openstack.org/openstack/nova': {
+                'required': True,
+                'short_name': 'nova',
+            },
+            'git.openstack.org/openstack/oslo.messaging': {
+                'required': True,
+                'short_name': 'oslo.messaging',
+            },
+            'git.openstack.org/openstack/devstack-plugin': {
+                'required': False,
+                'short_name': 'devstack-plugin',
+            },
+        }
+        p = dict(localrc=localrc,
+                 base_services=[],
+                 base_dir='./test',
+                 path=os.path.join(self.tmpdir, 'test.local.conf'),
+                 projects=projects)
+        lc = LocalConf(p.get('localrc'),
+                       p.get('local_conf'),
+                       p.get('base_services'),
+                       p.get('services'),
+                       p.get('plugins'),
+                       p.get('base_dir'),
+                       p.get('projects'),
+                       p.get('project'))
+        lc.write(p['path'])
+
+        lfg = None
+        with open(p['path']) as f:
+            for line in f:
+                if line.startswith('LIBS_FROM_GIT'):
+                    lfg = line.strip().split('=')[1]
+        self.assertEqual('oslo.db', lfg)
+
+    def test_plugin_circular_deps(self):
+        "Test that plugins with circular dependencies fail"
+        os.makedirs(os.path.join(self.tmpdir, 'foo-plugin', 'devstack'))
+        os.makedirs(os.path.join(self.tmpdir, 'foo-plugin', '.git'))
+        os.makedirs(os.path.join(self.tmpdir, 'bar-plugin', 'devstack'))
+        os.makedirs(os.path.join(self.tmpdir, 'bar-plugin', '.git'))
+        with open(os.path.join(
+                self.tmpdir,
+                'foo-plugin', 'devstack', 'settings'), 'w') as f:
+            f.write('define_plugin foo\n')
+            f.write('plugin_requires foo bar\n')
+        with open(os.path.join(
+                self.tmpdir,
+                'bar-plugin', 'devstack', 'settings'), 'w') as f:
+            f.write('define_plugin bar\n')
+            f.write('plugin_requires bar foo\n')
+
+        localrc = {'test_localrc': '1'}
+        local_conf = {'install':
+                      {'nova.conf':
+                       {'main':
+                        {'test_conf': '2'}}}}
+        services = {'cinder': True}
+        # We use ordereddict here to make sure the plugins are in the
+        # *wrong* order for testing.
+        plugins = OrderedDict([
+            ('bar', 'git://git.openstack.org/openstack/bar-plugin'),
+            ('foo', 'git://git.openstack.org/openstack/foo-plugin'),
+            ])
+        p = dict(localrc=localrc,
+                 local_conf=local_conf,
+                 base_services=[],
+                 services=services,
+                 plugins=plugins,
+                 base_dir=self.tmpdir,
+                 path=os.path.join(self.tmpdir, 'test.local.conf'))
+        with self.assertRaises(Exception):
+            lc = LocalConf(p.get('localrc'),
+                           p.get('local_conf'),
+                           p.get('base_services'),
+                           p.get('services'),
+                           p.get('plugins'),
+                           p.get('base_dir'))
+            lc.write(p['path'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/roles/write-devstack-local-conf/tasks/main.yaml b/roles/write-devstack-local-conf/tasks/main.yaml
index cc21426..9a6b083 100644
--- a/roles/write-devstack-local-conf/tasks/main.yaml
+++ b/roles/write-devstack-local-conf/tasks/main.yaml
@@ -8,3 +8,6 @@
     services: "{{ devstack_services|default(omit) }}"
     localrc: "{{ devstack_localrc|default(omit) }}"
     local_conf: "{{ devstack_local_conf|default(omit) }}"
+    base_dir: "{{ devstack_base_dir|default(omit) }}"
+    projects: "{{ zuul.projects }}"
+    project: "{{ zuul.project }}"
\ No newline at end of file
diff --git a/stack.sh b/stack.sh
index ba546c0..56e00bf 100755
--- a/stack.sh
+++ b/stack.sh
@@ -221,7 +221,7 @@
 
 # Warn users who aren't on an explicitly supported distro, but allow them to
 # override check and attempt installation with ``FORCE=yes ./stack``
-if [[ ! ${DISTRO} =~ (xenial|zesty|artful|stretch|jessie|f25|f26|f27|opensuse-42.2|opensuse-42.3|rhel7) ]]; then
+if [[ ! ${DISTRO} =~ (xenial|artful|bionic|stretch|jessie|f27|f28|opensuse-42.3|opensuse-tumbleweed|rhel7) ]]; then
     echo "WARNING: this script has not been tested on $DISTRO"
     if [[ "$FORCE" != "yes" ]]; then
         die $LINENO "If you wish to run this script anyway run with FORCE=yes"
@@ -778,6 +778,7 @@
 
 # Do the ugly hacks for broken packages and distros
 source $TOP_DIR/tools/fixup_stuff.sh
+fixup_all
 
 if [[ "$USE_SYSTEMD" == "True" ]]; then
     pip_install_gr systemd-python
@@ -893,6 +894,8 @@
     stack_install_service neutron
 fi
 
+# Nova configuration is used by placement so we need to create nova.conf
+# first.
 if is_service_enabled nova; then
     # Compute service
     stack_install_service nova
@@ -1183,6 +1186,13 @@
     init_cinder
 fi
 
+# Placement Service
+# ---------------
+
+if is_service_enabled placement; then
+    echo_summary "Configuring placement"
+    init_placement
+fi
 
 # Compute Service
 # ---------------
@@ -1201,11 +1211,6 @@
     init_nova_cells
 fi
 
-if is_service_enabled placement; then
-    echo_summary "Configuring placement"
-    init_placement
-fi
-
 
 # Extras Configuration
 # ====================
@@ -1397,11 +1402,6 @@
 # Check the status of running services
 service_check
 
-# ensure that all the libraries we think we installed from git,
-# actually were.
-check_libs_from_git
-
-
 # Configure nova cellsv2
 # ----------------------
 
diff --git a/stackrc b/stackrc
index 0b49fbc..2088bf4 100644
--- a/stackrc
+++ b/stackrc
@@ -133,7 +133,7 @@
 # base name of the directory from which they are installed. See
 # enable_python3_package to edit this variable and use_python3_for to
 # test membership.
-export ENABLED_PYTHON3_PACKAGES="nova,glance,cinder,uwsgi,python-openstackclient,python-openstacksdk"
+export ENABLED_PYTHON3_PACKAGES="nova,glance,cinder,uwsgi,python-openstackclient,openstacksdk"
 
 # Explicitly list services not to run under Python 3. See
 # disable_python3_package to edit this variable.
@@ -525,6 +525,10 @@
 GITBRANCH["ceilometermiddleware"]=${CEILOMETERMIDDLEWARE_BRANCH:-$TARGET_BRANCH}
 GITDIR["ceilometermiddleware"]=$DEST/ceilometermiddleware
 
+# openstacksdk OpenStack Python SDK
+GITREPO["openstacksdk"]=${OPENSTACKSDK_REPO:-${GIT_BASE}/openstack/openstacksdk.git}
+GITBRANCH["openstacksdk"]=${OPENSTACKSDK_BRANCH:-$TARGET_BRANCH}
+
 # os-brick library to manage local volume attaches
 GITREPO["os-brick"]=${OS_BRICK_REPO:-${GIT_BASE}/openstack/os-brick.git}
 GITBRANCH["os-brick"]=${OS_BRICK_BRANCH:-$TARGET_BRANCH}
@@ -542,10 +546,6 @@
 GITREPO["osc-lib"]=${OSC_LIB_REPO:-${GIT_BASE}/openstack/osc-lib.git}
 GITBRANCH["osc-lib"]=${OSC_LIB_BRANCH:-$TARGET_BRANCH}
 
-# python-openstacksdk OpenStack Python SDK
-GITREPO["python-openstacksdk"]=${OPENSTACKSDK_REPO:-${GIT_BASE}/openstack/python-openstacksdk.git}
-GITBRANCH["python-openstacksdk"]=${OPENSTACKSDK_BRANCH:-$TARGET_BRANCH}
-
 # ironic common lib
 GITREPO["ironic-lib"]=${IRONIC_LIB_REPO:-${GIT_BASE}/openstack/ironic-lib.git}
 GITBRANCH["ironic-lib"]=${IRONIC_LIB_BRANCH:-$TARGET_BRANCH}
@@ -625,12 +625,7 @@
 case "$VIRT_DRIVER" in
     ironic|libvirt)
         LIBVIRT_TYPE=${LIBVIRT_TYPE:-kvm}
-        # If ENABLE_VOLUME_MULTIATTACH is True, the Ubuntu Cloud Archive can't
-        # be used until it provides libvirt>=3.10, and with older versions of
-        # Ubuntu the group is "libvirtd".
-        # TODO(mriedem): Remove the ENABLE_VOLUME_MULTIATTACH check when
-        # UCA has libvirt>=3.10.
-        if [[ "$os_VENDOR" =~ (Debian|Ubuntu) && "${ENABLE_VOLUME_MULTIATTACH}" == "False" ]]; then
+        if [[ "$os_VENDOR" =~ (Debian|Ubuntu) ]]; then
             # The groups change with newer libvirt. Older Ubuntu used
             # 'libvirtd', but now uses libvirt like Debian. Do a quick check
             # to see if libvirtd group already exists to handle grenade's case.
@@ -737,11 +732,11 @@
 EXTRA_CACHE_URLS=""
 
 # etcd3 defaults
-ETCD_VERSION=${ETCD_VERSION:-v3.1.10}
-ETCD_SHA256_AMD64=${ETCD_SHA256_AMD64:-"2d335f298619c6fb02b1124773a56966e448ad9952b26fea52909da4fe80d2be"}
-# NOTE(sdague): etcd v3.1.10 doesn't have anything for these architectures, though 3.2.x does.
-ETCD_SHA256_ARM64=${ETCD_SHA256_ARM64:-""}
-ETCD_SHA256_PPC64=${ETCD_SHA256_PPC64:-""}
+ETCD_VERSION=${ETCD_VERSION:-v3.2.17}
+ETCD_SHA256_AMD64=${ETCD_SHA256_AMD64:-"0a75e794502e2e76417b19da2807a9915fa58dcbf0985e397741d570f4f305cd"}
+ETCD_SHA256_ARM64=${ETCD_SHA256_ARM64:-"0ab4621c44c79d17d94e43bd184d0f23b763a3669056ce4ae2d0b2942410a98f"}
+ETCD_SHA256_PPC64=${ETCD_SHA256_PPC64:-"69e1279c4a2a52256b78d2a8dd23346ac46b836e678b971a459f2afaef3c275e"}
+# etcd v3.2.x doesn't have anything for s390x
 ETCD_SHA256_S390X=${ETCD_SHA256_S390X:-""}
 # Make sure etcd3 downloads the correct architecture
 if is_arch "x86_64"; then
@@ -805,12 +800,7 @@
 # the memory used where there are a large number of CPUs present
 # (the default number of workers for many services is the number of CPUs)
 # Also sets the minimum number of workers to 2.
-if [[ "$VIRT_DRIVER" = 'fake' ]]; then
-    # we need more workers for the large ops job
-    API_WORKERS=${API_WORKERS:=$(( ($(nproc)/2)<2 ? 2 : ($(nproc)/2) ))}
-else
-    API_WORKERS=${API_WORKERS:=$(( ($(nproc)/4)<2 ? 2 : ($(nproc)/4) ))}
-fi
+API_WORKERS=${API_WORKERS:=$(( ($(nproc)/4)<2 ? 2 : ($(nproc)/4) ))}
 
 # Service startup timeout
 SERVICE_TIMEOUT=${SERVICE_TIMEOUT:-60}
@@ -824,11 +814,14 @@
 # Service graceful shutdown timeout
 WORKER_TIMEOUT=${WORKER_TIMEOUT:-90}
 
-# Support alternative yum -- in future Fedora 'dnf' will become the
-# only supported installer, but for now 'yum' and 'dnf' are both
-# available in parallel with compatible CLIs.  Allow manual switching
-# till we get to the point we need to handle this automatically
-YUM=${YUM:-yum}
+# Choose DNF on RedHat/Fedora platforms with it, or otherwise default
+# to YUM.  Can remove this when only dnf is supported (i.e. centos7
+# disappears)
+if [[ -e /usr/bin/dnf ]]; then
+    YUM=${YUM:-dnf}
+else
+    YUM=${YUM:-yum}
+fi
 
 # Common Configuration
 # --------------------
@@ -949,7 +942,6 @@
 fi
 
 # ``LOGDIR`` is always set at this point so it is not useful as a 'enable' for service logs
-# ``SCREEN_LOGDIR`` may be set, it is useful to enable the compat symlinks
 
 # System-wide ulimit file descriptors override
 ULIMIT_NOFILE=${ULIMIT_NOFILE:-2048}
diff --git a/tests/test_ini_config.sh b/tests/test_ini_config.sh
index a5e1107..f7dc89a 100755
--- a/tests/test_ini_config.sh
+++ b/tests/test_ini_config.sh
@@ -44,6 +44,9 @@
 multi = foo1
 multi = foo2
 
+[key_with_spaces]
+rgw special key = something
+
 # inidelete(a)
 [del_separate_options]
 a=b
@@ -82,8 +85,9 @@
 
 # test iniget_sections
 VAL=$(iniget_sections "${TEST_INI}")
-assert_equal "$VAL" "default aaa bbb ccc ddd eee del_separate_options \
-del_same_option del_missing_option del_missing_option_multi del_no_options"
+assert_equal "$VAL" "default aaa bbb ccc ddd eee key_with_spaces \
+del_separate_options del_same_option del_missing_option \
+del_missing_option_multi del_no_options"
 
 # Test with missing arguments
 BEFORE=$(cat ${TEST_INI})
@@ -209,6 +213,20 @@
 VAL=$(iniget ${INI_TMP_ETC_DIR}/test.new.ini test foo)
 assert_equal "$VAL" "bar" "iniset created file"
 
+# test creation of keys with spaces
+iniset ${SUDO_ARG} ${TEST_INI} key_with_spaces "rgw another key" somethingelse
+VAL=$(iniget ${TEST_INI} key_with_spaces "rgw another key")
+assert_equal "$VAL" "somethingelse" "iniset created a key with spaces"
+
+# test update of keys with spaces
+iniset ${SUDO_ARG} ${TEST_INI} key_with_spaces "rgw special key" newvalue
+VAL=$(iniget ${TEST_INI} key_with_spaces "rgw special key")
+assert_equal "$VAL" "newvalue" "iniset updated a key with spaces"
+
+inidelete ${SUDO_ARG} ${TEST_INI} key_with_spaces "rgw another key"
+VAL=$(iniget ${TEST_INI} key_with_spaces "rgw another key")
+assert_empty VAL "inidelete removed a key with spaces"
+
 $SUDO rm -rf ${INI_TMP_DIR}
 
 report_results
diff --git a/tests/test_libs_from_pypi.sh b/tests/test_libs_from_pypi.sh
index a544b56..c3b4457 100755
--- a/tests/test_libs_from_pypi.sh
+++ b/tests/test_libs_from_pypi.sh
@@ -38,7 +38,7 @@
 ALL_LIBS+=" oslo.serialization"
 ALL_LIBS+=" python-openstackclient osc-lib osc-placement"
 ALL_LIBS+=" os-client-config oslo.rootwrap"
-ALL_LIBS+=" oslo.i18n oslo.utils python-openstacksdk python-swiftclient"
+ALL_LIBS+=" oslo.i18n oslo.utils openstacksdk python-swiftclient"
 ALL_LIBS+=" python-neutronclient tooz ceilometermiddleware oslo.policy"
 ALL_LIBS+=" debtcollector os-brick os-traits automaton futurist oslo.service"
 ALL_LIBS+=" oslo.cache oslo.reports osprofiler cursive"
diff --git a/tests/test_write_devstack_local_conf_role.sh b/tests/test_write_devstack_local_conf_role.sh
new file mode 100755
index 0000000..b2bc0a2
--- /dev/null
+++ b/tests/test_write_devstack_local_conf_role.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+TOP=$(cd $(dirname "$0")/.. && pwd)
+
+# Import common functions
+source $TOP/functions
+source $TOP/tests/unittest.sh
+
+python ./roles/write-devstack-local-conf/library/test.py
diff --git a/tools/cap-pip.txt b/tools/cap-pip.txt
index c280267..f5278d7 100644
--- a/tools/cap-pip.txt
+++ b/tools/cap-pip.txt
@@ -1 +1 @@
-pip!=8
+pip!=8,<10
diff --git a/tools/fixup_stuff.sh b/tools/fixup_stuff.sh
index f78f05f..9147932 100755
--- a/tools/fixup_stuff.sh
+++ b/tools/fixup_stuff.sh
@@ -45,27 +45,29 @@
 # where Keystone will try and bind to the port and the port will already be
 # in use as an ephemeral port by another process. This places an explicit
 # exception into the Kernel for the Keystone AUTH ports.
-keystone_ports=${KEYSTONE_AUTH_PORT:-35357},${KEYSTONE_AUTH_PORT_INT:-35358}
+function fixup_keystone {
+    keystone_ports=${KEYSTONE_AUTH_PORT:-35357},${KEYSTONE_AUTH_PORT_INT:-35358}
 
-# Only do the reserved ports when available, on some system (like containers)
-# where it's not exposed we are almost pretty sure these ports would be
-# exclusive for our DevStack.
-if sysctl net.ipv4.ip_local_reserved_ports >/dev/null 2>&1; then
-    # Get any currently reserved ports, strip off leading whitespace
-    reserved_ports=$(sysctl net.ipv4.ip_local_reserved_ports | awk -F'=' '{print $2;}' | sed 's/^ //')
+    # Only do the reserved ports when available, on some system (like containers)
+    # where it's not exposed we are almost pretty sure these ports would be
+    # exclusive for our DevStack.
+    if sysctl net.ipv4.ip_local_reserved_ports >/dev/null 2>&1; then
+        # Get any currently reserved ports, strip off leading whitespace
+        reserved_ports=$(sysctl net.ipv4.ip_local_reserved_ports | awk -F'=' '{print $2;}' | sed 's/^ //')
 
-    if [[ -z "${reserved_ports}" ]]; then
-        # If there are no currently reserved ports, reserve the keystone ports
-        sudo sysctl -w net.ipv4.ip_local_reserved_ports=${keystone_ports}
+        if [[ -z "${reserved_ports}" ]]; then
+            # If there are no currently reserved ports, reserve the keystone ports
+            sudo sysctl -w net.ipv4.ip_local_reserved_ports=${keystone_ports}
+        else
+            # If there are currently reserved ports, keep those and also reserve the
+            # Keystone specific ports. Duplicate reservations are merged into a single
+            # reservation (or range) automatically by the kernel.
+            sudo sysctl -w net.ipv4.ip_local_reserved_ports=${keystone_ports},${reserved_ports}
+        fi
     else
-        # If there are currently reserved ports, keep those and also reserve the
-        # Keystone specific ports. Duplicate reservations are merged into a single
-        # reservation (or range) automatically by the kernel.
-        sudo sysctl -w net.ipv4.ip_local_reserved_ports=${keystone_ports},${reserved_ports}
+        echo_summary "WARNING: unable to reserve keystone ports"
     fi
-else
-    echo_summary "WARNING: unable to reserve keystone ports"
-fi
+}
 
 # Ubuntu Cloud Archive
 #---------------------
@@ -75,24 +77,23 @@
 # Make it possible to switch this based on an environment variable as
 # libvirt 2.5.0 doesn't handle nested virtualization quite well and this
 # is required for the trove development environment.
-# The Pike UCA has qemu 2.10 but libvirt 3.6, therefore if
-# ENABLE_VOLUME_MULTIATTACH is True, we can't use the Pike UCA
-# because multiattach won't work with those package versions.
-# We can remove this check when the UCA has libvirt>=3.10.
-if [[ "${ENABLE_UBUNTU_CLOUD_ARCHIVE}" == "True" && "$DISTRO" = "xenial" && \
-        "${ENABLE_VOLUME_MULTIATTACH}" == "False" ]]; then
+function fixup_uca {
+    if [[ "${ENABLE_UBUNTU_CLOUD_ARCHIVE}" == "False" || "$DISTRO" != "xenial" ]]; then
+        return
+    fi
+
     # This pulls in apt-add-repository
     install_package "software-properties-common"
-    # Use UCA for newer libvirt. Should give us libvirt 2.5.0.
+    # Use UCA for newer libvirt.
     if [[ -f /etc/ci/mirror_info.sh ]] ; then
         # If we are on a nodepool provided host and it has told us about where
         # we can find local mirrors then use that mirror.
         source /etc/ci/mirror_info.sh
 
-        sudo apt-add-repository -y "deb $NODEPOOL_UCA_MIRROR xenial-updates/pike main"
+        sudo apt-add-repository -y "deb $NODEPOOL_UCA_MIRROR xenial-updates/queens main"
     else
         # Otherwise use upstream UCA
-        sudo add-apt-repository -y cloud-archive:pike
+        sudo add-apt-repository -y cloud-archive:queens
     fi
 
     # Disable use of libvirt wheel since a cached wheel build might be
@@ -104,8 +105,7 @@
     # Force update our APT repos, since we added UCA above.
     REPOS_UPDATED=False
     apt_get_update
-fi
-
+}
 
 # Python Packages
 # ---------------
@@ -120,27 +120,32 @@
 # Pre-install affected packages so we can fix the permissions
 # These can go away once we are confident that pip 1.4.1+ is available everywhere
 
-# Fix prettytable 0.7.2 permissions
-# Don't specify --upgrade so we use the existing package if present
-pip_install 'prettytable>=0.7'
-PACKAGE_DIR=$(get_package_path prettytable)
-# Only fix version 0.7.2
-dir=$(echo $PACKAGE_DIR/prettytable-0.7.2*)
-if [[ -d $dir ]]; then
-    sudo chmod +r $dir/*
-fi
+function fixup_python_packages {
+    # Fix prettytable 0.7.2 permissions
+    # Don't specify --upgrade so we use the existing package if present
+    pip_install 'prettytable>=0.7'
+    PACKAGE_DIR=$(get_package_path prettytable)
+    # Only fix version 0.7.2
+    dir=$(echo $PACKAGE_DIR/prettytable-0.7.2*)
+    if [[ -d $dir ]]; then
+        sudo chmod +r $dir/*
+    fi
 
-# Fix httplib2 0.8 permissions
-# Don't specify --upgrade so we use the existing package if present
-pip_install httplib2
-PACKAGE_DIR=$(get_package_path httplib2)
-# Only fix version 0.8
-dir=$(echo $PACKAGE_DIR-0.8*)
-if [[ -d $dir ]]; then
-    sudo chmod +r $dir/*
-fi
+    # Fix httplib2 0.8 permissions
+    # Don't specify --upgrade so we use the existing package if present
+    pip_install httplib2
+    PACKAGE_DIR=$(get_package_path httplib2)
+    # Only fix version 0.8
+    dir=$(echo $PACKAGE_DIR-0.8*)
+    if [[ -d $dir ]]; then
+        sudo chmod +r $dir/*
+    fi
+}
 
-if is_fedora; then
+function fixup_fedora {
+    if ! is_fedora; then
+        return
+    fi
     # Disable selinux to avoid configuring to allow Apache access
     # to Horizon files (LP#1175444)
     if selinuxenabled; then
@@ -198,7 +203,7 @@
             pip_install --upgrade --force-reinstall requests
         fi
     fi
-fi
+}
 
 # The version of pip(1.5.4) supported by python-virtualenv(1.11.4) has
 # connection issues under proxy so re-install the latest version using
@@ -222,7 +227,17 @@
 #            install.d/pip-and-virtualenv-source-install/04-install-pip
 # [2] https://bugzilla.redhat.com/show_bug.cgi?id=1477823
 
-if [[ ! -f /etc/ci/mirror_info.sh ]]; then
-    install_package python-virtualenv
-    pip_install -U --force-reinstall virtualenv
-fi
+function fixup_virtualenv {
+    if [[ ! -f /etc/ci/mirror_info.sh ]]; then
+        install_package python-virtualenv
+        pip_install -U --force-reinstall virtualenv
+    fi
+}
+
+function fixup_all {
+    fixup_keystone
+    fixup_uca
+    fixup_python_packages
+    fixup_fedora
+    fixup_virtualenv
+}
diff --git a/tools/install_pip.sh b/tools/install_pip.sh
index dbe5278..1bd7392 100755
--- a/tools/install_pip.sh
+++ b/tools/install_pip.sh
@@ -129,10 +129,10 @@
 
 # Eradicate any and all system packages
 
-# Python in fedora depends on the python-pip package so removing it
+# Python in fedora/suse depends on the python-pip package so removing it
 # results in a nonfunctional system. pip on fedora installs to /usr so pip
 # can safely override the system pip for all versions of fedora
-if ! is_fedora ; then
+if ! is_fedora  && ! is_suse; then
     uninstall_package python-pip
     uninstall_package python3-pip
 fi