diff --git a/launch/README b/launch/README
index ce20e856bb..572daea128 100644
--- a/launch/README
+++ b/launch/README
@@ -38,7 +38,7 @@ To launch a node in the OpenStack Jenkins account (slave nodes)::
   export OS_REGION_NAME=DFW
   export FQDN=slavename01.slave.openstack.org
   openstack image list
-  export IMAGE='Ubuntu 12.04 LTS (Precise Pangolin) (PVHVM)'
+  export IMAGE='Ubuntu 18.04 LTS (Bionic Beaver) (PVHVM)'
   openstack flavor list
   export FLAVOR="8 GB Performance"
   ./launch-node.py $FQDN --image "$IMAGE" --flavor "$FLAVOR" \
diff --git a/launch/launch-node-ansible.py b/launch/launch-node-ansible.py
deleted file mode 100755
index 23cc793c20..0000000000
--- a/launch/launch-node-ansible.py
+++ /dev/null
@@ -1,353 +0,0 @@
-#!/usr/bin/env python
-
-# Launch a new OpenStack project infrastructure node.
-
-# Copyright (C) 2011-2012 OpenStack LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-# implied.
-#
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import argparse
-import os
-import shutil
-import subprocess
-import sys
-import threading
-import tempfile
-import time
-import traceback
-
-import dns
-import utils
-
-import openstack
-import os_client_config
-import paramiko
-
-SCRIPT_DIR = os.path.dirname(sys.argv[0])
-
-try:
-    # This unactionable warning does not need to be printed over and over.
-    import requests.packages.urllib3
-    requests.packages.urllib3.disable_warnings()
-except:
-    pass
-
-
-class JobDir(object):
-    def __init__(self, keep=False):
-        self.keep = keep
-        self.root = tempfile.mkdtemp()
-        self.inventory_root = os.path.join(self.root, 'inventory')
-        os.makedirs(self.inventory_root)
-        self.hosts = os.path.join(self.inventory_root, 'hosts')
-        self.groups = os.path.join(self.inventory_root, 'groups')
-        self.key = os.path.join(self.root, 'id_rsa')
-        self.ansible_log = os.path.join(self.root, 'ansible_log.txt')
-        # XXX if we need more, we might like to setup an ansible.cfg
-        # file and use that rather than env vars.  See
-        # zuul/launcher/ansiblelaunchserver.py as an example
-        self.env = os.environ.copy()
-        self.env['ANSIBLE_LOG_PATH'] = self.ansible_log
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, etype, value, tb):
-        if not self.keep:
-            shutil.rmtree(self.root)
-
-
-def run(cmd, **args):
-    args['stdout'] = subprocess.PIPE
-    args['stderr'] = subprocess.STDOUT
-    print("Running: %s" % (cmd,))
-    proc = subprocess.Popen(cmd, **args)
-    out = ''
-    for line in iter(proc.stdout.readline, b''):
-        line = line.decode('utf-8')
-        sys.stdout.write(line)
-        sys.stdout.flush()
-        out += line
-    ret = proc.wait()
-    print("Return code: %s" % (ret,))
-    if ret != 0:
-        raise subprocess.CalledProcessError(ret, cmd, out)
-    return ret
-
-
-def stream_syslog(ssh_client):
-    try:
-        ssh_client.ssh('tail -f /var/log/syslog')
-    except Exception:
-        print("Syslog stream terminated")
-
-
-def bootstrap_server(server, key, name, volume_device, keep,
-                     mount_path, fs_label, environment):
-
-    ip = server.public_v4
-    ssh_kwargs = dict(pkey=key)
-
-    print("--- Running initial configuration on host %s ---" % ip)
-    for username in ['root', 'ubuntu', 'centos', 'admin']:
-        ssh_client = utils.ssh_connect(ip, username, ssh_kwargs, timeout=600)
-        if ssh_client:
-            break
-
-    if not ssh_client:
-        raise Exception("Unable to log in via SSH")
-
-    # cloud-init puts the "please log in as user foo" message and
-    # subsequent exit() in root's authorized_keys -- overwrite it with
-    # a normal version to get root login working again.
-    if username != 'root':
-        ssh_client.ssh("sudo cp ~/.ssh/authorized_keys"
-                       " ~root/.ssh/authorized_keys")
-        ssh_client.ssh("sudo chmod 644 ~root/.ssh/authorized_keys")
-        ssh_client.ssh("sudo chown root.root ~root/.ssh/authorized_keys")
-
-    ssh_client = utils.ssh_connect(ip, 'root', ssh_kwargs, timeout=600)
-
-    # Something up with RAX images that they have the ipv6 interface in
-    # /etc/network/interfaces but eth0 hasn't noticed yet; reload it
-    ssh_client.ssh('(ifdown eth0 && ifup eth0) || true')
-
-    if server.public_v6:
-        ssh_client.ssh('ping6 -c5 -Q 0x10 review.openstack.org '
-                       '|| ping6 -c5 -Q 0x10 wiki.openstack.org')
-
-    ssh_client.scp(os.path.join(SCRIPT_DIR, '..', 'make_swap.sh'),
-                   'make_swap.sh')
-    ssh_client.ssh('bash -x make_swap.sh')
-
-    if volume_device:
-        ssh_client.scp(os.path.join(SCRIPT_DIR, '..', 'mount_volume.sh'),
-                       'mount_volume.sh')
-        ssh_client.ssh('bash -x mount_volume.sh %s %s %s' %
-                       (volume_device, mount_path, fs_label))
-
-    with JobDir(keep) as jobdir:
-        # Update the generated-groups file globally and incorporate it
-        # into our inventory
-        # Remove cloud and region from the environment to work
-        # around a bug in occ
-        expand_env = os.environ.copy()
-        for env_key in list(expand_env.keys()):
-            if env_key.startswith('OS_'):
-                expand_env.pop(env_key, None)
-        expand_env['ANSIBLE_LOG_PATH'] = jobdir.ansible_log
-
-        # Write out the private SSH key we generated
-        with open(jobdir.key, 'w') as key_file:
-            key.write_private_key(key_file)
-        os.chmod(jobdir.key, 0o600)
-
-        # Write out inventory
-        with open(jobdir.hosts, 'w') as inventory_file:
-            inventory_file.write(
-                "{host} ansible_host={ip} ansible_user=root {python}".format(
-                    host=name, ip=server.interface_ip,
-                    python='ansible_python_interpreter=/usr/bin/python3'))
-
-        t = threading.Thread(target=stream_syslog, args=(ssh_client,))
-        t.daemon = True
-        t.start()
-
-        ansible_cmd = [
-            'ansible-playbook',
-            '-i', jobdir.inventory_root, '-l', name,
-            '--private-key={key}'.format(key=jobdir.key),
-            "--ssh-common-args='-o StrictHostKeyChecking=no'",
-            '-e', 'target={name}'.format(name=name),
-        ]
-
-        # Run the remote puppet apply playbook limited to just this server
-        # we just created
-        for playbook in [
-                'set-hostnames.yaml',
-                'base.yaml',
-        ]:
-            run(ansible_cmd + [
-                os.path.join(SCRIPT_DIR, '..', 'playbooks', playbook)],
-                env=jobdir.env)
-
-    try:
-        ssh_client.ssh("reboot")
-    except Exception as e:
-        # Some init system kill the connection too fast after reboot.
-        # Deal with it by ignoring ssh errors when rebooting.
-        if e.rc == -1:
-            pass
-        else:
-            raise
-
-
-def build_server(cloud, name, image, flavor,
-                 volume, keep, network, boot_from_volume, config_drive,
-                 mount_path, fs_label, availability_zone, environment):
-    key = None
-    server = None
-
-    create_kwargs = dict(image=image, flavor=flavor, name=name,
-                         reuse_ips=False, wait=True,
-                         boot_from_volume=boot_from_volume,
-                         network=network,
-                         config_drive=config_drive)
-
-    if availability_zone:
-        create_kwargs['availability_zone'] = availability_zone
-
-    if volume:
-        create_kwargs['volumes'] = [volume]
-
-    key_name = 'launch-%i' % (time.time())
-    key = paramiko.RSAKey.generate(2048)
-    public_key = key.get_name() + ' ' + key.get_base64()
-    cloud.create_keypair(key_name, public_key)
-    create_kwargs['key_name'] = key_name
-
-    try:
-        server = cloud.create_server(**create_kwargs)
-    except Exception:
-        try:
-            cloud.delete_keypair(key_name)
-        except Exception:
-            print("Exception encountered deleting keypair:")
-            traceback.print_exc()
-        raise
-
-    try:
-        cloud.delete_keypair(key_name)
-
-        server = cloud.get_openstack_vars(server)
-        if volume:
-            volume = cloud.get_volume(volume)
-            volume_device = cloud.get_volume_attach_device(volume,
-                                                           server['id'])
-        else:
-            volume_device = None
-        bootstrap_server(server, key, name, volume_device, keep,
-                         mount_path, fs_label, environment)
-        print('UUID=%s\nIPV4=%s\nIPV6=%s\n' % (
-            server.id, server.public_v4, server.public_v6))
-    except Exception:
-        print("****")
-        print("Server %s failed to build!" % (server.id))
-        try:
-            if keep:
-                print("Keeping as requested")
-                # Write out the private SSH key we generated, as we
-                # may not have got far enough for ansible to run
-                with open('/tmp/%s.id_rsa' % server.id, 'w') as key_file:
-                    key.write_private_key(key_file)
-                    os.chmod(key_file.name, 0o600)
-                    print("Private key saved in %s" % key_file.name)
-                print(
-                    "Run to delete -> openstack server delete %s" % \
-                    (server.id))
-            else:
-                cloud.delete_server(server.id, delete_ips=True)
-        except Exception:
-            print("Exception encountered deleting server:")
-            traceback.print_exc()
-        print("The original exception follows:")
-        print("****")
-        # Raise the important exception that started this
-        raise
-
-    return server
-
-
-def main():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("name", help="server name")
-    parser.add_argument("--cloud", dest="cloud", required=True,
-                        help="cloud name")
-    parser.add_argument("--region", dest="region",
-                        help="cloud region")
-    parser.add_argument("--flavor", dest="flavor", default='1GB',
-                        help="name (or substring) of flavor")
-    parser.add_argument("--image", dest="image",
-                        default="Ubuntu 18.04 LTS (Bionic Beaver) (PVHVM)",
-                        help="image name")
-    parser.add_argument("--environment", dest="environment",
-                        help="Puppet environment to use",
-                        default=None)
-    parser.add_argument("--volume", dest="volume",
-                        help="UUID of volume to attach to the new server.",
-                        default=None)
-    parser.add_argument("--mount-path", dest="mount_path",
-                        help="Path to mount cinder volume at.",
-                        default=None)
-    parser.add_argument("--fs-label", dest="fs_label",
-                        help="FS label to use when mounting cinder volume.",
-                        default=None)
-    parser.add_argument("--boot-from-volume", dest="boot_from_volume",
-                        help="Create a boot volume for the server and use it.",
-                        action='store_true',
-                        default=False)
-    parser.add_argument("--keep", dest="keep",
-                        help="Don't clean up or delete the server on error.",
-                        action='store_true',
-                        default=False)
-    parser.add_argument("--verbose", dest="verbose", default=False,
-                        action='store_true',
-                        help="Be verbose about logging cloud actions")
-    parser.add_argument("--network", dest="network", default=None,
-                        help="network label to attach instance to")
-    parser.add_argument("--config-drive", dest="config_drive",
-                        help="Boot with config_drive attached.",
-                        action='store_true',
-                        default=False)
-    parser.add_argument("--az", dest="availability_zone", default=None,
-                        help="AZ to boot in.")
-    options = parser.parse_args()
-
-    openstack.enable_logging(debug=options.verbose)
-
-    cloud_kwargs = {}
-    if options.region:
-        cloud_kwargs['region_name'] = options.region
-    cloud = openstack.connect(cloud=options.cloud, **cloud_kwargs)
-
-    flavor = cloud.get_flavor(options.flavor)
-    if flavor:
-        print("Found flavor", flavor.name)
-    else:
-        print("Unable to find matching flavor; flavor list:")
-        for i in cloud.list_flavors():
-            print(i.name)
-        sys.exit(1)
-
-    image = cloud.get_image_exclude(options.image, 'deprecated')
-    if image:
-        print("Found image", image.name)
-    else:
-        print("Unable to find matching image; image list:")
-        for i in cloud.list_images():
-            print(i.name)
-        sys.exit(1)
-
-    server = build_server(cloud, options.name, image, flavor,
-                          options.volume, options.keep,
-                          options.network, options.boot_from_volume,
-                          options.config_drive,
-                          options.mount_path, options.fs_label,
-                          options.availability_zone,
-                          options.environment)
-    dns.print_dns(cloud, server)
-
-if __name__ == '__main__':
-    main()
diff --git a/launch/launch-node.py b/launch/launch-node.py
index 1d164aa24e..e6cbcc1a4c 100755
--- a/launch/launch-node.py
+++ b/launch/launch-node.py
@@ -72,15 +72,16 @@ class JobDir(object):
 def run(cmd, **args):
     args['stdout'] = subprocess.PIPE
     args['stderr'] = subprocess.STDOUT
-    print "Running: %s" % (cmd,)
+    print("Running: %s" % (cmd,))
     proc = subprocess.Popen(cmd, **args)
     out = ''
-    for line in iter(proc.stdout.readline, ''):
+    for line in iter(proc.stdout.readline, b''):
+        line = line.decode('utf-8')
         sys.stdout.write(line)
         sys.stdout.flush()
         out += line
     ret = proc.wait()
-    print "Return code: %s" % (ret,)
+    print("Return code: %s" % (ret,))
     if ret != 0:
         raise subprocess.CalledProcessError(ret, cmd, out)
     return ret
@@ -90,7 +91,7 @@ def stream_syslog(ssh_client):
     try:
         ssh_client.ssh('tail -f /var/log/syslog')
     except Exception:
-        print "Syslog stream terminated"
+        print("Syslog stream terminated")
 
 
 def bootstrap_server(server, key, name, volume_device, keep,
@@ -137,16 +138,10 @@ def bootstrap_server(server, key, name, volume_device, keep,
         ssh_client.ssh('bash -x mount_volume.sh %s %s %s' %
                        (volume_device, mount_path, fs_label))
 
-    # This next chunk should really exist as a playbook, but whatev
-    ssh_client.scp(os.path.join(SCRIPT_DIR, '..', 'install_puppet.sh'),
-                   'install_puppet.sh')
-    ssh_client.ssh('bash -x install_puppet.sh')
-
     # Zero the ansible inventory cache so that next run finds the new server
-    inventory_cache = '/var/cache/ansible-inventory/ansible-inventory.cache'
-    if os.path.exists(inventory_cache):
-        with open(inventory_cache, 'w'):
-            pass
+    inventory_cache_dir = '/var/cache/ansible/inventory'
+    for inventory_cache in os.listdir(inventory_cache_dir):
+        os.unlink(inventory_cache)
 
     with JobDir(keep) as jobdir:
         # Update the generated-groups file globally and incorporate it
@@ -154,25 +149,11 @@ def bootstrap_server(server, key, name, volume_device, keep,
         # Remove cloud and region from the environment to work
         # around a bug in occ
         expand_env = os.environ.copy()
-        for env_key in expand_env.keys():
+        for env_key in list(expand_env.keys()):
             if env_key.startswith('OS_'):
                 expand_env.pop(env_key, None)
         expand_env['ANSIBLE_LOG_PATH'] = jobdir.ansible_log
 
-        # Regenerate inventory cache, throwing an error if there is an issue
-        # so that we don't generate a bogus groups file
-        try:
-            run(['/etc/ansible/hosts/openstack_inventory', '--list'],
-                env=expand_env)
-        except subprocess.CalledProcessError as e:
-            print "Inventory regeneration failed"
-            print e.output
-            raise
-
-        run('/usr/local/bin/expand-groups.sh',
-            env=expand_env,
-            stderr=subprocess.STDOUT)
-
         # Write out the private SSH key we generated
         with open(jobdir.key, 'w') as key_file:
             key.write_private_key(key_file)
@@ -181,11 +162,9 @@ def bootstrap_server(server, key, name, volume_device, keep,
         # Write out inventory
         with open(jobdir.hosts, 'w') as inventory_file:
             inventory_file.write(
-                "{host} ansible_host={ip} ansible_user=root".format(
-                    host=name, ip=server.interface_ip))
-
-        os.symlink('/etc/ansible/hosts/generated-groups',
-                   jobdir.groups)
+                "{host} ansible_host={ip} ansible_user=root {python}".format(
+                    host=name, ip=server.interface_ip,
+                    python='ansible_python_interpreter=/usr/bin/python3'))
 
         t = threading.Thread(target=stream_syslog, args=(ssh_client,))
         t.daemon = True
@@ -199,15 +178,11 @@ def bootstrap_server(server, key, name, volume_device, keep,
             '-e', 'target={name}'.format(name=name),
         ]
 
-        if environment is not None:
-            ansible_cmd += [
-                '-e',
-                'puppet_environment={env}'.format(env=environment)]
-        # Run the remote puppet apply playbook limited to just this server
-        # we just created
+        # Run the base playbook limited to just this server we just created
         for playbook in [
                 'set-hostnames.yaml',
-                'remote_puppet_adhoc.yaml']:
+                'base.yaml',
+        ]:
             run(ansible_cmd + [
                 os.path.join(SCRIPT_DIR, '..', 'playbooks', playbook)],
                 env=jobdir.env)
@@ -253,7 +228,7 @@ def build_server(cloud, name, image, flavor,
         try:
             cloud.delete_keypair(key_name)
         except Exception:
-            print "Exception encountered deleting keypair:"
+            print("Exception encountered deleting keypair:")
             traceback.print_exc()
         raise
 
@@ -272,26 +247,27 @@ def build_server(cloud, name, image, flavor,
         print('UUID=%s\nIPV4=%s\nIPV6=%s\n' % (
             server.id, server.public_v4, server.public_v6))
     except Exception:
-        print "****"
-        print "Server %s failed to build!" % (server.id)
+        print("****")
+        print("Server %s failed to build!" % (server.id))
         try:
             if keep:
-                print "Keeping as requested"
+                print("Keeping as requested")
                 # Write out the private SSH key we generated, as we
                 # may not have got far enough for ansible to run
                 with open('/tmp/%s.id_rsa' % server.id, 'w') as key_file:
                     key.write_private_key(key_file)
                     os.chmod(key_file.name, 0o600)
-                    print "Private key saved in %s" % key_file.name
-                print "Run to delete -> openstack server delete %s" % \
-                    (server.id)
+                    print("Private key saved in %s" % key_file.name)
+                print(
+                    "Run to delete -> openstack server delete %s" % \
+                    (server.id))
             else:
                 cloud.delete_server(server.id, delete_ips=True)
         except Exception:
-            print "Exception encountered deleting server:"
+            print("Exception encountered deleting server:")
             traceback.print_exc()
-        print "The original exception follows:"
-        print "****"
+        print("The original exception follows:")
+        print("****")
         # Raise the important exception that started this
         raise
 
@@ -308,7 +284,7 @@ def main():
     parser.add_argument("--flavor", dest="flavor", default='1GB',
                         help="name (or substring) of flavor")
     parser.add_argument("--image", dest="image",
-                        default="Ubuntu 16.04 LTS (Xenial Xerus) (PVHVM)",
+                        default="Ubuntu 18.04 LTS (Bionic Beaver) (PVHVM)",
                         help="image name")
     parser.add_argument("--environment", dest="environment",
                         help="Puppet environment to use",
@@ -352,20 +328,20 @@ def main():
 
     flavor = cloud.get_flavor(options.flavor)
     if flavor:
-        print "Found flavor", flavor.name
+        print("Found flavor", flavor.name)
     else:
-        print "Unable to find matching flavor; flavor list:"
+        print("Unable to find matching flavor; flavor list:")
         for i in cloud.list_flavors():
-            print i.name
+            print(i.name)
         sys.exit(1)
 
     image = cloud.get_image_exclude(options.image, 'deprecated')
     if image:
-        print "Found image", image.name
+        print("Found image", image.name)
     else:
-        print "Unable to find matching image; image list:"
+        print("Unable to find matching image; image list:")
         for i in cloud.list_images():
-            print i.name
+            print(i.name)
         sys.exit(1)
 
     server = build_server(cloud, options.name, image, flavor,