← Back to team overview

canonical-ubuntu-qa team mailing list archive

[Merge] ~hyask/autopkgtest-cloud:skia/submit_check_membership_recursive into autopkgtest-cloud:master

 

Skia has proposed merging ~hyask/autopkgtest-cloud:skia/submit_check_membership_recursive into autopkgtest-cloud:master.

Requested reviews:
  Canonical's Ubuntu QA (canonical-ubuntu-qa)

For more details, see:
https://code.launchpad.net/~hyask/autopkgtest-cloud/+git/autopkgtest-cloud/+merge/476975

When submitting a test request, users were checked only for direct membership, but indirect membership is actually very commonly used.
-- 
Your team Canonical's Ubuntu QA is requested to review the proposed merge of ~hyask/autopkgtest-cloud:skia/submit_check_membership_recursive into autopkgtest-cloud:master.
diff --git a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/filter-amqp b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/filter-amqp
index a6f7f09..c5bec9c 100755
--- a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/filter-amqp
+++ b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/filter-amqp
@@ -1,35 +1,54 @@
 #!/usr/bin/python3
 # Filter out AMQP requests that match a given regex
 
+import argparse
 import configparser
 import logging
-import optparse  # pylint: disable=deprecated-module
+import os
 import re
 import sys
 import time
-import urllib.parse
 
-import amqplib.client_0_8 as amqp
+import amqp
 import distro_info
 
 
-def filter_amqp(options, host, queue_name, regex):
-    url_parts = urllib.parse.urlsplit(host, allow_fragments=False)
-    filter_re = re.compile(regex.encode("UTF-8"), re.DOTALL)
-    amqp_con = amqp.Connection(
-        url_parts.hostname,
-        userid=url_parts.username,
-        password=url_parts.password,
-    )
-    ch = amqp_con.channel()
+def get_amqp_channel():
+    try:
+        cp = configparser.ConfigParser()
+        with open("/home/ubuntu/rabbitmq.cred", "r") as f:
+            cp.read_string("[rabbit]\n" + f.read().replace('"', ""))
+        amqp_con = amqp.Connection(
+            cp["rabbit"]["RABBIT_HOST"],
+            cp["rabbit"]["RABBIT_USER"],
+            cp["rabbit"]["RABBIT_PASSWORD"],
+        )
+    except (FileNotFoundError, KeyError):
+        amqp_con = amqp.Connection(
+            os.environ["RABBIT_HOST"],
+            userid=os.environ["RABBIT_USER"],
+            password=os.environ["RABBIT_PASSWORD"],
+        )
+    amqp_con.connect()
+    return amqp_con, amqp_con.channel()
+
+
+def filter_amqp(options, queue_name, regex):
     num_items_deleted = 0
+    filter_re = re.compile(regex.encode("UTF-8"), re.DOTALL)
+
+    amqp_con, channel = get_amqp_channel()
 
     while True:
-        r = ch.basic_get(queue_name)
+        try:
+            r = channel.basic_get(queue_name)
+        except amqp.NotFound:
+            logging.warning(f"Queue {queue_name} not found")
+            return None
         if r is None:
-            logging.debug("r is none, exiting")
-            ch.close()
-            amqp_con.close()
+            logging.info(
+                "Message empty, we probably reached the end of the queue"
+            )
             break
         if isinstance(r.body, str):
             body = r.body.encode("UTF-8")
@@ -45,8 +64,10 @@ def filter_amqp(options, host, queue_name, regex):
                 logging.info("queue item: %s (would delete)", body)
             else:
                 logging.info("queue item: %s (deleting)", body)
-                ch.basic_ack(r.delivery_tag)
+                channel.basic_ack(r.delivery_tag)
                 num_items_deleted += 1
+    channel.close()
+    amqp_con.close()
     return num_items_deleted
 
 
@@ -69,25 +90,34 @@ def generate_queue_names():
 
 
 def main():
-    parser = optparse.OptionParser(
-        usage="usage: %prog [options] queue_name regex\n"
-        "Pass `all` for queue_name to filter all queues"
+    parser = argparse.ArgumentParser(
+        description="""Filter queue based on a regex
+
+This script can be used whenever a new upload happens, obsoleting a previous
+one, and that previous upload still had a lot of tests scheduled. To avoid
+processing useless jobs, the queue can be filtered on the trigger of the
+obsolete upload.
+
+If ~/rabbitmq.cred is not present, this script will load credentials from
+$RABBIT_HOST, $RABBIT_USER, and $RABBIT_PASSWORD environment variables.
+""",
+        formatter_class=argparse.RawTextHelpFormatter,
     )
-    parser.add_option(
+    parser.add_argument(
         "-n",
         "--dry-run",
         default=False,
         action="store_true",
         help="only show the operations that would be performed",
     )
-    parser.add_option(
+    parser.add_argument(
         "-v",
         "--verbose",
         default=False,
         action="store_true",
         help="additionally show queue items that are not removed",
     )
-    parser.add_option(
+    parser.add_argument(
         "-a",
         "--all-items-in-queue",
         default=False,
@@ -97,42 +127,47 @@ def main():
             "When using this option, the provided regex will be ignored."
         ),
     )
-    cp = configparser.ConfigParser()
-    with open("/home/ubuntu/rabbitmq.cred", "r") as f:
-        cp.read_string("[rabbit]\n" + f.read().replace('"', ""))
-    creds = "amqp://%s:%s@%s" % (
-        cp["rabbit"]["RABBIT_USER"],
-        cp["rabbit"]["RABBIT_PASSWORD"],
-        cp["rabbit"]["RABBIT_HOST"],
+    parser.add_argument(
+        "queue_name",
+        help="The name of the queue to filter. `all` is a valid value.",
+    )
+    parser.add_argument(
+        "regex", help="The regex with which to filter the queue"
     )
 
-    opts, args = parser.parse_args()
+    args = parser.parse_args()
 
     logging.basicConfig(
-        level=logging.DEBUG if opts.verbose else logging.INFO,
+        level=logging.DEBUG if args.verbose else logging.INFO,
         format="%(asctime)s - %(message)s",
     )
 
-    if len(args) != 2:
-        parser.error("Need to specify queue name and regex")
-
-    if opts.all_items_in_queue:
+    if args.all_items_in_queue:
         print("""Do you really want to flush this queue? [yN]""", end="")
         sys.stdout.flush()
         response = sys.stdin.readline()
         if not response.strip().lower().startswith("y"):
             print("""Exiting""")
             sys.exit(1)
-    queues = [args[0]] if args[0] != "all" else generate_queue_names()
+    queues = (
+        [args.queue_name]
+        if args.queue_name != "all"
+        else generate_queue_names()
+    )
 
-    deletion_count_history = []
     for this_queue in queues:
+        deletion_count_history = []
         while True:
-            num_deleted = filter_amqp(opts, creds, this_queue, args[1])
+            num_deleted = filter_amqp(args, this_queue, args.regex)
+            if num_deleted is None:
+                logging.info("Skipping %s", this_queue)
+                break
             deletion_count_history.append(num_deleted)
-            if opts.dry_run:
+            if args.dry_run:
                 break
-            if all([x == 0 for x in deletion_count_history[-5:]]):
+            if len(deletion_count_history) >= 5 and all(
+                [x == 0 for x in deletion_count_history[-5:]]
+            ):
                 logging.info(
                     "Finished filtering queue objects, run history:\n%s"
                     % "\n".join(str(x) for x in deletion_count_history)
diff --git a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/filter-amqp-dupes-upstream b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/filter-amqp-dupes-upstream
index c255239..8f81e92 100755
--- a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/filter-amqp-dupes-upstream
+++ b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/filter-amqp-dupes-upstream
@@ -1,14 +1,14 @@
 #!/usr/bin/python3
 # Filter out all but the latest request for a given upstream PR
 
+import argparse
+import configparser
 import json
 import logging
-import optparse  # pylint: disable=deprecated-module
 import os
-import urllib.parse
 from collections import defaultdict
 
-import amqplib.client_0_8 as amqp
+import amqp
 import dateutil.parser
 import distro_info
 
@@ -19,13 +19,23 @@ SUPPORTED_UBUNTU_RELEASES = sorted(
 )
 
 
-def filter_amqp(options, host):
-    url_parts = urllib.parse.urlsplit(host, allow_fragments=False)
-    amqp_con = amqp.Connection(
-        url_parts.hostname,
-        userid=url_parts.username,
-        password=url_parts.password,
-    )
+def filter_amqp(options):
+    try:
+        cp = configparser.ConfigParser()
+        with open("/home/ubuntu/rabbitmq.cred", "r") as f:
+            cp.read_string("[rabbit]\n" + f.read().replace('"', ""))
+        amqp_con = amqp.Connection(
+            cp["rabbit"]["RABBIT_HOST"],
+            cp["rabbit"]["RABBIT_USER"],
+            cp["rabbit"]["RABBIT_PASSWORD"],
+        )
+    except FileNotFoundError:
+        amqp_con = amqp.Connection(
+            os.environ["RABBIT_HOST"],
+            userid=os.environ["RABBIT_USER"],
+            password=os.environ["RABBIT_PASSWORD"],
+        )
+    amqp_con.connect()
     dry_run = "[dry-run] " if options.dry_run else ""
 
     queues = (
@@ -40,10 +50,7 @@ def filter_amqp(options, host):
         while True:
             try:
                 r = ch.basic_get(queue_name)
-            except amqp.AMQPChannelException as e:
-                (code, _, _, _) = e.args
-                if code != 404:
-                    raise
+            except amqp.NotFound:
                 logging.debug(f"No such queue {queue_name}")
                 break
             if r is None:
@@ -52,7 +59,7 @@ def filter_amqp(options, host):
                 body = r.body.decode("UTF-8")
             else:
                 body = r.body
-            (pkg, params) = body.split(" ", 1)
+            (pkg, params) = body.split("\n", 1)
             params_j = json.loads(params)
             submit_time = dateutil.parser.parse(params_j["submit-time"])
             pr = [
@@ -80,37 +87,38 @@ def filter_amqp(options, host):
 
 
 def main():
-    parser = optparse.OptionParser(
-        usage="usage: %prog [options] amqp://user:pass@host queue_name regex"
+    parser = argparse.ArgumentParser(
+        description="""Deduplicates jobs in the upstream queue.
+
+The upstream integration is different than regular jobs pushed by Britney.
+If a developer pushes two times in a row on a pull request, then two test
+requests get queued. This script is here to remove any duplicate requests.
+
+If ~/rabbitmq.cred is not present, this script will load credentials from
+$RABBIT_HOST, $RABBIT_USER, and $RABBIT_PASSWORD environment variables.
+""",
+        formatter_class=argparse.RawTextHelpFormatter,
     )
-    parser.add_option(
-        "-n",
+    parser.add_argument(
         "--dry-run",
-        default=False,
         action="store_true",
         help="only show the operations that would be performed",
     )
-    parser.add_option(
+    parser.add_argument(
         "-v",
         "--verbose",
-        default=False,
         action="store_true",
         help="additionally show queue items that are not removed",
     )
 
-    # pylint: disable=unused-variable
-    opts, args = parser.parse_args()
+    args = parser.parse_args()
 
     logging.basicConfig(
-        level=logging.DEBUG if opts.verbose else logging.INFO,
+        level=logging.DEBUG if args.verbose else logging.INFO,
         format="%(asctime)s - %(message)s",
     )
 
-    user = os.environ["RABBIT_USER"]
-    password = os.environ["RABBIT_PASSWORD"]
-    host = os.environ["RABBIT_HOST"]
-    uri = f"amqp://{user}:{password}@{host}"
-    filter_amqp(opts, uri)
+    filter_amqp(args)
 
 
 if __name__ == "__main__":
diff --git a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/pull-amqp b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/pull-amqp
index cdda67a..fbd2092 100755
--- a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/pull-amqp
+++ b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/pull-amqp
@@ -2,10 +2,11 @@
 
 import argparse
 import configparser
+import os
 import re
 import sys
 
-import amqplib.client_0_8 as amqp
+import amqp
 
 
 def parse_args():
@@ -45,14 +46,22 @@ You can alter the queue messages however you please, but be careful :)
 
 def main():
     args = parse_args()
-    cp = configparser.ConfigParser()
-    with open("/home/ubuntu/rabbitmq.cred", "r") as f:
-        cp.read_string("[rabbit]\n" + f.read().replace('"', ""))
-    amqp_con = amqp.Connection(
-        cp["rabbit"]["RABBIT_HOST"],
-        cp["rabbit"]["RABBIT_USER"],
-        cp["rabbit"]["RABBIT_PASSWORD"],
-    )
+    try:
+        cp = configparser.ConfigParser()
+        with open("/home/ubuntu/rabbitmq.cred", "r") as f:
+            cp.read_string("[rabbit]\n" + f.read().replace('"', ""))
+        amqp_con = amqp.Connection(
+            cp["rabbit"]["RABBIT_HOST"],
+            cp["rabbit"]["RABBIT_USER"],
+            cp["rabbit"]["RABBIT_PASSWORD"],
+        )
+    except FileNotFoundError:
+        amqp_con = amqp.Connection(
+            os.environ["RABBIT_HOST"],
+            userid=os.environ["RABBIT_USER"],
+            password=os.environ["RABBIT_PASSWORD"],
+        )
+    amqp_con.connect()
     if args.regex is not None:
         filter_re = re.compile(args.regex.encode("UTF-8"), re.DOTALL)
     with amqp_con.channel() as ch:
diff --git a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/push-amqp b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/push-amqp
index 80f70e2..10d94a1 100755
--- a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/push-amqp
+++ b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/push-amqp
@@ -3,9 +3,10 @@
 import argparse
 import ast
 import configparser
+import os
 import sys
 
-import amqplib.client_0_8 as amqp
+import amqp
 
 
 def parse_args():
@@ -68,14 +69,22 @@ def main():
                     file=sys.stderr,
                 )
 
-    cp = configparser.ConfigParser()
-    with open("/home/ubuntu/rabbitmq.cred", "r") as f:
-        cp.read_string("[rabbit]\n" + f.read().replace('"', ""))
-    amqp_con = amqp.Connection(
-        cp["rabbit"]["RABBIT_HOST"],
-        cp["rabbit"]["RABBIT_USER"],
-        cp["rabbit"]["RABBIT_PASSWORD"],
-    )
+    try:
+        cp = configparser.ConfigParser()
+        with open("/home/ubuntu/rabbitmq.cred", "r") as f:
+            cp.read_string("[rabbit]\n" + f.read().replace('"', ""))
+        amqp_con = amqp.Connection(
+            cp["rabbit"]["RABBIT_HOST"],
+            cp["rabbit"]["RABBIT_USER"],
+            cp["rabbit"]["RABBIT_PASSWORD"],
+        )
+    except FileNotFoundError:
+        amqp_con = amqp.Connection(
+            os.environ["RABBIT_HOST"],
+            userid=os.environ["RABBIT_USER"],
+            password=os.environ["RABBIT_PASSWORD"],
+        )
+    amqp_con.connect()
     ch = amqp_con.channel()
     queue_name = args.queue_name
     if args.message:
@@ -103,10 +112,7 @@ def main():
                 continue
             try:
                 push(message, queue_name, ch)
-            except (
-                amqp.AMQPChannelException,
-                amqp.AMQPConnectionException,
-            ) as _:
+            except amqp.AMQPError:
                 print(
                     f"Pushing message `{message}` to queue {queue_name} failed.",
                     file=sys.stderr,
diff --git a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/run-autopkgtest b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/run-autopkgtest
index 3dc8326..7a85403 100755
--- a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/run-autopkgtest
+++ b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/run-autopkgtest
@@ -9,9 +9,9 @@ import os
 import sys
 import urllib.parse
 import uuid
-from datetime import datetime
+from datetime import datetime, timezone
 
-import amqplib.client_0_8 as amqp
+import amqp
 
 my_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
 
@@ -172,7 +172,7 @@ if __name__ == "__main__":
     if args.readable_by:
         params["readable-by"] = args.readable_by
     if args.all_proposed:
-        params["all-proposed"] = True
+        params["all-proposed"] = "1"
     if args.requester:
         params["requester"] = args.requester
     else:
@@ -181,7 +181,7 @@ if __name__ == "__main__":
         except KeyError:
             pass
     params["submit-time"] = datetime.strftime(
-        datetime.utcnow(), "%Y-%m-%d %H:%M:%S%z"
+        datetime.now().astimezone(timezone.utc), "%Y-%m-%d %H:%M:%S%z"
     )
     params["uuid"] = str(uuid.uuid4())
     params = "\n" + json.dumps(params)
diff --git a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/with-distributed-lock b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/with-distributed-lock
index 442e914..aaca3d2 100755
--- a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/with-distributed-lock
+++ b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/with-distributed-lock
@@ -15,7 +15,7 @@ import os
 import subprocess
 import sys
 
-import amqplib.client_0_8 as amqp
+import amqp
 
 
 @contextlib.contextmanager
@@ -33,15 +33,14 @@ def amqp_lock(name):
         userid=os.environ["RABBIT_USER"],
         password=os.environ["RABBIT_PASSWORD"],
     )
+    amqp_con.connect()
     channel = amqp_con.channel()
-    channel.queue_declare(
-        name, arguments={"args.queue.x-single-active-consumer": True}
-    )
+    channel.queue_declare(name, arguments={"x-single-active-consumer": True})
     channel.basic_publish(amqp.Message(""), routing_key=name)
     consumer_tag = channel.basic_consume(queue=name, callback=callback)
 
     while channel.callbacks and not callback.called:
-        channel.wait()
+        amqp_con.drain_events()
 
     try:
         yield
diff --git a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/worker/worker b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/worker/worker
index 758ac0e..99203fd 100755
--- a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/worker/worker
+++ b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/worker/worker
@@ -2,7 +2,7 @@
 # autopkgtest cloud worker
 # Author: Martin Pitt <martin.pitt@xxxxxxxxxx>
 #
-# Requirements: python3-amqplib python3-swiftclient python3-influxdb
+# Requirements: python3-amqp python3-swiftclient python3-influxdb
 # Requirements for running autopkgtest from git: python3-debian libdpkg-perl
 #
 # pylint: disable=too-many-lines,line-too-long
@@ -27,7 +27,7 @@ import urllib.request
 import uuid
 from urllib.error import HTTPError
 
-import amqplib.client_0_8 as amqp
+import amqp
 import distro_info
 import novaclient.client
 import novaclient.exceptions
@@ -562,7 +562,6 @@ def call_autopkgtest(
     # set up status AMQP exchange
     global amqp_con
     status_amqp = amqp_con.channel()
-    status_amqp.access_request("/data", active=True, read=False, write=True)
     status_amqp.exchange_declare(
         status_exchange_name, "fanout", durable=False, auto_delete=True
     )
@@ -1576,9 +1575,6 @@ def request(msg):
 
     global amqp_con
     complete_amqp = amqp_con.channel()
-    complete_amqp.access_request(
-        "/complete", active=True, read=False, write=True
-    )
     complete_amqp.exchange_declare(
         complete_exchange_name, "fanout", durable=True, auto_delete=False
     )
@@ -1629,6 +1625,7 @@ def amqp_connect(cfg, callback):
         password=os.environ["RABBIT_PASSWORD"],
         confirm_publish=True,
     )
+    amqp_con.connect()
     queue = amqp_con.channel()
     # avoids greedy grabbing of the entire queue while being too busy
     queue.basic_qos(0, 1, True)
@@ -1665,7 +1662,7 @@ def amqp_connect(cfg, callback):
         queue.queue_declare(queue_name, durable=True, auto_delete=False)
         queue.basic_consume(queue=queue_name, callback=request)
 
-    return queue
+    return amqp_con
 
 
 def main():
@@ -1740,13 +1737,13 @@ def main():
         swiftclient.Connection(**swift_creds).close()
 
     # connect to AMQP queues
-    queue = amqp_connect(cfg, request)
+    amqp_con = amqp_connect(cfg, request)
 
     # process queues forever
     try:
         while exit_requested is None:
             logging.info("Waiting for and processing AMQP requests")
-            queue.wait()
+            amqp_con.drain_events()
     except IOError:
         if exit_requested is None:
             raise
diff --git a/charms/focal/autopkgtest-cloud-worker/layer.yaml b/charms/focal/autopkgtest-cloud-worker/layer.yaml
index d8f8c0a..fd41b99 100644
--- a/charms/focal/autopkgtest-cloud-worker/layer.yaml
+++ b/charms/focal/autopkgtest-cloud-worker/layer.yaml
@@ -18,7 +18,7 @@ options:
       - libdpkg-perl
       - lxd-client
       - make
-      - python3-amqplib
+      - python3-amqp
       - python3-debian
       - python3-distro-info
       - python3-glanceclient
diff --git a/charms/focal/autopkgtest-web/config.yaml b/charms/focal/autopkgtest-web/config.yaml
index 1cab2a2..0d1d94f 100644
--- a/charms/focal/autopkgtest-web/config.yaml
+++ b/charms/focal/autopkgtest-web/config.yaml
@@ -15,6 +15,10 @@ options:
     type: string
     default: autopkgtest.local
     description: "Public host name of this web server"
+  archive-url:
+    type: string
+    default:
+    description: "URL of the Ubuntu archive, in case you want to use a mirror"
   github-secrets:
     type: string
     default:
@@ -30,10 +34,6 @@ options:
     default:
     description: "project:user:token github credentials \
                   for POSTing to statuses_url"
-  swift-web-credentials:
-    type: string
-    default:
-    description: "SWIFT login credentials for the private-result web frontend"
   https-proxy:
     type: string
     default:
@@ -43,12 +43,6 @@ options:
     default:
     description: "$no_proxy environment variable (for accessing sites \
                   other than github, like login.ubuntu.com and launchpad.net)"
-  cookies:
-    type: string
-    default:
-    description: "SRVNAME cookies for each autopkgtest-web unit. \
-                  Each web unit has a cookie assigned which tells the \
-                  haproxy server which web unit to redirect a request to."
   indexed-packages-fp:
     type: string
     default:
diff --git a/charms/focal/autopkgtest-web/layer.yaml b/charms/focal/autopkgtest-web/layer.yaml
index 529b3c6..471439d 100644
--- a/charms/focal/autopkgtest-web/layer.yaml
+++ b/charms/focal/autopkgtest-web/layer.yaml
@@ -15,7 +15,7 @@ options:
       - libjs-bootstrap
       - libjs-jquery
       - make
-      - python3-amqplib
+      - python3-amqp
       - python3-distro-info
       - python3-flask
       - python3-flask-openid
diff --git a/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py b/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py
index 7b43cee..bd9e725 100644
--- a/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py
+++ b/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py
@@ -35,17 +35,7 @@ GITHUB_SECRETS_PATH = os.path.expanduser("~ubuntu/github-secrets.json")
 GITHUB_STATUS_CREDENTIALS_PATH = os.path.expanduser(
     "~ubuntu/github-status-credentials.txt"
 )
-SWIFT_WEB_CREDENTIALS_PATH = os.path.expanduser(
-    "~ubuntu/swift-web-credentials.conf"
-)
 API_KEYS_PATH = "/home/ubuntu/external-web-requests-api-keys.json"
-CONFIG_DIR = pathlib.Path("/home/ubuntu/.config/autopkgtest-web/")
-if not CONFIG_DIR.exists():
-    set_flag("autopkgtest-web.config-needs-writing")
-for parent in reversed(CONFIG_DIR.parents):
-    parent.mkdir(mode=0o775, exist_ok=True)
-CONFIG_DIR.mkdir(mode=0o775, exist_ok=True)
-ALLOWED_REQUESTOR_TEAMS_PATH = CONFIG_DIR / "allowed-requestor-teams"
 
 PUBLIC_SWIFT_CREDS_PATH = os.path.expanduser("~ubuntu/public-swift-creds")
 
@@ -126,38 +116,61 @@ def setup_rabbitmq(rabbitmq):
 
 @when_all(
     "amqp.available",
+    "config.changed",
     "config.set.storage-url-internal",
     "config.set.hostname",
-    "config.set.cookies",
-    "config.set.indexed-packages-fp",
+    "config.set.archive-url",
+    "config.set.public-swift-creds",
+    "config.set.allowed-requestor-teams",
 )
 def write_autopkgtest_cloud_conf(rabbitmq):
     status.maintenance("Writing autopkgtest-cloud config")
     swiftinternal = config().get("storage-url-internal")
     hostname = config().get("hostname")
-    cookies = config().get("cookies")
+    allowed_requestor_teams = ",".join(
+        config().get("allowed-requestor-teams").split("\n")
+    )
+    public_swift_creds = {
+        key.lower(): value
+        for key, value in [
+            line.split("=")
+            for line in config().get("public-swift-creds").strip().split("\n")
+        ]
+    }
+    archive_url = config().get("archive-url")
     rabbituser = rabbitmq.username()
     rabbitpassword = rabbitmq.password()
     rabbithost = rabbitmq.private_address()
-    indexed_packages_fp = config().get("indexed-packages-fp")
     clear_flag("autopkgtest-web.config-written")
     with open(f"{AUTOPKGTEST_CLOUD_CONF}.new", "w") as f:
         f.write(
             dedent(
-                """\
+                f"""\
             [web]
             database=/home/ubuntu/autopkgtest.db
             database_ro=/home/ubuntu/public/autopkgtest.db
             SwiftURL={swiftinternal}
             ExternalURL=https://{hostname}/results
-            cookies={cookies}
-            indexed_packages_fp={indexed_packages_fp}
             stats_fallback_dir=/home/ubuntu
+            archive_url={archive_url}
+            indexed_packages=/run/autopkgtest-web/indexed-packages.json
+            amqp_queue_cache=/run/autopkgtest-web/queued.json
+            running_cache=/run/autopkgtest-web/running.json
+            allowed_requestors={allowed_requestor_teams}
 
             [amqp]
-            uri=amqp://{rabbituser}:{rabbitpassword}@{rabbithost}""".format(
-                    **locals()
-                )
+            uri=amqp://{rabbituser}:{rabbitpassword}@{rabbithost}
+
+            [swift]
+            os_region_name = {public_swift_creds['os_region_name']}
+            os_auth_url = {public_swift_creds['os_auth_url']}
+            os_project_domain_name = {public_swift_creds['os_project_domain_name']}
+            os_username = {public_swift_creds['os_username']}
+            os_user_domain_name = {public_swift_creds['os_user_domain_name']}
+            os_project_name = {public_swift_creds['os_project_name']}
+            os_password = {public_swift_creds['os_password']}
+            os_identity_api_version = {public_swift_creds['os_identity_api_version']}
+            """
             )
         )
     os.rename(f"{AUTOPKGTEST_CLOUD_CONF}.new", AUTOPKGTEST_CLOUD_CONF)
@@ -346,19 +359,7 @@ def set_up_web_config(apache):
 
 
 @when_any(
-    "config.changed.allowed-requestor-teams",
-    "config.set.allowed-requestor-teams",
-    "autopkgtest-web.config-needs-writing",
-)
-def write_allowed_teams():
-    allowed_requestor_teams = config().get("allowed-requestor-teams")
-    allowed_teams_path = pathlib.Path(ALLOWED_REQUESTOR_TEAMS_PATH)
-    allowed_teams_path.write_text(allowed_requestor_teams, encoding="utf-8")
-
-
-@when_any(
     "config.changed.github-secrets",
-    "autopkgtest-web.config-needs-writing",
 )
 def write_github_secrets():
     status.maintenance("Writing github secrets")
@@ -408,26 +409,6 @@ def clear_github_secrets():
     status.maintenance("Done clearing github secrets")
 
 
-@when_all(
-    "config.changed.swift-web-credentials", "config.set.swift-web-credentials"
-)
-def write_swift_web_credentials():
-    status.maintenance("Writing swift web credentials")
-    swift_credentials = config().get("swift-web-credentials")
-
-    with open(SWIFT_WEB_CREDENTIALS_PATH, "w") as f:
-        f.write(swift_credentials)
-
-    try:
-        os.symlink(
-            SWIFT_WEB_CREDENTIALS_PATH,
-            os.path.expanduser("~www-data/swift-web-credentials.conf"),
-        )
-    except FileExistsError:
-        pass
-    status.maintenance("Done writing swift web credentials")
-
-
 @when_all("config.changed.public-swift-creds", "config.set.public-swift-creds")
 def write_openstack_creds():
     status.maintenance("Writing openstack credentials")
@@ -438,21 +419,6 @@ def write_openstack_creds():
     status.maintenance("Done writing openstack credentials")
 
 
-@when_not("config.set.swift-web-credentials")
-def clear_swift_web_credentials():
-    status.maintenance("Clearing swift web credentials")
-    try:
-        os.unlink(SWIFT_WEB_CREDENTIALS_PATH)
-    except FileNotFoundError:
-        pass
-
-    try:
-        os.unlink(os.path.expanduser("~www-data/swift-web-credentials.conf"))
-    except FileNotFoundError:
-        pass
-    status.maintenance("Done clearing swift web credentials")
-
-
 @when_all(
     "config.changed.github-status-credentials",
     "config.set.github-status-credentials",
@@ -547,9 +513,7 @@ def symlink_running():
     status.maintenance("Creating symlink to running.json")
     try:
         os.symlink(
-            os.path.join(
-                os.path.sep, "run", "amqp-status-collector", "running.json"
-            ),
+            os.path.join(os.path.expanduser("~ubuntu"), "running.json"),
             os.path.join(
                 os.path.expanduser("~ubuntu"),
                 "webcontrol",
@@ -592,13 +556,10 @@ def symlink_public_db():
                     symlink_file,
                 ),
             )
-            set_flag(symlink_flag)
             status.active(f"Done creating symlink for {symlink_file}")
         except FileExistsError:
-            clear_flag(symlink_flag)
-            status.active(
-                "symlinking public db and sha256 checksum already done"
-            )
+            status.active(f"Symlink {symlink_file} already exists")
+        set_flag(symlink_flag)
 
 
 @when("leadership.is_leader")
diff --git a/charms/focal/autopkgtest-web/units/amqp-status-collector.service b/charms/focal/autopkgtest-web/units/amqp-status-collector.service
index bfb92ae..5b98105 100644
--- a/charms/focal/autopkgtest-web/units/amqp-status-collector.service
+++ b/charms/focal/autopkgtest-web/units/amqp-status-collector.service
@@ -4,6 +4,7 @@ StartLimitIntervalSec=60s
 StartLimitBurst=60
 
 [Service]
+User=ubuntu
 RuntimeDirectory=amqp-status-collector
 RuntimeDirectoryPreserve=yes
 ExecStart=/home/ubuntu/webcontrol/amqp-status-collector
diff --git a/charms/focal/autopkgtest-web/units/db-backup.service b/charms/focal/autopkgtest-web/units/db-backup.service
index 9c3d038..29f4763 100644
--- a/charms/focal/autopkgtest-web/units/db-backup.service
+++ b/charms/focal/autopkgtest-web/units/db-backup.service
@@ -4,5 +4,4 @@ Description=Backup sql database
 [Service]
 Type=oneshot
 User=ubuntu
-EnvironmentFile=/home/ubuntu/public-swift-creds
 ExecStart=/home/ubuntu/webcontrol/db-backup
diff --git a/charms/focal/autopkgtest-web/units/indexed-packages.service b/charms/focal/autopkgtest-web/units/indexed-packages.service
index e518740..858ff0f 100644
--- a/charms/focal/autopkgtest-web/units/indexed-packages.service
+++ b/charms/focal/autopkgtest-web/units/indexed-packages.service
@@ -2,5 +2,6 @@
 Description=Get index of packages
 
 [Service]
+User=ubuntu
 Type=oneshot
 ExecStart=/home/ubuntu/webcontrol/indexed-packages
diff --git a/charms/focal/autopkgtest-web/units/publish-db.service b/charms/focal/autopkgtest-web/units/publish-db.service
index 1c9c282..3a5728c 100644
--- a/charms/focal/autopkgtest-web/units/publish-db.service
+++ b/charms/focal/autopkgtest-web/units/publish-db.service
@@ -2,6 +2,7 @@
 Description=publish autopkgtest.db
 
 [Service]
+User=ubuntu
 Type=oneshot
 ExecStart=/home/ubuntu/webcontrol/publish-db
 TimeoutSec=300
diff --git a/charms/focal/autopkgtest-web/units/sqlite-writer.service b/charms/focal/autopkgtest-web/units/sqlite-writer.service
index cf3b48e..3a47c08 100644
--- a/charms/focal/autopkgtest-web/units/sqlite-writer.service
+++ b/charms/focal/autopkgtest-web/units/sqlite-writer.service
@@ -5,7 +5,6 @@ StartLimitBurst=60
 
 [Service]
 User=ubuntu
-EnvironmentFile=/home/ubuntu/public-swift-creds
 ExecStart=/home/ubuntu/webcontrol/sqlite-writer
 Restart=on-failure
 RestartSec=1s
diff --git a/charms/focal/autopkgtest-web/units/update-github-jobs.service b/charms/focal/autopkgtest-web/units/update-github-jobs.service
index 90de852..15627b7 100644
--- a/charms/focal/autopkgtest-web/units/update-github-jobs.service
+++ b/charms/focal/autopkgtest-web/units/update-github-jobs.service
@@ -2,8 +2,7 @@
 Description=Update GitHub job status
 
 [Service]
+User=ubuntu
 Type=oneshot
-User=www-data
-Group=www-data
 TimeoutStartSec=10m
 ExecStart=/home/ubuntu/webcontrol/update-github-jobs
diff --git a/charms/focal/autopkgtest-web/webcontrol/.gitignore b/charms/focal/autopkgtest-web/webcontrol/.gitignore
new file mode 100644
index 0000000..dcdfe9b
--- /dev/null
+++ b/charms/focal/autopkgtest-web/webcontrol/.gitignore
@@ -0,0 +1 @@
+autopkgtest-cloud.conf
diff --git a/charms/focal/autopkgtest-web/webcontrol/README.md b/charms/focal/autopkgtest-web/webcontrol/README.md
index b45f7f6..44368db 100644
--- a/charms/focal/autopkgtest-web/webcontrol/README.md
+++ b/charms/focal/autopkgtest-web/webcontrol/README.md
@@ -1,15 +1,49 @@
 # autopkgtest-cloud web frontend
 
-## Developing browse.cgi locally
+## Developing locally
 
-Install the dependencies:
-`sudo apt install python3-flask python3-distro-info libjs-jquery libjs-bootstrap`
+Most of the scripts in this folder can be run locally for easier development.
+
+The first thing to do is to provide an `autopkgtest-cloud.conf` file.
+In this current folder:
+`cp autopkgtest-cloud.conf.example autopkgtest-cloud.conf`
+
+Install the main dependencies (Others are usually less important. Have a look at
+the charm definition for an exhaustive list.):
+`sudo apt install python3-amqp python3-flask python3-distro-info libjs-jquery libjs-bootstrap`
+
+Then you can start each script individually, without argument.
+Here is a quick non exhaustive list of the main ones:
+
+* sqlite-writer:
+  probably one of the most important one: it's the only script that will
+  actually write to the `autopkgtest.db` database (because of lack of concurrent
+  write support in sqlite).
+* download-results:
+  will listen to finished results from the worker, and will push DB write
+  through AMQP to sqlite-writer.
+* amqp-status-collector:
+  this is the script monitoring the ongoing jobs processed by the workers, and
+  dumping that information into `running.json`, mostly displayed on the
+  `/running` page.
+* cache-amqp:
+  this script is basically dumping the AMQP test requests queues into a JSON,
+  used throughout the web UI.
+* publish-db:
+  this is taking the rw database, and copying it to the ro one, actually used
+  by many scripts in production, and adding a bit more information like the
+  `current_version` table.
+
+Please note that the default configuration is compatible with a local run of the
+`worker` part, meaning you can have the whole stack running on your laptop.
+
+## Notes on developing browse.cgi locally
 
 *Optional*: `python3 -m pip install --user --break-system-packages flask-debugtoolbar`
 This will automatically activate the Falsk DebugToolbar that brings valuable
 information for developers.
 
-Then simply run `./browse-test-py`, it will launch the flask application locally
+Simply run `./browse-test-py`, it will launch the flask application locally
 with some mocked data.
 As the import of `browse.cgi` is done trough `importlib`, changes in that file
 will not be reloaded automatically, so you'll still need to restart the app
diff --git a/charms/focal/autopkgtest-web/webcontrol/amqp-status-collector b/charms/focal/autopkgtest-web/webcontrol/amqp-status-collector
index 1169f2e..ae2226c 100755
--- a/charms/focal/autopkgtest-web/webcontrol/amqp-status-collector
+++ b/charms/focal/autopkgtest-web/webcontrol/amqp-status-collector
@@ -7,15 +7,12 @@ import logging
 import os
 import socket
 import time
-import urllib.parse
 
-import amqplib.client_0_8 as amqp
-from helpers.utils import get_autopkgtest_cloud_conf
+from helpers.utils import amqp_connect, get_autopkgtest_cloud_conf
 
 exchange_name = "teststatus.fanout"
-running_name = os.path.join(
-    os.path.sep, "run", "amqp-status-collector", "running.json"
-)
+cp = get_autopkgtest_cloud_conf()
+running_name = cp["web"]["running_cache"]
 running_name_new = "{}.new".format(running_name)
 
 # package -> runhash -> release -> arch -> (params, duration, logtail)
@@ -23,22 +20,6 @@ running_tests = {}
 last_update = 0
 
 
-def amqp_connect():
-    """Connect to AMQP server"""
-
-    cp = get_autopkgtest_cloud_conf()
-    amqp_uri = cp["amqp"]["uri"]
-    parts = urllib.parse.urlsplit(amqp_uri, allow_fragments=False)
-    amqp_con = amqp.Connection(
-        parts.hostname, userid=parts.username, password=parts.password
-    )
-    logging.info(
-        "Connected to AMQP server at %s@%s" % (parts.username, parts.hostname)
-    )
-
-    return amqp_con
-
-
 def update_output(amqp_channel, force_update=False):
     """Update report"""
 
@@ -100,12 +81,11 @@ def process_message(msg):
 #
 
 logging.basicConfig(
-    level=("DEBUG" in os.environ and logging.DEBUG or logging.INFO)
+    level=(logging.DEBUG if "DEBUG" in os.environ else logging.INFO)
 )
 
 amqp_con = amqp_connect()
 status_ch = amqp_con.channel()
-status_ch.access_request("/data", active=True, read=True, write=False)
 status_ch.exchange_declare(
     exchange_name, "fanout", durable=False, auto_delete=True
 )
@@ -116,4 +96,4 @@ status_ch.queue_bind(queue_name, exchange_name, queue_name)
 logging.info("Listening to requests on %s" % queue_name)
 status_ch.basic_consume("", callback=process_message, no_ack=True)
 while status_ch.callbacks:
-    status_ch.wait()
+    amqp_con.drain_events()
diff --git a/charms/focal/autopkgtest-web/webcontrol/autopkgtest-cloud.conf.example b/charms/focal/autopkgtest-web/webcontrol/autopkgtest-cloud.conf.example
new file mode 100644
index 0000000..17dc6ce
--- /dev/null
+++ b/charms/focal/autopkgtest-web/webcontrol/autopkgtest-cloud.conf.example
@@ -0,0 +1,29 @@
+# For local development copy me to `./autopkgtest-cloud.conf`
+# All scripts in that folder should use that by default, provided you don't have
+# a `~/autopkgtest-cloud.conf` file existing.
+[web]
+database=/tmp/autopkgtest-cloud-web/autopkgtest.db
+database_ro=/tmp/autopkgtest-cloud-web/public/autopkgtest.db
+ExternalURL=http://127.0.0.1:5000/results
+stats_fallback_dir=/tmp/autopkgtest-cloud-web
+archive_url=http://archive.ubuntu.com/ubuntu
+indexed_packages=/tmp/autopkgtest-cloud-web/indexed-packages.json
+amqp_queue_cache=/tmp/autopkgtest-cloud-web/queued.json
+running_cache=/tmp/autopkgtest-cloud-web/running.json
+allowed_requestors=autopkgtest-requestors,canonical-ubuntu-qa
+
+[amqp]
+uri=amqp://guest:guest@127.0.0.1
+
+[swift]
+os_region_name = canonistack-bos01
+os_auth_url = https://keystone.bos01.canonistack.canonical.com:5000/v3
+os_project_domain_name = default
+# change this
+os_username = user
+os_user_domain_name = default
+# change this
+os_project_name = user_project
+# change this
+os_password = complicatedpassword
+os_identity_api_version = 3
diff --git a/charms/focal/autopkgtest-web/webcontrol/browse-test.py b/charms/focal/autopkgtest-web/webcontrol/browse-test.py
index a01d944..0486057 100755
--- a/charms/focal/autopkgtest-web/webcontrol/browse-test.py
+++ b/charms/focal/autopkgtest-web/webcontrol/browse-test.py
@@ -56,40 +56,40 @@ def parse_args():
 
 if __name__ == "__main__":
     args = parse_args()
+    browse.init_config()
+    config = utils.get_autopkgtest_cloud_conf()
+
     if args.data_dir:
-        browse.AMQP_QUEUE_CACHE = Path(args.data_dir + "/queued.json")
-        browse.RUNNING_CACHE = Path(args.data_dir + "/running.json")
-        browse.db_con = utils.init_db(
-            args.data_dir + "/autopkgtest.db",
-            check_same_thread=False,
-        )
+        browse.CONFIG["amqp_queue_cache"] = Path(args.data_dir) / "queued.json"
+        browse.CONFIG["running_cache"] = Path(args.data_dir) / "running.json"
+        browse.CONFIG["database"] = args.data_dir + "/autopkgtest.db"
     else:
         if args.database:
-            browse.db_con = utils.init_db(
-                args.database,
-                check_same_thread=False,
-            )
+            browse.CONFIG["database"] = args.database
         else:
-            browse.db_con = utils.init_db(
-                ":memory:",
-                check_same_thread=False,
-            )
-            with browse.db_con:
-                tests.populate_dummy_db(browse.db_con)
+            # For convenience, the development Flask app uses database instead
+            # of database_ro.
+            # This is different from production deployment, where `publish-db`
+            # produces database_ro, that browse.cgi uses.
+            browse.CONFIG["database"] = config["web"]["database"]
 
         if args.queue:
-            browse.AMQP_QUEUE_CACHE = Path(args.queue)
+            browse.CONFIG["amqp_queue_cache"] = Path(args.queue)
         else:
-            browse.AMQP_QUEUE_CACHE = Path("/dev/shm/queue.json")
-            tests.populate_dummy_amqp_cache(browse.AMQP_QUEUE_CACHE)
+            tests.populate_dummy_amqp_cache(browse.CONFIG["amqp_queue_cache"])
 
         if args.running:
-            browse.RUNNING_CACHE = Path(args.running)
+            browse.CONFIG["running_cache"] = Path(args.running)
         else:
-            browse.RUNNING_CACHE = Path("/dev/shm/running.json")
-            tests.populate_dummy_running_cache(browse.RUNNING_CACHE)
+            tests.populate_dummy_running_cache(browse.CONFIG["running_cache"])
 
-    browse.swift_container_url = "swift-%s"
+    utils.init_db(browse.CONFIG["database"])
+    browse.connect_db("file:%s?mode=ro" % browse.CONFIG["database"])
+    if utils.is_db_empty(browse.db_con):
+        browse.connect_db("file:%s?mode=rw" % browse.CONFIG["database"])
+        with browse.db_con:
+            tests.populate_dummy_db(browse.db_con)
+        browse.connect_db("file:%s?mode=ro" % browse.CONFIG["database"])
 
     if activate_debugtoolbar:
         browse.app.debug = True
diff --git a/charms/focal/autopkgtest-web/webcontrol/browse.cgi b/charms/focal/autopkgtest-web/webcontrol/browse.cgi
index f91821a..3e7838a 100755
--- a/charms/focal/autopkgtest-web/webcontrol/browse.cgi
+++ b/charms/focal/autopkgtest-web/webcontrol/browse.cgi
@@ -47,31 +47,35 @@ secret_path = os.path.join(PATH, "secret_key")
 setup_key(app, secret_path)
 
 db_con = None
-swift_container_url = None
+CONFIG = {}
 
 ALL_UBUNTU_RELEASES = get_all_releases()
 SUPPORTED_UBUNTU_RELEASES = get_supported_releases()
-INDEXED_PACKAGES_FP = ""
-AMQP_QUEUE_CACHE = "/var/lib/cache-amqp/queued.json"
-RUNNING_CACHE = "/run/amqp-status-collector/running.json"
 
 
 def init_config():
-    global db_con, swift_container_url, INDEXED_PACKAGES_FP
+    global CONFIG
 
     cp = get_autopkgtest_cloud_conf()
 
-    db_con = sqlite3.connect(
-        "file:%s?mode=ro" % cp["web"]["database_ro"],
-        uri=True,
-        check_same_thread=False,
-    )
     try:
-        url = cp["web"]["ExternalURL"]
+        CONFIG["swift_container_url"] = (
+            cp["web"]["ExternalURL"] + "/autopkgtest-%s"
+        )
     except KeyError:
-        url = cp["web"]["SwiftURL"]
-    INDEXED_PACKAGES_FP = cp["web"]["indexed_packages_fp"]
-    swift_container_url = os.path.join(url, "autopkgtest-%s")
+        CONFIG["swift_container_url"] = (
+            cp["web"]["SwiftURL"] + "/autopkgtest-%s"
+        )
+    CONFIG["indexed_packages"] = Path(cp["web"]["indexed_packages"])
+    CONFIG["amqp_queue_cache"] = Path(cp["web"]["amqp_queue_cache"])
+    CONFIG["running_cache"] = Path(cp["web"]["running_cache"])
+    CONFIG["database"] = Path(cp["web"]["database_ro"])
+
+
+def connect_db(path: str):
+    global db_con
+
+    db_con = sqlite3.connect(path, uri=True, check_same_thread=False)
 
 
 def get_package_release_arch(test_id):
@@ -104,7 +108,7 @@ def get_test_id(release, arch, src):
 
 def get_running_jobs():
     try:
-        with open(RUNNING_CACHE) as f:
+        with open(CONFIG["running_cache"]) as f:
             # package -> runhash -> release -> arch -> (params, duration, logtail)
             return json.load(f)
     except FileNotFoundError as e:
@@ -182,7 +186,7 @@ def get_queues_info():
     Return (releases, arches, context -> release -> arch -> (queue_size, [requests])).
     """
 
-    with open(AMQP_QUEUE_CACHE, "r") as json_file:
+    with open(CONFIG["amqp_queue_cache"], "r") as json_file:
         queue_info_j = json.load(json_file)
 
         arches = queue_info_j["arches"]
@@ -238,7 +242,7 @@ def get_results_for_user(user: str, limit: int, offset: int) -> list:
             code = human_exitcode(row["exitcode"])
             package, release, arch = get_package_release_arch(test_id)
             url = os.path.join(
-                swift_container_url % release,
+                CONFIG["swift_container_url"] % release,
                 release,
                 arch,
                 srchash(package),
@@ -556,7 +560,7 @@ def package_release_arch(package, release, arch, _=None):
         show_retry = code != "pass" and identifier not in seen
         seen.add(identifier)
         url = os.path.join(
-            swift_container_url % release,
+            CONFIG["swift_container_url"] % release,
             release,
             arch,
             srchash(package),
@@ -637,9 +641,11 @@ def package_release_arch(package, release, arch, _=None):
                         (
                             "N/A",
                             item_info.get("triggers"),
-                            "all-proposed=1"
-                            if "all-proposed" in item_info.keys()
-                            else "",
+                            (
+                                "all-proposed=1"
+                                if "all-proposed" in item_info.keys()
+                                else ""
+                            ),
                             human_date(item_info.get("submit-time")),
                             "N/A",
                             "-",
@@ -709,7 +715,7 @@ def get_by_uuid(uuid):
 
     show_retry = code != "pass"
     url = os.path.join(
-        swift_container_url % release,
+        CONFIG["swift_container_url"] % release,
         release,
         arch,
         srchash(package),
@@ -771,7 +777,7 @@ def release(release, arch=None):
             # Version + triggers uniquely identifies this result
             show_retry = code != "pass"
             url = os.path.join(
-                swift_container_url % release,
+                CONFIG["swift_container_url"] % release,
                 release,
                 run_arch,
                 srchash(package),
@@ -875,14 +881,16 @@ def queues_json():
 
 @app.route("/queued.json")
 def return_queued_exactly():
-    return flask.send_file(AMQP_QUEUE_CACHE, mimetype="application/json")
+    return flask.send_file(
+        CONFIG["amqp_queue_cache"], mimetype="application/json"
+    )
 
 
 @app.route("/testlist")
 def testlist():
     indexed_pkgs = {}
     try:
-        with open(INDEXED_PACKAGES_FP, "r") as f:
+        with open(CONFIG["indexed_packages"], "r") as f:
             indexed_pkgs = json.load(f)
     except FileNotFoundError:
         indexed_pkgs = {}
@@ -954,4 +962,5 @@ if __name__ == "__main__":
 
     app.config["DEBUG"] = True
     init_config()
+    connect_db("file:%s?mode=ro" % CONFIG["database"])
     CGIHandler().run(app)
diff --git a/charms/focal/autopkgtest-web/webcontrol/cache-amqp b/charms/focal/autopkgtest-web/webcontrol/cache-amqp
index e953c9d..aefae4b 100755
--- a/charms/focal/autopkgtest-web/webcontrol/cache-amqp
+++ b/charms/focal/autopkgtest-web/webcontrol/cache-amqp
@@ -10,9 +10,9 @@ import tempfile
 import time
 import urllib.parse
 
-import amqplib.client_0_8 as amqp
-from amqplib.client_0_8.exceptions import AMQPChannelException
-from helpers.utils import get_autopkgtest_cloud_conf, is_db_empty
+import amqp
+from amqp.exceptions import AMQPError
+from helpers.utils import amqp_connect, get_autopkgtest_cloud_conf, is_db_empty
 
 AMQP_CONTEXTS = ["ubuntu", "huge", "ppa", "upstream"]
 
@@ -29,9 +29,7 @@ class AutopkgtestQueueContents:
 
         # connect to AMQP
         parts = urllib.parse.urlsplit(self.amqp_uri, allow_fragments=False)
-        self.amqp_con = amqp.Connection(
-            parts.hostname, userid=parts.username, password=parts.password
-        )
+        self.amqp_con = amqp_connect()
         self.amqp_channel = self.amqp_con.channel()
         logger.info("Connected to AMQP host %s", parts.hostname)
 
@@ -50,8 +48,8 @@ class AutopkgtestQueueContents:
                             queue_name, durable=True, passive=True
                         )
                         logger.info(f"Semaphore queue '{queue_name}' exists")
-                    except AMQPChannelException as e:
-                        (code, _, _, _) = e.args
+                    except AMQPError as e:
+                        code = e.code
                         if code != 404:
                             raise
                         if os.path.exists("/run/autopkgtest-web-is-leader"):
@@ -182,8 +180,8 @@ class AutopkgtestQueueContents:
                         )
                     try:
                         requests = self.get_queue_requests(queue_name)
-                    except AMQPChannelException as e:
-                        (code, _, _, _) = e.args
+                    except AMQPError as e:
+                        code = e.code
                         if code != 404:
                             raise
                         requests = []
@@ -217,10 +215,9 @@ class AutopkgtestQueueContents:
 
 
 if __name__ == "__main__":
-    try:
-        state_directory = os.environ["STATE_DIRECTORY"]
-    except KeyError:
-        state_directory = "/var/lib/cache-amqp/"
+    cp = get_autopkgtest_cloud_conf()
+
+    state_directory = cp["web"]["amqp_queue_cache"]
 
     parser = argparse.ArgumentParser(
         description="Fetch AMQP queues into a file"
@@ -244,13 +241,11 @@ if __name__ == "__main__":
         "--output",
         dest="output",
         type=str,
-        default=os.path.join(state_directory, "queued.json"),
+        default=state_directory,
     )
 
     args = parser.parse_args()
 
-    cp = get_autopkgtest_cloud_conf()
-
     formatter = logging.Formatter(
         "%(asctime)s: %(message)s", "%Y-%m-%d %H:%M:%S"
     )
diff --git a/charms/focal/autopkgtest-web/webcontrol/db-backup b/charms/focal/autopkgtest-web/webcontrol/db-backup
index 16cf7ee..f2bbba3 100755
--- a/charms/focal/autopkgtest-web/webcontrol/db-backup
+++ b/charms/focal/autopkgtest-web/webcontrol/db-backup
@@ -17,7 +17,7 @@ import swiftclient
 from helpers.utils import (
     get_autopkgtest_cloud_conf,
     init_db,
-    init_swift_con,
+    swift_connect,
     zstd_compress,
 )
 
@@ -104,7 +104,7 @@ def upload_backup_to_swift(
                 "Retry %i out of %i failed, exception: %s"
                 % (retry, SWIFT_RETRIES, str(e))
             )
-            swift_conn = init_swift_con()
+            swift_conn = swift_connect()
     return swift_conn
 
 
@@ -135,7 +135,7 @@ def delete_old_backups(
                         "Retry %i out of %i failed, exception: %s"
                         % (retry, SWIFT_RETRIES, str(e))
                     )
-                    swift_conn = init_swift_con()
+                    swift_conn = swift_connect()
     return swift_conn
 
 
@@ -160,7 +160,7 @@ if __name__ == "__main__":
     logging.info("Registering cleanup function")
     atexit.register(cleanup)
     logging.info("Setting up swift connection")
-    swift_conn = init_swift_con()
+    swift_conn = swift_connect()
     create_container_if_it_doesnt_exist(swift_conn)
     logging.info("Uploading db to swift!")
     swift_conn = upload_backup_to_swift(swift_conn)
diff --git a/charms/focal/autopkgtest-web/webcontrol/download-all-results b/charms/focal/autopkgtest-web/webcontrol/download-all-results
index dcfacf5..e9ce8ed 100755
--- a/charms/focal/autopkgtest-web/webcontrol/download-all-results
+++ b/charms/focal/autopkgtest-web/webcontrol/download-all-results
@@ -10,10 +10,8 @@
 # notification of completed jobs, in case of bugs or network outages etc, this
 # script can be used to find any results which were missed and insert them.
 
-import configparser
 import datetime
 import io
-import itertools
 import json
 import logging
 import os
@@ -21,38 +19,24 @@ import sqlite3
 import sys
 import tarfile
 import time
-import urllib.parse
 
-import amqplib.client_0_8 as amqp
+import amqp
 import swiftclient
 from distro_info import UbuntuDistroInfo
-from helpers.utils import SqliteWriterConfig, get_autopkgtest_cloud_conf
+from helpers.utils import (
+    SqliteWriterConfig,
+    amqp_connect,
+    get_db_path,
+    swift_connect,
+)
 
 LOGGER = logging.getLogger(__name__)
-SWIFT_CREDS_FILE = "/home/ubuntu/public-swift-creds"
 
 config = None
 db_con = None
 amqp_con = None
 
 
-def amqp_connect():
-    """Connect to AMQP server"""
-
-    cp = configparser.ConfigParser()
-    cp.read(os.path.expanduser("~ubuntu/autopkgtest-cloud.conf"))
-    amqp_uri = cp["amqp"]["uri"]
-    parts = urllib.parse.urlsplit(amqp_uri, allow_fragments=False)
-    amqp_con = amqp.Connection(
-        parts.hostname, userid=parts.username, password=parts.password
-    )
-    logging.info(
-        "Connected to AMQP server at %s@%s", parts.username, parts.hostname
-    )
-
-    return amqp_con
-
-
 def list_remote_container(container_name, swift_conn, marker, limit=1000):
     LOGGER.debug("Listing container %s", container_name)
     _, list_of_test_results = swift_conn.get_container(
@@ -141,18 +125,15 @@ def fetch_one_result(container_name, object_name, swift_conn):
     env_vars = []
     env_spec = ["all-proposed"]
     for env in env_spec:
-        value = str(testinfo.get(env))
+        value = testinfo.get(env)
         if value is not None:
-            env_vars.append("=".join([env, value]))
+            env_vars.append("=".join([env, str(value)]))
 
     start = datetime.datetime.now()
     # Insert the write request into the queue
     while True:
         try:
             complete_amqp = amqp_con.channel()
-            complete_amqp.access_request(
-                "/complete", active=True, read=False, write=True
-            )
             complete_amqp.exchange_declare(
                 SqliteWriterConfig.writer_exchange_name,
                 "fanout",
@@ -217,12 +198,12 @@ def fetch_container(release, swift_conn):
                     object_name=known_results[run_id],
                     swift_conn=swift_conn,
                 )
-        except swiftclient.ClientException as e:
+        except swiftclient.exceptions.ClientException as e:
             LOGGER.error(
                 "Something went wrong accessing container %s\nTraceback: %s"
                 % (container_name, str(e))
             )
-            raise
+            return
 
 
 if __name__ == "__main__":
@@ -241,34 +222,11 @@ if __name__ == "__main__":
     )
     releases.sort(key=UbuntuDistroInfo().all.index, reverse=True)
 
-    config = get_autopkgtest_cloud_conf()
     amqp_con = amqp_connect()
     db_con = sqlite3.connect(
-        "file:%s%s" % (config["web"]["database"], "?mode=ro"), uri=True
+        "file:%s%s" % (get_db_path(), "?mode=ro"), uri=True
     )
-
-    swift_cfg = configparser.ConfigParser()
-
-    with open(SWIFT_CREDS_FILE) as fp:
-        swift_cfg.read_file(
-            itertools.chain(["[swift]"], fp), source=SWIFT_CREDS_FILE
-        )
-
-    swift_creds = {
-        "authurl": swift_cfg["swift"]["OS_AUTH_URL"],
-        "user": swift_cfg["swift"]["OS_USERNAME"],
-        "key": swift_cfg["swift"]["OS_PASSWORD"],
-        "os_options": {
-            "region_name": swift_cfg["swift"]["OS_REGION_NAME"],
-            "project_domain_name": swift_cfg["swift"][
-                "OS_PROJECT_DOMAIN_NAME"
-            ],
-            "project_name": swift_cfg["swift"]["OS_PROJECT_NAME"],
-            "user_domain_name": swift_cfg["swift"]["OS_USER_DOMAIN_NAME"],
-        },
-        "auth_version": 3,
-    }
-    swift_conn = swiftclient.Connection(**swift_creds)
+    swift_conn = swift_connect()
 
     try:
         for release in releases:
diff --git a/charms/focal/autopkgtest-web/webcontrol/download-results b/charms/focal/autopkgtest-web/webcontrol/download-results
index 2c4ea47..02b98d2 100755
--- a/charms/focal/autopkgtest-web/webcontrol/download-results
+++ b/charms/focal/autopkgtest-web/webcontrol/download-results
@@ -7,31 +7,14 @@ import os
 import socket
 import sys
 import time
-import urllib.parse
 
-import amqplib.client_0_8 as amqp
-from helpers.utils import SqliteWriterConfig, get_autopkgtest_cloud_conf
+import amqp
+from helpers.utils import SqliteWriterConfig, amqp_connect
 
 EXCHANGE_NAME = "testcomplete.fanout"
 amqp_con = None
 
 
-def amqp_connect():
-    """Connect to AMQP server"""
-
-    cp = get_autopkgtest_cloud_conf()
-    amqp_uri = cp["amqp"]["uri"]
-    parts = urllib.parse.urlsplit(amqp_uri, allow_fragments=False)
-    amqp_con = amqp.Connection(
-        parts.hostname, userid=parts.username, password=parts.password
-    )
-    logging.info(
-        "Connected to AMQP server at %s@%s", parts.username, parts.hostname
-    )
-
-    return amqp_con
-
-
 def process_message(msg):
     global amqp_con
     body = msg.body
@@ -77,9 +60,6 @@ def process_message(msg):
         try:
             # add to queue instead of writing to db
             with amqp_con.channel() as complete_amqp:
-                complete_amqp.access_request(
-                    "/complete", active=True, read=False, write=True
-                )
                 complete_amqp.exchange_declare(
                     SqliteWriterConfig.writer_exchange_name,
                     "fanout",
@@ -120,12 +100,11 @@ def process_message(msg):
 
 if __name__ == "__main__":
     logging.basicConfig(
-        level=("DEBUG" in os.environ and logging.DEBUG or logging.INFO)
+        level=(logging.DEBUG if "DEBUG" in os.environ else logging.INFO)
     )
 
     amqp_con = amqp_connect()
     status_ch = amqp_con.channel()
-    status_ch.access_request("/complete", active=True, read=True, write=False)
     status_ch.exchange_declare(
         EXCHANGE_NAME, "fanout", durable=True, auto_delete=False
     )
@@ -136,4 +115,4 @@ if __name__ == "__main__":
     logging.info("Listening to requests on %s" % queue_name)
     status_ch.basic_consume("", callback=process_message)
     while status_ch.callbacks:
-        status_ch.wait()
+        amqp_con.drain_events()
diff --git a/charms/focal/autopkgtest-web/webcontrol/helpers/tests.py b/charms/focal/autopkgtest-web/webcontrol/helpers/tests.py
index 8053282..51477bd 100644
--- a/charms/focal/autopkgtest-web/webcontrol/helpers/tests.py
+++ b/charms/focal/autopkgtest-web/webcontrol/helpers/tests.py
@@ -1,5 +1,6 @@
 import json
 from datetime import datetime
+from pathlib import Path
 from uuid import uuid4
 
 from .utils import get_supported_releases
@@ -44,8 +45,9 @@ def populate_dummy_db(db_con):
     db_con.commit()
 
 
-def populate_dummy_amqp_cache(path):
+def populate_dummy_amqp_cache(path: Path):
     supported_releases = get_supported_releases()
+    path.parent.mkdir(parents=True, exist_ok=True)
     with open(path, "w") as f:
         # pylint: disable=line-too-long
         json.dump(
@@ -100,8 +102,9 @@ def populate_dummy_amqp_cache(path):
         )
 
 
-def populate_dummy_running_cache(path):
+def populate_dummy_running_cache(path: Path):
     supported_releases = get_supported_releases()
+    path.parent.mkdir(parents=True, exist_ok=True)
     with open(path, "w") as f:
         json.dump(
             {
diff --git a/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py b/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py
index b92d468..5c3e4ce 100644
--- a/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py
+++ b/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py
@@ -14,10 +14,12 @@ import sqlite3
 import subprocess
 import time
 import typing
+import urllib.parse
 
 # introduced in python3.7, we use 3.8
 from dataclasses import dataclass
 
+import amqp
 import distro_info
 import swiftclient
 
@@ -89,7 +91,29 @@ def read_config_file(
 
 
 def get_autopkgtest_cloud_conf():
-    return read_config_file("/home/ubuntu/autopkgtest-cloud.conf")
+    try:
+        return read_config_file(
+            pathlib.Path("~ubuntu/autopkgtest-cloud.conf").expanduser()
+        )
+    except FileNotFoundError:
+        try:
+            return read_config_file(
+                pathlib.Path("~/autopkgtest-cloud.conf").expanduser()
+            )
+        except FileNotFoundError:
+            try:
+                return read_config_file(
+                    pathlib.Path(__file__).parent.parent
+                    / "autopkgtest-cloud.conf"
+                )
+            except FileNotFoundError as fnfe:
+                raise FileNotFoundError(
+                    "No config file found. Have a look at %s"
+                    % (
+                        pathlib.Path(__file__).parent.parent
+                        / "autopkgtest-cloud.conf.example"
+                    )
+                ) from fnfe
 
 
 def get_autopkgtest_db_conn():
@@ -216,6 +240,8 @@ def setup_key(app, path):
 
 def init_db(path, **kwargs):
     """Create DB if it does not exist, and connect to it"""
+    path = pathlib.Path(path)
+    path.parent.mkdir(parents=True, exist_ok=True)
 
     db = sqlite3.connect(path, **kwargs)
     c = db.cursor()
@@ -275,6 +301,23 @@ def init_db(path, **kwargs):
     return db
 
 
+def amqp_connect():
+    """Connect to AMQP server"""
+
+    cp = get_autopkgtest_cloud_conf()
+    amqp_uri = cp["amqp"]["uri"]
+    parts = urllib.parse.urlsplit(amqp_uri, allow_fragments=False)
+    amqp_con = amqp.Connection(
+        parts.hostname, userid=parts.username, password=parts.password
+    )
+    amqp_con.connect()
+    logging.info(
+        "Connected to AMQP server at %s@%s", parts.username, parts.hostname
+    )
+
+    return amqp_con
+
+
 def get_test_id(db_con, release, arch, src):
     """
     get id of test
@@ -339,24 +382,30 @@ def get_test_id(db_con, release, arch, src):
         return test_id
 
 
-def init_swift_con() -> swiftclient.Connection:
+def swift_connect() -> swiftclient.Connection:
     """
     Establish connection to swift storage
     """
-    swift_creds = {
-        "authurl": os.environ["OS_AUTH_URL"],
-        "user": os.environ["OS_USERNAME"],
-        "key": os.environ["OS_PASSWORD"],
-        "os_options": {
-            "region_name": os.environ["OS_REGION_NAME"],
-            "project_domain_name": os.environ["OS_PROJECT_DOMAIN_NAME"],
-            "project_name": os.environ["OS_PROJECT_NAME"],
-            "user_domain_name": os.environ["OS_USER_DOMAIN_NAME"],
-        },
-        "auth_version": 3,
-    }
-    swift_conn = swiftclient.Connection(**swift_creds)
-    return swift_conn
+    try:
+        config = get_autopkgtest_cloud_conf()
+        swift_creds = {
+            "authurl": config["swift"]["os_auth_url"],
+            "user": config["swift"]["os_username"],
+            "key": config["swift"]["os_password"],
+            "os_options": {
+                "region_name": config["swift"]["os_region_name"],
+                "project_domain_name": config["swift"][
+                    "os_project_domain_name"
+                ],
+                "project_name": config["swift"]["os_project_name"],
+                "user_domain_name": config["swift"]["os_user_domain_name"],
+            },
+            "auth_version": config["swift"]["os_identity_api_version"],
+        }
+        swift_conn = swiftclient.Connection(**swift_creds)
+        return swift_conn
+    except KeyError as e:
+        raise swiftclient.ClientException(repr(e))
 
 
 def is_db_empty(db_con):
@@ -374,9 +423,7 @@ def is_db_empty(db_con):
 
 
 def get_db_path():
-    cp = configparser.ConfigParser()
-    cp.read(os.path.expanduser("~ubuntu/autopkgtest-cloud.conf"))
-    return cp["web"]["database"]
+    return get_autopkgtest_cloud_conf()["web"]["database"]
 
 
 get_test_id._cache = {}
diff --git a/charms/focal/autopkgtest-web/webcontrol/indexed-packages b/charms/focal/autopkgtest-web/webcontrol/indexed-packages
index 1122bd2..114d6d8 100755
--- a/charms/focal/autopkgtest-web/webcontrol/indexed-packages
+++ b/charms/focal/autopkgtest-web/webcontrol/indexed-packages
@@ -15,7 +15,7 @@ def srchash(src):
 
 if __name__ == "__main__":
     cp = get_autopkgtest_cloud_conf()
-    indexed_packages_fp = cp["web"]["indexed_packages_fp"]
+    indexed_packages = cp["web"]["indexed_packages"]
 
     db_con = sqlite3.connect(
         "file:%s?mode=ro" % cp["web"]["database_ro"],
@@ -33,5 +33,5 @@ if __name__ == "__main__":
         # strip off epoch
         v = row[1][row[1].find(":") + 1 :]
         indexed_packages.setdefault(srchash(row[0]), []).append((row[0], v))
-    with open(indexed_packages_fp, "w") as f:
+    with open(indexed_packages, "w") as f:
         json.dump(indexed_packages, f)
diff --git a/charms/focal/autopkgtest-web/webcontrol/private_results/app.py b/charms/focal/autopkgtest-web/webcontrol/private_results/app.py
index 36b1fd0..ab43603 100644
--- a/charms/focal/autopkgtest-web/webcontrol/private_results/app.py
+++ b/charms/focal/autopkgtest-web/webcontrol/private_results/app.py
@@ -1,4 +1,5 @@
 """Test Result Fetcher Flask App"""
+
 import logging
 import os
 import sys
@@ -14,11 +15,7 @@ from flask import (
     session,
 )
 from flask_openid import OpenID
-from helpers.utils import (
-    get_autopkgtest_cloud_conf,
-    read_config_file,
-    setup_key,
-)
+from helpers.utils import setup_key, swift_connect
 from request.submit import Submit
 from werkzeug.middleware.proxy_fix import ProxyFix
 
@@ -96,38 +93,8 @@ app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
 secret_path = os.path.join(PATH, "secret_key")
 setup_key(app, secret_path)
 oid = OpenID(app, os.path.join(PATH, "openid"), safe_roots=[])
-# Load configuration
-cfg = read_config_file(
-    os.path.expanduser("~ubuntu/swift-web-credentials.conf")
-)
-# The web configuration as well
-cfg_web = get_autopkgtest_cloud_conf()
-# Build swift credentials
-auth_url = cfg.get("swift", "auth_url")
-if "/v2.0" in auth_url:
-    swift_creds = {
-        "authurl": auth_url,
-        "user": cfg.get("swift", "username"),
-        "key": cfg.get("swift", "password"),
-        "tenant_name": cfg.get("swift", "tenant"),
-        "os_options": {"region_name": cfg.get("swift", "region_name")},
-        "auth_version": "2.0",
-    }
-else:
-    swift_creds = {
-        "authurl": auth_url,
-        "user": cfg.get("swift", "username"),
-        "key": cfg.get("swift", "password"),
-        "os_options": {
-            "region_name": cfg.get("swift", "region_name"),
-            "project_name": cfg.get("swift", "project_name"),
-            "object_storage_url": cfg_web["web"]["SwiftURL"],
-        },
-        "auth_version": "3.0",
-    }
-cfg_web = None
 # Connect to swift
-connection = swiftclient.Connection(**swift_creds)
+connection = swift_connect()
 
 
 #
diff --git a/charms/focal/autopkgtest-web/webcontrol/publish-db b/charms/focal/autopkgtest-web/webcontrol/publish-db
index 78b4b92..4f97cda 100755
--- a/charms/focal/autopkgtest-web/webcontrol/publish-db
+++ b/charms/focal/autopkgtest-web/webcontrol/publish-db
@@ -14,21 +14,19 @@ import sqlite3
 import sys
 import tempfile
 import urllib.request
+from pathlib import Path
 
 import apt_pkg
 from helpers.utils import get_autopkgtest_cloud_conf, is_db_empty
 
 sqlite3.paramstyle = "named"
 
-config = None
-db_con = None
-
-archive_url = "http://ftpmaster.internal/ubuntu";
-components = ["main", "restricted", "universe", "multiverse"]
+COMPONENTS = ["main", "restricted", "universe", "multiverse"]
 
 
 def init_db(path, path_current, path_rw):
     """Create DB if it does not exist, and connect to it"""
+    Path(path).parent.mkdir(parents=True, exist_ok=True)
     db = sqlite3.connect(path)
     db_rw = sqlite3.connect("file:%s?mode=ro" % path_rw, uri=True)
 
@@ -94,12 +92,7 @@ def init_db(path, path_current, path_rw):
         logging.debug("Old current_versions copied over")
         current_version_copied = True
     except sqlite3.OperationalError as e:
-        if "no such column: pocket" not in str(
-            e
-        ) and "no such column: component" not in str(
-            e
-        ):  # schema upgrade
-            raise
+        logging.debug("failed to copy current_version: %s", str(e))
         current_version_copied = False
 
     try:
@@ -143,8 +136,8 @@ def get_last_checked(db_con, url):
         return None
 
 
-def get_sources(db_con, release):
-    for component in components:
+def get_sources(db_con, release, archive_url):
+    for component in COMPONENTS:
         for pocket in (release, release + "-updates"):
             logging.debug("Processing %s/%s", pocket, component)
             try:
@@ -200,8 +193,9 @@ def get_sources(db_con, release):
 
 
 if __name__ == "__main__":
-    if "DEBUG" in os.environ:
-        logging.basicConfig(level=logging.DEBUG)
+    logging.basicConfig(
+        level=(logging.DEBUG if "DEBUG" in os.environ else logging.INFO)
+    )
 
     config = get_autopkgtest_cloud_conf()
 
@@ -210,6 +204,8 @@ if __name__ == "__main__":
     target_checksum = "{}.sha256".format(target)
     target_checksum_new = "{}.new".format(target_checksum)
 
+    archive_url = config["web"]["archive_url"]
+
     try:
         # systemd makes sure to not run us in parallel, so we can safely
         # delete any leftover file.
@@ -219,7 +215,7 @@ if __name__ == "__main__":
     db_con = init_db(target_new, target, config["web"]["database"])
 
     for row in db_con.execute("SELECT DISTINCT release FROM test"):
-        get_sources(db_con, row[0])
+        get_sources(db_con, row[0], archive_url)
     db_con.commit()
     db_con.close()
 
diff --git a/charms/focal/autopkgtest-web/webcontrol/request/submit.py b/charms/focal/autopkgtest-web/webcontrol/request/submit.py
index 9635223..f91c0a1 100644
--- a/charms/focal/autopkgtest-web/webcontrol/request/submit.py
+++ b/charms/focal/autopkgtest-web/webcontrol/request/submit.py
@@ -7,17 +7,16 @@ import base64
 import json
 import logging
 import os
-import pathlib
 import re
 import sqlite3
 import urllib.parse
 import urllib.request
 import uuid
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from time import time
 from urllib.error import HTTPError
 
-import amqplib.client_0_8 as amqp
+import amqp
 from distro_info import UbuntuDistroInfo
 from helpers.cache import KeyValueCache
 from helpers.exceptions import (
@@ -29,7 +28,7 @@ from helpers.exceptions import (
     RequestInQueue,
     RequestRunning,
 )
-from helpers.utils import get_autopkgtest_cloud_conf, timeout
+from helpers.utils import amqp_connect, get_autopkgtest_cloud_conf, timeout
 
 # Launchpad REST API base
 LP = "https://api.launchpad.net/1.0/";
@@ -40,35 +39,19 @@ ENV = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]+=[a-zA-Z0-9.:~/ -=]*$")
 # URL and optional branch name
 GIT = re.compile(r"^https?://[a-zA-Z0-9._/~+-]+(#[a-zA-Z0-9._/-]+)?$")
 
-ALLOWED_REQUESTOR_TEAMS = []
-try:
-    allowed_teams = pathlib.Path(
-        "/home/ubuntu/.config/autopkgtest-web/allowed-requestor-teams"
-    )
-    ALLOWED_REQUESTOR_TEAMS = allowed_teams.read_text(
-        encoding="utf-8"
-    ).splitlines()
-except Exception as e:
-    logging.warning(f"Reading allowed teams failed with {e}")
-
 # not teams
 ALLOWED_USERS_PERPACKAGE = {"snapcraft": ["snappy-m-o"]}
 
-# Path to json file detailing the queue
-QUEUE_FP = "/var/lib/cache-amqp/queued.json"
-# Path to json file detailing the running tests
-RUNNING_FP = "/run/amqp-status-collector/running.json"
-
 ALLOWED_USER_CACHE_TIME = timedelta(hours=3)
 
 
 class Submit:
     def __init__(self):
-        cp = get_autopkgtest_cloud_conf()
+        self.config = get_autopkgtest_cloud_conf()
 
         # read valid releases and architectures from DB
         self.db_con = sqlite3.connect(
-            "file:%s?mode=ro" % cp["web"]["database_ro"], uri=True
+            "file:%s?mode=ro" % self.config["web"]["database_ro"], uri=True
         )
         self.releases = set(
             UbuntuDistroInfo().supported() + UbuntuDistroInfo().supported_esm()
@@ -85,13 +68,6 @@ class Submit:
             self.architectures.add(row[0])
         logging.debug("Valid architectures: %s" % self.architectures)
 
-        # dissect AMQP URL
-        self.amqp_creds = urllib.parse.urlsplit(
-            cp["amqp"]["uri"], allow_fragments=False
-        )
-        assert self.amqp_creds.scheme == "amqp"
-        logging.debug("AMQP credentials: %s" % repr(self.amqp_creds))
-
         self.allowed_user_cache = KeyValueCache(
             "/dev/shm/autopkgtest_users.json"
         )
@@ -330,11 +306,7 @@ class Submit:
 
         count = 0
 
-        with amqp.Connection(
-            self.amqp_creds.hostname,
-            userid=self.amqp_creds.username,
-            password=self.amqp_creds.password,
-        ) as amqp_con:
+        with amqp_connect() as amqp_con:
             with amqp_con.channel() as ch:
                 while True:
                     message = ch.basic_get(queue)
@@ -364,17 +336,13 @@ class Submit:
             queue = "debci-%s-%s" % (release, arch)
 
         params["submit-time"] = datetime.strftime(
-            datetime.utcnow(), "%Y-%m-%d %H:%M:%S%z"
+            datetime.now().astimezone(timezone.utc), "%Y-%m-%d %H:%M:%S%z"
         )
         params["uuid"] = str(uuid.uuid4())
         body = "%s\n%s" % (package, json.dumps(params, sort_keys=True))
         try:
             with timeout(seconds=60):
-                with amqp.Connection(
-                    self.amqp_creds.hostname,
-                    userid=self.amqp_creds.username,
-                    password=self.amqp_creds.password,
-                ) as amqp_con:
+                with amqp_connect() as amqp_con:
                     with amqp_con.channel() as ch:
                         ch.basic_publish(
                             amqp.Message(body, delivery_mode=2),  # persistent
@@ -542,8 +510,8 @@ class Submit:
         return code >= 200 and code < 300
 
     # pylint: disable=dangerous-default-value
-    def in_allowed_team(self, person, teams=[]):
-        """Check if person is in ALLOWED_REQUESTOR_TEAMS"""
+    def in_allowed_team(self, person):
+        """Check if person is allowed to queue tests"""
         cached_entry = self.allowed_user_cache.get(person)
         if cached_entry is not None:
             cached_entry = datetime.fromtimestamp(float(cached_entry))
@@ -557,12 +525,12 @@ class Submit:
         # 300 teams are alphabetically before "autopkgtest-requestors",
         # the following will fail.
         _, response = self.lp_request(
-            "~%s/memberships_details?ws.size=300" % person, {}
+            "~%s/super_teams?ws.size=300" % person, {}
         )
         entries = response.get("entries")
         for e in entries:
-            for team in teams or ALLOWED_REQUESTOR_TEAMS:
-                if team in e["team_link"]:
+            for team in self.config["web"]["allowed_requestors"].split(","):
+                if team == e["name"]:
                     self.allowed_user_cache.set(person, time())
                     return True
         return False
@@ -615,10 +583,10 @@ class Submit:
         ppas,
         git,
     ):
-        if not os.path.isfile(RUNNING_FP):
+        if not os.path.isfile(self.config["web"]["running_cache"]):
             return False
         data = {}
-        with open(RUNNING_FP, "r") as f:
+        with open(self.config["web"]["running_cache"], "r") as f:
             data = json.load(f)
         if data == {}:
             return False
@@ -673,10 +641,10 @@ class Submit:
         ppas,
         git,
     ):
-        if not os.path.isfile(QUEUE_FP):
+        if not os.path.isfile(self.config["web"]["amqp_queue_cache"]):
             return False
         data = {}
-        with open(QUEUE_FP, "r") as f:
+        with open(self.config["web"]["amqp_queue_cache"], "r") as f:
             data = json.load(f)
         data = data["queues"]
         this_test = {
diff --git a/charms/focal/autopkgtest-web/webcontrol/request/tests/test_submit.py b/charms/focal/autopkgtest-web/webcontrol/request/tests/test_submit.py
index 6a3c7ed..9bc0df3 100644
--- a/charms/focal/autopkgtest-web/webcontrol/request/tests/test_submit.py
+++ b/charms/focal/autopkgtest-web/webcontrol/request/tests/test_submit.py
@@ -27,7 +27,13 @@ class SubmitTestBase(TestCase):
         MagicMock(
             return_value={
                 "amqp": {"uri": "amqp://user:s3kr1t@1.2.3.4"},
-                "web": {"database": "/ignored", "database_ro": "/ignored"},
+                "web": {
+                    "database": "/ignored",
+                    "database_ro": "/ignored",
+                    "running_cache": "/ignored",
+                    "amqp_queue_cache": "/ignored",
+                    "allowed_requestors": "list,of,groups,but,ignored",
+                },
                 "autopkgtest": {"releases": "testy grumpy"},
             }
         ),
@@ -68,9 +74,9 @@ class DistroRequestValidationTests(SubmitTestBase):
         releases.add("testy")
         self.assertEqual(self.submit.releases, releases)
         self.assertEqual(self.submit.architectures, {"6510", "C51", "hexium"})
-        self.assertEqual(self.submit.amqp_creds.hostname, "1.2.3.4")
-        self.assertEqual(self.submit.amqp_creds.username, "user")
-        self.assertEqual(self.submit.amqp_creds.password, "s3kr1t")
+        self.assertIn("web", self.submit.config)
+        self.assertIn("amqp", self.submit.config)
+        self.assertIn("allowed_requestors", self.submit.config["web"])
 
     def test_bad_release(self):
         """Unknown release"""
@@ -771,6 +777,12 @@ class SendAMQPTests(SubmitTestBase):
 
     @patch("request.submit.amqp.Connection")
     @patch("request.submit.amqp.Message")
+    @patch(
+        "helpers.utils.get_autopkgtest_cloud_conf",
+        MagicMock(
+            return_value={"amqp": {"uri": "amqp://user:s3kr1t@1.2.3.4"}}
+        ),
+    )
     def test_valid_request(self, message_con, mock_con):
         # mostly a passthrough, but ensure that we do wrap the string in Message()
         message_con.side_effect = lambda x, **kwargs: ">%s<" % x
@@ -792,8 +804,8 @@ class SendAMQPTests(SubmitTestBase):
         args, kwargs = cm_channel.basic_publish.call_args
         self.assertEqual({"routing_key": "debci-testy-C51"}, kwargs)
         search = (
-            '>foo\n{"ppas": \["my\/ppa"], "requester": "joe", '
-            + '"submit-time": .*, "triggers": \["ab\/1"], "uuid": ".*"}<'
+            r'>foo\n{"ppas": \["my\/ppa"], "requester": "joe", '
+            + r'"submit-time": .*, "triggers": \["ab\/1"], "uuid": ".*"}<'
         )
         self.assertIsNotNone(re.match(search, args[0]))
 
diff --git a/charms/focal/autopkgtest-web/webcontrol/sqlite-writer b/charms/focal/autopkgtest-web/webcontrol/sqlite-writer
index 50043cc..f7b2939 100755
--- a/charms/focal/autopkgtest-web/webcontrol/sqlite-writer
+++ b/charms/focal/autopkgtest-web/webcontrol/sqlite-writer
@@ -1,7 +1,6 @@
 #!/usr/bin/python3
 # pylint: disable=wrong-import-position
 
-import configparser
 import datetime
 import json
 import logging
@@ -10,44 +9,25 @@ import socket
 import sqlite3
 
 import swiftclient
-
-sqlite3.paramstyle = "named"
-import urllib.parse
-
-import amqplib.client_0_8 as amqp
 from helpers.utils import (
     SqliteWriterConfig,
+    amqp_connect,
     get_db_path,
     get_test_id,
     init_db,
-    init_swift_con,
     is_db_empty,
+    swift_connect,
     zstd_decompress,
 )
 
+sqlite3.paramstyle = "named"
+
 LAST_CHECKPOINT = datetime.datetime.now()
 
 config = None
 db_con = None
 
 
-def amqp_connect():
-    """Connect to AMQP server"""
-
-    cp = configparser.ConfigParser()
-    cp.read(os.path.expanduser("~ubuntu/autopkgtest-cloud.conf"))
-    amqp_uri = cp["amqp"]["uri"]
-    parts = urllib.parse.urlsplit(amqp_uri, allow_fragments=False)
-    amqp_con = amqp.Connection(
-        parts.hostname, userid=parts.username, password=parts.password
-    )
-    logging.info(
-        "Connected to AMQP server at %s@%s", parts.username, parts.hostname
-    )
-
-    return amqp_con
-
-
 def check_msg(queue_msg):
     queue_keys = set(queue_msg.keys())
     if set(SqliteWriterConfig.amqp_entry_fields) == queue_keys:
@@ -113,7 +93,8 @@ def restore_db_from_backup(db_con: sqlite3.Connection):
     backups_container = "db-backups"
     logging.info("Connecting to swift")
     try:
-        swift_conn = init_swift_con()
+        swift_conn = swift_connect()
+        _, objects = swift_conn.get_container(container=backups_container)
     except swiftclient.ClientException as e:
         logging.warning(
             (
@@ -126,7 +107,7 @@ def restore_db_from_backup(db_con: sqlite3.Connection):
         f"Connected to swift! Getting backups from container: {backups_container}"
     )
     db_con.execute("PRAGMA wal_checkpoint(TRUNCATE);")
-    _, objects = swift_conn.get_container(container=backups_container)
+
     latest = objects[-1]
     _, compressed_db_dump = swift_conn.get_object(
         container=backups_container, obj=latest["name"]
@@ -149,7 +130,9 @@ def restore_db_from_backup(db_con: sqlite3.Connection):
 
 
 def main():
-    logging.basicConfig(level=logging.INFO)
+    logging.basicConfig(
+        level=(logging.DEBUG if "DEBUG" in os.environ else logging.INFO)
+    )
     db_con = init_db(get_db_path())
     if is_db_empty(db_con):
         logging.info(
@@ -159,7 +142,6 @@ def main():
         restore_db_from_backup(db_con)
     amqp_con = amqp_connect()
     status_ch = amqp_con.channel()
-    status_ch.access_request("/complete", active=True, read=True, write=False)
     status_ch.exchange_declare(
         SqliteWriterConfig.writer_exchange_name,
         "fanout",
@@ -174,7 +156,7 @@ def main():
     logging.info("Listening to requests on %s" % queue_name)
     status_ch.basic_consume("", callback=lambda msg: msg_callback(msg, db_con))
     while status_ch.callbacks:
-        status_ch.wait()
+        amqp_con.drain_events()
 
 
 if __name__ == "__main__":
diff --git a/charms/focal/autopkgtest-web/webcontrol/swift-cleanup b/charms/focal/autopkgtest-web/webcontrol/swift-cleanup
index 14578a4..8f58eed 100755
--- a/charms/focal/autopkgtest-web/webcontrol/swift-cleanup
+++ b/charms/focal/autopkgtest-web/webcontrol/swift-cleanup
@@ -1,7 +1,5 @@
 #!/usr/bin/python3
-import configparser
 import io
-import itertools
 import json
 import logging
 import os
@@ -12,8 +10,8 @@ import time
 
 import swiftclient
 from distro_info import UbuntuDistroInfo
+from helpers.utils import swift_connect
 
-SWIFT_CREDS_FILE = "/home/ubuntu/public-swift-creds"
 RETRY_WAIT_TIME = 5
 
 
@@ -181,29 +179,6 @@ def fix_testinfo_jsons_for_release(release, swift_conn):
         raise
 
 
-def get_swift_con():
-    swift_cfg = configparser.ConfigParser()
-    with open(SWIFT_CREDS_FILE) as fp:
-        swift_cfg.read_file(
-            itertools.chain(["[swift]"], fp), source=SWIFT_CREDS_FILE
-        )
-    swift_creds = {
-        "authurl": swift_cfg["swift"]["OS_AUTH_URL"],
-        "user": swift_cfg["swift"]["OS_USERNAME"],
-        "key": swift_cfg["swift"]["OS_PASSWORD"],
-        "os_options": {
-            "region_name": swift_cfg["swift"]["OS_REGION_NAME"],
-            "project_domain_name": swift_cfg["swift"][
-                "OS_PROJECT_DOMAIN_NAME"
-            ],
-            "project_name": swift_cfg["swift"]["OS_PROJECT_NAME"],
-            "user_domain_name": swift_cfg["swift"]["OS_USER_DOMAIN_NAME"],
-        },
-        "auth_version": 3,
-    }
-    return swiftclient.Connection(**swift_creds)
-
-
 def get_releases():
     releases = list(
         set(
@@ -217,7 +192,7 @@ def get_releases():
 def main():
     logging.basicConfig(level=logging.INFO)
     logging.info("Setting up swift connection")
-    swift_conn = get_swift_con()
+    swift_conn = swift_connect()
     releases = get_releases()
     for release in releases:
         fix_testinfo_jsons_for_release(release, swift_conn)
diff --git a/charms/focal/autopkgtest-web/webcontrol/update-github-jobs b/charms/focal/autopkgtest-web/webcontrol/update-github-jobs
index ed81915..a90a75e 100755
--- a/charms/focal/autopkgtest-web/webcontrol/update-github-jobs
+++ b/charms/focal/autopkgtest-web/webcontrol/update-github-jobs
@@ -9,17 +9,15 @@ import tarfile
 from datetime import datetime, timedelta
 from pathlib import Path
 
-import swiftclient
 from helpers.utils import (
     get_autopkgtest_cloud_conf,
     get_github_context,
-    read_config_file,
+    swift_connect,
 )
 from request.submit import Submit
 
 PENDING_DIR = Path("/run/autopkgtest_webcontrol/github-pending")
 RUNNING_CACHE = Path("/run/amqp-status-collector/running.json")
-SWIFT_CREDS_FILE = Path("/home/ubuntu/public-swift-creds")
 MAX_DAY_DIFF = 30
 
 swift_container_cache = None
@@ -271,24 +269,8 @@ if __name__ == "__main__":
 
     config = get_autopkgtest_cloud_conf()
     external_url = config["web"]["ExternalURL"]
-    swift_cfg = read_config_file(filepath=SWIFT_CREDS_FILE, cfg_key="swift")
-
-    swift_creds = {
-        "authurl": swift_cfg["swift"]["OS_AUTH_URL"],
-        "user": swift_cfg["swift"]["OS_USERNAME"],
-        "key": swift_cfg["swift"]["OS_PASSWORD"],
-        "os_options": {
-            "region_name": swift_cfg["swift"]["OS_REGION_NAME"],
-            "project_domain_name": swift_cfg["swift"][
-                "OS_PROJECT_DOMAIN_NAME"
-            ],
-            "project_name": swift_cfg["swift"]["OS_PROJECT_NAME"],
-            "user_domain_name": swift_cfg["swift"]["OS_USER_DOMAIN_NAME"],
-        },
-        "auth_version": 3,
-    }
 
-    swift_conn = swiftclient.Connection(**swift_creds)
+    swift_conn = swift_connect()
 
     jobs = sys.argv[1:]
 
diff --git a/mojo/service-bundle b/mojo/service-bundle
index d23d61d..58df44e 100644
--- a/mojo/service-bundle
+++ b/mojo/service-bundle
@@ -4,12 +4,16 @@
 {%- elif stage_name == "staging" or stage_name == "devel" %}
     {%- set releases = "focal jammy noble oracular" %}
     {%- set channel = "latest/edge" %}
+{%- else %}
+    {%- set releases = "noble" %}
 {%- endif %}
 
 {%- if stage_name == "production" %}
     {%- set hostname = "autopkgtest.ubuntu.com" %}
 {%- elif stage_name == "staging" %}
     {%- set hostname = "autopkgtest.staging.ubuntu.com" %}
+{%- elif stage_name == "devel" %}
+    {%- set hostname = "autopkgtest.localhost" %}
 {%- endif %}
 
 {%- if stage_name == "production" or stage_name == "staging" %}
@@ -22,11 +26,13 @@ description: "autopkgtest-cloud"
 series: {{ series }}
 applications:
     autopkgtest-cloud-worker:
+{%- if stage_name == "production" or stage_name == "staging" %}
         charm: ubuntu-release-autopkgtest-cloud-worker
         channel: {{ channel }}
-{%- if stage_name == "production" or stage_name == "staging" %}
         num_units: 3
 {%- else %}
+        # Don't use ~ here, it doesn't work!
+        charm: XXX/path/to/autopkgtest-cloud-git-repo/XXX/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud-worker_ubuntu-20.04-amd64.charm
         num_units: 1
 {%- endif %}
         constraints: mem=16G cores=8 root-disk=40G
@@ -63,11 +69,14 @@ applications:
             swift-project-name: stg-proposed-migration-environment_project
             swift-user-domain-name: Default
 {%- elif stage_name == "devel" %}
-            swift-auth-url: XXX
-            swift-username: XXX
-            swift-region: XXX
+            influxdb-username: dev_proposed_migration
+            influxdb-context: devel
+            # Most Canonistack values can be found in your canonistack novarc file
+            swift-auth-url: https://keystone.bos01.canonistack.canonical.com:5000/v3
+            swift-username: XXX  # canonistack username
+            swift-region: canonistack-bos01
             swift-project-domain-name: default
-            swift-project-name: XXX
+            swift-project-name: XXX_project  # canonistack project
             swift-user-domain-name: default
             swift-auth-version: 3
 {%- endif %}
@@ -102,6 +111,9 @@ applications:
               default: stag-cpu2-ram4-disk20
               big: stag-cpu4-ram16-disk50
               bos03:
+                arm64:
+                  default: autopkgtest
+                  big: autopkgtest-big
                 ppc64el:
                   default: builder-ppc64el-cpu2-ram4-disk20
                   big: builder-ppc64el-cpu2-ram4-disk100
@@ -109,22 +121,24 @@ applications:
                   default: autopkgtest-s390x
                   big: autopkgtest-big-s390x
             worker-net-names: |-
-              default: net_stg-proposed-migration
+              default: net_stg-proposed-migration-environment
               bos03:
+                arm64: net_stg-proposed-migration-arm64
                 ppc64el: net_stg-proposed-migration-ppc64el
                 s390x: net_stg-proposed-migration-s390x
 {%- elif stage_name == "devel" %}
-            net-name: net_instances
             worker-flavor-config: |-
-              default: stag-cpu2-ram4-disk20
-              big: stag-cpu4-ram16-disk50
+              default: cpu2-ram4-disk20
+              big: cpu4-ram16-disk50
+            worker-net-names: |-
+              default: external-network
 {%- endif %}
 {%- if stage_name == "production" or stage_name == "staging" %}
             mirror: http://ftpmaster.internal/ubuntu/
             worker-args: ssh -s /CHECKOUTDIR//ssh-setup/nova -- --flavor $PACKAGESIZE --security-groups $SECGROUP --name adt-$RELEASE-$ARCHITECTURE-$PACKAGENAME-$TIMESTAMP-$HOSTNAME-$UUID --image adt/ubuntu-$RELEASE-$HOSTARCH-server --keyname testbed-/HOSTNAME/ --net-id=/NET_NAME/ -e TERM=linux -e 'http_proxy={{ http_proxy }}' -e 'https_proxy={{ https_proxy }}' -e 'no_proxy={{ no_proxy }}' --mirror=/MIRROR/
             worker-setup-command: /AUTOPKGTEST_CLOUD_DIR//worker-config-production/setup-canonical.sh
 {% else %}
-            mirror: http://ports.ubuntu.com/ubuntu-ports/
+            mirror: http://archive.ubuntu.com/ubuntu/
             worker-args: ssh -s /CHECKOUTDIR//ssh-setup/nova -- --flavor $PACKAGESIZE --security-groups $SECGROUP --name adt-$RELEASE-$ARCHITECTURE-$PACKAGENAME-$TIMESTAMP-$HOSTNAME-$UUID --image adt/ubuntu-$RELEASE-$ARCHITECTURE-server-.* --keyname testbed-/HOSTNAME/ --mirror=/MIRROR/
 {% endif %}
 {%- if stage_name == "production" %}
@@ -146,13 +160,21 @@ applications:
                   s390x: 1
 {%- elif stage_name == "devel" %}
             n-workers: |-
+              devstack:
+                  amd64: 1
               bos03:
+                  amd64: 1
                   arm64: 0
                   ppc64el: 0
 {%- endif %}
     autopkgtest-lxd-worker:
+{%- if stage_name == "production" or stage_name == "staging" %}
         charm: ubuntu-release-autopkgtest-cloud-worker
         channel: {{ channel }}
+{%- else %}
+        # Don't use ~ here, it doesn't work!
+        charm: XXX/path/to/autopkgtest-cloud-git-repo/XXX/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud-worker_ubuntu-20.04-amd64.charm
+{%- endif %}
         num_units: 1
         constraints: mem=16G cores=8 root-disk=40G
 {%- if stage_name == "production" or stage_name == "staging" %}
@@ -217,8 +239,13 @@ applications:
             enable_modules: include cgi proxy proxy_http remoteip
             mpm_type: prefork
     autopkgtest-web:
+{%- if stage_name == "production" or stage_name == "staging" %}
         charm: ubuntu-release-autopkgtest-web
         channel: {{ channel }}
+{% else %}
+        # Don't use ~ here, it doesn't work!
+        charm: XXX/path/to/autopkgtest-cloud-git-repo/XXX/charms/focal/autopkgtest-web/autopkgtest-web_ubuntu-20.04-amd64.charm
+{%- endif %}
         options:
             hostname: {{ hostname }}
             allowed-requestor-teams: |-
@@ -230,38 +257,37 @@ applications:
                 canonical-server
                 canonical-ubuntu-qa
 {%- if stage_name == "production" %}
+            archive-url: http://ftpmaster.internal/ubuntu/
             influxdb-context: production
             influxdb-username: prod_proposed_migration
             {%- set storage_host_internal = "objectstorage.prodstack5.canonical.com:443" %}
             {%- set storage_path_internal = "/swift/v1/AUTH_0f9aae918d5b4744bf7b827671c86842" %}
 {%- elif stage_name == "staging" %}
+            archive-url: http://ftpmaster.internal/ubuntu/
             {%- set storage_host_internal = "objectstorage.prodstack5.canonical.com:443" %}
             {%- set storage_path_internal = "/swift/v1/AUTH_cc509e38c54f4edebda2fd17557309bb" %}
             influxdb-username: stg_proposed_migration
             influxdb-context: staging
 {%- elif stage_name == "devel" %}
+            archive-url: http://archive.ubuntu.com/ubuntu/
             influxdb-context: XXX
             influxdb-username: XXX
-            storage_host_internal: XXX
-            storage_path_internal: XXX
-            influxdb-hostname: XXX
-            influxdb-password: XXX
-            influxdb-database: XXX
+            {# canonistack objectstorage URL, find this with `swift auth` #}
+            {%- set storage_host_internal = "swift-proxy.bos01.canonistack.canonical.com:8080" %}
+            {# canonistack swift path, find this with `swift auth` #}
+            {%- set storage_path_internal = "/v1/AUTH_0123456789abcdef0123456789abcdef" %}
 {%- endif %}
             storage-url-internal: https://{{ storage_host_internal }}{{ storage_path_internal }}
-{%- if stage_name == "production" or stage_name == "staging" %}
             github-secrets: include-file://{{local_dir}}/github-secrets.json
-            github-status-credentials: include-file://{{local_dir}}/github-status-credentials.txt
-            swift-web-credentials: include-file://{{local_dir}}/swift-web-credentials.conf
-            public-swift-creds: include-file://{{local_dir}}/public-swift-creds
             external-web-requests-api-keys: include-file://{{local_dir}}/external-web-requests-api-keys.json
+            public-swift-creds: include-file://{{local_dir}}/public-swift-creds
+{%- if stage_name == "production" or stage_name == "staging" %}
+            github-status-credentials: include-file://{{local_dir}}/github-status-credentials.txt
             influxdb-hostname: include-file://{{ local_dir }}/influx-hostname.txt
             influxdb-password: include-file://{{ local_dir }}/influx-password.txt
             influxdb-database: metrics
             https-proxy: {{ https_proxy }}
             no-proxy: {{ no_proxy }}
-            cookies: S0 S1
-            indexed-packages-fp: /home/ubuntu/indexed-packages.json
 {%- endif %}
     haproxy:
         charm: cs:haproxy
diff --git a/mojorc b/mojorc
new file mode 100644
index 0000000..cc81516
--- /dev/null
+++ b/mojorc
@@ -0,0 +1,14 @@
+# This file will setup your environment for a local develop `mojo run`.
+# Please have a look at the "Deploying" documentation page to know how to use this file.
+
+base_dir="$(dirname "$(realpath "$0")")"
+
+export MOJO_ROOT=~/.local/share/mojo
+export MOJO_SERIES=focal
+export MOJO_PROJECT=autopkgtest-cloud
+export MOJO_WORKSPACE=autopkgtest-cloud
+export MOJO_SPEC="$base_dir/mojo/"
+export MOJO_STAGE=devel
+
+mojo project-new $MOJO_PROJECT -s $MOJO_SERIES --container containerless
+mojo workspace-new --project $MOJO_PROJECT -s $MOJO_SERIES $MOJO_SPEC $MOJO_WORKSPACE

Follow ups