← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~lgp171188/launchpad:charm-launchpad-codehosting into launchpad:master

 

Guruprasad has proposed merging ~lgp171188/launchpad:charm-launchpad-codehosting into launchpad:master.

Commit message:
Add a launchpad-codehosting charm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~lgp171188/launchpad/+git/launchpad/+merge/452140
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~lgp171188/launchpad:charm-launchpad-codehosting into launchpad:master.
diff --git a/charm/launchpad-codehosting/actions.yaml b/charm/launchpad-codehosting/actions.yaml
new file mode 100644
index 0000000..86aae0a
--- /dev/null
+++ b/charm/launchpad-codehosting/actions.yaml
@@ -0,0 +1,11 @@
+start-services:
+  description: |
+    Start the launchpad-bzr-sftp service.  Usually run after
+    maintenance.
+stop-services:
+  description: |
+    Stop the launchpad-bzr-sftp service.  Usually run in preparation
+    for maintenance.  (Note that this does not stop services in a way that
+    will persist across a reboot.  It also doesn't disable cron jobs, since
+    those are handled by the cron-control mechanism instead; see
+    lp.services.scripts.base.cronscript_enabled.)
diff --git a/charm/launchpad-codehosting/actions/actions.py b/charm/launchpad-codehosting/actions/actions.py
new file mode 100644
index 0000000..cbac43e
--- /dev/null
+++ b/charm/launchpad-codehosting/actions/actions.py
@@ -0,0 +1,53 @@
+#! /usr/bin/python3
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+import subprocess
+import sys
+import traceback
+from pathlib import Path
+
+sys.path.append("lib")
+
+from charms.layer import basic  # noqa: E402
+
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+from charmhelpers.core import hookenv  # noqa: E402
+
+
+def start_services():
+    service = "launchpad-bzr-sftp.service"
+    hookenv.log(f"Starting {service}.")
+    subprocess.run(["systemctl", "start", service], check=True)
+    hookenv.action_set({"result": "Services started"})
+
+
+def stop_services():
+    service = "launchpad-bzr-sftp.service"
+    hookenv.log(f"Stopping {service}.")
+    subprocess.run(["systemctl", "stop", service], check=True)
+    hookenv.action_set({"result": "Services stopped"})
+
+
+def main(argv):
+    action = Path(argv[0]).name
+    try:
+        if action == "start-services":
+            start_services()
+        elif action == "stop-services":
+            stop_services()
+        else:
+            hookenv.action_fail(f"Action {action} not implemented.")
+    except Exception:
+        hookenv.action_fail("Unhandled exception")
+        tb = traceback.format_exc()
+        hookenv.action_set(dict(traceback=tb))
+        hookenv.log(f"Unhandled exception in action {action}:")
+        for line in tb.splitlines():
+            hookenv.log(line)
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/charm/launchpad-codehosting/actions/start-services b/charm/launchpad-codehosting/actions/start-services
new file mode 120000
index 0000000..405a394
--- /dev/null
+++ b/charm/launchpad-codehosting/actions/start-services
@@ -0,0 +1 @@
+actions.py
\ No newline at end of file
diff --git a/charm/launchpad-codehosting/actions/stop-services b/charm/launchpad-codehosting/actions/stop-services
new file mode 120000
index 0000000..405a394
--- /dev/null
+++ b/charm/launchpad-codehosting/actions/stop-services
@@ -0,0 +1 @@
+actions.py
\ No newline at end of file
diff --git a/charm/launchpad-codehosting/charmcraft.yaml b/charm/launchpad-codehosting/charmcraft.yaml
new file mode 100644
index 0000000..302c2f4
--- /dev/null
+++ b/charm/launchpad-codehosting/charmcraft.yaml
@@ -0,0 +1,76 @@
+type: charm
+bases:
+  - build-on:
+    - name: ubuntu
+      channel: "20.04"
+      architectures: [amd64]
+    run-on:
+    - name: ubuntu
+      channel: "20.04"
+      architectures: [amd64]
+parts:
+  charm-wheels:
+    source: https://git.launchpad.net/~ubuntuone-hackers/ols-charm-deps/+git/wheels
+    source-commit: "42c89d9c66dbe137139b047fd54aed49b66d1a5e"
+    source-submodules: []
+    source-type: git
+    plugin: dump
+    organize:
+      "*": charm-wheels/
+    prime:
+      - "-charm-wheels"
+  ols-layers:
+    source: https://git.launchpad.net/ols-charm-deps
+    source-commit: "9c59a9804f1f40e2a74be7dac9bf18a655a7864f"
+    source-submodules: []
+    source-type: git
+    plugin: dump
+    organize:
+      "*": layers/
+    stage:
+      - layers
+    prime:
+      - "-layers"
+  launchpad-layers:
+    after:
+      - ols-layers
+    source: https://git.launchpad.net/launchpad-layers
+    source-commit: "58edb3e5a88794c3baa2274a94e21d3a298a6c79"
+    source-submodules: []
+    source-type: git
+    plugin: dump
+    organize:
+      launchpad-base: layers/layer/launchpad-base
+      launchpad-db: layers/layer/launchpad-db
+      launchpad-payload: layers/layer/launchpad-payload
+    stage:
+      - layers
+    prime:
+      - "-layers"
+  interface-apache-website:
+    source: https://github.com/juju-solutions/interface-apache-website
+    source-commit: "2f736ebcc90d19ac142a2d898a2ec7e1aafaa13f"
+    source-submodules: []
+    source-type: git
+    plugin: dump
+    organize:
+      "*": layers/interface/apache-website/
+    stage:
+      - layers
+    prime:
+      - "-layers"
+  charm:
+    after:
+      - charm-wheels
+      - launchpad-layers
+      - interface-apache-website
+    source: .
+    plugin: reactive
+    build-snaps: [charm]
+    build-packages: [libpq-dev, python3-dev]
+    build-environment:
+      - CHARM_LAYERS_DIR: $CRAFT_STAGE/layers/layer
+      - CHARM_INTERFACES_DIR: $CRAFT_STAGE/layers/interface
+      - PIP_NO_INDEX: "true"
+      - PIP_FIND_LINKS: $CRAFT_STAGE/charm-wheels
+    reactive-charm-build-arguments: [--binary-wheels-from-source]
diff --git a/charm/launchpad-codehosting/config.yaml b/charm/launchpad-codehosting/config.yaml
new file mode 100644
index 0000000..68f4a1b
--- /dev/null
+++ b/charm/launchpad-codehosting/config.yaml
@@ -0,0 +1,46 @@
+options:
+  active:
+    type: boolean
+    description: If true, enable jobs that may change the database.
+    default: true
+  blocklisted_hostnames:
+    type: string
+    description: Comma-separated list of hostnames to blocklist.
+    default: "localhost,127.0.0.1"
+  codebrowse_internal_endpoint:
+    type: string
+    description: |
+      The internal-only endpoint at which the codebrowse service is
+      accessible.
+  codehosting_private_ssh_key:
+    type: string
+    description: >
+      Base64-encoded private SSH RSA host key to be used by the codehosting
+      service. Existing key pair, if any, will be deleted if this is unset.
+    default: ""
+  codehosting_public_ssh_key:
+    type: string
+    description: >
+      Base64-encoded public SSH RSA host key to be used by the codehosting
+      service. Existing key pair, if any, will be deleted if this is unset.
+    default: ""
+  domain_bzr_internal:
+    type: string
+    description: |
+      The internal-only domain name to expose the bazaar get branch by ID
+      service on.
+  port_bzr_sftp_base:
+    type: int
+    description: Base port number for the bzr-sftp service.
+    default: 2224
+  port_web_status_base:
+    type: int
+    description: Base port for the web status service.
+    default: 8024
+  workers:
+    type: int
+    description: >
+      Number of bzr-sftp worker processes.  If set, each worker will listen
+      on consecutive ports starting from each of `port_bzr_sftp_base` and
+      `port_web_status`, so make sure there is enough space between those.
+    default: 1
diff --git a/charm/launchpad-codehosting/layer.yaml b/charm/launchpad-codehosting/layer.yaml
new file mode 100644
index 0000000..408f80b
--- /dev/null
+++ b/charm/launchpad-codehosting/layer.yaml
@@ -0,0 +1,10 @@
+includes:
+  - layer:launchpad-db
+  - interface:apache-website
+options:
+  ols-pg:
+    databases:
+      db:
+        name: launchpad_dev
+        roles: []
+repo: https://git.launchpad.net/launchpad
diff --git a/charm/launchpad-codehosting/metadata.yaml b/charm/launchpad-codehosting/metadata.yaml
new file mode 100644
index 0000000..43c7884
--- /dev/null
+++ b/charm/launchpad-codehosting/metadata.yaml
@@ -0,0 +1,19 @@
+name: launchpad-codehosting
+display-name: launchpad-codehosting
+summary: Launchpad bazaar codehosting service
+maintainer: Launchpad Developers <launchpad-dev@xxxxxxxxxxxxxxxxxxx>
+description: |
+  Launchpad is an open source suite of tools that help people and teams
+  to work together on software projects.
+
+  This charm runs the Launchpad bazaar code hosting service.
+tags:
+  # https://juju.is/docs/charm-metadata#heading--charm-store-fields
+  - network
+series:
+  - focal
+subordinate: true
+requires:
+  apache-website:
+    interface: apache-website
+    scope: container
diff --git a/charm/launchpad-codehosting/reactive/launchpad-codehosting.py b/charm/launchpad-codehosting/reactive/launchpad-codehosting.py
new file mode 100644
index 0000000..ccfe765
--- /dev/null
+++ b/charm/launchpad-codehosting/reactive/launchpad-codehosting.py
@@ -0,0 +1,313 @@
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+import base64
+import os
+import subprocess
+
+from charmhelpers.core import hookenv, host, templating
+from charms.launchpad.base import configure_email, get_service_config
+from charms.launchpad.db import lazr_config_files
+from charms.launchpad.payload import (
+    config_file_path,
+    configure_cron,
+    configure_lazr,
+)
+from charms.reactive import (
+    clear_flag,
+    endpoint_from_flag,
+    helpers,
+    remove_state,
+    set_flag,
+    set_state,
+    when,
+    when_not,
+    when_not_all,
+)
+from ols import base
+
+
+def base64_decode(value):
+    return base64.b64decode(value.encode("ASCII"))
+
+
+def configure_logrotate(config):
+    hookenv.log("Writing logrotate configuration.")
+    templating.render(
+        "logrotate.conf.j2",
+        "/etc/logrotate.d/launchpad",
+        config,
+        perms=0o644,
+    )
+
+
+def configure_systemd(config):
+    hookenv.log("Writing launchpad-bzr-sftp systemd service.")
+    templating.render(
+        "launchpad-bzr-sftp.service.j2",
+        "/lib/systemd/system/launchpad-bzr-sftp.service",
+        config,
+    )
+    templating.render(
+        "launchpad-bzr-sftp@.service.j2",
+        "/lib/systemd/system/launchpad-bzr-sftp@.service",
+        config,
+    )
+    templating.render(
+        "launchpad-bzr-sftp-generator.j2",
+        "/lib/systemd/system-generators/launchpad-bzr-sftp-generator",
+        config,
+        perms=0o555,
+    )
+    subprocess.run(["systemctl", "daemon-reload"])
+
+
+def config_files():
+    files = []
+    files.extend(lazr_config_files())
+    config = get_service_config()
+    for i in range(config["workers"]):
+        files.append(
+            config_file_path(
+                f"launchpad-codehosting{i + 1}/launchpad-lazr.conf"
+            )
+        )
+    files.append(config_file_path("launchpad-codehosting-secrets-lazr.conf"))
+    return files
+
+
+def configure_scripts(config):
+    hookenv.log(f"Creating {config['scripts_dir']}, if doesn't exist already.")
+    host.mkdir(
+        config["scripts_dir"],
+        owner=config["user"],
+        group=config["user"],
+        perms=0o755,
+        force=True,
+    )
+    hookenv.log("Writing the cleanlogs script.")
+    templating.render(
+        "cleanlogs.j2",
+        f"{config['scripts_dir']}/cleanlogs",
+        config,
+        perms=0o755,
+    )
+    host.mkdir(
+        f"{config['logs_dir']}/sftp-logs",
+        owner=config["user"],
+        group=config["user"],
+        perms=0o755,
+        force=True,
+    )
+    hookenv.log("Writing the branch rewrite wrapper script.")
+    templating.render(
+        "rewrite_wrapper.sh.j2",
+        f"{config['scripts_dir']}/rewrite_wrapper.sh",
+        config,
+        owner=config["user"],
+        group=config["user"],
+        perms=0o755,
+    )
+
+
+def configure_ssh_keys(config):
+    host_key_pair_path = f"/srv/{config['domain_bzr']}/keys"
+    hookenv.log(f"Creating {host_key_pair_path} to store the SSH host keys.")
+    user = config["user"]
+    host.mkdir(
+        host_key_pair_path,
+        owner=user,
+        group=user,
+        perms=0o755,
+        force=True,
+    )
+    ssh_private_key_file = f"{host_key_pair_path}/ssh_host_key_rsa"
+    ssh_public_key_file = f"{ssh_private_key_file}.pub"
+    if (
+        config["codehosting_private_ssh_key"]
+        and config["codehosting_public_ssh_key"]
+    ):
+        hookenv.log("Writing the SSH host key pair.")
+        host.write_file(
+            ssh_private_key_file,
+            base64_decode(config["codehosting_private_ssh_key"]),
+            owner=user,
+            group=user,
+            perms=0o600,
+        )
+        host.write_file(
+            ssh_public_key_file,
+            base64_decode(config["codehosting_public_ssh_key"]),
+            owner=user,
+            group=user,
+            perms=0o644,
+        )
+    else:
+        hookenv.log(
+            "SSH key pair not configured. Deleting existing keys, if present."
+        )
+        for path in (ssh_private_key_file, ssh_public_key_file):
+            if os.path.exists(path):
+                os.unlink(path)
+
+
+def configure_codehosting_lazr_config(config):
+    hookenv.log("Writing lazr configuration.")
+    for i in range(config["workers"]):
+        config["service_access_log_file"] = f"codehosting-{i + 1}-access.log"
+        config["service_sftp_port"] = config["port_bzr_sftp_base"] + i
+        config["service_web_status_port"] = config["port_web_status_base"] + i
+        config["service_oops_prefix"] = f"{config['oops_prefix']}{i + 1}"
+        configure_lazr(
+            config,
+            "launchpad-codehosting-lazr.conf.j2",
+            f"launchpad-codehosting{i + 1}/launchpad-lazr.conf",
+        )
+    configure_lazr(
+        config,
+        "launchpad-codehosting-secrets-lazr.conf.j2",
+        "launchpad-codehosting-secrets-lazr.conf",
+        secret=True,
+    )
+
+
+@when("launchpad.db.configured")
+@when_not("service.configured")
+def configure():
+    config = get_service_config()
+    configure_codehosting_lazr_config(config)
+    configure_email(config, "launchpad-codehosting")
+    configure_logrotate(config)
+    configure_cron(config, "crontab.j2")
+    configure_scripts(config)
+    configure_ssh_keys(config)
+    configure_systemd(config)
+    if config["active"]:
+        if helpers.any_file_changed(
+            [
+                base.version_info_path(),
+                "/lib/systemd/system/launchpad-bzr-sftp.service",
+                "/lib/systemd/system/launchpad-bzr-sftp@.service",
+                "/lib/systemd/system-generators/launchpad-bzr-sftp-generator",
+            ]
+            + config_files()
+        ):
+            hookenv.log(
+                "Config files changed; restarting"
+                " the launchpad-bzr-sftp service."
+            )
+            for i in range(config["workers"]):
+                host.service_restart(f"launchpad-bzr-sftp@{i + 1}")
+        else:
+            hookenv.log("Not restarting since no config files were changed.")
+        host.service_resume("launchpad-bzr-sftp.service")
+
+    set_state("service.configured")
+
+
+def get_vhost_config(config):
+    hookenv.log("Rendering the virtual hosts configuration.")
+    return "\n".join(
+        [
+            templating.render("vhosts/common.conf", None, config),
+            templating.render("vhosts/bazaar_http.conf.j2", None, config),
+            templating.render("vhosts/bazaar_https.conf.j2", None, config),
+            templating.render(
+                "vhosts/bazaar_internal_branch_by_id.conf.j2", None, config
+            ),
+        ]
+    )
+
+
+def configure_document_root(config):
+    hookenv.log("Configuring the document root.")
+    document_root_base_dir = f"/srv/{config['domain_bzr']}"
+    user = config["user"]
+    host.mkdir(
+        document_root_base_dir,
+        owner=user,
+        group="root",
+        perms=0o755,
+        force=True,
+    )
+    config["bzr_repositories_root"] = f"{document_root_base_dir}/mirrors"
+    host.mkdir(
+        config["bzr_repositories_root"],
+        owner=user,
+        group=user,
+        perms=0o755,
+        force=True,
+    )
+
+    static_files_root = f"{document_root_base_dir}/www"
+    config["static_files_root"] = static_files_root
+    host.mkdir(
+        static_files_root,
+        owner=user,
+        group=user,
+        perms=0o755,
+        force=True,
+    )
+    templating.render(
+        "robots.txt",
+        f"{static_files_root}/robots.txt",
+        config,
+        owner=user,
+        group=user,
+        perms=0o644,
+    )
+    code_dir = config["code_dir"]
+    host.symlink(
+        f"{code_dir}/lib/canonical/launchpad/offline-unplanned.html",
+        f"{static_files_root}/offline.html",
+    )
+    site_packages_dir = (
+        subprocess.check_output(
+            [
+                f"{code_dir}/env/bin/python",
+                "-c",
+                "import sysconfig; print(sysconfig.get_path('purelib'))",
+            ]
+        )
+        .strip()
+        .decode("utf-8")
+    )
+    assert site_packages_dir.startswith(f"{code_dir}/env")
+    host.symlink(
+        f"{site_packages_dir}/loggerhead/static", f"{static_files_root}/static"
+    )
+
+
+@when(
+    "service.configured",
+    "config.set.domain_bzr",
+    "config.set.domain_bzr_internal",
+    "apache-website.available",
+)
+@when_not("service.apache-website.configured")
+def configure_apache_website():
+    apache_website = endpoint_from_flag("apache-website.available")
+    config = get_service_config()
+    configure_document_root(config)
+    apache_website.set_remote(
+        domain=config["domain_bzr"],
+        enabled="true",
+        ports="80 8080 8081",
+        site_config=get_vhost_config(config),
+        site_modules="headers proxy proxy_http rewrite",
+    )
+    hookenv.status_set("active", "Ready")
+    set_flag("service.apache-website.configured")
+
+
+@when("service.apache-website.configured")
+@when_not_all("service.configured", "apache-website.available")
+def apache_deconfigured():
+    hookenv.status_set("blocked", "Website not yet configured")
+    clear_flag("service.apache-website.configured")
+
+
+@when("service.configured")
+@when_not("launchpad.db.configured")
+def deconfigure():
+    remove_state("service.configured")
diff --git a/charm/launchpad-codehosting/templates/cleanlogs.j2 b/charm/launchpad-codehosting/templates/cleanlogs.j2
new file mode 100644
index 0000000..b2cf752
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/cleanlogs.j2
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+ROOTDIR={{ logs_dir }}/sftp-logs
+DEST=$ROOTDIR/archives/$(date +%F)
+
+mkdir -p $DEST
+find -L $ROOTDIR -maxdepth 1 -type f -name 'bzr-sftp?.log.*' -exec mv {} $DEST/ \;
+
+cd $DEST
+for i in $(ls)
+do
+	bzip2 $i
+done
+
+# Remove older than 90 days
+find -L $ROOTDIR/archives -maxdepth 1 -type d -mtime +90 -print0 | xargs -0r rm -r
+
diff --git a/charm/launchpad-codehosting/templates/crontab.j2 b/charm/launchpad-codehosting/templates/crontab.j2
new file mode 100644
index 0000000..b6ae48d
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/crontab.j2
@@ -0,0 +1,29 @@
+TZ=UTC
+MAILFROM={{ bounce_address }}
+MAILTO={{ cron_mailto }}
+LPCONFIG=launchpad-codehosting
+
+{%- if active %}
+
+* * * * * {% if http_proxy %}http_proxy={{ http_proxy }} https_proxy={{ http_proxy }}{% endif %}{{ code_dir }}/cronscripts/supermirror-pull.py -q --log-file=INFO:{{ logs_dir }}/puller.log
+
+# remove from disk, deleted branches
+10 0 * * * {{ code_dir }}/cronscripts/process-job-source.py IReclaimBranchSpaceJobSource -q --log-file=DEBUG:{{ logs_dir }}/process-job-source.IReclaimBranchSpaceJobSource.log
+
+# Archive supermirror SFTP log files per https://portal.admin.canonical.com/C26974
+5 0 * * * {{ scripts_dir }}/cleanlogs
+
+# Translations to branch script https://portal.admin.canonical.com/C35040
+30 04 * * * {{ code_dir }}/cronscripts/translations-export-to-branch.py -q --log-file=DEBUG:{{ logs_dir }}/translations-export-to-branch.log
+
+# Upgrade branches script
+*/10 * * * * {{ code_dir }}/cronscripts/process-job-source.py IBranchUpgradeJobSource -q --log-file=DEBUG:{{ logs_dir }}/process-job-source.IBranchUpgradeJobSource.log
+
+# cleanup old crud in /tmp cf. https://bugs.launchpad.net/launchpad/+bug/979511
+6 4 * * *  find /tmp -maxdepth 1 -name 'bzr-index-*' -type f -mtime +15 -delete
+
+{%- endif %}
+
+# OOPS amqp
+*/15 * * * * {{ code_dir }}/bin/datedir2amqp --exchange oopses --host {{ rabbitmq_host }} --username {{ rabbitmq_username }} --password {{ rabbitmq_password }} --vhost {{ rabbitmq_vhost }} --repo {{ oopses_dir }} --key ""
+
diff --git a/charm/launchpad-codehosting/templates/launchpad-bzr-sftp-generator.j2 b/charm/launchpad-codehosting/templates/launchpad-bzr-sftp-generator.j2
new file mode 100644
index 0000000..af3670b
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/launchpad-bzr-sftp-generator.j2
@@ -0,0 +1,18 @@
+#! /bin/sh
+# Copyright 2023 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+# Part of the launchpad-codehosting Juju charm.
+
+set -e
+
+wantdir="$1/launchpad-bzr-sftp.service.wants"
+template=/lib/systemd/system/launchpad-bzr-sftp@.service
+
+# Generate systemd unit dependency symlinks for all configured
+# launchpad-bzr-sftp instances.
+mkdir -p "$wantdir"
+for i in $(seq {{ workers }}); do
+    ln -s "$template" "$wantdir/launchpad-bzr-sftp@$i.service"
+done
+
diff --git a/charm/launchpad-codehosting/templates/launchpad-bzr-sftp.service.j2 b/charm/launchpad-codehosting/templates/launchpad-bzr-sftp.service.j2
new file mode 100644
index 0000000..36dec59
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/launchpad-bzr-sftp.service.j2
@@ -0,0 +1,18 @@
+# This service is really a systemd target, but we use a service since
+# targets cannot be reloaded.  Starting, stopping, or reloading this service
+# causes all the individual instances (defined in
+# launchpad-bzr-sftp@.service) to be started, stopped, or reloaded
+# respectively.
+
+[Unit]
+Description=Launchpad bzr sftp
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/bin/true
+ExecReload=/bin/true
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/charm/launchpad-codehosting/templates/launchpad-bzr-sftp@.service.j2 b/charm/launchpad-codehosting/templates/launchpad-bzr-sftp@.service.j2
new file mode 100644
index 0000000..d3c9b57
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/launchpad-bzr-sftp@.service.j2
@@ -0,0 +1,24 @@
+[Unit]
+Description=Launchpad bzr sftp service (%i)
+PartOf=launchpad-bzr-sftp.service
+Before=launchpad-bzr-sftp.service
+ReloadPropagatedFrom=launchpad-bzr-sftp.service
+After=network.target
+ConditionPathExists=!{{ code_dir }}/maintenance.txt
+
+[Service]
+User=launchpad
+Group=launchpad
+WorkingDirectory={{ code_dir }}
+# https://portal.admin.canonical.com/C44221
+Environment=LPCONFIG=launchpad-codehosting%i
+SyslogIdentifier=bzr-sftp
+ExecStart={{ code_dir }}/bin/twistd --python daemons/sftp.tac --pidfile {{ var_dir }}/bzr-sftp%i.pid --prefix bzr-sftp --logfile {{ logs_dir }}/sftp-logs/bzr-sftp%i.log --nodaemon
+ExecReload=/bin/kill -USR1 $MAINPID
+KillMode=mixed
+Restart=on-failure
+PrivateTmp=true
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/charm/launchpad-codehosting/templates/launchpad-codehosting-lazr.conf.j2 b/charm/launchpad-codehosting/templates/launchpad-codehosting-lazr.conf.j2
new file mode 100644
index 0000000..e3e0abe
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/launchpad-codehosting-lazr.conf.j2
@@ -0,0 +1,29 @@
+# Public configuration data.  The contents of this file may be freely shared
+# with developers if needed for debugging.
+
+# A schema's sections, keys, and values are automatically inherited,
+# except for '.optional' sections. Update this config to override key
+# values. Values are strings, except for numbers that look like ints.
+# The tokens true, false, and none are treated as True, False, and None.
+
+{% from "macros.j2" import opt -%}
+
+[meta]
+extends: ../launchpad-db-lazr.conf
+
+[launchpad]
+launch: False
+
+[error_reports]
+oops_prefix: {{ service_oops_prefix }}
+
+[codehosting]
+access_log: {{ logs_dir }}/{{ service_access_log_file }}
+blacklisted_hostnames: {{ blocklisted_hostnames }}
+host_key_pair_path: /srv/{{ domain_bzr }}/keys/
+{{- opt("internal_branch_by_id_root", internal_branch_by_id_root) }}
+mirrored_branches_root: /srv/{{ domain_bzr }}/mirrors
+port: tcp:{{ service_sftp_port }}
+rewrite_script_log_file: {{ logs_dir }}/rewrite.log
+web_status_port: tcp:{{ service_web_status_port }}
+
diff --git a/charm/launchpad-codehosting/templates/launchpad-codehosting-secrets-lazr.conf.j2 b/charm/launchpad-codehosting/templates/launchpad-codehosting-secrets-lazr.conf.j2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/launchpad-codehosting-secrets-lazr.conf.j2
diff --git a/charm/launchpad-codehosting/templates/logrotate.conf.j2 b/charm/launchpad-codehosting/templates/logrotate.conf.j2
new file mode 100644
index 0000000..1b87f30
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/logrotate.conf.j2
@@ -0,0 +1,16 @@
+{{ logs_dir }}/puller.log {{ logs_dir }}/rewrite.log {
+        rotate 30
+        daily
+        dateext
+        compress
+        delaycompress
+}
+
+{{ logs_dir }}/codehosting-*-access.log {{ logs_dir }}/process-job-source.*.log {
+        rotate 90
+        daily
+        dateext
+        compress
+        delaycompress
+}
+
diff --git a/charm/launchpad-codehosting/templates/rewrite_wrapper.sh.j2 b/charm/launchpad-codehosting/templates/rewrite_wrapper.sh.j2
new file mode 100644
index 0000000..fab9c16
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/rewrite_wrapper.sh.j2
@@ -0,0 +1,3 @@
+#!/bin/sh
+sudo -u {{ user }} LPCONFIG=launchpad-codehosting {{ code_dir }}/scripts/branch-rewrite.py
+
diff --git a/charm/launchpad-codehosting/templates/robots.txt b/charm/launchpad-codehosting/templates/robots.txt
new file mode 100644
index 0000000..e62cb38
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/robots.txt
@@ -0,0 +1,4 @@
+# go away
+User-agent: *
+Disallow: /
+
diff --git a/charm/launchpad-codehosting/templates/vhosts/bazaar_http.conf.j2 b/charm/launchpad-codehosting/templates/vhosts/bazaar_http.conf.j2
new file mode 100644
index 0000000..ff53bf2
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/vhosts/bazaar_http.conf.j2
@@ -0,0 +1,66 @@
+<VirtualHost *:80>
+  ServerName {{ domain_bzr }}
+
+  CustomLog /var/log/apache2/{{ domain_bzr }}-access.log combined_timer
+  ErrorLog /var/log/apache2/{{ domain_bzr }}-error.log
+
+  ProxyRequests off
+  <Proxy *>
+    <RequireAll>
+      Require all granted
+      # Cowboy to stop Sogou web spider smashing bazaar, 13-03-18
+      Require expr %{HTTP_USER_AGENT} !~ /^Sogou web spider/
+    </RequireAll>
+
+    ErrorDocument 500 /offline.html
+    ErrorDocument 502 /offline.html
+    ErrorDocument 503 /offline.html
+  </Proxy>
+  ProxyTimeout 20
+
+  Alias /robots.txt {{ static_files_root }}/robots.txt
+  Alias /favicon.ico {{ static_files_root }}/images/favicon.ico
+  Alias /offline.html {{ static_files_root }}/offline.html
+  Alias /static {{ static_files_root }}/static
+  <Directory {{ static_files_root }}/www>
+    Options -Indexes +SymLinksIfOwnerMatch
+    AllowOverride None
+    Require all granted
+  </Directory>
+
+  # Rewrite Logic Flow:
+  # (1) People hitting the frontpage (i.e. /) are redirected to the
+  # Launchpad frontpage
+  # (2) The /favicon.ico file is served normally.
+  # (3) The /robots.txt file is served normally.
+  # (4) The /offline.html file is served normally.
+  # (5) Any /static/* files are served normally.
+  # (6) Everything else is passed through the branch-rewrite wrapper script
+
+  RewriteEngine On
+  RewriteMap branch-rewrite prg:{{ scripts_dir }}/rewrite_wrapper.sh
+
+  # (1) / -> Launchpad frontpage
+  RewriteRule ^/$ https://{{ domain }} [L]
+  # (2) The /favicon.ico file is served normally.
+  RewriteRule ^/favicon.ico$ - [L]
+  # (3) The /robots.txt file is served normally.
+  RewriteRule ^/robots.txt$ - [L]
+  # (4) The /offline.html file is served normally.
+  RewriteRule ^/offline.html$ - [L]
+  # (5) Any /static/* files are served normally.
+  RewriteRule ^/static/(.*)$ - [L]
+
+  # (6) The branch-rewrite wrapper script. This sets the appropriate ENV
+  # setting(s) necessary.
+  RewriteMap escape int:escape
+  RewriteMap unescape int:unescape
+  RewriteRule ^(/.*)$ ${unescape:${branch-rewrite:${escape:$1}}} [L,P]
+
+  # https://portal.admin.canonical.com/C79782 yes this looks mad but it
+  # is just making NOOP proxypass commands so
+  # the rewrite [P] lines have persistent pools to work with.
+  ProxyPass / !
+  ProxyPass / http://{{ domain_bzr }}/
+</VirtualHost>
+
diff --git a/charm/launchpad-codehosting/templates/vhosts/bazaar_https.conf.j2 b/charm/launchpad-codehosting/templates/vhosts/bazaar_https.conf.j2
new file mode 100644
index 0000000..414d7d9
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/vhosts/bazaar_https.conf.j2
@@ -0,0 +1,38 @@
+<VirtualHost *:8080>
+  ServerName      {{ domain_bzr }}
+  ServerAdmin     webmaster@xxxxxxxxxxxxx
+
+  CustomLog       /var/log/apache2/{{ domain_bzr }}-access.log combined_timer
+  ErrorLog        /var/log/apache2/{{ domain_bzr }}-error.log
+
+  # This virtual host serves a few files/statics only; all else is reverse
+  # proxy to codebrowse.
+  DocumentRoot {{ static_files_root }}
+
+  <Directory {{ static_files_root }}>
+    Options -Indexes +SymLinksIfOwnerMatch
+    Require all granted
+  </Directory>
+
+  ProxyRequests off
+  <Proxy *>
+    <RequireAll>
+      Require all granted
+      # Cowboy to stop Sogou web spider smashing bazaar, 13-03-18
+      Require expr %{HTTP_USER_AGENT} !~ /^Sogou web spider/
+   </RequireAll>
+    ErrorDocument 500 /offline.html
+    ErrorDocument 502 /offline.html
+    ErrorDocument 503 /offline.html
+  </Proxy>
+  ProxyTimeout 20
+  ProxyPassReverse / {{ codebrowse_internal_endpoint }}
+
+  RewriteEngine On
+  RewriteRule ^/offline.html$ - [L]
+  RewriteRule ^/robots.txt$ - [L]
+  RewriteRule ^/favicon.ico$ /static/images/favicon.ico [L]
+  RewriteRule ^/static/(.*)$ - [L]
+  RewriteRule ^/(.*)$ {{ codebrowse_internal_endpoint }}/$1 [P,L]
+</VirtualHost>
+
diff --git a/charm/launchpad-codehosting/templates/vhosts/bazaar_internal_branch_by_id.conf.j2 b/charm/launchpad-codehosting/templates/vhosts/bazaar_internal_branch_by_id.conf.j2
new file mode 100644
index 0000000..a92f2fd
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/vhosts/bazaar_internal_branch_by_id.conf.j2
@@ -0,0 +1,18 @@
+<VirtualHost *:8081>
+  ServerName {{ domain_bzr_internal }}
+
+  CustomLog /var/log/apache2/{{ domain_bzr_internal }}-access.log combined_timer
+  ErrorLog /var/log/apache2/{{ domain_bzr_internal }}-error.log
+
+  # Default location is for all the .bzr repositories in mirrors/*
+  # We serve .bzr/* via http or bzr+ssh; not https
+  DocumentRoot {{ bzr_repositories_root }}
+
+  <Directory {{ bzr_repositories_root }}>
+    Options SymLinksIfOwnerMatch Indexes
+    AllowOverride None
+    Require all granted
+  </Directory>
+
+</VirtualHost>
+
diff --git a/charm/launchpad-codehosting/templates/vhosts/common.conf b/charm/launchpad-codehosting/templates/vhosts/common.conf
new file mode 100644
index 0000000..bea1e98
--- /dev/null
+++ b/charm/launchpad-codehosting/templates/vhosts/common.conf
@@ -0,0 +1,6 @@
+# Needed outside VirtualHost config
+LogFormat "%h %D %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_timer
+
+# For debugging ONLY - do not turn on in normal operation
+# LogLevel alert rewrite:trace4
+