diff --git a/launch/dns.py b/launch/dns.py
index 22addc6b8f..573040514d 100755
--- a/launch/dns.py
+++ b/launch/dns.py
@@ -43,35 +43,35 @@ def print_dns(cloud, server):
         raise
     href = get_href(raw_server)
 
-    print
-    print "Run the following commands to set up DNS:"
-    print
-    print ". ~root/ci-launch/openstackci-rs-nova.sh"
-    print ". ~root/rackdns-venv/bin/activate"
-    print
-    print (
+    print("\n")
+    print("Run the following commands to set up DNS:")
+    print("\n")
+    print(". ~root/ci-launch/openstackci-rs-nova.sh")
+    print(". ~root/rackdns-venv/bin/activate")
+    print("\n")
+    print(
         "rackdns rdns-create --name %s \\\n"
         "    --data %s \\\n"
         "    --server-href %s \\\n"
         "    --ttl 3600" % (
             server.name, ip6, href))
-    print
-    print (
+    print("\n")
+    print(
         "rackdns rdns-create --name %s \\\n"
         "    --data %s \\\n"
         "    --server-href %s \\\n"
         "    --ttl 3600" % (
             server.name, ip4, href))
-    print
-    print ". ~root/ci-launch/openstack-rs-nova.sh"
-    print
-    print (
+    print("\n")
+    print(". ~root/ci-launch/openstack-rs-nova.sh")
+    print("\n")
+    print(
         "rackdns record-create --name %s \\\n"
         "    --type AAAA --data %s \\\n"
         "    --ttl 3600 openstack.org" % (
             server.name, ip6))
-    print
-    print (
+    print("\n")
+    print(
         "rackdns record-create --name %s \\\n"
         "    --type A --data %s \\\n"
         "    --ttl 3600 openstack.org" % (
diff --git a/launch/launch-node-ansible.py b/launch/launch-node-ansible.py
new file mode 100755
index 0000000000..23cc793c20
--- /dev/null
+++ b/launch/launch-node-ansible.py
@@ -0,0 +1,353 @@
+#!/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 080fd5709d..1d164aa24e 100755
--- a/launch/launch-node.py
+++ b/launch/launch-node.py
@@ -206,7 +206,7 @@ def bootstrap_server(server, key, name, volume_device, keep,
         # Run the remote puppet apply playbook limited to just this server
         # we just created
         for playbook in [
-                'set_hostnames.yml',
+                'set-hostnames.yaml',
                 'remote_puppet_adhoc.yaml']:
             run(ansible_cmd + [
                 os.path.join(SCRIPT_DIR, '..', 'playbooks', playbook)],
diff --git a/launch/utils.py b/launch/utils.py
index 077a1e32a3..3e1387dfe2 100644
--- a/launch/utils.py
+++ b/launch/utils.py
@@ -43,7 +43,7 @@ def ssh_connect(ip, username, connect_kwargs={}, timeout=60):
             client = SSHClient(ip, username, **connect_kwargs)
             break
         except socket.error as e:
-            print "While testing ssh access:", e
+            print("While testing ssh access:", e)
             time.sleep(5)
         except paramiko.ssh_exception.AuthenticationException:
             return None
diff --git a/make_swap.sh b/make_swap.sh
index 6939ba82ac..0713cf8905 100644
--- a/make_swap.sh
+++ b/make_swap.sh
@@ -27,7 +27,7 @@ if [ `grep SwapTotal /proc/meminfo | awk '{ print $2; }'` -eq 0 ]; then
         MEMKB=`grep MemTotal /proc/meminfo | awk '{print $2; }'`
         # Use the nearest power of two in MB as the swap size.
         # This ensures that the partitions below are aligned properly.
-        MEM=`python -c "import math ; print 2**int(round(math.log($MEMKB/1024, 2)))"`
+        MEM=`python3 -c "import math ; print(2**int(round(math.log($MEMKB/1024, 2))))"`
         if mount | grep ${DEV} > /dev/null; then
             echo "*** ${DEV} appears to already be mounted"
             echo "*** ${DEV} unmounting and reformating"
diff --git a/modules/openstack_project/files/puppetmaster/groups.txt b/modules/openstack_project/files/puppetmaster/groups.txt
index 6ba474f1bd..4b050cdcbf 100644
--- a/modules/openstack_project/files/puppetmaster/groups.txt
+++ b/modules/openstack_project/files/puppetmaster/groups.txt
@@ -11,6 +11,7 @@ files ~files\d+\.openstack\.org
 git-loadbalancer ~git(-fe\d+)?\.openstack\.org
 git-server ~git\d+\.openstack\.org
 logstash-worker ~logstash-worker\d+\.openstack\.org
+mailman ~lists\d*\.openstack\.org:~lists\d*\.katacontainers\.io
 nodepool nodepool*.openstack.org:nb*.openstack.org:nl*.openstack.org
 review ~review\d+\.openstack\.org
 review-dev ~review-dev\d*\.openstack\.org
diff --git a/playbooks/base.yaml b/playbooks/base.yaml
new file mode 100644
index 0000000000..814e2f663c
--- /dev/null
+++ b/playbooks/base.yaml
@@ -0,0 +1,5 @@
+- hosts: "!disabled"
+  roles:
+    - users
+    - base-repos
+    - base-server
diff --git a/playbooks/group_vars/all.yaml b/playbooks/group_vars/all.yaml
index 39e8789cf2..cac3158d80 100644
--- a/playbooks/group_vars/all.yaml
+++ b/playbooks/group_vars/all.yaml
@@ -10,3 +10,208 @@ puppet:
   copy_puppet: true
   manifest: /opt/system-config/production/manifests/site.pp
   manifest_base: /opt/system-config
+
+all_users:
+  mordred:
+    comment: Monty Taylor
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLsTZJ8hXTmzjKxYh/7V07mIy8xl2HL+9BaUlt6A6TMsL3LSvaVQNSgmXX5g0XfPWSCKmkZb1O28q49jQI2n7n7+sHkxn0dJDxj1N2oNrzNY7pDuPrdtCijczLFdievygXNhXNkQ2WIqHXDquN/jfLLJ9L0jxtxtsUMbiL2xxZEZcaf/K5MqyPhscpqiVNE1MjE4xgPbIbv8gCKtPpYIIrktOMb4JbV7rhOp5DcSP5gXtLhOF5fbBpZ+szqrTVUcBX0oTYr3iRfOje9WPsTZIk9vBfBtF416mCNxMSRc7KhSW727AnUu85hS0xiP0MRAf69KemG1OE1pW+LtDIAEYp mordred@camelot
+    uid: 2000
+    gid: 2000
+
+  corvus:
+    comment: James E. Blair
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvKYcWK1T7e3PKSFiqb03EYktnoxVASpPoq2rJw2JvhsP0JfS+lKrPzpUQv7L4JCuQMsPNtZ8LnwVEft39k58Kh8XMebSfaqPYAZS5zCNvQUQIhP9myOevBZf4CDeG+gmssqRFcWEwIllfDuIzKBQGVbomR+Y5QuW0HczIbkoOYI6iyf2jB6xg+bmzR2HViofNrSa62CYmHS6dO04Z95J27w6jGWpEOTBjEQvnb9sdBc4EzaBVmxCpa2EilB1u0th7/DvuH0yP4T+X8G8UjW1gZCTOVw06fqlBCST4KjdWw1F/AuOCT7048klbf4H+mCTaEcPzzu3Fkv8ckMWtS/Z9Q== jeblair@operational-necessity
+    uid: 2001
+    gid: 2001
+
+  smaffulli:
+    comment: Stefano Maffulli
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDD/zAvXaOUXCAT6/B4sCMu/38d/PyOIg/tYsYFAMgfDUzuZwkjZWNGrTpp/HFrOAZISER5KmOg48DKPvm91AeZOHfAXHCP6x9/FcogP9rmc48ym1B5XyIc78QVQjgN6JMSlEZsl0GWzFhQsPDjXundflY07TZfSC1IhpG9UgzamEVFcRjmNztnBuvq2uYVGpdI+ghmqFw9kfvSXJvUbj/F7Pco5XyJBx2e+gofe+X/UNee75xgoU/FyE2a6dSSc4uP4oUBvxDNU3gIsUKrSCmV8NuVQvMB8C9gXYR+JqtcvUSS9DdUAA8StP65woVsvuU+lqb+HVAe71JotDfOBd6f stefano@mattone-E6420
+    uid: 2002
+    gid: 2002
+
+  oubiwann:
+    comment: Duncan McGreggor
+    uid: 2003
+    gid: 2003
+
+  rockstar:
+    comment: Paul Hummer
+    uid: 2004
+    gid: 2004
+
+  clarkb:
+    comment: Clark Boylan
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCnfoVhOTkrY7uoebL8PoHXb0Fg4jJqGCbwkxUdNUdheIdbnfyjuRG3iL8WZnzf7nzWnD+IGo6kkAo8BkNMK9L0P0Y+5IjI8NH49KU22tQ1umij4EIf5tzLh4gsqkJmy6QLrlbf10m6UF4rLFQhKzOd4b2H2K6KbP00CIymvbW3BwvNDODM4xRE2uao387qfvXZBUkB0PpRD+7fWPoN58gpFUm407Eba3WwX5PCD+1DD+RVBsG8maIDXerQ7lvFLoSuyMswv1TfkvCj0ZFhSFbfTd2ZysCu6eryFfeixR7NY9SNcp9YTqG6LrxGA7Ci6wz+hycFHXlDrlBgfFJDe5At clark@work
+    uid: 2005
+    gid: 2005
+
+  rlane:
+    comment: Ryan Lane
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCdtI7H+fsgSrjrdG8aGVcrN0GFW3XqLVsLG4n7JW4qH2W//hqgdL7A7cNVQNPoB9I1jAqvnO2Ct6wrVSh84QU89Uufw412M3qNSNeiGgv2c2KdxP2XBrnsLYAaJRbgOWJX7nty1jpO0xwF503ky2W3OMUsCXMAbYmYNSod6gAdzf5Xgo/3+eXRh7NbV1eKPrzwWoMOYh9T0Mvmokon/GXV5PiAA2bIaQvCy4BH/BzWiQwRM7KtiEt5lHahY172aEu+dcWxciuxHqkYqlKhbU+x1fwZJ+MpXSj5KBU+L0yf3iKySob7g6DZDST/Ylcm4MMjpOy8/9Cc6Xgpx77E/Pvd laner@Free-Public-Wifi.local
+    uid: 2006
+    gid: 2006
+
+  fungi:
+    comment: Jeremy Stanley
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3KnRBTH5QPpKjf4RWu4akzYt2gwp796cMkFl5vu8e7G/cHuh4979FeNJXMVP6F3rvZB+yXDHLCU5LBVLq0K+1GbAZT/hH38hpMOIvniwKIquvI6C/drkVPHO6YmVlapw/NI530PGnT/TAqCOycHBO5eF1bYsaqV1yZqvs9v7UZc6J4LukoLZwpmyWZ5P3ltAiiy8+FGq3SLCKWDMmv/Bjz4zTsaNbSWThJi0BydINjC1/0ze5Tyc/XgW1sDuxmmXJxgQp4EvLpronqb2hT60iA52kj8lrmoCIryRpgnbaRA7BrxKF8zIr0ZALHijxEUeWHhFJDIVRGUf0Ef0nrmBv fungi-openstack-2015
+    uid: 2007
+    gid: 2007
+
+  ttx:
+    comment: Thierry Carrez
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDCGpMtSehQNZL0/EJ7VUbklJygsxvii2Qi4HPSUFcLJUWAx4VltsmPkmx43D9ITwnRPRMPNtZrOvhY7v0myVlFuRnyTYAqZwigf5gxrktb+4PwCWb+2XobziUVnfJlbOTjneWSTYoZ+OjTaWd5AcVbUvgYAP2qbddycc5ACswpPDo5VrS6fQfCwE4z3BqLFNeOnqxbECHwHeFYIR6Kd6mnKAzDNZxZIkviWg9eIwwuFf5V5bUPiVkeFHVL3EJlCoYs2Em4bvYZBtrV7kUseN85X/+5Uail4uYBEcB3GLL32e6HeD1Qk4xIxLTI2bFPGUp0Oq7iPgrQQe4zCBsGi7Dx+JVy+U0JqLLAN94UPCn2fhsX7PdKfTPcxFPFKeX/PRutkb7qxdbS2ubCdOEhc6WN7OkQmbdK8lk6ms4v4dFc0ooMepWELqKC6thICsVdizpuij0+h8c4SRD3gtwGDPxrkJcodPoAimVVlW1p+RpMxsCFrK473TzgeNPVeAdSZVpqZ865VOwFqoFQB6WpmCDZQPFlkS2VDe9R54ePDHWKYLvVW6yvQqWTx3KrIrS1twSoydj+gADgBYsZaW5MNkWYHAWotEX67j6fMZ6ZSTS5yaTeLywB2Ykh0kjo4jpTFk5JNL7DINkfmCEZMLw60da29iN4QzAJr9cP1bwjf/QDqw== ttx@mercury
+    uid: 2008
+    gid: 2008
+
+  rbryant:
+    comment: Russell Bryant
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZVikFz5KoRg3gKdiSa3PQ0i2bN5+bUyc4lMMg6P+jEStVddwN+nAgpa3zJaokmNAOp+MjcGa7K1Zi4b9Fe2ufusTzSKdNVlRDiw0R4Lk0LwTIfkhLywKvgcAz8hkqWPUIgTMU4xIizh50KTL9Ttsu9ULop8t7urTpPE4TthHX4nz1Y9NwYLU0W8cWhzgRonBbqtGs/Lif0NC+TdWGkVyTaP3x1A48s0SMPcZKln1hDv7KbKdknG4XyS4jlr4qI+R+har7m2ED/PH93PSXi5QnT4U6laWRg03HTxpPKWq077u/tPW9wcbkgpBcYMmDKTo/NDPtoN+r/jkbdW7zKJHx russel@russelbryant.net
+    uid: 2009
+    gid: 2009
+
+  pabelanger:
+    comment: Paul Belanger
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCuP0CZE8AYnbm8gxecCxKeRw0wHRyryd+FKmNNsdr0d3UvfCbqNzLigrqEBZsKpofi3M4qCWNpKRyfhnjPynLTQjP1vnX9AbL9UGoiHxScfvh3skntTYMs9ezJRd0rMJJZO76FPo8bJLDlwxAQl8m/nuj3HfYiO5hYE7P+a3rhsJh4nEfBb7xh+Q5yM0PWObkkBl6IRiBYjlcsXNZHgTA5kNuihUk5bHqAw54sHh05DhpgOITpTw4LFbh4Ew2NKq49dEb2xbTuAyAr2DHNOGgIwKEZpwtKZEIGEuiLbb4DQRsfivrvyOjnK2NFjQzGyNOHfsOldWHRQwUKUs8nrxKdXvqcrfMnSVaibeYK2TRL+6jd9kc5SIhWI3XLm7HbX7uXMD7/JQrkL25Rcs6nndDCH72DJLz+ynA/T5umMbNBQ9tybL5z73IOpfShRGjQYego22CxDOy7e/5OEMHNoksbFb1S02viM9O2puS7LDqqfT9JIbbPqCrbRi/zOXo0f4EXo6xKUAmd8qlV+6f/p57/qFihzQDaRFVlFEH3k7qwsw7PYGUTwkPaThe6xyZN6D5jqxCZU3aSYu+FGb0oYo+M5IxOm0Cb4NNsvvkRPxWtwSayfFGu6+m/+/RyA3GBcAMev7AuyKN+K2vGMsLagHOx4i+5ZAcUwGzLeXAENNum3w== pabelanger@redhat.com
+    uid: 2010
+    gid: 2010
+
+  mkiss:
+    comment: Marton Kiss
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCb5qdaiKaRqBRgLW8Df+zD3C4a+gO/GFZYEDEd5nvk+LDGPuzi6s639DLqdfx6yvJ1sxxNUOOYhE/T7raDeS8m8fjk0hdVzARXraYDbckt6AELl7B16ZM4aEzjAPoSByizmfwIVkO1zP6kghyumV1kr5Nqx0hTd5/thIzgwdaGBY4I+5iqcWncuLyBCs34oTh/S+QFzjmMgoT86PrdLSsBIINx/4rb2Br2Sb6pRHmzbU+3evnytdlDFwDUPfdzoCaQEdXtjISC0xBdmnjEvHJYgmSkWMZGgRgomrA06Al9M9+2PR7x+burLVVsZf9keRoC7RYLAcryRbGMExC17skL marton.kiss@gmail.com
+    uid: 2011
+    gid: 2011
+
+  smarcet:
+    comment: Sebastian Marcet
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDP5ce0Ywtbgi3LGMZWA5Zlv/EQ07F/gWnZOMN6TRfiCiiBNyf8ARtKgmYSINS8W537HJYBt3qTfa5xkZmpBrtE6x8OTfR5y1L+x/PrLTUkQhVDY19EixD9wDIrQIIjo2ZVq+zErXBRQuGmJ3Hl+OGw+wtvGS8f768kMnwhKUgyITjWV2tKr/q88J8mBOep48XUcRhidDWsOjgIDJQeY2lbsx1bbZ7necrJS17PHqxhUbWntyR/VKKbBbrNmf2bhtTRUSYoJuqabyGDTZ0J25A88Qt2IKELy6jsVTxHj9Y5D8oH57uB7GaNsNiU+CaOcVfwOenES9mcWOr1t5zNOdrp smarcet@gmail.com
+    uid: 2012
+    gid: 2012
+
+  zaro:
+    comment: Khai Do
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJqB//ilMx7Y1tKzviAn/6yeXSRAi2VnaGN0/bfaa5Gciz+SWt8vAEAUE99fzuqeJ/ezjkuIXDFm/sjZr93y567a6sDT6CuhVUac1FZIhXRTs0J+pBOiENbwQ7RZxbkyNHQ0ndvtz3kBA1DF5D+MDkluBlIWb085Z31rFJmetsB2Zb8s1FKUjHVk/skyeKSj0qAK5KN3Wme6peWhYjwBiM0gUlxIsEZM6JLYdoPIbD5B8GYAktMN2FvJU9LgKGL93jLZ/vnMtoQIHHAG/85NdPURL1Zbi92Xlxbm4LkbcHnruBdmtPfSgaEupwJ+zFmK264OHD7QFt10ztPMbAFCFn khaido@khaido-HP-EliteBook-Folio-9470m
+    uid: 2013
+    gid: 2013
+
+  slukjanov:
+    comment: Sergey Lukjanov
+    uid: 2014
+    gid: 2014
+
+  elizabeth:
+    comment: Elizabeth K. Joseph
+    uid: 2015
+    gid: 2015
+
+  jhesketh:
+    comment: Joshua Hesketh
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC3onVLOZiiGpQWTCIV0QwHmc3Jvqyl7UaJxIu7D49OQcLHqVZsozI9pSiCdTnWyAaM+E+5wD9yVcSTqMWqn2AZmZSwQ+Fh6KnCgPZ/o63+iCZPGL0RNk20M1iNh5dvdStDnn+j2fpeV/JONF0tBn07QvNL2eF4BwtbTG9Zhl186QNsXjXDghrSO3Etl6DSfcUhxyvMoA2LnclWWD5hLmiRhcBm+PIxveVsr4B+o0k1HV5SUOvJMWtbEC37AH5I818O4fNOob6CnOFaCsbA9oUDzB5rqxutPZb9SmNJpNoLqYqDgyppM0yeql0Kn97tUt7H4j5xHrWoGnJ4IXfuDc0AMmmy4fpcLGkNf7zcBftKS6iz/3AlOXjlp5WZvKxngJj9HIir2SE/qV4Lxw9936BzvAcQyw5+bEsLQJwi+LPZxEqLC6oklkX9dg/+1yBFHsz6mulA0b4Eq7VF9omRzrhhN4iPpU5KQYPRNz7yRYckXDxYnp2lz6yHgSYh2/lqMc+UqmCL9EAWcDw3jsgvJ6kH/YUVUojiRHD9QLqlhOusu1wrTfojjwF05mqkXKmH+LH8f8AJAlMdYg0c2WLlrcxnwCkLLxzU5cYmKcZ41LuLtQR3ik+EKjYzBXXyCEzFm6qQEbR2akpXyxvONgrf7pijrgNOi0GeatUt0bUQcAONYw== jhesketh@infra
+    uid: 2016
+    gid: 2016
+
+  nibz:
+    comment: Spencer Krum
+    uid: 2017
+    gid: 2017
+
+  yolanda:
+    comment: Yolanda Robla
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSR2NmJC8PSanHUpKJuaMmohG80COO2IPkE3Mxhr7US8P1B3p1c6lOrT6M1txRzBY8FlbxfOinGtutP+ADCB2taXfpO8UiaG9eOqojAT/PeP2Y2ov72rVMSWupLozUv2uAR5yyFVFHOjKPYGAa01aJtfzfJujSak8dM0ifFeFwgp/8RBGEfC7atq+45TdrfAURRcEgcOLiF5Aq6fprCOwpllnrH6VoId9YS7u/5xF2/zBjr9PuOP7jEgCaL/+FNqu7jgj87aG5jiZPlweb7GTLJON9H6eFpyfpoJE0sZ1yR9Q+e9FAqQIA44Zi748qKBlFKbLxzoC4mc0SbNUAleEL yolanda@infra
+    uid: 2018
+    gid: 2018
+
+  rcarrillocruz:
+    comment: Ricardo Carrillo Cruz
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCz1CW5E87v8o7O8B5fe7j1uaPCToRdaBukjH2HzQZ+DSGTIPjirLpp5ZXPuyNnmtRMzwld6mlHYlevVEwuZTNyQwS7ut5o0LjyW6yoEcvPq0xMEZLxaso5dZAtzNgf3FzbtaUYBnkhSwX7c24lf8wPGAl7TC3yO0dePQh2lXVdaBiGB9ybVeQr+kwJIxleUE4puuQ+ONJE2D+hHjoQ/huUMpb996pb/YzkjkAxqHguMid0c1taelyW8n17nEDoWvlV9Qqbo8cerhgURo1OBt2zENLjQQ0kOkPxJx4qx3652e0kbkr11y50r9BMs418mnJdWselMxkSqQNZ+XotoH5Dwn+3K2a6Wv4OX3Dqb9SF/JTD7lA/tIkNfxgsRlzfEQ01rK1+g7Je10EnDCLEzHpFjvZ5q4EEMcYqY+osLFpHAOWGLMx+3eY4pz/xEzRP/x3sjGU09uNOZ3oCWUfSkE4xebnnWtxwWZKyFmv3GHtaqJn2UvpAbODPEYyYcOS3XV3zd233W3C09YYnFUyZbGLXpD05Yet5fZfGTnveMRn5/9LZai+dBPwoMWUJdX4yPnGXgOG8zk0u1nWfcNJfYg+xajSUDiMKjDhlkuFK/GXNYuINe42s1TxzL7pJ4X4UhqLiopeJvPg/U5xdCV5pxVKf1MVenrGe2pfwf1Yr2WMv5w== rcarrillocruz@infra
+    uid: 2019
+    gid: 2019
+
+  krotscheck:
+    comment: Michael Krotscheck
+    uid: 2020
+    gid: 2020
+
+  colleen:
+    comment: Colleen Murphy
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDcHzySqYlH1TfAPx5PaVzqkuMbI3zksJ5E2aZBlsIN7wNSoyO0Dts6HegHZIgi5NGT05wRBAUMCNZwupqFoWDg41JBKzPKITkqvEe/FnNmJFxt591ltXigZZ+ZLoX8B12nww/eeA5nx9PT4hIsLQG50MxEm0iC4ApusaAXMXa7+gTDkzf6yyl4QwinyFFTYtyJwFw5XfQXXRQwL8Qv6mVGrhDz3Fj4VWawByQuxRHgt5G3Ux/PnZzatJ3tuSK66o1uXrvuOiGdUtDCuAFUx+kgcmUTpCC6vgMZdDbrfyw0CGxkmAUNfeEMOw0TWbdioJ2FwH5+4BEvMgiFgsCTjIwDqqyFV9eK8sd0mbJ+I82EyOXPlFPKGan6Ie6LD1qotdUW9vT3pfpR/44s/Id2un3FBnVg7GZkGJshikGO1UqjmZfhEpQ6Q+auLir+mBv2X/ril6qJ2NuQpwMRVzZmriPMxdJDs6xhzg2fGEYRvEvh0kzsqNf4OgKbSWiVOB3WALM30Cx3YdmnB6JonRGA+6CqD+LO4HQMbD7LBVcYzEIS1WtP8aPx/NiybemrF0LWmIgl34A0Tpcc+5MLzzUtgUt6lYFyWxltCP43u1N7ODH+FsFALzo6CO9DjyMxEd6Ay61hyx8Btfhn8NH/wEdCQj1WAMHU+d2ljk5ndAfp8c6LRQ== krinkle@gir
+    uid: 2021
+    gid: 2021
+
+  Zara:
+    comment: Zara Zaimeche
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCt9wQvGgQIvLvifm7n5g+2sjgjGCQLt03D0v5Fb5xEMufJncIDkwBNDzGvsASwHGjP9YEAA8+f8Ya+Yc9EaDgqQl9r9YEO9CoEC6O1Euk41nQJYYRnzkgmMaxTSlUKNur8XSmzoElLut6ivlLW71fZmSKHAcg9O4lgd9weDDjCcWLD1C9WmRVdtEnw6NQJd5Mn/llHqdbmMlf3I5VL8QvzPndxZEyESdSBz0ywLO5ygtUxtPaCxaanHSTz1yNooT9t2vwDnfc1LB9oT4CaEnVG+FugCPGFnn204eJ2BVEQ945ZsabgFndyvfmEwxlzAeA6+YjQYrukMijb1Owxh1fv zara.zaimeche@codethink.co.uk
+    uid: 2022
+    gid: 2022
+
+  SotK:
+    comment: Adam Coldrick
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDL2oe+2lRld58OiTjpdR3yUTobWcDWaYhWpU3bWz36rQAcbtYQmCBJRF8Ec2ZazvLNrmv075k/kb18eWjBLzItorBppIlNkIazG002LsrvlME6FDrZ3MoeDiswXG8a0P0IJyUyvfald7EBkjjiCVO3CwyMdFF2fXb+oqKxrSL9nKyPZtSXAzHmq01Eqm6Jok971+C+tvk47W4w7LXy+H/1GfMJdppwIWD6fQ5NmxQp9fHowh3ztNthhEk6Vn46qGrtMru4HImIw6nVU+0tHNRgxRjn9SRTPSsYPiBKJJ90rXl7WB5Ep42hGZySdz7l0LjxXAGxZgiHso/ANPYzRgpr adam@arreliam
+    uid: 2023
+    gid: 2023
+
+  maxwell:
+    comment: JP Maxwell
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2b5I7Yff9FCrtRmSjpILUePi54Vbc8zqJTbzrIAQZGFLBi3xd2MLlhV5QVgpDBC9H3lGjbdnc81D3aFd3HwHT4dvvvyedT12PR3VDEpftdW84vw3jzdtALcayOQznjbGnScwvX5SgnRhNxuX9Rkh8qNvOsjYPUafRr9azkQoomJFkdNVI4Vb5DbLhTpt18FPeOf0UuqDt/J2tHI4SjZ3kjzr7Nbwpg8xGgANPNE0+2pJbwCA8YDt4g3bzfzvVafQs5o9Gfc9tudkR9ugQG1M+EWCgu42CleOwMTd/rYEB2fgNNPsZAWqwQfdPajVuk70EBKUEQSyoA09eEZX+xJN9Q== jpmaxman@tipit.net
+    uid: 2024
+    gid: 2024
+
+  ianw:
+    comment: Ian Wienand
+    key_type: ssh-ed25519
+    key: ssh-rsa AAAAC3NzaC1lZDI1NTE5AAAAILOjz+dkwRWTJcW9Gt3iGHSzRBsvVlTAK6G2oH3+0D41 iwienand+osinfra@redhat.com
+    uid: 2025
+    gid: 2025
+
+  shrews:
+    comment: David Shrewsbury
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCtNtbgLw0dyRVnuwZz4oUcWTzEUtpO2V47t4ykijdH1hkEe7qkuusM5bD8pC4L3wDZP5U3lsIAvZ97LCQp+MNJz1j8cjXuAboqP5FC3TtCJR1WtCWmOBSO7sIvcsgwse/9KZN/TETOGA9no1oKS43Adi9bXrRFAKDAAM34IVt/UHNS51vxUhuGv+56yJmaki7CjxrGtXcB4hi+TCQAfKJPzhAMwcFQUyvXJkRei6NN6uYyHnVtLR3KXEkeTesZ2GQxmQ+1jmCMN1zUN2VLypmDqAvlKtuQW+3nY89q4HDwzCpuC1rscJgOuncdMahTMoKA3/dQtT4WuJIwLQa3tEEn shrews2018
+    uid: 2026
+    gid: 2026
+
+  jbryce:
+    comment: Jonathan Bryce
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApFGM9q1gfiawBX5EnCQGxx2T1hwPDxrX2M64MfqcoBRpdrWRjxWm6Vhczfl+Ar2EQtGsuIm1QQiyiPL4zsJSQOfYXB0TqOQaAuFamSzZSNEm8coSa93E3zfXR9uln1lgCGutaWwH/KmGcSeAuuQCipKmKxc8QSAepGNP4Jx2L/EnXQh850xTQEIviJkJpA9oTRzXu12T7vzxsUCw041Q/KX16UvvGpt9IAoMAWFlQrMPzPFmqbUOIr7pRvv8TKcK9BNFS8S8jjT+wN0y/LY7cbTblgDfwSAl1P/naME5ugRVD5MZKixIE1F+x/j+M8+fpZ/EyR/6jSA3DYjEXOk2zQ== jbryce@jbryce-mbp-3.local
+    uid: 2027
+    gid: 2027
+
+  dmsimard:
+    comment: David Moreau-Simard
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDyXfFj44OTsJZnHbecbYrwA8zXuMiULb+o326maOh3wh5/6fk+MzivkUzJC2uZqAlKvBnNXsrb/07eV1gRjaIQBQJxaV9HQUwMNX7AkjkDzaMXVDjG/JOium90R23gVGMukzp9IamezUscAqAxVK+2C10k3tq8dZ/GeZfHl3NFGRHlIAXsJ/SIQoxJAEA0IQ/8Y50nR1Hp2mV2xsfxH9oZhLR/eiFdhJpNupdfw/oE9+vpCHS8SG88KGeLYbn+EhH6LSCD+6WNthF6oE7NANnScqn1Fl0ZpSd3RlRb+kDVKGqNxfB7EJTeimYvqaYmrTiTZTaTJua5Bj5yBTudqnBgdHCz3xMb2Nv2s2INNcJmP/CKpivYQ8AJs6cVlqRWnLJiNQQYCj+xAXBvY5T0Xq/qOhVifLiWZvZQOTHFWqFP9asZkrGa1mFWIaR9VPQY0FoYlUOT9t6J6TRbzktJIuP5AiOVoJLL6wjZuUMjghHfkYbqtyBqE4BbCY8YF3JSf8jx+9eWy+sD+dRwKXBCrGV0dNidioZR7ZJpBb6ye8wElebjPZizKhppsNpwtRxPfiAM52f55lXGD7IDpz9CZrOKUcV2uc3Rhl50u7T3psZfX7GysZvlnAH+Yr+UM+LPBAabXAfKlMnJp+SskLuOplTeQrvAwMluBmFnla8TnwnxQ== dmsimard@hostname
+    uid: 2028
+    gid: 2028
+
+  frickler:
+    comment: Jens Harbott
+    key_type: ssh-ed25519
+    key: ssh-rsa AAAAC3NzaC1lZDI1NTE5AAAAIGmc5fbzMptjAb5D86zSH13ZYCbf3QuV1jk9hL0r1qHw frickler@os-infra-2017
+    uid: 2029
+    gid: 2029
+
+  diablo_rojo:
+    comment: Kendall Nelson
+    key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCx96P1BVbRALeCz8jktUtT9qWzeXbG5yQrwQZ6n3NWsqEueCHp9DaVPDQLWIFAyvL0PKtlSOktClsUYuGfxB+dBuAFFMsx1Apk78EID4wvdXfEUDxZOsKX7zE9teJSxPEMppHAJIcnPu7dMFzZWxh+sA+fR8ZddPRunxtztGayNdYsCqDGIc9GqemjOqXDIFMIXgJLxNaHGSR56UcDHwgqmXXANkpTKsLW+U+VdNofHKpRhbXNS07jPFAAe1rBmoU/TRitzQFz7WYA4ml54ZiB7Q1O7RIyJWVBihHVrxSZbjn2a46CVeLo5Xw7loWF32wY/hA98hmpBNiF8tGSI6mh kennelson11@gmail.com
+    uid: 2030
+    gid: 2030
+
+# List of users to install on all hosts
+base_users:
+  - mordred
+  - corvus
+  - clarkb
+  - fungi
+  - jhesketh
+  - yolanda
+  - pabelanger
+  - rcarrillocruz
+  - ianw
+  - shrews
+  - dmsimard
+  - frickler
+# Default empty list of users to install on specific hosts or groups
+extra_users: []
+# Users who should be removed
+disabled_users:
+  - elizabeth
+  - nibz
+  - slukjanov
diff --git a/playbooks/roles/base-repos/defaults/main.yaml b/playbooks/roles/base-repos/defaults/main.yaml
new file mode 100644
index 0000000000..d3f6ff49b1
--- /dev/null
+++ b/playbooks/roles/base-repos/defaults/main.yaml
@@ -0,0 +1 @@
+purge_apt_sources: true
diff --git a/playbooks/roles/base-repos/files/80retry b/playbooks/roles/base-repos/files/80retry
new file mode 100644
index 0000000000..8ebe6de130
--- /dev/null
+++ b/playbooks/roles/base-repos/files/80retry
@@ -0,0 +1 @@
+APT::Acquire::Retries "20";
diff --git a/playbooks/roles/base-repos/files/90no-translations b/playbooks/roles/base-repos/files/90no-translations
new file mode 100644
index 0000000000..2318f84efe
--- /dev/null
+++ b/playbooks/roles/base-repos/files/90no-translations
@@ -0,0 +1 @@
+Acquire::Languages "none";
diff --git a/playbooks/roles/base-repos/files/sources.list.bionic.x86_64 b/playbooks/roles/base-repos/files/sources.list.bionic.x86_64
new file mode 100644
index 0000000000..3372ec5bdb
--- /dev/null
+++ b/playbooks/roles/base-repos/files/sources.list.bionic.x86_64
@@ -0,0 +1,10 @@
+# This file is kept updated by ansible, adapted from
+# https://help.ubuntu.com/lts/serverguide/configuration.html
+
+deb http://us.archive.ubuntu.com/ubuntu bionic main restricted
+deb http://us.archive.ubuntu.com/ubuntu bionic-updates main restricted
+deb http://us.archive.ubuntu.com/ubuntu bionic universe
+deb http://us.archive.ubuntu.com/ubuntu bionic-updates universe
+deb http://us.archive.ubuntu.com/ubuntu bionic-backports main restricted universe
+deb http://security.ubuntu.com/ubuntu bionic-security main restricted
+deb http://security.ubuntu.com/ubuntu bionic-security universe
diff --git a/playbooks/roles/base-repos/files/sources.list.trusty.x86_64 b/playbooks/roles/base-repos/files/sources.list.trusty.x86_64
new file mode 100644
index 0000000000..4a4b94516d
--- /dev/null
+++ b/playbooks/roles/base-repos/files/sources.list.trusty.x86_64
@@ -0,0 +1,13 @@
+# This file is kept updated by ansible, adapted from
+# http://ubuntuguide.org/wiki/Ubuntu_Trusty_Packages_and_Repositories
+
+deb http://us.archive.ubuntu.com/ubuntu trusty main restricted
+deb http://us.archive.ubuntu.com/ubuntu trusty-updates main restricted
+deb http://us.archive.ubuntu.com/ubuntu trusty universe
+deb http://us.archive.ubuntu.com/ubuntu trusty-updates universe
+deb http://us.archive.ubuntu.com/ubuntu trusty multiverse
+deb http://us.archive.ubuntu.com/ubuntu trusty-updates multiverse
+deb http://us.archive.ubuntu.com/ubuntu trusty-backports main restricted universe multiverse
+deb http://security.ubuntu.com/ubuntu trusty-security main restricted
+deb http://security.ubuntu.com/ubuntu trusty-security universe
+deb http://security.ubuntu.com/ubuntu trusty-security multiverse
diff --git a/playbooks/roles/base-repos/files/sources.list.xenial.aarch64 b/playbooks/roles/base-repos/files/sources.list.xenial.aarch64
new file mode 100644
index 0000000000..3c089f33f6
--- /dev/null
+++ b/playbooks/roles/base-repos/files/sources.list.xenial.aarch64
@@ -0,0 +1,35 @@
+# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
+# newer versions of the distribution.
+
+deb http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted multiverse
+deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted multiverse
+
+## Major bug fix updates produced after the final release of the
+## distribution.
+deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted multiverse
+deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted multiverse
+
+## Uncomment the following two lines to add software from the 'universe'
+## repository.
+## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu
+## team. Also, please note that software in universe WILL NOT receive any
+## review or updates from the Ubuntu security team.
+deb http://ports.ubuntu.com/ubuntu-ports/ xenial universe
+deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial universe
+deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates universe
+deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-updates universe
+
+## N.B. software from this repository may not have been tested as
+## extensively as that contained in the main release, although it includes
+## newer versions of some applications which may provide useful features.
+## Also, please note that software in backports WILL NOT receive any review
+## or updates from the Ubuntu security team.
+# deb http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted
+# deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted
+
+deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted multiverse
+deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted multiverse
+deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security universe
+deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security universe
+# deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security multiverse
+# deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security multiverse
\ No newline at end of file
diff --git a/playbooks/roles/base-repos/files/sources.list.xenial.x86_64 b/playbooks/roles/base-repos/files/sources.list.xenial.x86_64
new file mode 100644
index 0000000000..89e89230ad
--- /dev/null
+++ b/playbooks/roles/base-repos/files/sources.list.xenial.x86_64
@@ -0,0 +1,13 @@
+# This file is kept updated by ansible, adapted from
+# https://help.ubuntu.com/lts/serverguide/configuration.html
+
+deb http://us.archive.ubuntu.com/ubuntu xenial main restricted
+deb http://us.archive.ubuntu.com/ubuntu xenial-updates main restricted
+deb http://us.archive.ubuntu.com/ubuntu xenial universe
+deb http://us.archive.ubuntu.com/ubuntu xenial-updates universe
+deb http://us.archive.ubuntu.com/ubuntu xenial multiverse
+deb http://us.archive.ubuntu.com/ubuntu xenial-updates multiverse
+deb http://us.archive.ubuntu.com/ubuntu xenial-backports main restricted universe multiverse
+deb http://security.ubuntu.com/ubuntu xenial-security main restricted
+deb http://security.ubuntu.com/ubuntu xenial-security universe
+deb http://security.ubuntu.com/ubuntu xenial-security multiverse
diff --git a/playbooks/roles/base-repos/handlers/main.yaml b/playbooks/roles/base-repos/handlers/main.yaml
new file mode 100644
index 0000000000..603689e726
--- /dev/null
+++ b/playbooks/roles/base-repos/handlers/main.yaml
@@ -0,0 +1,3 @@
+- name: Update apt cache
+  apt:
+    update_cache: true
diff --git a/playbooks/roles/base-repos/tasks/Debian.yaml b/playbooks/roles/base-repos/tasks/Debian.yaml
new file mode 100644
index 0000000000..596691d072
--- /dev/null
+++ b/playbooks/roles/base-repos/tasks/Debian.yaml
@@ -0,0 +1,18 @@
+- name: Configure apt retries
+  copy:
+    mode: 0444
+    src: 80retry
+    dest: /etc/apt/apt.conf.d/80retry
+
+- name: Disable apt translations
+  copy:
+    mode: 0444
+    src: 90no-translations
+    dest: /etc/apt/apt.conf.d/90no-translations
+
+- name: Replace sources.list file
+  when: purge_apt_sources
+  copy:
+    src: 'sources.list.{{ ansible_facts.lsb.codename }}.{{ ansible_facts.architecture }}'
+    dest: /etc/apt/sources.list
+  notify: Update apt cache
diff --git a/playbooks/roles/base-repos/tasks/main.yaml b/playbooks/roles/base-repos/tasks/main.yaml
new file mode 100644
index 0000000000..779f0614b3
--- /dev/null
+++ b/playbooks/roles/base-repos/tasks/main.yaml
@@ -0,0 +1,6 @@
+- name: Set up additional repos
+  include_tasks: "{{ lookup('first_found', file_list) }}"
+  vars:
+    file_list:
+      - "{{ ansible_facts.distribution }}.yaml"
+      - "{{ ansible_facts.os_family }}.yaml"
diff --git a/playbooks/roles/base-server/defaults/main.yaml b/playbooks/roles/base-server/defaults/main.yaml
new file mode 100644
index 0000000000..764fac6880
--- /dev/null
+++ b/playbooks/roles/base-server/defaults/main.yaml
@@ -0,0 +1,12 @@
+bastion_ipv4: 23.253.245.198
+bastion_ipv6: 2001:4800:7818:101:3c21:a454:23ed:4072
+base_packages:
+  - at
+  - git
+  - lvm2
+  - parted
+  - rsync
+  - rsyslog
+  - strace
+  - tcpdump
+  - wget
diff --git a/playbooks/roles/base-server/files/bash-history.sh b/playbooks/roles/base-server/files/bash-history.sh
new file mode 100644
index 0000000000..e3f56e6e65
--- /dev/null
+++ b/playbooks/roles/base-server/files/bash-history.sh
@@ -0,0 +1 @@
+export HISTTIMEFORMAT="%Y-%m-%dT%T%z "
diff --git a/playbooks/roles/base-server/files/debian_limits.conf b/playbooks/roles/base-server/files/debian_limits.conf
new file mode 100644
index 0000000000..860c08b8d8
--- /dev/null
+++ b/playbooks/roles/base-server/files/debian_limits.conf
@@ -0,0 +1,4 @@
+# Original 1024
+*                soft   nofile           4096
+# Original 4096
+*                hard   nofile           8192
diff --git a/playbooks/roles/base-server/files/rsyslog.d_50-default.conf b/playbooks/roles/base-server/files/rsyslog.d_50-default.conf
new file mode 100644
index 0000000000..35e348ab1b
--- /dev/null
+++ b/playbooks/roles/base-server/files/rsyslog.d_50-default.conf
@@ -0,0 +1,69 @@
+#  Default rules for rsyslog.
+#
+#			For more information see rsyslog.conf(5) and /etc/rsyslog.conf
+
+#
+# First some standard log files.  Log by facility.
+#
+auth,authpriv.*			/var/log/auth.log
+*.*;auth,authpriv.none		-/var/log/syslog
+#cron.*				/var/log/cron.log
+#daemon.*			-/var/log/daemon.log
+kern.*				-/var/log/kern.log
+#lpr.*				-/var/log/lpr.log
+mail.*				-/var/log/mail.log
+#user.*				-/var/log/user.log
+
+#
+# Logging for the mail system.  Split it up so that
+# it is easy to write scripts to parse these files.
+#
+#mail.info			-/var/log/mail.info
+#mail.warn			-/var/log/mail.warn
+mail.err			/var/log/mail.err
+
+#
+# Logging for INN news system.
+#
+news.crit			/var/log/news/news.crit
+news.err			/var/log/news/news.err
+news.notice			-/var/log/news/news.notice
+
+#
+# Some "catch-all" log files.
+#
+#*.=debug;\
+#	auth,authpriv.none;\
+#	news.none;mail.none	-/var/log/debug
+#*.=info;*.=notice;*.=warn;\
+#	auth,authpriv.none;\
+#	cron,daemon.none;\
+#	mail,news.none		-/var/log/messages
+
+#
+# Emergencies are sent to everybody logged in.
+#
+*.emerg                                :omusrmsg:*
+
+#
+# I like to have messages displayed on the console, but only on a virtual
+# console I usually leave idle.
+#
+#daemon,mail.*;\
+#	news.=crit;news.=err;news.=notice;\
+#	*.=debug;*.=info;\
+#	*.=notice;*.=warn	/dev/tty8
+
+# The named pipe /dev/xconsole is for the `xconsole' utility.  To use it,
+# you must invoke `xconsole' with the `-file' option:
+#
+#    $ xconsole -file /dev/xconsole [...]
+#
+# NOTE: adjust the list below, or you'll go crazy if you have a reasonably
+#      busy site..
+#
+# Commenting out since we don't install xconsoles on headless servers.
+#daemon.*;mail.*;\
+#	news.err;\
+#	*.=debug;*.=info;\
+#	*.=notice;*.=warn	|/dev/xconsole
diff --git a/playbooks/roles/base-server/files/yum/yum-cron.conf b/playbooks/roles/base-server/files/yum/yum-cron.conf
new file mode 100644
index 0000000000..bd1ec68583
--- /dev/null
+++ b/playbooks/roles/base-server/files/yum/yum-cron.conf
@@ -0,0 +1,81 @@
+[commands]
+#  What kind of update to use:
+# default                            = yum upgrade
+# security                           = yum --security upgrade
+# security-severity:Critical         = yum --sec-severity=Critical upgrade
+# minimal                            = yum --bugfix update-minimal
+# minimal-security                   = yum --security update-minimal
+# minimal-security-severity:Critical =  --sec-severity=Critical update-minimal
+update_cmd = default
+
+# Whether a message should be emitted when updates are available,
+# were downloaded, or applied.
+update_messages = yes
+
+# Whether updates should be downloaded when they are available.
+download_updates = yes
+
+# Whether updates should be applied when they are available.  Note
+# that download_updates must also be yes for the update to be applied.
+apply_updates = yes
+
+# Maximum amout of time to randomly sleep, in minutes.  The program
+# will sleep for a random amount of time between 0 and random_sleep
+# minutes before running.  This is useful for e.g. staggering the
+# times that multiple systems will access update servers.  If
+# random_sleep is 0 or negative, the program will run immediately.
+# 6*60 = 360
+random_sleep = 360
+
+
+[emitters]
+# Name to use for this system in messages that are emitted.  If
+# system_name is None, the hostname will be used.
+system_name = None
+
+# How to send messages.  Valid options are stdio and email.  If
+# emit_via includes stdio, messages will be sent to stdout; this is useful
+# to have cron send the messages.  If emit_via includes email, this
+# program will send email itself according to the configured options.
+# If emit_via is None or left blank, no messages will be sent.
+emit_via = stdio
+
+# The width, in characters, that messages that are emitted should be
+# formatted to.
+output_width = 80
+
+
+[email]
+# The address to send email messages from.
+# NOTE: 'localhost' will be replaced with the value of system_name.
+email_from = root@localhost
+
+# List of addresses to send messages to.
+email_to = root
+
+# Name of the host to connect to to send email messages.
+email_host = localhost
+
+
+[groups]
+# NOTE: This only works when group_command != objects, which is now the default
+# List of groups to update
+group_list = None
+
+# The types of group packages to install
+group_package_types = mandatory, default
+
+[base]
+# This section overrides yum.conf
+
+# Use this to filter Yum core messages
+# -4: critical
+# -3: critical+errors
+# -2: critical+errors+warnings (default)
+debuglevel = -2
+
+# skip_broken = True
+mdpolicy = group:main
+
+# Uncomment to auto-import new gpg keys (dangerous)
+# assumeyes = True
diff --git a/playbooks/roles/base-server/handlers/main.yaml b/playbooks/roles/base-server/handlers/main.yaml
new file mode 100644
index 0000000000..a80bac59a0
--- /dev/null
+++ b/playbooks/roles/base-server/handlers/main.yaml
@@ -0,0 +1,4 @@
+- name: Restart rsyslog
+  service:
+    name: rsyslog
+    state: restarted
diff --git a/playbooks/roles/base-server/tasks/Debian.yaml b/playbooks/roles/base-server/tasks/Debian.yaml
new file mode 100644
index 0000000000..6d81390f5f
--- /dev/null
+++ b/playbooks/roles/base-server/tasks/Debian.yaml
@@ -0,0 +1,20 @@
+- name: Remove packages that make no sense for servers
+  apt:
+    name: '{{ item }}'
+    state: absent
+  loop:
+    - whoopsie
+    - popularity-contest
+
+- name: Configure file limits
+  copy:
+    mode: 0644
+    src: debian_limits.conf
+    dest: /etc/security/limits.d/60-nofile-limit.conf
+
+- name: Custom rsyslog config to disable /dev/xconsole noise
+  copy:
+    mode: 0644
+    src: rsyslog.d_50-default.conf
+    dest: /etc/rsyslog.d/50-default.conf
+  notify: Restart rsyslog
diff --git a/playbooks/roles/base-server/tasks/RedHat.yaml b/playbooks/roles/base-server/tasks/RedHat.yaml
new file mode 100644
index 0000000000..f747f1982b
--- /dev/null
+++ b/playbooks/roles/base-server/tasks/RedHat.yaml
@@ -0,0 +1,22 @@
+# NOTE(pabelanger): We need to ensure ntpdate service starts on boot for
+# centos-7.  Currently, ntpd explicitly require ntpdate to be running before
+# the sync process can happen in ntpd.  As a result, if ntpdate is not
+# running, ntpd will start but fail to sync because of DNS is not properly
+# setup.
+- name: Ensure ntpdate service is running
+  service:
+    name: ntpdate
+    enabled: yes
+    state: running
+
+- name: Configure yum cron
+  copy:
+    mode: 0644
+    src: yum/yum-cron.conf
+    dest: /etc/yum/yum-cron.conf
+
+- name: Ensure yum cron service is running
+  service:
+    name: yum-cron
+    enabled: yes
+    state: running
diff --git a/playbooks/roles/base-server/tasks/Ubuntu.xenial.aarch64.yaml b/playbooks/roles/base-server/tasks/Ubuntu.xenial.aarch64.yaml
new file mode 100644
index 0000000000..41e603e9d6
--- /dev/null
+++ b/playbooks/roles/base-server/tasks/Ubuntu.xenial.aarch64.yaml
@@ -0,0 +1,4 @@
+- name: Install HWE kernel for arm64
+  apt:
+    name: linux-generic-hwe-16.04
+    state: present
diff --git a/playbooks/roles/base-server/tasks/main.yaml b/playbooks/roles/base-server/tasks/main.yaml
new file mode 100644
index 0000000000..00476ab18d
--- /dev/null
+++ b/playbooks/roles/base-server/tasks/main.yaml
@@ -0,0 +1,62 @@
+- name: Install base packages
+  package:
+    state: present
+    name: '{{ item }}'
+  loop: '{{ base_packages }}'
+
+- name: Include OS-specific variables
+  include_vars: "{{ item }}"
+  with_first_found:
+    - "{{ ansible_facts.distribution }}.{{ ansible_facts.architecture }}.yaml"
+    - "{{ ansible_facts.distribution }}.yaml"
+    - "{{ ansible_facts.os_family }}.yaml"
+    - "default.yaml"
+
+- name: Install distro specific packages
+  package:
+    state: present
+    name: '{{ item }}'
+  loop: '{{ distro_packages }}'
+
+- name: Increase syslog message size in order to capture python tracebacks
+  copy:
+    content: '$MaxMessageSize 6k'
+    dest: /etc/rsyslog.d/99-maxsize.conf
+    mode: 0644
+  notify: Restart rsyslog
+
+- name: Ensure rsyslog is running
+  service:
+    name: rsyslog
+    enabled: yes
+    state: started
+
+- name: Set ssh key for managment
+  authorized_key:
+    state: present
+    user: root
+    key: |
+      ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSLlN41ftgxkNeUi/kATYPwMPjJdMaSbgokSb9PSkRPZE7GeNai60BCfhu+ky8h5eMe70Bpwb7mQ7GAtHGXPNU1SRBPhMuVN9EYrQbt5KSiwuiTXtQHsWyYrSKtB+XGbl2PhpMQ/TPVtFoL5usxu/MYaakVkCEbt5IbPYNg88/NKPixicJuhi0qsd+l1X1zoc1+Fn87PlwMoIgfLIktwaL8hw9mzqr+pPcDIjCFQQWnjqJVEObOcMstBT20XwKj/ymiH+6p123nnlIHilACJzXhmIZIZO+EGkNF7KyXpcBSfv9efPI+VCE2TOv/scJFdEHtDFkl2kdUBYPC0wQ92rp puppet-remote-2014-09-15
+    key_options: |
+      from="{{ bastion_ipv4 }}:{{ bastion_ipv6 }},localhost"
+
+- name: Disable byobu
+  file:
+    path: /etc/profile.d/Z98-byobu.sh
+    state: absent
+
+- name: Setup RFC3339 bash history timestamps
+  copy:
+    mode: 0644
+    src: bash-history.sh
+    dest: /etc/profile.d/bash-history.sh
+
+- name: Include OS-specific tasks
+  include_tasks: "{{ lookup('first_found', file_list) }}"
+  static: no
+  vars:
+    file_list:
+      - "{{ ansible_facts.distribution }}.{{ ansible_facts.lsb.codename }}.{{ ansible_facts.architecture }}.yaml"
+      - "{{ ansible_facts.distribution }}.{{ ansible_facts.architecture }}.yaml"
+      - "{{ ansible_facts.distribution }}.yaml"
+      - "{{ ansible_facts.os_family }}.yaml"
diff --git a/playbooks/roles/base-server/vars/Debian.yaml b/playbooks/roles/base-server/vars/Debian.yaml
new file mode 100644
index 0000000000..d6e870666f
--- /dev/null
+++ b/playbooks/roles/base-server/vars/Debian.yaml
@@ -0,0 +1,3 @@
+distro_packages:
+  - dnsutils
+  - iputils-ping
diff --git a/playbooks/roles/base-server/vars/RedHat.yaml b/playbooks/roles/base-server/vars/RedHat.yaml
new file mode 100644
index 0000000000..b7c79ba837
--- /dev/null
+++ b/playbooks/roles/base-server/vars/RedHat.yaml
@@ -0,0 +1,9 @@
+- distro_packages:
+  - bind-utils
+  - iputils
+  # Utils in ntp-perl are included in Debian's ntp package; we
+  # add it here for consistency.  See also
+  # https://tickets.puppetlabs.com/browse/MODULES-3660
+  - ntp-perl
+  - ntpdate
+  - yum-cron
diff --git a/playbooks/roles/set_hostname/tasks/main.yml b/playbooks/roles/set-hostname/tasks/main.yml
similarity index 100%
rename from playbooks/roles/set_hostname/tasks/main.yml
rename to playbooks/roles/set-hostname/tasks/main.yml
diff --git a/playbooks/roles/set_hostname/templates/hosts.j2 b/playbooks/roles/set-hostname/templates/hosts.j2
similarity index 100%
rename from playbooks/roles/set_hostname/templates/hosts.j2
rename to playbooks/roles/set-hostname/templates/hosts.j2
diff --git a/playbooks/roles/set_hostname/templates/mailname.j2 b/playbooks/roles/set-hostname/templates/mailname.j2
similarity index 100%
rename from playbooks/roles/set_hostname/templates/mailname.j2
rename to playbooks/roles/set-hostname/templates/mailname.j2
diff --git a/playbooks/roles/users/defaults/main.yaml b/playbooks/roles/users/defaults/main.yaml
new file mode 100644
index 0000000000..1256d6b56a
--- /dev/null
+++ b/playbooks/roles/users/defaults/main.yaml
@@ -0,0 +1,3 @@
+all_users: {}
+disabled_users: []
+extra_users: []
diff --git a/playbooks/roles/users/files/Debian/login.defs b/playbooks/roles/users/files/Debian/login.defs
new file mode 100644
index 0000000000..c7d5a15b4f
--- /dev/null
+++ b/playbooks/roles/users/files/Debian/login.defs
@@ -0,0 +1,340 @@
+#
+# /etc/login.defs - Configuration control definitions for the login package.
+#
+# Three items must be defined:  MAIL_DIR, ENV_SUPATH, and ENV_PATH.
+# If unspecified, some arbitrary (and possibly incorrect) value will
+# be assumed.  All other items are optional - if not specified then
+# the described action or option will be inhibited.
+#
+# Comment lines (lines beginning with "#") and blank lines are ignored.
+#
+# Modified for Linux.  --marekm
+
+# REQUIRED for useradd/userdel/usermod
+#   Directory where mailboxes reside, _or_ name of file, relative to the
+#   home directory.  If you _do_ define MAIL_DIR and MAIL_FILE,
+#   MAIL_DIR takes precedence.
+#
+#   Essentially:
+#      - MAIL_DIR defines the location of users mail spool files
+#        (for mbox use) by appending the username to MAIL_DIR as defined
+#        below.
+#      - MAIL_FILE defines the location of the users mail spool files as the
+#        fully-qualified filename obtained by prepending the user home
+#        directory before $MAIL_FILE
+#
+# NOTE: This is no more used for setting up users MAIL environment variable
+#       which is, starting from shadow 4.0.12-1 in Debian, entirely the
+#       job of the pam_mail PAM modules
+#       See default PAM configuration files provided for
+#       login, su, etc.
+#
+# This is a temporary situation: setting these variables will soon
+# move to /etc/default/useradd and the variables will then be
+# no more supported
+MAIL_DIR        /var/mail
+#MAIL_FILE      .mail
+
+#
+# Enable logging and display of /var/log/faillog login failure info.
+# This option conflicts with the pam_tally PAM module.
+#
+FAILLOG_ENAB		yes
+
+#
+# Enable display of unknown usernames when login failures are recorded.
+#
+# WARNING: Unknown usernames may become world readable.
+# See #290803 and #298773 for details about how this could become a security
+# concern
+LOG_UNKFAIL_ENAB	no
+
+#
+# Enable logging of successful logins
+#
+LOG_OK_LOGINS		no
+
+#
+# Enable "syslog" logging of su activity - in addition to sulog file logging.
+# SYSLOG_SG_ENAB does the same for newgrp and sg.
+#
+SYSLOG_SU_ENAB		yes
+SYSLOG_SG_ENAB		yes
+
+#
+# If defined, all su activity is logged to this file.
+#
+#SULOG_FILE	/var/log/sulog
+
+#
+# If defined, file which maps tty line to TERM environment parameter.
+# Each line of the file is in a format something like "vt100  tty01".
+#
+#TTYTYPE_FILE	/etc/ttytype
+
+#
+# If defined, login failures will be logged here in a utmp format
+# last, when invoked as lastb, will read /var/log/btmp, so...
+#
+FTMP_FILE	/var/log/btmp
+
+#
+# If defined, the command name to display when running "su -".  For
+# example, if this is defined as "su" then a "ps" will display the
+# command is "-su".  If not defined, then "ps" would display the
+# name of the shell actually being run, e.g. something like "-sh".
+#
+SU_NAME		su
+
+#
+# If defined, file which inhibits all the usual chatter during the login
+# sequence.  If a full pathname, then hushed mode will be enabled if the
+# user's name or shell are found in the file.  If not a full pathname, then
+# hushed mode will be enabled if the file exists in the user's home directory.
+#
+HUSHLOGIN_FILE	.hushlogin
+#HUSHLOGIN_FILE	/etc/hushlogins
+
+#
+# *REQUIRED*  The default PATH settings, for superuser and normal users.
+#
+# (they are minimal, add the rest in the shell startup files)
+ENV_SUPATH	PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ENV_PATH	PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
+
+#
+# Terminal permissions
+#
+#	TTYGROUP	Login tty will be assigned this group ownership.
+#	TTYPERM		Login tty will be set to this permission.
+#
+# If you have a "write" program which is "setgid" to a special group
+# which owns the terminals, define TTYGROUP to the group number and
+# TTYPERM to 0620.  Otherwise leave TTYGROUP commented out and assign
+# TTYPERM to either 622 or 600.
+#
+# In Debian /usr/bin/bsd-write or similar programs are setgid tty
+# However, the default and recommended value for TTYPERM is still 0600
+# to not allow anyone to write to anyone else console or terminal
+
+# Users can still allow other people to write them by issuing
+# the "mesg y" command.
+
+TTYGROUP	tty
+TTYPERM		0600
+
+#
+# Login configuration initializations:
+#
+#	ERASECHAR	Terminal ERASE character ('\010' = backspace).
+#	KILLCHAR	Terminal KILL character ('\025' = CTRL/U).
+#	UMASK		Default "umask" value.
+#
+# The ERASECHAR and KILLCHAR are used only on System V machines.
+#
+# UMASK is the default umask value for pam_umask and is used by
+# useradd and newusers to set the mode of the new home directories.
+# 022 is the "historical" value in Debian for UMASK
+# 027, or even 077, could be considered better for privacy
+# There is no One True Answer here : each sysadmin must make up his/her
+# mind.
+#
+# If USERGROUPS_ENAB is set to "yes", that will modify this UMASK default value
+# for private user groups, i. e. the uid is the same as gid, and username is
+# the same as the primary group name: for these, the user permissions will be
+# used as group permissions, e. g. 022 will become 002.
+#
+# Prefix these values with "0" to get octal, "0x" to get hexadecimal.
+#
+ERASECHAR	0177
+KILLCHAR	025
+UMASK		022
+
+#
+# Password aging controls:
+#
+#	PASS_MAX_DAYS	Maximum number of days a password may be used.
+#	PASS_MIN_DAYS	Minimum number of days allowed between password changes.
+#	PASS_WARN_AGE	Number of days warning given before a password expires.
+#
+PASS_MAX_DAYS	99999
+PASS_MIN_DAYS	0
+PASS_WARN_AGE	7
+
+#
+# Min/max values for automatic uid selection in useradd
+#
+SYS_UID_MAX		  999
+UID_MIN			 3000
+UID_MAX			60000
+# System accounts
+#SYS_UID_MIN		  100
+#SYS_UID_MAX		  999
+
+#
+# Min/max values for automatic gid selection in groupadd
+#
+SYS_GID_MAX		  999
+GID_MIN			 3000
+GID_MAX			60000
+# System accounts
+#SYS_GID_MIN		  100
+#SYS_GID_MAX		  999
+
+#
+# Max number of login retries if password is bad. This will most likely be
+# overriden by PAM, since the default pam_unix module has it's own built
+# in of 3 retries. However, this is a safe fallback in case you are using
+# an authentication module that does not enforce PAM_MAXTRIES.
+#
+LOGIN_RETRIES		5
+
+#
+# Max time in seconds for login
+#
+LOGIN_TIMEOUT		60
+
+#
+# Which fields may be changed by regular users using chfn - use
+# any combination of letters "frwh" (full name, room number, work
+# phone, home phone).  If not defined, no changes are allowed.
+# For backward compatibility, "yes" = "rwh" and "no" = "frwh".
+#
+CHFN_RESTRICT		rwh
+
+#
+# Should login be allowed if we can't cd to the home directory?
+# Default in no.
+#
+DEFAULT_HOME	yes
+
+#
+# If defined, this command is run when removing a user.
+# It should remove any at/cron/print jobs etc. owned by
+# the user to be removed (passed as the first argument).
+#
+#USERDEL_CMD	/usr/sbin/userdel_local
+
+#
+# Enable setting of the umask group bits to be the same as owner bits
+# (examples: 022 -> 002, 077 -> 007) for non-root users, if the uid is
+# the same as gid, and username is the same as the primary group name.
+#
+# If set to yes, userdel will remove the user“s group if it contains no
+# more members, and useradd will create by default a group with the name
+# of the user.
+#
+USERGROUPS_ENAB yes
+
+#
+# Instead of the real user shell, the program specified by this parameter
+# will be launched, although its visible name (argv[0]) will be the shell's.
+# The program may do whatever it wants (logging, additional authentification,
+# banner, ...) before running the actual shell.
+#
+# FAKE_SHELL /bin/fakeshell
+
+#
+# If defined, either full pathname of a file containing device names or
+# a ":" delimited list of device names.  Root logins will be allowed only
+# upon these devices.
+#
+# This variable is used by login and su.
+#
+#CONSOLE	/etc/consoles
+#CONSOLE	console:tty01:tty02:tty03:tty04
+
+#
+# List of groups to add to the user's supplementary group set
+# when logging in on the console (as determined by the CONSOLE
+# setting).  Default is none.
+#
+# Use with caution - it is possible for users to gain permanent
+# access to these groups, even when not logged in on the console.
+# How to do it is left as an exercise for the reader...
+#
+# This variable is used by login and su.
+#
+#CONSOLE_GROUPS		floppy:audio:cdrom
+
+#
+# If set to "yes", new passwords will be encrypted using the MD5-based
+# algorithm compatible with the one used by recent releases of FreeBSD.
+# It supports passwords of unlimited length and longer salt strings.
+# Set to "no" if you need to copy encrypted passwords to other systems
+# which don't understand the new algorithm.  Default is "no".
+#
+# This variable is deprecated. You should use ENCRYPT_METHOD.
+#
+#MD5_CRYPT_ENAB	no
+
+#
+# If set to MD5 , MD5-based algorithm will be used for encrypting password
+# If set to SHA256, SHA256-based algorithm will be used for encrypting password
+# If set to SHA512, SHA512-based algorithm will be used for encrypting password
+# If set to DES, DES-based algorithm will be used for encrypting password (default)
+# Overrides the MD5_CRYPT_ENAB option
+#
+# Note: It is recommended to use a value consistent with
+# the PAM modules configuration.
+#
+ENCRYPT_METHOD SHA512
+
+#
+# Only used if ENCRYPT_METHOD is set to SHA256 or SHA512.
+#
+# Define the number of SHA rounds.
+# With a lot of rounds, it is more difficult to brute forcing the password.
+# But note also that it more CPU resources will be needed to authenticate
+# users.
+#
+# If not specified, the libc will choose the default number of rounds (5000).
+# The values must be inside the 1000-999999999 range.
+# If only one of the MIN or MAX values is set, then this value will be used.
+# If MIN > MAX, the highest value will be used.
+#
+# SHA_CRYPT_MIN_ROUNDS 5000
+# SHA_CRYPT_MAX_ROUNDS 5000
+
+################# OBSOLETED BY PAM ##############
+#						#
+# These options are now handled by PAM. Please	#
+# edit the appropriate file in /etc/pam.d/ to	#
+# enable the equivelants of them.
+#
+###############
+
+#MOTD_FILE
+#DIALUPS_CHECK_ENAB
+#LASTLOG_ENAB
+#MAIL_CHECK_ENAB
+#OBSCURE_CHECKS_ENAB
+#PORTTIME_CHECKS_ENAB
+#SU_WHEEL_ONLY
+#CRACKLIB_DICTPATH
+#PASS_CHANGE_TRIES
+#PASS_ALWAYS_WARN
+#ENVIRON_FILE
+#NOLOGINS_FILE
+#ISSUE_FILE
+#PASS_MIN_LEN
+#PASS_MAX_LEN
+#ULIMIT
+#ENV_HZ
+#CHFN_AUTH
+#CHSH_AUTH
+#FAIL_DELAY
+
+################# OBSOLETED #######################
+#						  #
+# These options are no more handled by shadow.    #
+#                                                 #
+# Shadow utilities will display a warning if they #
+# still appear.                                   #
+#                                                 #
+###################################################
+
+# CLOSE_SESSIONS
+# LOGIN_STRING
+# NO_PASSWORD_CONSOLE
+# QMAIL_DIR
diff --git a/playbooks/roles/users/files/RedHat/login.defs b/playbooks/roles/users/files/RedHat/login.defs
new file mode 100644
index 0000000000..b3f6e5934e
--- /dev/null
+++ b/playbooks/roles/users/files/RedHat/login.defs
@@ -0,0 +1,69 @@
+#
+# Please note that the parameters in this configuration file control the
+# behavior of the tools from the shadow-utils component. None of these
+# tools uses the PAM mechanism, and the utilities that use PAM (such as the
+# passwd command) should therefore be configured elsewhere. Refer to
+# /etc/pam.d/system-auth for more information.
+#
+
+# *REQUIRED*
+#   Directory where mailboxes reside, _or_ name of file, relative to the
+#   home directory.  If you _do_ define both, MAIL_DIR takes precedence.
+#   QMAIL_DIR is for Qmail
+#
+#QMAIL_DIR	Maildir
+MAIL_DIR	/var/spool/mail
+#MAIL_FILE	.mail
+
+# Password aging controls:
+#
+#	PASS_MAX_DAYS	Maximum number of days a password may be used.
+#	PASS_MIN_DAYS	Minimum number of days allowed between password changes.
+#	PASS_MIN_LEN	Minimum acceptable password length.
+#	PASS_WARN_AGE	Number of days warning given before a password expires.
+#
+PASS_MAX_DAYS	99999
+PASS_MIN_DAYS	0
+PASS_MIN_LEN	5
+PASS_WARN_AGE	7
+
+#
+# Min/max values for automatic uid selection in useradd
+#
+SYS_UID_MIN		  201
+SYS_UID_MAX		  499
+UID_MIN			 3000
+UID_MAX			60000
+
+#
+# Min/max values for automatic gid selection in groupadd
+#
+SYS_GID_MIN		  201
+SYS_GID_MAX		  499
+GID_MIN			 3000
+GID_MAX			60000
+
+#
+# If defined, this command is run when removing a user.
+# It should remove any at/cron/print jobs etc. owned by
+# the user to be removed (passed as the first argument).
+#
+#USERDEL_CMD	/usr/sbin/userdel_local
+
+#
+# If useradd should create home directories for users by default
+# On RH systems, we do. This option is overridden with the -m flag on
+# useradd command line.
+#
+CREATE_HOME	yes
+
+# The permission mask is initialized to this value. If not specified,
+# the permission mask will be initialized to 022.
+UMASK           077
+
+# This enables userdel to remove user groups if no members exist.
+#
+USERGROUPS_ENAB yes
+
+# Use SHA512 to encrypt password.
+ENCRYPT_METHOD SHA512
diff --git a/playbooks/roles/users/tasks/main.yaml b/playbooks/roles/users/tasks/main.yaml
new file mode 100644
index 0000000000..9e33ce06df
--- /dev/null
+++ b/playbooks/roles/users/tasks/main.yaml
@@ -0,0 +1,49 @@
+- name: Setup login.defs file
+  copy:
+    dest: /etc/login.defs
+    src: '{{ ansible_facts.os_family }}/login.defs'
+    owner: root
+    group: root
+    mode: 0644
+
+- name: Delete old users
+  loop: "{{ disabled_users }}"
+  user:
+    name: "{{ item }}"
+    state: absent
+    remove: yes
+
+- name: Add groups
+  loop: "{{ base_users + extra_users }}"
+  group:
+    name: "{{ item }}"
+    state: present
+    gid: "{{ all_users[item].gid|default(omit) }}"
+  when:
+    - item in all_users
+    - "'gid' in all_users[item]"
+
+- name: Add users
+  loop: "{{ base_users + extra_users }}"
+  user:
+    name: "{{ item }}"
+    state: present
+    uid: "{{ all_users[item].uid }}"
+    group: "{{ item }}"
+    comment: "{{ all_users[item].comment }}"
+    groups: admin,sudo
+    shell: /bin/bash
+  when:
+    - item in all_users
+    - "'uid' in all_users[item]"
+
+- name: Add ssh keys to users
+  loop: "{{ base_users + extra_users }}"
+  authorized_key:
+    user: "{{ item }}"
+    state: present
+    key: "{{ all_users[item].key }}"
+    exclusive: yes
+  when:
+    - item in all_users
+    - "'key' in all_users[item]"
diff --git a/playbooks/set-hostnames.yaml b/playbooks/set-hostnames.yaml
new file mode 100644
index 0000000000..c781b5fb38
--- /dev/null
+++ b/playbooks/set-hostnames.yaml
@@ -0,0 +1,4 @@
+- hosts: "{{ target }}"
+  user: root
+  roles:
+    - set-hostname
diff --git a/playbooks/set_hostnames.yml b/playbooks/set_hostnames.yml
deleted file mode 100644
index 05947d09ae..0000000000
--- a/playbooks/set_hostnames.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-# file: set_hostnames.yml
-- hosts: "{{ target }}"
-  user: root
-  roles:
-    - { role: set_hostname }