Merge "app/matrix: bump synapse to 1.37.1"
diff --git a/WORKSPACE b/WORKSPACE
index 9370030..bb535aa 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -9,65 +9,61 @@
 # Load this as early as possible, to avoid a different version being pulled in by deps of something else
 http_archive(
     name = "com_google_protobuf",
-    sha256 = "bb8ce9ba11eb7bccf080599fe7cad9cc461751c8dd1ba61701c0070d58cde973",
-    strip_prefix = "protobuf-3.12.2",
-    urls = ["https://github.com/google/protobuf/archive/v3.12.2.tar.gz"],
+    sha256 = "c6003e1d2e7fefa78a3039f19f383b4f3a61e81be8c19356f85b6461998ad3db",
+    strip_prefix = "protobuf-3.17.3",
+    urls = ["https://github.com/google/protobuf/archive/v3.17.3.tar.gz"],
 )
 
 load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
 
 protobuf_deps()
 
-# Force rules_python at a bleeding edge version (for pip3_import).
+# Force rules_python at a bleeding edge version (for setuptools >44).
+rules_python_version = "929d5a13d4eb1b930086d9353fc6f2d6ad306e43"
+
 http_archive(
     name = "rules_python",
-    url = "https://github.com/bazelbuild/rules_python/releases/download/0.0.3/rules_python-0.0.3.tar.gz",
-    sha256 = "e46612e9bb0dae8745de6a0643be69e8665a03f63163ac6610c210e80d14c3e4",
+    strip_prefix = "rules_python-{}".format(rules_python_version),
+    url = "https://github.com/bazelbuild/rules_python/archive/{}.zip".format(rules_python_version),
+    sha256 = "b590e4fc07ec842b8cc8a39a4ca0336f44d7d5f96753229d240884cd016dc1e3",
 )
 
 # Download Go/Gazelle rules
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "207fad3e6689135c5d8713e5a17ba9d1290238f47b9ba545b63d9303406209c6",
+    sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.24.7/rules_go-v0.24.7.tar.gz",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.24.7/rules_go-v0.24.7.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
     ],
 )
 
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "bfd86b3cbe855d6c16c6fce60d76bd51f5c8dbc9cfcaef7a2bb5c1aafd0710e8",
+    sha256 = "62ca106be173579c0a167deb23358fdfe71ffa1e4cfdddf5582af26520f1c66f",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.0/bazel-gazelle-v0.21.0.tar.gz",
-        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.0/bazel-gazelle-v0.21.0.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
     ],
 )
 
 # Python rules
 # Important: rules_python must be loaded before protobuf (and grpc) because they load an older version otherwise
-load("@rules_python//python:repositories.bzl", "py_repositories")
+load("@rules_python//python:pip.bzl", "pip_parse")
 
-py_repositories()
-
-load("@rules_python//python:pip.bzl", "pip_repositories")
-
-pip_repositories()
-
-load("@rules_python//python:pip.bzl", "pip3_import")
-
-pip3_import(
+pip_parse(
     name = "pydeps",
-    requirements = "//third_party/py:requirements.txt",
+    requirements_lock = "//third_party/py:requirements.txt",
 )
 
-load("@pydeps//:requirements.bzl", "pip_install")
+load("@pydeps//:requirements.bzl", "install_deps")
 
-pip_install()
+install_deps()
 
 # Setup Go toolchain.
 load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains")
-go_register_toolchains()
+
+go_register_toolchains(version = "1.17")
 
 # IMPORTANT: match protobuf version above with the one loaded by grpc
 http_archive(
@@ -163,8 +159,9 @@
 
 gerrit_api()
 
-load("//devtools/gerrit/gerrit-oauth-provider:external_plugin_deps.bzl", gerrit_oauth_deps="external_plugin_deps")
-gerrit_oauth_deps(omit_commons_codec=False)
+load("//devtools/gerrit/gerrit-oauth-provider:external_plugin_deps.bzl", gerrit_oauth_deps = "external_plugin_deps")
+
+gerrit_oauth_deps(omit_commons_codec = False)
 
 # Gerrit 3.3.2 built by q3k, backported with fix for 'empty reviewers column' bug.
 # See: https://bugs.chromium.org/p/gerrit/issues/detail?id=13899
@@ -247,7 +244,9 @@
     commit = "17817c9e319073c03513f9d5177b6142b8fd567b",
     shallow_since = "1593642470 +0200",
 )
-load("@com_googlesource_gerrit_plugin_owners//:external_plugin_deps_standalone.bzl", gerrit_owners_deps="external_plugin_deps_standalone")
+
+load("@com_googlesource_gerrit_plugin_owners//:external_plugin_deps_standalone.bzl", gerrit_owners_deps = "external_plugin_deps_standalone")
+
 gerrit_owners_deps()
 
 # Go image repos for Docker
@@ -297,24 +296,3 @@
 )
 """,
 )
-
-go_repository(
-    name = "com_github_gorilla_sessions",
-    importpath = "github.com/gorilla/sessions",
-    sum = "h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=",
-    version = "v1.2.1",
-)
-
-go_repository(
-    name = "com_github_boltdb_bolt",
-    importpath = "github.com/boltdb/bolt",
-    sum = "h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=",
-    version = "v1.3.1",
-)
-
-go_repository(
-    name = "com_github_gorilla_securecookie",
-    importpath = "github.com/gorilla/securecookie",
-    sum = "h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=",
-    version = "v1.1.1",
-)
diff --git a/app/matrix/lib/matrix-ng.libsonnet b/app/matrix/lib/matrix-ng.libsonnet
index 5110af4..97812a7 100644
--- a/app/matrix/lib/matrix-ng.libsonnet
+++ b/app/matrix/lib/matrix-ng.libsonnet
@@ -65,7 +65,7 @@
             synapse: "matrixdotorg/synapse:v1.37.1",
             riot: "vectorim/riot-web:v1.7.29",
             casProxy: "registry.k0.hswaw.net/q3k/oauth2-cas-proxy:0.1.4",
-            appserviceIRC: "matrixdotorg/matrix-appservice-irc:release-0.26.0",
+            appserviceIRC: "matrixdotorg/matrix-appservice-irc:release-0.27.0",
             appserviceTelegram: "dock.mau.dev/tulir/mautrix-telegram@sha256:c6e25cb57e1b67027069e8dc2627338df35d156315c004a6f2b34b6aeaa79f77",
             wellKnown: "registry.k0.hswaw.net/q3k/wellknown:1611960794-adbf560851a46ad0e58b42f0daad7ef19535687c",
         },
diff --git a/bgpwtf/internet/kube/prod.jsonnet b/bgpwtf/internet/kube/prod.jsonnet
index fa712e0..11a93c6 100644
--- a/bgpwtf/internet/kube/prod.jsonnet
+++ b/bgpwtf/internet/kube/prod.jsonnet
@@ -8,8 +8,8 @@
         appName: "internet-landing",
         domain: "internet.hackerspace.pl",
 
-        tag: "201907091256",
-        image: "registry.k0.hswaw.net/app/internet:" + cfg.tag,
+        tag: "202108261700",
+        image: "registry.k0.hswaw.net/q3k/internet:" + cfg.tag,
 
         resources: {
             requests: {
diff --git a/bgpwtf/internet/static/cennik-konsumenci.pdf b/bgpwtf/internet/static/cennik-konsumenci.pdf
new file mode 100644
index 0000000..3934016
--- /dev/null
+++ b/bgpwtf/internet/static/cennik-konsumenci.pdf
Binary files differ
diff --git a/bgpwtf/internet/static/index.html b/bgpwtf/internet/static/index.html
index 6de086c..b06fc9c 100644
--- a/bgpwtf/internet/static/index.html
+++ b/bgpwtf/internet/static/index.html
@@ -41,7 +41,7 @@
             <h1>Warszawski Hackerspace</h1>
             <h2>Dostęp do Internetu</h2>
             <p>
-                Świadczymy usługi dostępu do Internetu drogą kablową i światłowodową w obrębie budynku na ul. Wolność 2A w Warszawie.
+                Świadczymy usługi dostępu do Internetu drogą kablową i światłowodową w obrębie budynku na ul. Wolność 2A w Warszawie, oraz drogą radiową w okolicach bundynku na ul. Wolność 2A (w zależności od widoczności).
             </p>
             <p>
                 <b>Kontakt</b>: kontakt@hackerspace.pl
@@ -54,11 +54,25 @@
             </p>
         </div>
         <div class="container">
-            <h2>Zasoby</h2>
+            <h2>Internet Domowy</h2>
             <p>
-                <a href="/regulamin.pdf">Regulamin świadczenia usług telekomunikacyjnych - Dostęp do Internetu</a> (obowiązuje od 2019/07/01)
+                Poniższy regulamin i cennik obowiązują tylko dla użytkowników będącymi konsumentami.
             </p>
             <p>
+                <a href="/regulamin-konsumenci.pdf">Regulamin Świadczenia Usług Telekomunikacyjnych Dostępu do Internetu</a> (obowiązuje od 2021/08/26)
+            </p>
+            <p>
+                <a href="/cennik-konsumenci.pdf">Cennik Dostępu do Internetu</a> (obowiązuje od 2021/08/26)
+            </p>
+            <h2>Internet Biznesowy</h2>
+            <p>
+                Poniższy regulamin obowiązuje tylko dla użytkowników <b>niebędących konsumentami</b> (stowarzyszenia, firmy, osoby prowadzące działalność gospodarczą...).
+            </p>
+            <p>
+                <a href="/regulamin.pdf">Regulamin Świadczenia Usług Telekomunikacyjnych Dostępu do Internetu</a> (obowiązuje od 2019/07/01)
+            </p>
+            <h2>Infrastruktura, prędkość łacza, inne zasoby</h2>
+            <p>
                 <a href="https://speedtest.hackerspace.pl">Pomiar prędkości łącza - speedtest</a>
             </p>
             <p>
@@ -67,7 +81,7 @@
         </div>
         <div class="container">
             <p style="font-size: 0.8em;">
-                Stowarzysznie “Warszawski Hackerspace” (KRS 0000424347) zarejestrowane jest w Rejestrze Przedsiębiorców Telekomunikacyjnych UKE pod numerem 12216.
+                Stowarzysznie “Warszawski Hackerspace” (KRS 0000424347) zarejestrowane jest w Rejestrze Przedsiębiorców Telekomunikacyjnych UKE pod numerem 12216. Adres korespondencyjny: Stowarzyszenie Warszawski Hackerspace, ul. Wolność 2A, 01-018 Warszawa.
             </p>
         </div>
     </body>
diff --git a/bgpwtf/internet/static/regulamin-konsumenci.pdf b/bgpwtf/internet/static/regulamin-konsumenci.pdf
new file mode 100644
index 0000000..b71243b
--- /dev/null
+++ b/bgpwtf/internet/static/regulamin-konsumenci.pdf
Binary files differ
diff --git a/bgpwtf/machines/edge01.waw.bgp.wtf.nix b/bgpwtf/machines/edge01.waw.bgp.wtf.nix
index 8427f25..d26f219 100644
--- a/bgpwtf/machines/edge01.waw.bgp.wtf.nix
+++ b/bgpwtf/machines/edge01.waw.bgp.wtf.nix
@@ -172,7 +172,7 @@
   '';
   hscloud.routing.originate = {
     # WAW prefixes, exposed into internet BGP table.
-    v4.waw = { table = "internet"; address = "185.236.240.0"; prefixLength = 23; };
+    v4.waw = { table = "internet"; address = "185.236.240.0"; prefixLength = 24; };
     v6.waw = { table = "internet"; address = "2a0d:eb00::"; prefixLength = 32; };
 
     # Default gateway via us, exposed into aggregated table.
@@ -191,7 +191,10 @@
     };
   in {
     v4."internet_to_kernel" = copySourcesToKernel ["BGP" "OSPF"] "internet" "";
-    v4."aggregate_to_kernel" = copySourcesToKernel ["BGP" "OSPF"] "aggregate" "";
+    v4."aggregate_to_kernel" = copySourcesToKernel ["BGP" "OSPF"] "aggregate" ''
+      # Static v4 routes for customers.
+      if proto ~ "static_static_ipv4_customer_*" then accept;
+    '';
     v6."internet_to_kernel" = copySourcesToKernel ["BGP" "OSPF"] "internet" "";
     v6."aggregate_to_kernel" = copySourcesToKernel ["BGP" "OSPF"] "aggregate" ''
       # Static v6 routes for customers.
@@ -410,6 +413,8 @@
       filterIn = ''
         # dcsw01 l2 general purpose
         if net ~ [ 2a0d:eb00:2137::/48+ ] then accept;
+        # customer
+        if net ~ [ 2a0d:eb00:8004::/48+ ] then accept;
         reject;
       '';
     };
diff --git a/bgpwtf/machines/modules/prometheus.nix b/bgpwtf/machines/modules/prometheus.nix
index 704c257..1ebb435 100644
--- a/bgpwtf/machines/modules/prometheus.nix
+++ b/bgpwtf/machines/modules/prometheus.nix
@@ -10,15 +10,15 @@
 
   birdExporter = pkgs.buildGoModule rec {
     pname = "bird-exporter";
-    version = "1.2.5";
+    version = "1.2.6";
     src = pkgs.fetchFromGitHub {
       owner = "czerwonk";
       repo = "bird_exporter";
       rev = version;
-      sha256 = "1qrhncy1f119f5rfgn2d1l6nvapaqkld4zb9bxzdqmmw6kicc7bs";
+      sha256 = "1yqizzlvwyxlrd2priqd1jx9s87yvsypqkmk81dacm1ra4xrs0nd";
     };
 
-    vendorSha256 = null;
+    vendorSha256 = "0wczj3g0c917hwjkz23xg5blb4z5a04v3wbx6kg0wfyb09c9bwx3";
   };
 
 in {
diff --git a/bgpwtf/machines/modules/router.nix b/bgpwtf/machines/modules/router.nix
index 88ad004..bdd5336 100644
--- a/bgpwtf/machines/modules/router.nix
+++ b/bgpwtf/machines/modules/router.nix
@@ -37,6 +37,10 @@
   boot.kernel.sysctl."net.ipv6.conf.*.router_solicitations" = 0;
   boot.kernel.sysctl."net.ipv6.route.max_size" = 2147483647;
 
+  # Limit nscd memory usage, as it sometimes just blows up and the OOMkiller
+  # sucks at picking it up.
+  systemd.services.nscd.serviceConfig.MemoryMax = "1G";
+
   # enable coredumpctl
   systemd.coredump.enable = true;
 
diff --git a/bgpwtf/machines/secrets/cipher/edge01.waw.bgp.wtf-private.nix b/bgpwtf/machines/secrets/cipher/edge01.waw.bgp.wtf-private.nix
index aea2b80..4eace24 100644
--- a/bgpwtf/machines/secrets/cipher/edge01.waw.bgp.wtf-private.nix
+++ b/bgpwtf/machines/secrets/cipher/edge01.waw.bgp.wtf-private.nix
@@ -1,51 +1,54 @@
 -----BEGIN PGP MESSAGE-----
 
-hQEMAzhuiT4RC8VbAQgAtWAfrUok8EKsWRY2FEZbNeawMXXpuBrMDARxNY1xhV6b
-3Pxz+148na8+KQR0asleOO+9qPKQP0N+HA6W6SJEgrfQ7q3XKdsaZVMJjNaRhb9j
-eOAe1MLr9Ps0Lx93nknI6bPsX8odpa7oNQYqI7QWBphQLVtdBKaYVkoGN7P+xHlu
-j2HDyD3TOfNH2UxywWOMAJixYkcZ6/v1KNS4JsDUe4b5Tf/IegX2LSoY9qH60Psm
-BFMCmYmGg/MlFyQpyo/CYebJu9BWMHHcj2o29W+OaJqCCYVC+XR+h5EtssnPwedK
-D7A5jLu83pzonZQxiheP0JWSrfMlo8HZNcbhZw0IIoUBDANcG2tp6fXqvgEH/1Gp
-3qsm/MfFtRoHzbRaOEIofaKjv79PhdN8p+9tr4J31oMnuJNIVWozW8R1YBzyL6Pg
-UeKZaAsW9zP9+HhQw8ZahX1A3Paz3LhO9By4wkgOt5up7s5QS2klWWUBaF8AIxKF
-FBoNJcc52VH5yBXyiGd5UAHigKRldwE9yIzWKzt4/60/NtVzkfK6j8KFdRyLJZFE
-0IqRjbFxdvMr4hyc1h5wibBonWKRIDEvXqIeOWdUbDHqTekJcXVUrtxw5u8rry2F
-XaqN9FM+++QPFX4hrbIJe1w7/gINH72PnAPApN/MfUaQsGE/noX3CjaINspB+Nhq
-AwUITmBSjdZ0vuEGoWaFAgwDodoT8VqRl4UBD/9d9rQRlpfKi6K3WlLJra4OEtQm
-+RTE8I7OQYCQ/C9QFPw1ux0RtCTQF/uL8nKzoWG+LbUgKVRoZQSV1k4L9QZ/YKhd
-5AC4YT76lPCxCemuSfCQ3sb0I4uEa3JiXeBSVPAfwcS3nXhnyhdFLGhdmAs1hnzo
-E9wD4oxk6yHhozWH1QwJ4Syioywmoy5kKJ4tQREpceCvyoZD2b2h4kLUiZKkBZ+a
-Nzo3AQdQJF/Yr4BL2afMwAwd9x9zpdu/LL7k6INsXuM0S1I8ipdBpiKpZTQcAwK9
-P6RF8p8lKpMX5u1PWq7CMJMJGRQnKAXCJoKYathUygePewq3muOx2Cn4fjNGLT0/
-O795/QtCDI9Q4xsY7/uMLvqpr6skFXecH304Mp23unTBK+knBkWRYpaoA9Gfri8j
-ELMkPvsLCjE9gg3vUdgyPDq3Ov2XDXvgsNW36ghiUYmBRhfoXHG/TuG2iEGP32mM
-coW+4q7DpcyjYUXeh80PLYWe68gCmQ4XN8oh2+xPhFwvjKOidW8o1TbRAdf+A+yu
-d6EvKSzSpG8SABrlCofD7HPyrAbwmTP2SdmoamCT8NSiLoXQZk2ZO3xSYp8gR4aP
-fDENcYtqipDyMTPRfPEPMjYnn+AR4d4UD0jowcaeKThg/fKmzwP0N4pm6uaR9pJ6
-+MwKToisQk+tLIv44YUCDAPiA8lOXOuz7wEP/0xTmMro6Jb7LG8lOykEomYBrtT+
-meWftSGjQgSQITkw/cVqIzpYTy7HRtP8luiJyvh1Mt8Fm3MlZLgorp0TtxaUPq0y
-yNUWkDXOIu0pcmX1c38rmEIMvnUcREyJlcFv7Y8XAYf8ZP4TUfA3t+wZaFYoiV0M
-Ai4tHGCIDdCf4Q9fSEFIU+UwJ9/zuBglMPJ74x3IPpEnlKtqkgzpT6kUGypIFKA9
-n83ycOiu78FTHQF3ULY05Of2cBKTtNYc0R71QIyHovh1bT+o3EgMwxcKfzZjBAgI
-lGtJG1+mi44FS90zUPWVtRRf0TwCFXw1HSCS7nbL+5831WdcdhWVgWp2RCczVORv
-G4q5SP1yunBOi7HKVRHGW16bAhM/OOgyJ2lLqRSglvrYC6ympXvw85Y6xXKiG9tC
-wAKSJOvkSZ8xoahoVVQwbbD6uiQqpTzl3befxkLd0uz9U43pfW0z9x+r38ussUbM
-bnLMCwGJ9N7dZevN6GkX886S1Tk0CwM8H39tRyb9xowcc/D9Hoy+/jOq1YOtpHXA
-NXc7gbDppTlRJqbrniH9YxIG00+x1K1RKMBjLgBVOhT64n3U0DIBxvVQLmg9kQUN
-6tiDe0toQUhXVV5eu5PivoPuDcHZ7+4FdoAtIeQ7Y2A34HZ4KXSvF3qkf3RPHWvo
-l77A/4GZzFCQW38E0ukBD1W1hTHZcwNaPwRGtD1rvM1f6o7c60sDAAWkMeKzsFRd
-0ADg08xVj447WnZtgy7j2LPd6JeaodHumw95Cwkdd06lupYwG/CV/ZmvB5Ae+llu
-3mPewaSfpmcWvg0QtfDnv8NWW/BQSnPCjjPlVAdVZxm/uRHNKWSApK1UrfDK463e
-Dg60CXE64j/7bX9TCSf7KVHqan08IMvS8i3gqKYNv/9yGR9EnpNvVWv59zHZao64
-yHO/NHPBXgQk+Vn+P2iER6/bZaMkq7HdjkM9KeTBZUmvzfQ69wYhlrTI5HvkLHNP
-FV+oqwgG9KwPtr0zzBp9fyjfMw6081NedsMH+GwiNvibM5ryBULTnziJ4Hm0MbXH
-Yg/XFi/mMwq3rGq+ZauyjSjIuQXxZMQLpuzSWRQx5thwDePGJ7Dx5nGEJQC5NS/x
-HsPsvKEtl/n/CY5x3qV1NScCTKlXiD7mGE/whuO/Aoun+tHXNah/kwXFWyeQkHkS
-xpjm10vq4s6CvieCDKc+QenxpLt1PGBL7yvGVBXBTQbp1N4laYATXzTFr9b/RG5s
-C0aWWLuraflILCD0wxDuZnFVrPVmsfMp86+donIaNvBFwrYyMw9cnVWGoVAIG4LV
-B9vfZaVzhbNgynnwu1JifZzwIytLBHsemRMq5vRUE8ju0z9FP9hhqHLu1pF1dJH2
-fyqFYL44br5M0c2f2xnzGpsca9C7mDBXN5ktR1ts+fHdLELsqg9SwAtqqWCEB/jQ
-T96vkVEydwQ/mVqCtPLGk3NVJ7NjUVISFvQAj4w9vG9fCgD1NIHJco0VgIkvn9Yu
-2pPCrY+NVeibz9vaaUIuOf6lXZCZFOyVTZIPXQyhumel/f0MDs3Lx5ZA3rUL6jS+
-=nGiW
+hQEMAzhuiT4RC8VbAQf/brR1fpqwnOPSkBg7YG3WPiwI/FN4y4ArNaqZeOaNTvp3
+mg0peGDiIVh/Vc0TctLRfPalCNmraMenQYQozlatbrvmq4r15g1cSU1Y5bx7BNSm
+v4HiUqwdpSqKqj4+htmUoUcOic0svpDBHmOvFq3W+tY+xUJiayRyxDoRhLvXgsNo
+uhkB2ILMdUgO59PEjLY10mQVKx9g/nD4DAHHIijwV/bsNgnWM4FPZlFA8h7GahCW
+uFsNX2GVnN9BuYXkgu4/GjEGoo1fziYorLrMkBXMraN9rh1qcC6hlEMfSlvurQyw
+n6Mun8h1b5S5fb2Xsmi3iZ0RhljDWcQ9exIDqT6TxIUBDANcG2tp6fXqvgEIAJf5
+wwM7TUKgm8YKCfkD1TTxQ5uPAX+zMaN8uGxT35mAOlTLhG1CuyH/u+FD0Zoj9oZg
+7RbYGaWPvvC8YDiGC8c9H8gyX6zfN3mc93uF7q7PD+giaH+SgfzWooOfLo7R8bPx
+9ZEVbWGqDodaHVRsr9/3ggZVFU27soW5CT51jxSI3yTrT4x6QS37UGpHyjtsZ90C
+5cqcPJ4TdExpieOfcPI5n10e9fzqSZgSKs7megaS7cD0AwTQVls05mw6n5tLK4Pe
+eq10zdlBjT3RpVwFwnjBfDiyMxBLyWTqiyCTyWOyVWuhDAuvu3ifakz/5EAU0hnB
+fBu+COIaKd2P4OzRxfiFAgwDodoT8VqRl4UBD/44cxVe5VaQ0qKNj3LZU3xcYTxP
+FGJtpPRYXfnXv7yjmCbyhEelJe9fxwtzT9bG39OwSZgRRFRC2oGq7yc3xrh/xMyU
+Ct2uQaYQilNiIqL/eDWwVSD0vmGlOaByD46wraB+SMoK6EuILMu7dIyGi7t2iJQ8
+Xdf1H2SyZ5eGP9nWdwoN/P/WvA1umWc5wI8pfgld2Mg9JuxevIGoZdsoeHRxwwqD
+T/vIlsgM+E0PZdkvv4ZsBMvWYGdgAbY1j8ZNtJNOUlg97kDHdCsQCVwNBzGpufaO
+H51aBZdb4h0Us9cAdLiKvTfnBKVo7DCmjPRv+HAKVvvesOjwxaN9SoMqM7MkVH1x
+L5747krwH/YWSynKZeBIL8C/WcRHTBllin+e5BoauazUkCb7Qnj1tM34N9j9Lxgc
+4NhIup/aVzt+oqq0sGdGif0qxg0cxZB5QczLkAamcolmEKFQt8TZfku1Zi9UT2ve
+RXb2mh+tdQ5gNJExLZmlCaju/9kEzktisdqI6qzSbg2YF4LvdM6aW5TI4HphiD6Z
+sJD25gbcwkNEHtVVi/itgP+ye1WazgFNX0/t2VAfbjlV94roa/uJuPeS3oxqLuyZ
+/3LfJ5x29z1dsQWeZZEE7BMr/afgt4+0/79Rr6xrhVdB9RvQs6Oh9RwJb+BkL0ul
+Uz/n8sKu1LVRvZ6L1IUCDAPiA8lOXOuz7wEQAJA/RyONXM6s3w4ZhcVQwUZfZTvE
+mjx1FzeQ2RVolSOSDkdULXmK7UiufWOdhSZzOrBnwcRj3TSRSFZvao7P6f5bT7hv
+zmEkiyqDcxxzuWn7HIblmlrl30PDaefAloryRfpJ5Gvg4Wko5qWQP9mzx3YVETgM
+JgQTp57BbymLjxj/7yxbX6fHBaJnbHoNsSQvGyWmuxrCTigEt/urUIil02jDyG80
+tH/yT5WEkLS/lXQJ93L7BBkgIzlAaTT+reBiq/PFeftsauY49CbmY1C0ayFl5Ni5
+mxRdzw5RNBawctKbffffWfcVlMkSJcXkqvwHT7OPXvqv2j4NxRxkdxbpNHk+OIgF
+GMRo1dQqWwNf69ATNfo2itwJOAdrUwS6d3vP0IioaDu4YSHZddJuXIYvhZ0ADMFh
+Zn3dhygCt4rARU4dJgakvGFvkf3I8C4Pha7LZw22W/prS4G7G2t6w7eCPaFayThj
+YgX1EVxyVzg3xZSd7DRWCbU2goYNv8d5zhjM10D/vWv344C2yV5lgk1Ovlb6uqhf
+HOaPc45T2WLt/pkssK+BC1VgrOPXT0zWdcdtr37+duTf3fP297uiGjKNqkjoWrAW
+1FtI9H8lXaOlR3ox1GLdTRoEuB5Lcwn/REVF5ojNMESAGs3WcMndnzFUh7AZAiHd
+n9ar+tNIYmJn+RZ80ukBhJW5FvBzOkCSfsDZmPX2qXspf7/8oGKjRj5zcVyeL8Ku
+9HDDTxoVmRA6g4X/fLf0yRYIe+1MD1lbFQnIcA0ah2dcJEMCcvtKIzJY5UgWZdcy
+J8BgdzAmM3nJLFhS7SuANX63A+cZSAvc9KbDi5gs/XadN+AMdl/BQ5wFXXSeyQSV
+xvYOIY+LBvVz+syfQD4WZniDtzy8/sA41BJLA1Q1VxcmI8UpOPYxFH+qEwXm1x2J
+3On/UzWXRZ6XeHTgELZJuaCP5nkgsKX6lywYLurSn+NaKNwEcAChJKFbbSho2BRG
+kdD5gSdSGK1sUTF4Xx0jdqlVq4hm6O6u1c49cArRBjNT3zZSKoGkcBqC5L8thanM
+C5QKHrlHC0MlTneY9bTTNWtLaB4GB94yYtu73Ngk+bnd+5SLxQdDmhvs/Bh9fBX6
+GGOpNMa7LnZTlJq/MapO9TFSIxpOjg7F4KVinkgoj00kdEW2tGWaRa7vVJekrmSQ
+rlq/ZsPbgigCiDljMTtl2p1Az6YnUbiDAOMHaR1J4AJPEun/Tt+AYgOFDWi/TloX
+zBMWpK89rzq+O1CJDgeLPp24n6Xbx8A6Hfx8UpYha9AmMpeoQQLzeu9HV7hInrSD
+kVlcfWZX558BMtIUd9RFpyF1+Gz0v+TDx0+8hssf79VXZi1ZSx6GfxZnM+V7WMBe
+f6q4P4pVTibOV6bYtoFvD+V+FjxAQqfI0VaWHA0IavZirKqHGg1HdGXR7JcVkK7n
+rPkyNza048gHVTjbuYlnCoAIHLWzbxBhFZHy3qWst6lS5oTg41ac6p/iSBXcOgCc
+GFVeMBfg0kseSgPY6V4d2/LhNJHzJUzjn7Ht4CtdwEqVHZXkRoPQ2fQuk6Ot1hSt
+rEG/cTEtIpmEJg6pdMYegAevyg/9NP8421x1J4zYdESscsaF+2PcuVnfD6gux85u
+I4/AG8aR0C/e9SjWfqcQnsyEFZt7L0QB+KlioREXYeFE+eAJyLTsQZbU/dbkuC+E
+3PjIf4mQ3Xd6EHWq99BPnhxLldo/YCuoYOdHV9aEGkNGmMaWCDmBuLZyJo/C5w==
+=HhUX
 -----END PGP MESSAGE-----
diff --git a/bgpwtf/machines/tests/edge01-waw.nix b/bgpwtf/machines/tests/edge01-waw.nix
index 1d724e1..6b641b5 100644
--- a/bgpwtf/machines/tests/edge01-waw.nix
+++ b/bgpwtf/machines/tests/edge01-waw.nix
@@ -262,7 +262,7 @@
     # edge01 must announce exactly one v4 prefix.
     bgpspeaker.succeed("birdc show route protocol bgp_globalmix_v4 | grep unicast")
     bgpspeaker.fail(
-        "birdc show route protocol bgp_globalmix_v4 | grep unicast | grep -v 185.236.240.0/23"
+        "birdc show route protocol bgp_globalmix_v4 | grep unicast | grep -v 185.236.240.0/24"
     )
 
     # edge01 must announce exactly one v6 prefix.
diff --git a/cluster/certs/ca-kube-prodvider.cert b/cluster/certs/ca-kube-prodvider.cert
index bd969f0..c95ef3b 100644
--- a/cluster/certs/ca-kube-prodvider.cert
+++ b/cluster/certs/ca-kube-prodvider.cert
@@ -1,9 +1,9 @@
 -----BEGIN CERTIFICATE-----
-MIIFQzCCBCugAwIBAgIUARcjVskxjZNWpolOFXdx0LRJPt8wDQYJKoZIhvcNAQEL
+MIIFQzCCBCugAwIBAgIURBH2lkCPblJBzR+SgfxpGhdDWS0wDQYJKoZIhvcNAQEL
 BQAwgYMxCzAJBgNVBAYTAlBMMRQwEgYDVQQIEwtNYXpvd2llY2tpZTEPMA0GA1UE
 BxMGV2Fyc2F3MRswGQYDVQQKExJXYXJzYXcgSGFja2Vyc3BhY2UxEzARBgNVBAsT
-CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMDA5
-MDEyMTMwMDBaFw0yMTA5MDEyMTMwMDBaMIGsMQswCQYDVQQGEwJQTDEUMBIGA1UE
+CmNsdXN0ZXJjZmcxGzAZBgNVBAMTEmt1YmVybmV0ZXMgbWFpbiBDQTAeFw0yMTA4
+MjkxNjM3MDBaFw0yMjA4MjkxNjM3MDBaMIGsMQswCQYDVQQGEwJQTDEUMBIGA1UE
 CBMLTWF6b3dpZWNraWUxDzANBgNVBAcTBldhcnNhdzEbMBkGA1UEChMSV2Fyc2F3
 IEhhY2tlcnNwYWNlMSowKAYDVQQLEyFrdWJlcm5ldGVzIHByb2R2aWRlciBpbnRl
 cm1lZGlhdGUxLTArBgNVBAMTJGt1YmVybmV0ZXMgcHJvZHZpZGVyIGludGVybWVk
@@ -21,11 +21,11 @@
 DWl93HqdKeINlp9Q0HQ7nR+LUkeodWf7AgMBAAGjgYMwgYAwDgYDVR0PAQH/BAQD
 AgGmMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNVHRMBAf8EBTAD
 AQH/MB0GA1UdDgQWBBRpjeqS08ZAgwwhQZnMEmrNN2PdszAfBgNVHSMEGDAWgBSY
-Ml0OTzMe+wnpiSQTFkJqgNGZ0DANBgkqhkiG9w0BAQsFAAOCAQEApx4PthrJy1Mu
-HXcTox9OgLYR7DSJq6lsIxnrqYSo08RtZ/yp/z0C52Ju65GpCYZZeNcD7TH0gTlr
-NbVXkowM/n+Uad8AmEn4CVvAuAKr1T9OZ9nvDPkx6hTzzxwpTyVLruW/PtFc+/7P
-qSAKrnDuIaSWfNC6Lv0FDGdG5MjqM30J74LiGfl69/rqjV/gBrDR9rtImwsGgGQV
-wPyy3hc3G2SCzaI3x3w7Epm7AwjjcW2YbkG5BBiHOG2Ul9Ps6ikCskwrWnu+VFsG
-zuefuvH+OIr58rlLtSgr+sTRTdiZcD/xUc0r+o2KJqU8AHUvcs1OLArEBBZDN4vs
-1PR0+NIOKg==
+Ml0OTzMe+wnpiSQTFkJqgNGZ0DANBgkqhkiG9w0BAQsFAAOCAQEAk9/HtagOqgci
+LEy4ZjS56zm7Lgnksabju6ezbJAJTFCUcQZXxTGp7MfideOctGcRRfIZ0ZBG1ndK
+ycKag9J8WTmqKP5j7UcvP491RXnguteKxdyb14WcHqiz43DNFF7+ntJU1Ut3BF63
+5ev0t0UUBIZ7kUmU+H0V9/x9zuKlpxkLsp0VTo5m3xPSBFWmpf/aWlyhRjKuKd4j
+gXKU3wjfVkyWt8PAI8DmQeADqJlM9a47L3y/t1VjbBjcb13yzvoLqhStvqY8CjKG
+/ESUNVm6zr+XaO+FXrp0ul54HCQOQkoF2h0tRbG0IPZX5WNvPt0++1ApKbp90noE
+AVJSBdJ51A==
 -----END CERTIFICATE-----
diff --git a/cluster/clustercfg/clustercfg.py b/cluster/clustercfg/clustercfg.py
index 0adef40..d852d6a 100644
--- a/cluster/clustercfg/clustercfg.py
+++ b/cluster/clustercfg/clustercfg.py
@@ -206,10 +206,12 @@
         ca_admitomatic = ca.CA(ss, certs_root, 'admitomatic', 'admitomatic webhook CA')
         ca_admitomatic.make_cert('admitomatic-webhook', ou='Admitomatic Webhook', hosts=['admitomatic.admitomatic.svc'])
 
-    subprocess.check_call(["nix", "run",
-                           "-f", local_root,
-                           "cluster.nix.provision",
-                           "-c", "provision-{}".format(fqdn.split('.')[0])])
+    toplevel = subprocess.check_output([
+        "nix-build",
+        local_root,
+        "-A", "ops.machines.\"" + fqdn + "\".config.passthru.hscloud.provision",
+    ]).decode().strip()
+    subprocess.check_call([toplevel])
 
 
 def usage():
diff --git a/cluster/kube/cluster.libsonnet b/cluster/kube/cluster.libsonnet
index 6e9da28..fb0456c 100644
--- a/cluster/kube/cluster.libsonnet
+++ b/cluster/kube/cluster.libsonnet
@@ -212,7 +212,7 @@
         rook: rook.Operator {
             operator+: {
                 spec+: {
-                    replicas: 1,
+                    replicas: 0,
                 },
             },
         },
diff --git a/cluster/kube/k0.libsonnet b/cluster/kube/k0.libsonnet
index 57d39d0..9d14dbf 100644
--- a/cluster/kube/k0.libsonnet
+++ b/cluster/kube/k0.libsonnet
@@ -121,7 +121,6 @@
                         nodes: [
                             {
                                 name: "dcr01s22.hswaw.net",
-                                location: "rack=dcr01 host=dcr01s22",
                                 devices: [
                                     // https://github.com/rook/rook/issues/1228
                                     //{ name: "disk/by-id/wwan-0x" + wwan }
@@ -139,7 +138,6 @@
                             },
                             {
                                 name: "dcr01s24.hswaw.net",
-                                location: "rack=dcr01 host=dcr01s22",
                                 devices: [
                                     // https://github.com/rook/rook/issues/1228
                                     //{ name: "disk/by-id/wwan-0x" + wwan }
diff --git a/cluster/kube/lib/rook.libsonnet b/cluster/kube/lib/rook.libsonnet
index c8e38a8..e9ed75b 100644
--- a/cluster/kube/lib/rook.libsonnet
+++ b/cluster/kube/lib/rook.libsonnet
@@ -27,10 +27,7 @@
         policyInsecure: policies.AllowNamespaceInsecure(cfg.namespace),
 
         crds: {
-            # BUG: cannot control this because of:
-            # ERROR Error updating customresourcedefinitions cephclusters.ceph.rook.io: expected kind, but got map
-            # TODO(q3k): debug and fix kubecfg (it's _not_ just https://github.com/bitnami/kubecfg/issues/259 )
-            cephclusters:: kube.CustomResourceDefinition("ceph.rook.io", "v1", "CephCluster") {
+            cephclusters: kube.CustomResourceDefinition("ceph.rook.io", "v1", "CephCluster") {
                 spec+: {
                     additionalPrinterColumns: [
                         { name: "DataDirHostPath", type: "string", description: "Directory used on the K8s nodes", JSONPath: ".spec.dataDirHostPath" },
@@ -1219,7 +1216,6 @@
             metadata+: cluster.metadata,
             spec: store.spec {
                 gateway: {
-                    type: "s3",
                     port: 80,
                     instances: 1,
                     allNodes: false,
diff --git a/cluster/nix/defs-cluster-k0.nix b/cluster/nix/defs-cluster-k0.nix
index c3519cc..cd0fcac 100644
--- a/cluster/nix/defs-cluster-k0.nix
+++ b/cluster/nix/defs-cluster-k0.nix
@@ -10,8 +10,60 @@
   fqdn = machineName + domain;
   machine = (builtins.head (builtins.filter (n: n.fqdn == fqdn) machines));
   otherMachines = (builtins.filter (n: n.fqdn != fqdn) machines);
+  machinesByName = builtins.listToAttrs (map (m: { name = m.name; value = m; }) machines);
   inherit machines;
 
+  # Ceph cluster to run systemd modules for.
+  cephCluster = {
+    fsid = "74592dc2-31b7-4dbe-88cf-40459dfeb354";
+    name = "k0";
+
+    # Map from node name to mon configuration (currently always empty).
+    #
+    # Each mon also runs a mgr daemon (which is a leader-elected kitchen
+    # sink^W^Whousekeeping service hanging off of a mon cluster).
+    #
+    # Consult the Ceph documentation
+    # (https://docs.ceph.com/en/pacific/rados/operations/add-or-rm-mons/) on
+    # how to actually carry out mon-related maintenance operations.
+    mons = {
+      bc01n02 = {};
+    };
+
+    # Map from node name to list of disks on node.
+    # Each disk is:
+    #  id:   OSD numerical ID, eg. 0 for osd.0. You get this after running
+    #        ceph-lvm volume create.
+    #  path: Filesystem path for disk backing drive. This should be something
+    #        in /dev/disk/by-id for safety. This is only used to gate OSD
+    #        daemon startup by disk presence.
+    #  uuid: OSD uuid/fsid. You get this after running ceph-lvm volume create.
+    #
+    # Quick guide how to set up a new OSD (but please refer to the Ceph manual):
+    # 0. Copy /var/lib/ceph/bootstrap-osd/k0.keyring from another OSD node to
+    #    the new OSD node, if this is a new node. Remember to chown ceph:ceph
+    #    chmod 0600!
+    # 1. nix-shell -p ceph lvm2 cryptsetup (if on a node that's not yet an OSD)
+    # 2. ceph-volume --cluster k0 lvm create --bluestore --data /dev/sdX --no-systemd --dmcrypt
+    # 3. The above will mount a tmpfs on /var/lib/ceph/osd/k0-X. X is the new
+    #    osd id. A file named fsid inside this directory is the new OSD fsid/uuid.
+    # 4. Configure osds below with the above information, redeploy node from nix.
+    osds = {
+      dcr01s22 = [
+        { id = 0; path = "/dev/disk/by-id/scsi-35000c500850293e3"; uuid = "314034c5-474c-4d0d-ba41-36a881c52560";}
+        { id = 1; path = "/dev/disk/by-id/scsi-35000c500850312cb"; uuid = "a7f1baa0-0fc3-4ab1-9895-67abdc29de03";}
+        { id = 2; path = "/dev/disk/by-id/scsi-35000c5008508e3ef"; uuid = "11ac8316-6a87-48a7-a0c7-74c3cef6c2fa";}
+        { id = 3; path = "/dev/disk/by-id/scsi-35000c5008508e23f"; uuid = "c6b838d1-b08c-4788-936c-293041ed2d4d";}
+      ];
+      dcr01s24 = [
+        { id = 4; path = "/dev/disk/by-id/scsi-35000c5008509199b"; uuid = "a2b4663d-bd8f-49b3-b0b0-195c56ba252f";}
+        { id = 5; path = "/dev/disk/by-id/scsi-35000c50085046abf"; uuid = "a2242989-ccce-4367-8813-519b64b5afdb";}
+        { id = 6; path = "/dev/disk/by-id/scsi-35000c5008502929b"; uuid = "7deac89c-22dd-4c2b-b3cc-43ff7f990fd6";}
+        { id = 7; path = "/dev/disk/by-id/scsi-35000c5008502a323"; uuid = "e305ebb3-9cac-44d2-9f1d-bbb72c8ab51f";}
+      ];
+    };
+  };
+
   pki = rec {
     make = (radix: name: rec {
       ca = ./../certs + "/ca-${radix}.crt";
diff --git a/cluster/nix/modules/base.nix b/cluster/nix/modules/base.nix
index 034d1cd..29f2072 100644
--- a/cluster/nix/modules/base.nix
+++ b/cluster/nix/modules/base.nix
@@ -54,6 +54,15 @@
   # Use Chrony instead of systemd-timesyncd
   services.chrony.enable = true;
 
+  # Symlink lvm into /sbin/lvm on activation. This is needed by Rook OSD
+  # instances running on Kubernetes.
+  # See: https://github.com/rook/rook/commit/f3c4975e353e3ce3599c958ec6d2cae8ee8f6f61
+  system.activationScripts.sbinlvm =
+    ''
+      mkdir -m 0755 -p /sbin
+      ln -sfn ${pkgs.lvm2.bin}/bin/lvm /sbin/lvm
+    '';
+
   # Enable the OpenSSH daemon.
   services.openssh.enable = true;
   users.users.root.openssh.authorizedKeys.keys = [
diff --git a/cluster/nix/modules/ceph.nix b/cluster/nix/modules/ceph.nix
new file mode 100644
index 0000000..bc3180f
--- /dev/null
+++ b/cluster/nix/modules/ceph.nix
@@ -0,0 +1,145 @@
+# This runs Ceph on hscloud cluster(s).
+#
+# This lightly wraps the upstream NixOS ceph module, which is already fairly light.
+#
+# Most importantly, it does _not_ attempt to do any cluster
+# bootstrapping/maintenance. This means, that any configuration action that
+# does the following:
+#  0. Bringing up a cluster
+#  1. Adding/removing Mons
+#  2. Changing a Mon IP address
+#  3. Adding/removing OSDs
+# ... must be done in tandem with manual operations on the affected nodes. For
+# example, bootstrapping a cluster will involve keychain and monmap management,
+# changing anything with mons will involve monmap management, adding new OSDs
+# will require provisioning them with ceph-volume, etc.
+#
+# This is in stark contrast to a fully-managed solution like rook. Since we
+# don't have hundreds of clusters, none of the above is automated, especially
+# as that kind of automation is quite tricky to do reliably.
+
+{ config, lib, pkgs, ... }:
+
+with builtins;
+with lib;
+
+with (( import ../defs-cluster-k0.nix ) config.networking.hostName);
+
+let
+  machineName = config.networking.hostName;
+  isMon = hasAttr machineName cephCluster.mons;
+  isOsd = hasAttr machineName cephCluster.osds;
+  hasCeph = isMon || isOsd;
+
+  # This NixOS Ceph option fragment is present on every machine that runs a
+  # mon, and basically tells the NixOS machinery to run mons/mgrs if needed on
+  # this machine.
+  cephMonConfig = if isMon then {
+    mon = {
+      enable = true;
+      daemons = [ machineName ];
+    };
+    mgr = {
+      enable = true;
+      daemons = [ machineName ];
+    };
+  } else {};
+
+  # Same as for cephMonConfig, but this time for OSDs.
+  cephOsdConfig = if isOsd then {
+    osd = {
+      enable = true;
+      daemons = map (el: "${toString el.id}") cephCluster.osds.${machineName};
+    };
+  } else {};
+
+  # The full option fragment for services.ceph. It contains ceph.conf fragments
+  # (in .global.*) and merges ceph{Mon,Osd}Config.
+  cephConfig = {
+    enable = true;
+    global = {
+      fsid = cephCluster.fsid;
+      clusterName = cephCluster.name;
+
+      # Every Ceph node always attempts to connect to all mons.
+      monHost = concatStringsSep "," (mapAttrsToList (k: _: machinesByName.${k}.ipAddr) cephCluster.mons);
+      monInitialMembers = concatStringsSep "," (builtins.attrNames cephCluster.mons);
+    };
+  } // cephMonConfig // cephOsdConfig;
+
+  # Merge ceph-volume lvm activate into ceph-osd-ID services.
+  #
+  # This is because the upstream module seems to have been written with
+  # filestore in mind, not bluestore. Filestore is relatively simple: an xfs
+  # filesystem is mounted into /var/lib/caph/osd/$cluster-$id, that in turn
+  # contains everything for that OSD to work. 
+  #
+  # Bluestore is a bit different. Instead of a normal filesystem being mounted,
+  # Ceph manages a block device fully using LVM (and in our case, dmcrypt).
+  # Every bluestore volume needs to be 'activated' before it can be used by an
+  # OSD. Activation takes care of doing LVM and dmcrypt mounts, and prepares
+  # the /var/lib/ceph/osd/$cluster-$id directory as if a filestore was present
+  # there. However, instead of this being a diskmount, it's instead a tmpfs
+  # into which a bunch of files are dropped, loaded from the LVM raw device.
+  #
+  # To make the upstream NixOS module OSD work with bluestore, we do the following:
+  #  1. Change ConditionPathExists from the OSD mount into a /dev/disk/by-id
+  #     path. This gates the service on that device being present.
+  #  2. Inject an ExecStartPre which runs ceph-volume lvm activate, if needed.
+  #  3. Add lvm/cryptsetup to the PATH of the service (as used by ceph-volume,
+  #     which seems to look for them on PATH instead of being properly
+  #     nixified).
+  #
+  # We also inject smartmontools into PATH for smartctl, which allows the OSD
+  # to monitor device health.
+  osdActivateServices = listToAttrs (map (el: let
+      osdId = toString el.id;
+      osdUuid = el.uuid;
+      diskPath = el.path;
+    in {
+    name = "ceph-osd-${osdId}";
+    value = {
+      path = with pkgs; [
+        lvm2
+        cryptsetup
+        smartmontools
+      ];
+      serviceConfig = {
+        ExecStartPre = lib.mkForce [
+          ("+" + (toString (pkgs.writeScript "ceph-osd-${osdId}-activate.sh" ''
+            #!/bin/sh
+            set -e
+            dir="/var/lib/ceph/osd/${cephCluster.name}-${osdId}/"
+            disk="${el.path}"
+            uuid="${osdUuid}"
+            if [ -d "$dir" ] && [ -f "$dir"/keyring ]; then
+              echo "Volume $dir already activated, skipping..."
+            else
+              echo "Activating $dir with $disk, uuid $uuid..."
+              ${pkgs.ceph}/bin/ceph-volume lvm activate --bluestore --no-systemd ${osdId} $uuid
+            fi
+
+          '')))
+
+          "${pkgs.ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${osdId} --cluster ${cephCluster.name}"
+        ];
+      };
+      unitConfig = {
+        ConditionPathExists = lib.mkForce el.path;
+      };
+    };
+  }) (if isOsd then cephCluster.osds.${machineName} else []));
+
+in rec {
+  services.ceph = if hasCeph then cephConfig else {};
+
+  environment.systemPackages = with pkgs; [
+    ceph cryptsetup smartmontools
+  ];
+
+  systemd.services = osdActivateServices;
+
+  # Hack - the upstream ceph module should generate ${clusterName}.conf instead
+  # of ceph.conf, let's just symlink it.
+  environment.etc."ceph/${cephCluster.name}.conf".source = "/etc/ceph/ceph.conf";
+}
diff --git a/cluster/nix/provision.nix b/cluster/nix/provision.nix
deleted file mode 100644
index 7ab7e71..0000000
--- a/cluster/nix/provision.nix
+++ /dev/null
@@ -1,49 +0,0 @@
-{ hscloud, pkgs, ... }:
-
-with builtins;
-
-let 
-  machines = (import ./defs-machines.nix);
-  configurations = builtins.listToAttrs (map (machine: {
-    name = machine.fqdn;
-    value = pkgs.nixos ({ config, pkgs, ... }: {
-      networking.hostName = machine.name;
-      imports = [
-        ./modules/base.nix
-        ./modules/kubernetes.nix
-      ];
-    });
-  }) machines);
-
-  scriptForMachine = machine: let
-    configuration = configurations."${machine.fqdn}";
-  in ''
-   set -e
-   remote=root@${machine.fqdn}
-   echo "Configuration for ${machine.fqdn} is ${configuration.toplevel}"
-   nix copy --no-check-sigs -s --to ssh://$remote ${configuration.toplevel}
-   echo "/etc/systemd/system diff:"
-   ssh $remote diff -ur /var/run/current-system/etc/systemd/system ${configuration.toplevel}/etc/systemd/system || true
-   echo ""
-   echo ""
-   ssh $remote ${configuration.toplevel}/bin/switch-to-configuration dry-activate
-   read -p "Do you want to switch to this configuration? " -n 1 -r
-   echo
-   if [[ $REPLY =~ ^[Yy]$ ]]; then
-       ssh $remote ${configuration.toplevel}/bin/switch-to-configuration switch
-   fi
-  '';
-
-  provisioners = (map (machine:
-    pkgs.writeScriptBin "provision-${machine.name}" (scriptForMachine machine)
-  ) machines);
-
-  provision = pkgs.writeScriptBin "provision" (
-    ''
-      echo "Available provisioniers:"
-    '' + (concatStringsSep "\n" (map (machine: "echo '  provision-${machine.name}'") machines)));
-in
-pkgs.symlinkJoin {
-  name = "provision";
-  paths = [ provision ] ++ provisioners;
-}
diff --git a/cluster/tools/kartongips/cmd/BUILD.bazel b/cluster/tools/kartongips/cmd/BUILD.bazel
index dee1b41..a75ee83 100644
--- a/cluster/tools/kartongips/cmd/BUILD.bazel
+++ b/cluster/tools/kartongips/cmd/BUILD.bazel
@@ -19,6 +19,7 @@
         "//cluster/tools/kartongips/utils:go_default_library",
         "@com_github_genuinetools_reg//registry:go_default_library",
         "@com_github_google_go_jsonnet//:go_default_library",
+        "@com_github_mattn_go_isatty//:go_default_library",
         "@com_github_sirupsen_logrus//:go_default_library",
         "@com_github_spf13_cobra//:go_default_library",
         "@io_k8s_apimachinery//pkg/api/meta:go_default_library",
diff --git a/cluster/tools/kartongips/cmd/diff.go b/cluster/tools/kartongips/cmd/diff.go
index 47d92ab..8a9207c 100644
--- a/cluster/tools/kartongips/cmd/diff.go
+++ b/cluster/tools/kartongips/cmd/diff.go
@@ -16,22 +16,54 @@
 package cmd
 
 import (
+	"fmt"
+	"os"
+
+	"github.com/mattn/go-isatty"
 	"github.com/spf13/cobra"
 
 	"code.hackerspace.pl/hscloud/cluster/tools/kartongips/pkg/kubecfg"
 )
 
 const (
+	// TODO(b/49): remove this flag
 	flagDiffStrategy = "diff-strategy"
 	flagOmitSecrets  = "omit-secrets"
 )
 
 func init() {
-	diffCmd.PersistentFlags().String(flagDiffStrategy, "all", "Diff strategy, all or subset.")
+	diffCmd.PersistentFlags().String(flagDiffStrategy, "", "Diff strategy - no op (will be removed soon)")
 	diffCmd.PersistentFlags().Bool(flagOmitSecrets, false, "hide secret details when showing diff")
 	RootCmd.AddCommand(diffCmd)
 }
 
+// nagStrategy nags the user about selecting strategy=subset explicitly -
+// either hint at not having to use it if subset is specified, or fail hard if
+// something else is set.
+//
+// TODO(b/49): remove this
+func nagStrategy(chosen string) {
+	if chosen == "" {
+		return
+	}
+	color := isatty.IsTerminal(os.Stdout.Fd())
+
+	if chosen == "subset" {
+		if color {
+			fmt.Fprintf(os.Stdout, "\x1b[92m")
+		}
+		fmt.Fprintf(os.Stdout, "--diff-strategy=subset is now the default behaviour of kartongips/kubecfg, no need to explicitly set it.\n")
+		fmt.Fprintf(os.Stdout, "Work on your muscle memory and fix your scripts! This flag will be removed soon (see: b.hswaw.net/49).\n")
+		if color {
+			fmt.Fprintf(os.Stdout, "\x1b[0m")
+		}
+		return
+	}
+
+	fmt.Fprintf(os.Stderr, "--diff-strategy is deprecated, the default behaviour is now 'subset' and all other modes of operation have been removed. See: b.hswaw.net/49.\n")
+	os.Exit(1)
+}
+
 var diffCmd = &cobra.Command{
 	Use:   "diff",
 	Short: "Display differences between server and local config",
@@ -42,10 +74,11 @@
 
 		c := kubecfg.DiffCmd{}
 
-		c.DiffStrategy, err = flags.GetString(flagDiffStrategy)
+		diffStrategy, err := flags.GetString(flagDiffStrategy)
 		if err != nil {
 			return err
 		}
+		nagStrategy(diffStrategy)
 
 		c.OmitSecrets, err = flags.GetBool(flagOmitSecrets)
 		if err != nil {
@@ -67,6 +100,8 @@
 			return err
 		}
 
-		return c.Run(cmd.Context(), objs, cmd.OutOrStdout())
+		err = c.Run(cmd.Context(), objs, cmd.OutOrStdout())
+		nagStrategy(diffStrategy)
+		return err
 	},
 }
diff --git a/cluster/tools/kartongips/pkg/kubecfg/diff.go b/cluster/tools/kartongips/pkg/kubecfg/diff.go
index f1136be..fd21043 100644
--- a/cluster/tools/kartongips/pkg/kubecfg/diff.go
+++ b/cluster/tools/kartongips/pkg/kubecfg/diff.go
@@ -50,8 +50,6 @@
 	Mapper           meta.RESTMapper
 	DefaultNamespace string
 	OmitSecrets      bool
-
-	DiffStrategy string
 }
 
 func (c DiffCmd) Run(ctx context.Context, apiObjects []*unstructured.Unstructured, out io.Writer) error {
@@ -89,9 +87,8 @@
 		}
 
 		liveObjObject := liveObj.Object
-		if c.DiffStrategy == "subset" {
-			liveObjObject = removeMapFields(obj.Object, liveObjObject)
-		}
+		liveObjObject = removeMapFields(obj.Object, liveObjObject)
+		liveObjObject = removeClusterRoleAggregatedRules(liveObjObject)
 
 		liveObjText, _ := json.MarshalIndent(liveObjObject, "", "  ")
 		objText, _ := json.MarshalIndent(obj.Object, "", "  ")
@@ -211,6 +208,38 @@
 	return result
 }
 
+// removeClusterRoleAggregatedRules clears the rules field from live
+// ClusterRole objects which have an aggregationRule. This allows us to diff a
+// config object (which doesn't have these rules materialized) against a live
+// obejct (which does have these rules materialized) without spurious diffs.
+//
+// See the Aggregated ClusterRole section of the Kubernetes RBAC docuementation
+// for more information:
+//
+// https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles
+func removeClusterRoleAggregatedRules(live map[string]interface{}) map[string]interface{} {
+	if version, ok := live["apiVersion"].(string); !ok || version != "rbac.authorization.k8s.io/v1" {
+		return live
+	}
+
+	if kind, ok := live["kind"].(string); !ok || kind != "ClusterRole" {
+		return live
+	}
+
+	if _, ok := live["aggregationRule"].(map[string]interface{}); !ok {
+		return live
+	}
+
+	// Make copy of map.
+	res := make(map[string]interface{})
+	for k, v := range live {
+		res[k] = v
+	}
+	// Clear rules field.
+	res["rules"] = []interface{}{}
+	return res
+}
+
 func removeListFields(config, live []interface{}) []interface{} {
 	// If live is longer than config, then the extra elements at the end of the
 	// list will be returned as is so they appear in the diff.
diff --git a/cluster/tools/kartongips/pkg/kubecfg/update.go b/cluster/tools/kartongips/pkg/kubecfg/update.go
index 928104b..d035c2e 100644
--- a/cluster/tools/kartongips/pkg/kubecfg/update.go
+++ b/cluster/tools/kartongips/pkg/kubecfg/update.go
@@ -102,11 +102,11 @@
 		tmp := unstructured.Unstructured{}
 		err := utils.CompactDecodeObject(data, &tmp)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("CompactDecodeObject original object: %w", err)
 		}
 		origData, err = tmp.MarshalJSON()
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("MarshalJSON original object: %w", err)
 		}
 	}
 
@@ -116,7 +116,7 @@
 	utils.DeleteMetaDataAnnotation(new, AnnotationOrigObject)
 	data, err := utils.CompactEncodeObject(new)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("CompactEncodeObject: %w", err)
 	}
 	utils.SetMetaDataAnnotation(new, AnnotationOrigObject, data)
 
@@ -124,41 +124,59 @@
 
 	newData, err := new.MarshalJSON()
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("MarshalJSON new: %w", err)
 	}
 
 	existingData, err := existing.MarshalJSON()
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("MarshalJSON new: %w", err)
 	}
 
-	var resData []byte
-	if schema == nil {
-		// No schema information - fallback to JSON merge patch
+	schemaless := func() ([]byte, error) {
 		patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(origData, newData, existingData)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("CreateThreeWayJSONMergePatch (schemaless): %w", err)
 		}
-		resData, err = jsonpatch.MergePatch(existingData, patch)
+		resData, err := jsonpatch.MergePatch(existingData, patch)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("MergePatch (schemaless): %w", err)
 		}
-	} else {
+		return resData, nil
+	}
+	schemaful := func() ([]byte, error) {
 		patchMeta := strategicpatch.NewPatchMetaFromOpenAPI(schema)
 
 		patch, err := strategicpatch.CreateThreeWayMergePatch(origData, newData, existingData, patchMeta, true)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("CreateThreeWayMergePatch (schemaful): %w", err)
 		}
-		resData, err = strategicpatch.StrategicMergePatchUsingLookupPatchMeta(existingData, patch, patchMeta)
+		resData, err := strategicpatch.StrategicMergePatchUsingLookupPatchMeta(existingData, patch, patchMeta)
+		if err != nil {
+			return nil, fmt.Errorf("StrategicMergePatch (schemaful): %w", err)
+		}
+		return resData, nil
+	}
+
+	var resData []byte
+	if schema == nil {
+		resData, err = schemaless()
 		if err != nil {
 			return nil, err
 		}
+	} else {
+		resData, err = schemaful()
+		if err != nil {
+			log.Warningf("Schemaful/Three-way merge failed (%v), attempting schemaless/JSON merge...", err)
+			resData, err = schemaless()
+			if err != nil {
+				return nil, err
+			}
+		}
 	}
 
 	result, _, err := unstructured.UnstructuredJSONScheme.Decode(resData, nil, nil)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Decode: %w", err)
 	}
 
 	return result.(*unstructured.Unstructured), nil
@@ -171,7 +189,7 @@
 
 		data, err := utils.CompactEncodeObject(obj)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("CompactEncodeObject: %w", err)
 		}
 		utils.SetMetaDataAnnotation(obj, AnnotationOrigObject, data)
 
@@ -183,12 +201,12 @@
 		return newobj, err
 	}
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Get: %w", err)
 	}
 
 	mergedObj, err := patch(existing, obj, schema)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("patch: %w", err)
 	}
 
 	// Kubernetes is a bit odd when/how it reports
@@ -207,7 +225,7 @@
 	log.Debug("About to make change: ", diff.ObjectDiff(existing, mergedObj))
 	log.Info("Updating ", desc, dryRunText)
 	if dryRun {
-		return mergedObj, nil
+		return mergedObj, fmt.Errorf("ObjectDiff: %v", nil)
 	}
 	newobj, err := rc.Update(ctx, mergedObj, metav1.UpdateOptions{})
 	log.Debugf("Update(%s) returned (%v, %v)", mergedObj.GetName(), newobj, err)
diff --git a/default.nix b/default.nix
index 4540157..f06a4d3 100644
--- a/default.nix
+++ b/default.nix
@@ -5,34 +5,25 @@
 let
   fix = f: let x = f x; in x;
 
-  readTree = import ./nix/readtree.nix {};
+  readTree = import ./nix/readtree {};
 
-  # Tracking nixos-unstable as of 2021-01-31.
-  nixpkgsCommit = "44ad80ab1036c5cc83ada4bfa451dac9939f2a10";
+  # Tracking nixos-unstable as of 2021-08-11.
+  nixpkgsCommit = "e26c0ffdb013cd378fc2528a44689a8bf35d2a6c";
   nixpkgsSrc = fetchTarball {
     url = "https://github.com/NixOS/nixpkgs/archive/${nixpkgsCommit}.tar.gz";
-    sha256 = "1b61nzvy0d46cspy07szkc0rggacxiqg9v1py27pkqpj7rvawfsk";
+    sha256 = "1b33hw35fqb9rzszdg5jpiyfvhx2cxpv0qrkyr19zkdpdahzdbss";
   };
   nixpkgs = import nixpkgsSrc {
     config.allowUnfree = true;
     config.allowBroken = true;
   };
 
-in fix (self: rec {
-  config = {
-    hscloud = self // {
-      root = ./.;
-    };
-    pkgs = nixpkgs;
-    pkgsSrc = nixpkgsSrc;
-
-    inherit (nixpkgs) lib stdenv;
-  };
-
-  bgpwtf = readTree config ./bgpwtf;
-  cluster = readTree config ./cluster;
-  hswaw = readTree config ./hswaw;
-  ops = readTree config ./ops;
-
+in fix (self: (readTree rec {
+  hscloud = self;
+  pkgs = nixpkgs;
+  pkgsSrc = nixpkgsSrc;
+  inherit (nixpkgs) lib stdenv;
+} ./.) // {
+  root = ./.;
   pkgs = nixpkgs;
 })
diff --git a/games/factorio/kube/prod.jsonnet b/games/factorio/kube/prod.jsonnet
index 0e3888f..cf3ac02 100644
--- a/games/factorio/kube/prod.jsonnet
+++ b/games/factorio/kube/prod.jsonnet
@@ -35,6 +35,14 @@
     local mod = function(name, version) { name: name, version: version },
     pymods: prod.instance("pymods", "1.1.35-1") {
         cfg+: {
+            // Bump up to 2G/2CPU request/limit.
+            resources: {
+                requests: {
+                    cpu: "2",
+                    memory: "2Gi",
+                },
+                limits: self.requests,
+            },
             mods: [
                 // Stdlib for mods
                 mod("stdlib", "1.4.6"),
diff --git a/hswaw/cebulacamp/landing/cebula2020.jpeg b/hswaw/cebulacamp/landing/cebula2020.jpeg
index 5794856..1025ab8 100644
--- a/hswaw/cebulacamp/landing/cebula2020.jpeg
+++ b/hswaw/cebulacamp/landing/cebula2020.jpeg
Binary files differ
diff --git a/hswaw/cebulacamp/landing/index.html b/hswaw/cebulacamp/landing/index.html
index b2d4e95..31e7753 100644
--- a/hswaw/cebulacamp/landing/index.html
+++ b/hswaw/cebulacamp/landing/index.html
@@ -1,18 +1,18 @@
 <!DOCTYPE html>
 <meta charset="utf-8">
-<title>Cebulacamp 2020</title>
+<title>Cebulacamp 2021</title>
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <link rel="stylesheet" type="text/css" href="style/main.css">
 <header class="hero">
     <h1>Kongres Komunikacyjny Cebula 2021</h1>
     <h2>Testing in <s>production</s> pandemic</h2>
-    <p class="acc0">2021/xx/xx - 2021/xx/xx</p>
+    <p class="acc0">2021/10/01 - 2021/10/03</p>
     <p class="acc1">Hotel Orle, Gdańsk</p>
     <p>
         Three days of under-organized hacking and talking in a hotel in northern Poland. We might talk about the Polish hacker scene. We might get technical. We might even speak mostly Polish, but we'll do our best to welcome people who don't speak encrypted. We're friendly.
     </p>
     <p class="acc2">
-        Happening any day now! Hopefully in 2021. Waiting for vaccines.
+        Tentatively planned. Still arranging details, including hygiene/vaccination/test rules. Stay tuned.
     </p>
 </header>
 <main>
@@ -24,10 +24,13 @@
             <header>
                 <h2>Tickets</h2>
                 <p>
-                    Get your tickets now at <a href="https://tickets.hackerspace.pl/cebulacamp/kkc20/1">tickets.hackerspace.pl/cebulacamp/kkc20/1</a>.
+                    If you already have tickets from 2020: they're still valid. We also have some extra tickets for sale on our <a href="https://tickets.hackerspace.pl/cebulacamp/kkc20/1">ticket shop</a> available under September 1st. Get them while they're hot!
                 </p>
                 <p>
-                    An all-inclusive ticket (bed in shared room for two nights, food for three days) is 500 PLN. If you want a solo room, or to share a room with someone in particular, contact <a href="mailto:cebula@hackerspace.pl">cebula@hackerspace.pl</a>, or (once you bought your ticket) <a href="mailto:info@orle.pl">info@orle.com.pl</a>.
+                    <b>If you don't have a valid tickets.hackerspace.pl order marked as 'Paid', you <i>do not have a ticket</i>. If you paid the hotel and don't have an order marked as 'Paid', <a href="mailto:cebula@hackerspace.pl">get in touch with us ASAP</a>, or <i>we will not have a room for you.</i></b>
+                </p>
+                <p>
+                    An all-inclusive ticket (bed in shared room for two nights, food for three days) is 500 PLN. If you want a solo room, or to share a room with someone in particular, contact <a href="mailto:cebula@hackerspace.pl">cebula@hackerspace.pl</a>.
                 </p>
                 <p>
                     <img class="hotel-pic" src="hotel-orle.jpg">
@@ -53,10 +56,10 @@
             <header>
                 <h2>Contact</h2>
                 <p>
-                    Reach the organizers at <a href="mailto:cebula@hackerspace.pl">cebula@hackerspace.pl</a>. Reach the hotel (in case of room requests, etc) at <a href="mailto:info@orle.com.pl">info@orle.com.pl</a>, and CC cebula@hackerspace.pl if you so with.
+                    Reach the organizers at <a href="mailto:cebula@hackerspace.pl">cebula@hackerspace.pl</a>. Reach the hotel (in case of room requests, etc) at <a href="mailto:info@orle.com.pl">info@orle.com.pl</a>, and CC cebula@hackerspace.pl if you so wish.
                 </p>
                 <p>
-                    irc: #cebulacamp on Freenode
+                    <a href="https://kiwiirc.com/nextclient/irc.libera.chat/cebulacamp">irc: #cebulacamp on Libera.chat</a>
                 </p>
                 <p>
                     Cebula Camp is an inclusive event. Be excellent to each other, or stay home. Harassment and discrimination are not welcome or tolerated, online or AFK. If you're a subject, observer, or third-party to any of these, please, get in touch, write to <a href="mailto:cebula@hackerspace.pl">cebula@hackerspace.pl</a> or <a href="mailto:q3k@q3k.org">q3k personally</a>.
diff --git a/hswaw/cebulacamp/landing/style/main.css b/hswaw/cebulacamp/landing/style/main.css
index 5668e45..8ed6d76 100644
--- a/hswaw/cebulacamp/landing/style/main.css
+++ b/hswaw/cebulacamp/landing/style/main.css
@@ -114,7 +114,7 @@
     border: 8px solid rgba(61, 140, 208, 1);;
 }
 
-section:nth-child(odd) h1 {
+section:nth-child(odd) h2 {
     background-color: rgba(61, 140, 208, 1);;
 }
 
diff --git a/hswaw/kube/cebulacamp.libsonnet b/hswaw/kube/cebulacamp.libsonnet
index 3380bdf..ffc7f1d 100644
--- a/hswaw/kube/cebulacamp.libsonnet
+++ b/hswaw/kube/cebulacamp.libsonnet
@@ -3,7 +3,7 @@
 
 {
     cfg:: {
-        image: "registry.k0.hswaw.net/q3k/cebulacamp-landing:315532800-f25fd84f02caf48122babfbd24acb3ce8a7979b0",
+        image: "registry.k0.hswaw.net/q3k/cebulacamp-landing:315532800-bbf56cf7e14df954dcddedfe44c967246f11b72c",
         webFQDN: error "webhookFQDN must be set",
     },
 
diff --git a/hswaw/kube/hswaw.jsonnet b/hswaw/kube/hswaw.jsonnet
index 41ff73d..9a1bec7 100644
--- a/hswaw/kube/hswaw.jsonnet
+++ b/hswaw/kube/hswaw.jsonnet
@@ -7,6 +7,7 @@
 local frab = import "frab.libsonnet";
 local pretalx = import "pretalx.libsonnet";
 local cebulacamp = import "cebulacamp.libsonnet";
+local site = import "site.libsonnet";
 
 {
     hswaw(name):: mirko.Environment(name) {
@@ -20,6 +21,7 @@
             frab: frab.cfg,
             pretalx: pretalx.cfg,
             cebulacamp: cebulacamp.cfg,
+            site: site.cfg,
         },
 
         components: {
@@ -30,6 +32,7 @@
             frab: frab.component(cfg.frab, env),
             pretalx: pretalx.component(cfg.pretalx, env),
             cebulacamp: cebulacamp.component(cfg.cebulacamp, env),
+            site: site.component(cfg.site, env),
         },
     },
 
@@ -69,6 +72,9 @@
             cebulacamp+: {
                 webFQDN: "cebula.camp",
             },
+            site+: {
+                webFQDN: "new.hackerspace.pl",
+            },
         },
     },
 }
diff --git a/hswaw/kube/site.libsonnet b/hswaw/kube/site.libsonnet
new file mode 100644
index 0000000..fbe2896
--- /dev/null
+++ b/hswaw/kube/site.libsonnet
@@ -0,0 +1,26 @@
+local mirko = import "../../kube/mirko.libsonnet";
+local kube = import "../../kube/kube.libsonnet";
+
+{
+    cfg:: {
+        image: "registry.k0.hswaw.net/q3k/hswaw-site:1630780895-62e50da881f666719aa8b5c632f2a5b33695a058",
+        webFQDN: error "webFQDN must be set",
+    },
+
+    component(cfg, env):: mirko.Component(env, "site") {
+        local site = self,
+        cfg+: {
+            image: cfg.image,
+            container: site.GoContainer("main", "/hswaw/site/site") {
+            },
+            ports+: {
+                publicHTTP: {
+                    web: {
+                        port: 8080,
+                        dns: cfg.webFQDN,
+                    }
+                },
+            },
+        },
+    },
+}
diff --git a/hswaw/site/BUILD.bazel b/hswaw/site/BUILD.bazel
index 21edded..231cb51 100644
--- a/hswaw/site/BUILD.bazel
+++ b/hswaw/site/BUILD.bazel
@@ -4,14 +4,18 @@
 go_library(
     name = "go_default_library",
     srcs = [
+        "at.go",
+        "events.go",
         "feeds.go",
         "main.go",
+        "spaceapi.go",
         "views.go",
     ],
     importpath = "code.hackerspace.pl/hscloud/hswaw/site",
     visibility = ["//visibility:private"],
     deps = [
         "//go/mirko:go_default_library",
+        "//hswaw/site/calendar:go_default_library",
         "//hswaw/site/static:static_go",
         "//hswaw/site/templates:templates_go",
         "@com_github_golang_glog//:go_default_library",
@@ -25,8 +29,8 @@
 )
 
 container_image(
-    name="latest",
-    base="@prodimage-bionic//image",
+    name = "latest",
+    base = "@prodimage-bionic//image",
     files = [":site"],
     directory = "/hswaw/site/",
     entrypoint = ["/hswaw/site/site"],
@@ -38,5 +42,5 @@
     format = "Docker",
     registry = "registry.k0.hswaw.net",
     repository = "q3k/hswaw-site",
-    tag = "1622585979-{STABLE_GIT_COMMIT}",
+    tag = "1630780895-{STABLE_GIT_COMMIT}",
 )
diff --git a/hswaw/site/at.go b/hswaw/site/at.go
new file mode 100644
index 0000000..70df8c7
--- /dev/null
+++ b/hswaw/site/at.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+const (
+	atURL = "https://at.hackerspace.pl/api"
+)
+
+// atStatus is the result of queruing checkinator/at (Hackerspace presence
+// service).
+type atStatus struct {
+	// Users is the list of present and publicly visible users.
+	Users []atUser `json:"users"`
+	// ESPs is the number of ESP{8266,32} devices.
+	ESPs int `json:"esps"`
+	// Kektops is the number of nettop “Kektop” devices.
+	Kektops int `json:"kektops"`
+	// Unknown is the number of unknown devices in the network.
+	Unknown int `json:"unknown"`
+}
+
+type atUser struct {
+	Login string `json:"login"`
+}
+
+func getAt(ctx context.Context) (*atStatus, error) {
+	r, err := http.NewRequestWithContext(ctx, "GET", atURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("NewRequest(%q): %w", atURL, err)
+	}
+	res, err := http.DefaultClient.Do(r)
+	if err != nil {
+		return nil, fmt.Errorf("GET: %w", err)
+	}
+	defer res.Body.Close()
+
+	var status atStatus
+	if err := json.NewDecoder(res.Body).Decode(&status); err != nil {
+		return nil, fmt.Errorf("when decoding JSON: %w", err)
+	}
+
+	return &status, nil
+}
diff --git a/hswaw/site/calendar/BUILD.bazel b/hswaw/site/calendar/BUILD.bazel
new file mode 100644
index 0000000..fcdac53
--- /dev/null
+++ b/hswaw/site/calendar/BUILD.bazel
@@ -0,0 +1,29 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "event.go",
+        "load.go",
+        "time.go",
+    ],
+    importpath = "code.hackerspace.pl/hscloud/hswaw/site/calendar",
+    visibility = ["//hswaw/site:__subpackages__"],
+    deps = [
+        "@com_github_arran4_golang_ical//:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = [
+        "event_test.go",
+        "load_test.go",
+    ],
+    data = [
+        ":test.ical",
+    ],
+    embed = [":go_default_library"],
+    deps = ["@com_github_google_go_cmp//cmp:go_default_library"],
+)
diff --git a/hswaw/site/calendar/event.go b/hswaw/site/calendar/event.go
new file mode 100644
index 0000000..141e4a6
--- /dev/null
+++ b/hswaw/site/calendar/event.go
@@ -0,0 +1,125 @@
+package calendar
+
+import (
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/golang/glog"
+)
+
+// UpcomingEvent is a calendar event that will happen in the near future, or is
+// currently happening (relative to same arbitrary timestamp of 'now',
+// depending on the way the UpcomingEvent is crated).
+//
+// It is a best-effort parse of an ICS/iCal event into some event that can be
+// interpreted as a 'community event', to be displayed publicly on a site.
+type UpcomingEvent struct {
+	// UID is the unique ICS/iCal ID of this event.
+	UID string
+	// Summary is the 'title' of the event, usually a short one-liner.
+	Summary string
+	// Full description of event. Might contain multiple lines of test.
+	Description string
+	// Start and End of the events, potentially whole-day dates. See EventTime
+	// for more information.
+	// If Start is WholeDay then so is End, and vice-versa.
+	Start *EventTime
+	// End of the event, exclusive of the time range (ie. if a timestamp it
+	// defines the timestamp at which the next event can start; if it's whole
+	// day it defines the first day on which the event does not take place).
+	End *EventTime
+	// Tentative is whether this event is marked as 'Tentative' in the source
+	// calendar.
+	Tentative bool
+}
+
+// WholeDay returns true if this is a whole-day (or multi-day) event.
+func (u *UpcomingEvent) WholeDay() bool {
+	return u.Start.WholeDay
+}
+
+var (
+	// onceComplainWarsawGone gates throwing a very verbose message about being
+	// unable to localize UpcomingEvents into Warsaw local time by WarsawDate.
+	onceComplainWarsawGone sync.Once
+)
+
+// WarsawDate prints a human-readable timestamp that makes sense within the
+// context of this event taking place in Warsaw, or at least in the same
+// timezone as Warsaw.
+// It will return a time in one of the following formats:
+//
+//   YEAR/MONTH/DAY
+//   (For one-day events)
+//
+//   YEAR/MONTH/DAY - DAY
+//   (For multi-day events within the same month)
+//
+//   YEAR/MONTH/DAY - YEAR/MONTH/DAY
+//   (For multi-day events spanning more than one month)
+//
+//   YEAR/MONTH/DAY HH:MM - HH:MM
+//   (For timestamped events within the same day)
+//
+//   YEAR/MONTH/DAY HH:MM - YEAR/MONTH/DAY HH:MM
+//   (For timestamped events spanning more than one day)
+//
+func (u *UpcomingEvent) WarsawDate() string {
+	YM := "2006/01"
+	D := "02"
+	YMD := "2006/01/02"
+	HM := "15:04"
+	YMDHM := "2006/01/02 15:04"
+
+	if u.WholeDay() {
+		start := u.Start.Time
+		// ICS whole-day dates are [start, end), ie. 'end' is exclusive.
+		end := u.End.Time.AddDate(0, 0, -1)
+		if start == end {
+			// Event is one-day.
+			return start.Format(YMD)
+		}
+		if start.Year() == end.Year() && start.Month() == end.Month() {
+			// Event starts and ends on the same month, print shortened form.
+			return fmt.Sprintf("%s/%s - %s", start.Format(YM), start.Format(D), end.Format(D))
+		}
+		// Event spans multiple months, print full form.
+		return fmt.Sprintf("%s - %s", start.Format(YMD), end.Format(YMD))
+	}
+
+	warsaw, err := time.LoadLocation("Europe/Warsaw")
+	if err != nil {
+		onceComplainWarsawGone.Do(func() {
+			glog.Errorf("Could not load Europe/Warsaw timezone, did the city cease to exist? LoadLoaction: %v", err)
+		})
+		// Even in the face of a cataclysm, degrade gracefully and assume the
+		// users are local to this service's timezone.
+		warsaw = time.Local
+	}
+
+	start := u.Start.Time.In(warsaw)
+	end := u.End.Time.In(warsaw)
+	if start.Year() == end.Year() && start.Month() == end.Month() && start.Day() == end.Day() {
+		// Event starts and ends on same day, print shortened form.
+		return fmt.Sprintf("%s %s - %s", start.Format(YMD), start.Format(HM), end.Format(HM))
+	}
+	// Event spans multiple days, print full form.
+	return fmt.Sprintf("%s - %s", start.Format(YMDHM), end.Format(YMDHM))
+}
+
+func (u *UpcomingEvent) String() string {
+	return fmt.Sprintf("%s (%s)", u.Summary, u.WarsawDate())
+}
+
+func (e *UpcomingEvent) Elapsed(t time.Time) bool {
+	// Event hasn't started yet?
+	if e.Start.Time.After(t) {
+		return false
+	}
+	// Event has started, but hasn't ended?
+	if e.End.Time.After(t) {
+		return false
+	}
+	return true
+}
diff --git a/hswaw/site/calendar/event_test.go b/hswaw/site/calendar/event_test.go
new file mode 100644
index 0000000..1e95306
--- /dev/null
+++ b/hswaw/site/calendar/event_test.go
@@ -0,0 +1,73 @@
+package calendar
+
+import (
+	"fmt"
+	"testing"
+	"time"
+)
+
+func TestWarsawDate(t *testing.T) {
+	makeTime := func(s string) EventTime {
+		t.Helper()
+		warsaw, err := time.LoadLocation("Europe/Warsaw")
+		if err != nil {
+			t.Fatalf("could not get Warsaw timezone: %v", err)
+		}
+		ti, err := time.ParseInLocation("2006/01/02 15:04", s, warsaw)
+		if err != nil {
+			t.Fatal("could not parse test time %q: %v", s, err)
+		}
+		return EventTime{
+			Time: ti,
+		}
+	}
+	makeDay := func(s string) EventTime {
+		t.Helper()
+		ti, err := time.Parse("2006/01/02", s)
+		if err != nil {
+			t.Fatal("could not parse test day %q: %v", s, err)
+		}
+		return EventTime{
+			Time:     ti,
+			WholeDay: true,
+		}
+	}
+	for i, te := range []struct {
+		start EventTime
+		end   EventTime
+		want  string
+	}{
+		{
+			makeTime("2021/03/14 13:37"), makeTime("2021/04/20 21:37"),
+			"2021/03/14 13:37 - 2021/04/20 21:37",
+		},
+		{
+			makeTime("2021/04/20 13:37"), makeTime("2021/04/20 21:37"),
+			"2021/04/20 13:37 - 21:37",
+		},
+		{
+			makeDay("2021/06/01"), makeDay("2021/07/01"),
+			"2021/06/01 - 30",
+		},
+		{
+			makeDay("2021/03/14"), makeDay("2021/04/21"),
+			"2021/03/14 - 2021/04/20",
+		},
+		{
+			makeDay("2021/04/20"), makeDay("2021/04/21"),
+			"2021/04/20",
+		},
+	} {
+		te := te
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			ev := UpcomingEvent{
+				Start: &te.start,
+				End:   &te.end,
+			}
+			got := ev.WarsawDate()
+			if got != te.want {
+				t.Fatalf("wanted %q, got %q", te.want, got)
+			}
+		})
+	}
+}
diff --git a/hswaw/site/calendar/load.go b/hswaw/site/calendar/load.go
new file mode 100644
index 0000000..f9ae146
--- /dev/null
+++ b/hswaw/site/calendar/load.go
@@ -0,0 +1,129 @@
+package calendar
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"sort"
+	"strings"
+	"time"
+	_ "time/tzdata"
+
+	ics "github.com/arran4/golang-ical"
+	"github.com/golang/glog"
+)
+
+const (
+	// EventsURL is the calendar from which we load public Hackerspace events.
+	EventsURL = "https://owncloud.hackerspace.pl/remote.php/dav/public-calendars/g8toktZrA9fyAHNi/?export"
+)
+
+// eventsBySooner sorts upcoming events so the one that happens the soonest
+// will be first in the list.
+type eventBySooner []*UpcomingEvent
+
+func (e eventBySooner) Len() int      { return len(e) }
+func (e eventBySooner) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
+func (e eventBySooner) Less(i, j int) bool {
+	a, b := e[i], e[j]
+	if a.Start.Time == b.Start.Time {
+		if a.End.Time == b.End.Time {
+			return a.UID < b.UID
+		}
+		return a.End.Time.Before(b.End.Time)
+	}
+	return a.Start.Time.Before(b.Start.Time)
+}
+
+// parseUpcomingEvents generates a list of upcoming events from an open ICS/iCal file.
+func parseUpcomingEvents(now time.Time, data io.Reader) ([]*UpcomingEvent, error) {
+	cal, err := ics.ParseCalendar(data)
+	if err != nil {
+		return nil, fmt.Errorf("ParseCalendar(%q): %w", err)
+	}
+
+	var out []*UpcomingEvent
+	for _, event := range cal.Events() {
+		uidProp := event.GetProperty(ics.ComponentPropertyUniqueId)
+		if uidProp == nil || uidProp.Value == "" {
+			glog.Errorf("Event with no UID, ignoring: %+v", event)
+			continue
+		}
+		uid := uidProp.Value
+
+		summaryProp := event.GetProperty(ics.ComponentPropertySummary)
+		if summaryProp == nil || summaryProp.Value == "" {
+			glog.Errorf("Event %s has no summary, ignoring", uid)
+		}
+		summary := summaryProp.Value
+
+		var description string
+		descriptionProp := event.GetProperty(ics.ComponentPropertyDescription)
+		if descriptionProp != nil && descriptionProp.Value != "" {
+			// The ICS/iCal description has escaped newlines. Undo that.
+			description = strings.ReplaceAll(descriptionProp.Value, `\n`, "\n")
+		}
+
+		status := event.GetProperty(ics.ComponentPropertyStatus)
+		tentative := false
+		if status != nil {
+			if status.Value == string(ics.ObjectStatusCancelled) {
+				// NextCloud only has CONFIRMED, CANCELELD and TENTATIVE for
+				// events. We drop everything CANCELELD and keep things that are
+				// TENTATIVE.
+				continue
+			}
+			if status.Value == string(ics.ObjectStatusTentative) {
+				tentative = true
+			}
+		}
+
+		start, err := parseICSTime(event.GetProperty(ics.ComponentPropertyDtStart))
+		if err != nil {
+			glog.Errorf("Event %s has unparseable DTSTART, ignoring: %v", uid, err)
+			continue
+		}
+		end, err := parseICSTime(event.GetProperty(ics.ComponentPropertyDtEnd))
+		if err != nil {
+			glog.Errorf("Event %s has unparseable DTEND, ignoring: %v", uid, err)
+			continue
+		}
+
+		if (start.WholeDay && !end.WholeDay) || (!start.WholeDay && end.WholeDay) {
+			glog.Errorf("Event %s has whole-day inconsistencies, start: %s, end: %s, ignoring", uid, start, end)
+		}
+
+		u := &UpcomingEvent{
+			UID:         uid,
+			Summary:     summary,
+			Description: description,
+			Start:       start,
+			End:         end,
+			Tentative:   tentative,
+		}
+		if u.Elapsed(now) {
+			continue
+		}
+
+		out = append(out, u)
+	}
+	sort.Sort(eventBySooner(out))
+	return out, nil
+}
+
+// GetUpcomingEvents returns all public Hackerspace events that are upcoming
+// relative to the given time 'now' as per the Warsaw Hackerspace public
+// calender (from owncloud.hackerspace.pl).
+func GetUpcomingEvents(ctx context.Context, now time.Time) ([]*UpcomingEvent, error) {
+	r, err := http.NewRequestWithContext(ctx, "GET", EventsURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("NewRequest(%q): %w", EventsURL, err)
+	}
+	res, err := http.DefaultClient.Do(r)
+	if err != nil {
+		return nil, fmt.Errorf("Do(%q): %w", EventsURL, err)
+	}
+	defer res.Body.Close()
+	return parseUpcomingEvents(now, res.Body)
+}
diff --git a/hswaw/site/calendar/load_test.go b/hswaw/site/calendar/load_test.go
new file mode 100644
index 0000000..1b2945a
--- /dev/null
+++ b/hswaw/site/calendar/load_test.go
@@ -0,0 +1,53 @@
+package calendar
+
+import (
+	"os"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestUpcomingEvents(t *testing.T) {
+	r, err := os.Open("test.ical")
+	if err != nil {
+		t.Fatalf("Could not open test ical: %v", err)
+	}
+	ti := time.Unix(1626011785, 0)
+
+	events, err := parseUpcomingEvents(ti, r)
+	if err != nil {
+		t.Fatalf("getUpcomingEvents: %v", err)
+	}
+
+	want := []*UpcomingEvent{
+		{
+			UID:         "65cd51ba-2fd7-475e-a274-61d19c186b66",
+			Summary:     "test event please ignore",
+			Description: "I am a description",
+			Start: &EventTime{
+				Time: time.Unix(1626091200, 0),
+			},
+			End: &EventTime{
+				Time: time.Unix(1626093000, 0),
+			},
+		},
+		{
+			UID:         "2f874784-1e09-4cdc-8ae6-185c9ee36be0",
+			Summary:     "many days",
+			Description: "I am a multiline\n\ndescription\n\nwith a link: https://example.com/foo\n\nbarfoo",
+			Start: &EventTime{
+				Time:     time.Unix(1626134400, 0),
+				WholeDay: true,
+			},
+			End: &EventTime{
+				Time:     time.Unix(1626393600, 0),
+				WholeDay: true,
+			},
+		},
+	}
+
+	if diff := cmp.Diff(events, want); diff != "" {
+		t.Errorf("%s", diff)
+	}
+}
diff --git a/hswaw/site/calendar/test.ical b/hswaw/site/calendar/test.ical
new file mode 100644
index 0000000..1d5908d
--- /dev/null
+++ b/hswaw/site/calendar/test.ical
@@ -0,0 +1,49 @@
+BEGIN:VCALENDAR

+VERSION:2.0

+CALSCALE:GREGORIAN

+PRODID:-//SabreDAV//SabreDAV//EN

+X-WR-CALNAME:q3k test calendar (cc161907-84ed-42b3-b65f-8bdc79161ffe)

+X-APPLE-CALENDAR-COLOR:#1E78C1

+REFRESH-INTERVAL;VALUE=DURATION:PT4H

+X-PUBLISHED-TTL:PT4H

+BEGIN:VTIMEZONE

+TZID:Europe/Berlin

+BEGIN:DAYLIGHT

+TZOFFSETFROM:+0100

+TZOFFSETTO:+0200

+TZNAME:CEST

+DTSTART:19700329T020000

+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU

+END:DAYLIGHT

+BEGIN:STANDARD

+TZOFFSETFROM:+0200

+TZOFFSETTO:+0100

+TZNAME:CET

+DTSTART:19701025T030000

+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU

+END:STANDARD

+END:VTIMEZONE

+BEGIN:VEVENT

+CREATED:20210711T134245Z

+DTSTAMP:20210711T134342Z

+LAST-MODIFIED:20210711T134342Z

+SEQUENCE:3

+UID:2f874784-1e09-4cdc-8ae6-185c9ee36be0

+DTSTART;VALUE=DATE:20210713

+DTEND;VALUE=DATE:20210716

+SUMMARY:many days

+DESCRIPTION:I am a multiline\n\ndescription\n\nwith a link: https://example

+ .com/foo\n\nbarfoo

+END:VEVENT

+BEGIN:VEVENT

+CREATED:20210711T134220Z

+DTSTAMP:20210711T134323Z

+LAST-MODIFIED:20210711T134323Z

+SEQUENCE:3

+UID:65cd51ba-2fd7-475e-a274-61d19c186b66

+DTSTART;TZID=Europe/Berlin:20210712T140000

+DTEND;TZID=Europe/Berlin:20210712T143000

+SUMMARY:test event please ignore

+DESCRIPTION:I am a description

+END:VEVENT

+END:VCALENDAR

diff --git a/hswaw/site/calendar/time.go b/hswaw/site/calendar/time.go
new file mode 100644
index 0000000..f742a67
--- /dev/null
+++ b/hswaw/site/calendar/time.go
@@ -0,0 +1,73 @@
+package calendar
+
+import (
+	"fmt"
+	"time"
+
+	ics "github.com/arran4/golang-ical"
+)
+
+// EventTime is a timestamp for calendar events. It either represents a real
+// point-in time or a calender day, if it's a whole-day event.
+type EventTime struct {
+	// Time is a timestamp in the timezone originally defined for this event if
+	// WholeDay is true. Otherwise, it's a UTC time from which a year, month
+	// and day can be extracted and treated as the indication of a 'calendar
+	// day' in an unknown timezone.
+	Time time.Time
+	// WholeDay is true if this EventTime represents an entire calendar day.
+	WholeDay bool
+}
+
+func (e *EventTime) String() string {
+	if e.WholeDay {
+		return fmt.Sprintf("%s (whole day)", e.Time.Format("2006/01/02"))
+	} else {
+		return e.Time.String()
+	}
+}
+
+// parseICSTime attempts to parse a given ICS DT{START,END} object into an
+// EventTime, trying to figure out if the given object represents a timestamp
+// or a whole-day event.
+func parseICSTime(p *ics.IANAProperty) (*EventTime, error) {
+	// If this is has a VALUE of DATE, then this is a whole-day time.
+	// Otherwise, it's an actual timestamp.
+	valueList, ok := p.ICalParameters[string(ics.ParameterValue)]
+	if ok {
+		if len(valueList) != 1 || valueList[0] != "DATE" {
+			return nil, fmt.Errorf("unsupported time type: %v", valueList)
+		}
+		ts, err := time.Parse("20060102", p.Value)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse date %q: %w", p.Value, err)
+		}
+		return &EventTime{
+			Time:     ts,
+			WholeDay: true,
+		}, nil
+	}
+	// You would expect that nextcloud would emit VALUE == DATE-TIME for
+	// timestamps, but that just doesn't seem to be the case. Maye I should
+	// read the ICS standard...
+
+	tzidList, ok := p.ICalParameters[string(ics.ParameterTzid)]
+	if !ok || len(tzidList) != 1 {
+		return nil, fmt.Errorf("TZID missing")
+	}
+	tzid := tzidList[0]
+	location, err := time.LoadLocation(tzid)
+	if err != nil {
+		return nil, fmt.Errorf("could not parse TZID %q: %w", tzid, err)
+	}
+
+	ts, err := time.ParseInLocation("20060102T150405", p.Value, location)
+	if err != nil {
+		return nil, fmt.Errorf("could not parse time %q: %w", p.Value, err)
+	}
+
+	return &EventTime{
+		Time:     ts,
+		WholeDay: false,
+	}, nil
+}
diff --git a/hswaw/site/events.go b/hswaw/site/events.go
new file mode 100644
index 0000000..26d4866
--- /dev/null
+++ b/hswaw/site/events.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+	"context"
+	"time"
+
+	"code.hackerspace.pl/hscloud/hswaw/site/calendar"
+	"github.com/golang/glog"
+)
+
+func (s *service) eventsWorker(ctx context.Context) {
+	get := func() {
+		events, err := calendar.GetUpcomingEvents(ctx, time.Now())
+		if err != nil {
+			glog.Errorf("Geting events failed: %v", err)
+			return
+		}
+
+		s.eventsMu.Lock()
+		s.events = events
+		s.eventsMu.Unlock()
+	}
+	// Perform initial fetch.
+	get()
+
+	// .. and update very minute.
+	t := time.NewTicker(time.Minute)
+	defer t.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-t.C:
+			get()
+		}
+	}
+}
+
+func (s *service) getEvents() []*calendar.UpcomingEvent {
+	s.eventsMu.RLock()
+	events := s.events
+	s.eventsMu.RUnlock()
+	return events
+}
diff --git a/hswaw/site/main.go b/hswaw/site/main.go
index a7f3e54..c1cc1e3 100644
--- a/hswaw/site/main.go
+++ b/hswaw/site/main.go
@@ -3,15 +3,18 @@
 import (
 	"flag"
 	"fmt"
+	"math/rand"
 	"mime"
 	"net/http"
 	"regexp"
 	"strings"
 	"sync"
+	"time"
 
 	"code.hackerspace.pl/hscloud/go/mirko"
 	"github.com/golang/glog"
 
+	"code.hackerspace.pl/hscloud/hswaw/site/calendar"
 	"code.hackerspace.pl/hscloud/hswaw/site/static"
 )
 
@@ -25,12 +28,20 @@
 	feeds map[string]*atomFeed
 	// feedsMu locks the feeds field.
 	feedsMu sync.RWMutex
+
+	// events is a list of upcoming events, sorted so the first event is the
+	// one that will happen the soonests.
+	events []*calendar.UpcomingEvent
+	// eventsMu locks the events field.
+	eventsMu sync.RWMutex
 }
 
 func main() {
 	flag.StringVar(&flagSitePublic, "site_public", "0.0.0.0:8080", "Address at which to serve public HTTP requests")
 	flag.Parse()
 
+	rand.Seed(time.Now().UnixNano())
+
 	mi := mirko.New()
 	if err := mi.Listen(); err != nil {
 		glog.Exitf("Listen failed: %v", err)
@@ -38,6 +49,7 @@
 
 	s := &service{}
 	go s.feedWorker(mi.Context())
+	go s.eventsWorker(mi.Context())
 
 	mux := http.NewServeMux()
 	s.registerHTTP(mux)
@@ -102,5 +114,8 @@
 
 func (s *service) registerHTTP(mux *http.ServeMux) {
 	mux.HandleFunc("/static/", s.handleHTTPStatic)
+	mux.HandleFunc("/spaceapi", s.handleSpaceAPI)
+	mux.HandleFunc("/events.json", s.handleJSONEvents)
+	mux.HandleFunc("/event/", s.handleEvent)
 	mux.HandleFunc("/", s.handleIndex)
 }
diff --git a/hswaw/site/spaceapi.go b/hswaw/site/spaceapi.go
new file mode 100644
index 0000000..13c5aad
--- /dev/null
+++ b/hswaw/site/spaceapi.go
@@ -0,0 +1,119 @@
+package main
+
+import (
+	"context"
+
+	"code.hackerspace.pl/hscloud/hswaw/site/calendar"
+	"github.com/golang/glog"
+)
+
+// SpaceAPIResponse, per https://spaceapi.io/ - kinda. Mostly rewritten from
+// old implementation, someone should update this to use the official schema.
+type SpaceAPIResponse struct {
+	API                 string                      `json:"api"`
+	Space               string                      `json:"space"`
+	Logo                string                      `json:"logo"`
+	URL                 string                      `json:"url"`
+	Location            SpaceAPILocation            `json:"location"`
+	State               SpaceAPIState               `json:"state"`
+	Contact             map[string]string           `json:"contact"`
+	IssueReportChannels []string                    `json:"issue_report_channels"`
+	Projects            []string                    `json:"projects"`
+	Feeds               map[string]SpaceAPIFeed     `json:"feeds"`
+	Sensors             map[string][]SpaceAPISensor `json:"sensors"`
+}
+
+type SpaceAPILocation struct {
+	Latitude  float64 `json:"lat"`
+	Longitude float64 `json:"lon"`
+	Address   string  `json:"address"`
+}
+
+type SpaceAPIState struct {
+	Open    bool   `json:"open"`
+	Message string `json:"message"`
+	Icon    struct {
+		Open   string `json:"open"`
+		Closed string `json:"closed"`
+	} `json:"icon"`
+}
+
+type SpaceAPIFeed struct {
+	Type string `json:"type"`
+	URL  string `json:"url"`
+}
+
+type SpaceAPISensor struct {
+	Value int      `json:"value"`
+	Names []string `json:"names"`
+}
+
+func generateSpaceAPIResponse(ctx context.Context) SpaceAPIResponse {
+	state := SpaceAPIState{}
+	state.Icon.Open = "https://static.hackerspace.pl/img/status-open-small.png"
+	state.Icon.Closed = "https://static.hackerspace.pl/img/status-closed-small.png"
+	// TODO(q3k): post-coronavirus, make automatically open based on calendar
+	// events and Open Thursdays.
+	open := false
+	if open {
+		state.Open = true
+		state.Message = "open for public"
+	} else {
+		state.Open = false
+		state.Message = "members only"
+	}
+
+	peopleNowPresent := SpaceAPISensor{}
+	atState, err := getAt(ctx)
+	if err != nil {
+		glog.Errorf("Failed to get checkinator status: %v", err)
+	} else {
+		peopleNowPresent.Names = make([]string, len(atState.Users))
+		for i, u := range atState.Users {
+			peopleNowPresent.Names[i] = u.Login
+		}
+		peopleNowPresent.Value = len(peopleNowPresent.Names)
+	}
+
+	res := SpaceAPIResponse{
+		API:   "0.13",
+		Space: "Warsaw Hackerspace",
+		Logo:  "https://static.hackerspace.pl/img/syrenka-black.png",
+		URL:   "https://hackerspace.pl",
+		Location: SpaceAPILocation{
+			Latitude:  52.24160,
+			Longitude: 20.98485,
+			Address:   "ul. Wolność 2A, 01-018 Warszawa, Poland",
+		},
+		State: state,
+		Contact: map[string]string{
+			"irc":      "irc://irc.libera.chat/#hswaw",
+			"twitter":  "@hackerspacepl",
+			"facebook": "hackerspacepl",
+			"ml":       "waw@lists.hackerspace.pl",
+		},
+		IssueReportChannels: []string{"irc"},
+		Projects: []string{
+			"https://wiki.hackerspace.pl/projects",
+		},
+		Feeds: map[string]SpaceAPIFeed{
+			"blog": SpaceAPIFeed{
+				Type: "atom",
+				URL:  feedsURLs["blog"],
+			},
+			"calendar": SpaceAPIFeed{
+				Type: "ical",
+				URL:  calendar.EventsURL,
+			},
+			"wiki": SpaceAPIFeed{
+				Type: "rss",
+				URL:  "https://wiki.hackerspace.pl/feed.php",
+			},
+		},
+		Sensors: map[string][]SpaceAPISensor{
+			"people_now_present": []SpaceAPISensor{peopleNowPresent},
+		},
+	}
+
+	return res
+}
diff --git a/hswaw/site/static/BUILD.bazel b/hswaw/site/static/BUILD.bazel
index 7fdce2d..d5ea71d 100644
--- a/hswaw/site/static/BUILD.bazel
+++ b/hswaw/site/static/BUILD.bazel
@@ -4,9 +4,16 @@
 go_embed_data(
     name = "static",
     srcs = [
+        "animations.js",
         "landing.css",
-        "syrenka.png",
+        "led.js",
+        "neon-syrenka.svg",
         "@com_npmjs_leaflet//:distfiles",
+        "space.jpg",
+        "frezarka.jpg",
+        "tokarka.jpg",
+        "kuka.jpg",
+        "serwerownia.jpg",
     ],
     package = "static",
 )
diff --git a/hswaw/site/static/animations.js b/hswaw/site/static/animations.js
new file mode 100644
index 0000000..77d9562
--- /dev/null
+++ b/hswaw/site/static/animations.js
@@ -0,0 +1,273 @@
+// To add your own animation, extend 'Animation' and implement draw(), then add
+// your animation's class name to the list at the bottom of the script.
+
+class Animation {
+    // The constructor for Animation is called by the site rendering code when
+    // the site loads, so it should be fairly fast. Any delay causes the LED
+    // panel to take longer to load.
+    constructor(nx, ny) {
+        // LED array, indexed by x then y.
+        let leds = new Array(nx);
+        for (let x = 0; x < nx; x++) {
+            leds[x] = new Array(ny);
+            for (let y = 0; y < ny; y++) {
+                leds[x][y] = [0.0, 0.0, 0.0];
+            }
+        }
+        this.leds = leds;
+
+        // Number of LEDs, X and Y.
+        this.nx = nx;
+        this.ny = ny;
+    }
+
+    // Helper function that converts from HSV to RGB, can be used by your draw
+    // code.
+    // H, S and V values must be [0..1].
+    hsv2rgb(h, s, v) {
+        const i = Math.floor(h * 6);
+        const f = h * 6 - i;
+        const p = v * (1 - s);
+        const q = v * (1 - f * s);
+        const t = v * (1 - (1 - f) * s);
+
+        let r, g, b;
+        switch (i % 6) {
+            case 0: r = v, g = t, b = p; break;
+            case 1: r = q, g = v, b = p; break;
+            case 2: r = p, g = v, b = t; break;
+            case 3: r = p, g = q, b = v; break;
+            case 4: r = t, g = p, b = v; break;
+            case 5: r = v, g = p, b = q; break;
+        }
+        return [r, g, b];
+    }
+
+    draw(ts) {
+        // Implement your animation here.
+        // The 'ts' argument is a timestamp in seconds, floating point, of the
+        // frame being drawn.
+        //
+        // Your implementation should write to this.leds, which is two
+        // dimensional array containing [r,g,b] values. Colour values are [0..1].
+        //
+        // X coordinates are [0 .. this.nx), Y coordinates are [0 .. this.ny).
+        // The coordinate system is with X==Y==0 in the top-left part of the
+        // display.
+        //
+        // For example, for a 3x3 LED display the coordinates are as follors:
+        //
+        //  (x:0 y:0)  (x:1 y:0)  (x:2  y:0)
+        //  (x:0 y:1)  (x:1 y:1)  (x:2  y:1)
+        //  (x:0 y:2)  (x:1 y:2)  (x:2  y:2)
+        //
+        // The LED array (this.leds) is indexed by X first and Y second.
+        //
+        // For example, to set the LED red at coordinates x:1 y:2:
+        //
+        // this.leds[1][2] = [1.0, 0.0, 0.0];
+    }
+}
+
+// 'Snake' chase animation, a simple RGB chase that goes around in a zigzag.
+// By q3k.
+class SnakeChase extends Animation {
+    draw(ts) {
+        const nx = this.nx;
+        const ny = this.ny;
+        // Iterate over all pixels column-wise.
+        for (let i = 0; i < (nx*ny); i++) {
+            let x = Math.floor(i / ny);
+            let y = i % ny;
+
+            // Flip every second row to get the 'snaking'/'zigzag' effect
+            // during iteration.
+            if (x % 2 == 0) {
+                y = ny - (y + 1);
+            }
+
+            // Pick a hue for every pixel.
+            let h = (i / (nx*ny) * 10) + (ts/2);
+            h = h % 1;
+
+            // Convert to RGB.
+            let c = this.hsv2rgb(h, 1, 1);
+
+            // Poke.
+            this.leds[x][y] = c;
+        }
+    }
+}
+
+// Game of life on a torus, with random state. If cycles or stalls are
+// detected, the simulation is restarted.
+// By q3k.
+class Life extends Animation {
+    draw(ts) {
+        // Generate state if needed.
+        if (this.state === undefined) {
+            this.generateState();
+        }
+
+        // Step simulation every so often.
+        if (this.nextStep === undefined || this.nextStep < ts) {
+            if (this.nextStep !== undefined) {
+                this.step();
+                this.recordState();
+            }
+            // 10 steps per second.
+            this.nextStep = ts + 1.0/10;
+        }
+
+        if (this.shouldRestart(ts)) {
+            this.generateState();
+        }
+
+        // Render state into LED matrix.
+        for (let x = 0; x  < this.nx; x++) {
+            for (let y = 0; y < this.ny; y++) {
+                // Turn on and decay smoothly.
+                let [r, g, b] = this.leds[x][y];
+                if (this.state[x][y]) {
+                    r += 0.5;
+                    g += 0.5;
+                    b += 0.5;
+                } else {
+                    r -= 0.05;
+                    g -= 0.05;
+                    b -= 0.05;
+                }
+                r = Math.min(Math.max(r, 0.0), 1.0);
+                g = Math.min(Math.max(g, 0.0), 1.0);
+                b = Math.min(Math.max(b, 0.0), 1.0);
+                this.leds[x][y] = [r, g, b];
+            }
+        }
+    }
+
+    // recordState records the current state of the simulation within a
+    // 3-element FIFO. This data is used to detect 'stuck' simulations. Any
+    // time there is something repeating within the 3-element FIFO, it means
+    // we're in some boring loop or terminating step, and shouldRestart will
+    // then schedule a simulation restart.
+    recordState() {
+        if (this.recorded === undefined) {
+            this.recorded = [];
+        }
+        // Serialize state into string of 1 and 0.
+        const serialized = this.state.map((column) => { 
+            return column.map((value) => value ? "1" : "0").join("");
+        }).join("");
+        this.recorded.push(serialized);
+
+        // Ensure there's not more then 3 recorded state;
+        while (this.recorded.length > 3) {
+            this.recorded.shift();
+        }
+    }
+
+    // shouldRestart looks at the recorded state of simulation frames, and
+    // ensures that there isn't anything repeated within the recorded data. If
+    // so, it schedules a restart of the simulation in 5 seconds.
+    shouldRestart(ts) {
+        // Nothing to do if we have no recorded data.
+        if (this.recorded === undefined) {
+            return false;
+        }
+
+        // If we have a deadline for restarting set already, just obey that and
+        // return true when it expires.
+        if (this.restartDeadline !== undefined) {
+            if (this.restartDeadline < ts) {
+                this.restartDeadline = undefined;
+                return true;
+            }
+            return false;
+        }
+
+        // Otherwise, look for repeat data in the recorded history. If anything
+        // is recorded, schedule a restart deadline in 5 seconds.
+        let s = new Set();
+
+        let restart = false;
+        for (let key of this.recorded) {
+            if (s.has(key)) {
+                restart = true;
+                break;
+            }
+            s.add(key);
+        }
+        if (restart) {
+            console.log("shouldRestart detected restart condition, scheduling restart...");
+            this.restartDeadline = ts + 2;
+        }
+    }
+
+    // generateState builds the initial randomized state of the simulation.
+    generateState() {
+        this.state = new Array();
+        for (let x = 0; x < this.nx; x++) {
+            this.state.push(new Array());
+            for (let y = 0; y < this.ny; y++) {
+                this.state[x][y] = Math.random() > 0.5;
+            }
+        }
+        this.recorded = [];
+    }
+
+    // step runs a simulation step for the game of life board.
+    step() {
+        let next = new Array();
+        for (let x = 0; x < this.nx; x++) {
+            next.push(new Array());
+            for (let y = 0; y < this.ny; y++) {
+                next[x][y] = this.nextFor(x, y);
+            }
+        }
+        this.state = next;
+    }
+
+    // nextFor runs a simulation step for a game of life cell at given
+    // coordinates.
+    nextFor(x, y) {
+        let current = this.state[x][y];
+        // Build coordinates of neighbors, wrapped around (effectively a
+        // torus).
+        let neighbors = [
+            [x-1, y-1], [x, y-1], [x+1, y-1],
+            [x-1, y  ],           [x+1, y  ],
+            [x-1, y+1], [x, y+1], [x+1, y+1],
+        ].map(([x, y]) => {
+            x = x % this.nx;
+            y = y % this.ny;
+            if (x < 0) {
+                x += this.nx;
+            }
+            if (y < 0) {
+                y += this.ny;
+            }
+            return [x, y];
+        });
+        // Count number of live and dead neighbours.
+        const live = neighbors.filter(([x, y]) => { return this.state[x][y]; }).length;
+
+        if (current) {
+            if (live < 2 || live > 3) {
+                current = false;
+            }
+        } else {
+            if (live == 3) {
+                current = true;
+            }
+        }
+
+        return current;
+    }
+}
+
+// Add your animations here:
+export const animations = [
+    Life,
+    SnakeChase,
+];
+
diff --git a/hswaw/site/static/frezarka.jpg b/hswaw/site/static/frezarka.jpg
new file mode 100644
index 0000000..56210e9
--- /dev/null
+++ b/hswaw/site/static/frezarka.jpg
Binary files differ
diff --git a/hswaw/site/static/kuka.jpg b/hswaw/site/static/kuka.jpg
new file mode 100644
index 0000000..78f57f5
--- /dev/null
+++ b/hswaw/site/static/kuka.jpg
Binary files differ
diff --git a/hswaw/site/static/landing.css b/hswaw/site/static/landing.css
index 431d8ea..cf13754 100644
--- a/hswaw/site/static/landing.css
+++ b/hswaw/site/static/landing.css
@@ -1,149 +1,396 @@
+:root {
+  /* --primary: #7347d9ff; */
+  --primary100: #cfbff1;
+  --secondary: #d947adff;
+  --secondary50: #fae2f0;
+  --darkbgaccent: #1a1622ff;
+  --darkbg: #121212ff;
+  --darkbgalpha: #121212f8;
+}
+
+html {
+  min-height: 100%;
+}
+
 body {
-    margin: 0;
-    padding: 0;
-    background-color: #444;
-    color: #fffdf3;
-    font-weight: 100;
-    font-family: 'Lato', sans-serif;
+  min-height: 100%;
+
+  margin: 0;
+  padding: 0;
+  color: #fffdf3;
+  font-weight: 400;
+  font-family: "Inconsolata", monospace;
+  font-size: 20px;
+  line-height: 120%;
+
+  background-color: var(--darkbgaccent);
+}
+
+@media screen and (max-width: 1000px) {
+  body {
     font-size: 18px;
+  }
+}
+
+#ledsFloater {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  min-height: 100%;
+  overflow-x: hidden;
+  z-index: -11;
+}
+
+#ledsWrapper {
+  float: left; /* oh god */
+  width: 100%;
+  min-height: 100%;
+}
+
+#leds {
+  width: max(60vw, 600px);
+  height: max(60vw, 600px);
+  transform: rotate(-15deg);
+  position: relative;
+  top: min(-10vw, -100px);
+  left: min(-10vw, -100px);
+  z-index: -10;
 }
 
 #page {
-    max-width: 75rem;
-    margin: auto;
-    padding-top: 2rem;
-    padding: 1rem;
+  max-width: 60rem;
+  margin: 6em auto 2em auto;
+  background-color: var(--darkbgalpha);
 
-    display: flex;
-    flex-direction: column;
+  display: flex;
+  flex-direction: column;
 }
 
-.top {
-    display: flex;
-    flex-direction: row;
-    margin-bottom: 1rem;
+@media screen and (max-width: 1000px) {
+  #page {
+    background-color: #121212f0;
+  }
 }
 
-.top .logo {
-    display: flex;
-    flex-direction: row;
-    flex-grow: 1;
-    justify-content: right;
+.about img {
+  width: 100%;
+  display: block;
+  margin: 0 auto;
 }
 
-.top .logo img {
-    margin-top: 2rem;
-    max-height: 25rem
+#top {
+  display: flex;
+  flex-direction: row;
+  flex-flow: row wrap;
+  height: 10em;
+  margin: 2em 0 1em 0;
+  justify-content: center;
 }
 
-.top .mapcontainer {
-    flex-grow: 1;
-    min-width: 35%;
+@media screen and (max-width: 1000px) {
+  #top {
+    margin: 1em 0 1em 0;
+  }
+}
+
+#logo,
+#logo > img {
+  height: 100%;
+}
+
+@media screen and (max-width: 600px) {
+  #top {
+    height: 5em;
+  }
+
+  #top .type {
+    font-size: 18px;
+  }
+}
+
+#top .type {
+  max-width: 15em;
+  font-size: min(35px, 2.5vw);
+  margin-left: 1em;
+  line-height: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+#top .type h1 {
+  padding: 0 0 0.5em 0.2em;
+  font-family: "Comfortaa", sans-serif;
+  color: #fff;
+  text-shadow: 0 0 1px var(--secondary), 0 0 5px var(--secondary),
+    0 0 20px var(--secondary);
+  font-size: 60px;
+  animation: neon ease-in-out 4s infinite alternate, blink 5s infinite;
+}
+
+@keyframes neon {
+  from {
+    text-shadow: 0 0 1px var(--secondary), 0 0 3px var(--secondary),
+      0 0 15px var(--secondary);
+  }
+  to {
+    text-shadow: 0 0 1px var(--secondary), 0 0 15px var(--secondary),
+      0 0 40px var(--secondary);
+  }
+}
+@keyframes blink {
+  0% {
+    opacity: 1;
+  }
+  80% {
+    opacity: 1;
+  }
+  81% {
+    opacity: 0.4;
+  }
+  82.7% {
+    opacity: 0.4;
+  }
+  83% {
+    opacity: 1;
+  }
+  94% {
+    opacity: 1;
+  }
+  95% {
+    opacity: 0.4;
+  }
+  95.7% {
+    opacity: 0.4;
+  }
+  96% {
+    opacity: 1;
+  }
+}
+
+@media screen and (max-width: 1000px) {
+  #top .type h1 {
+    font-size: 40px;
+  }
+}
+
+@media screen and (max-width: 600px) {
+  #top .type h1 {
+    font-size: 24px;
+  }
 }
 
 #map {
-    margin-bottom: 1rem;
-    height: 16rem;
-}
-
-
-.logo h1 {
-    display: block;
-    max-width: 20rem;
-    text-align: center;
-    font-size: 52px;
-    margin-right: 8rem;
-    padding-top: 5rem;
-}
-
-.covid {
-    padding: 1rem 2rem;
-    background-color: #9f0000;
-}
-
-.covid span {
-    font-size: 20px;
-    font-style: italic;
-}
-
-.bottom {
-    display: flex;
-    flex-direction: row;
-}
-
-.bottom .about {
-    padding: 1rem 1rem 1rem 2rem;
-}
-
-.bottom .blog {
-    padding: 1rem 2rem 1rem 1rem;
-    flex-grow: 1;
-    min-width: 40%;
-}
-
-p {
-    line-height: 150%;
-    text-align: justify;
-}
-
-h1 {
-    font-size: 30px;
-}
-h2 {
-    font-size: 20px;
-}
-
-h1, h2, h3, h4 {
-    font-family: 'Allerta', sans-serif;
-}
-
-pre {
-    background-color: #141000;
-    padding: 1rem;
-}
-
-a {
-    color: #fffdf3;
-}
-
-b {
-    font-weight: 800;
-}
-
-ul {
-    padding: 0 0 0 1em;
-}
-
-li {
-    list-style: none;
-}
-
-li i {
-    font-size: 0.8em;
-}
-
-#background-logo {
-    position: absolute;
-    width: 100%;
-    height: 100%;
-    z-index: -10;
-}
-
-#background-logo img {
-    opacity: 3%;
-    margin-top: 2%;
-    margin-left: 5%;
-}
-
-#footer {
-    margin-top: 2rem;
-    font-size: 0.8rem;
-    opacity: 60%;
+  height: 28em;
 }
 
 #quicklinks {
-    float: right;
-    font-family: monospace;
+  font-size: 16px;
+  background-color: rgba(255, 255, 255, 0.05);
+}
+
+@media screen and (max-width: 1000px) {
+  #quicklinks {
     font-size: 14px;
-    margin: 2rem;
+  }
+}
+
+#quicklinks ul {
+  padding: 0;
+  margin: 0;
+  display: flex;
+  flex-direction: row;
+  flex-flow: row wrap;
+  justify-content: center;
+  font-family: "Comfortaa", sans-serif;
+}
+
+#quicklinks ul li {
+  display: flex;
+}
+
+#quicklinks ul li.left {
+  flex-grow: 1;
+  font-style: italic;
+}
+
+#quicklinks a {
+  text-decoration: none;
+  padding: 0.8rem 1rem;
+  color: #fff;
+}
+#quicklinks a:hover {
+  color: #fff;
+}
+
+#quicklinks li:not(.left) a:hover {
+  background-color: rgba(255, 255, 255, 0.05);
+}
+
+.img-wrapper {
+  position: relative;
+  height: 0;
+  padding-bottom: 60%;
+}
+
+.img-wrapper > img {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  object-fit: contain;
+}
+
+#gallery-nav {
+  display: flex;
+  flex-direction: row;
+  padding: 0;
+  margin: 0;
+  justify-content: center;
+  flex-wrap: wrap;
+}
+#gallery-nav > li {
+  margin: 0;
+  padding: 0;
+}
+#gallery-nav > li:hover {
+  background-color: var(--darkbgaccent);
+}
+#gallery-nav > li.active {
+  background-color: var(--secondary);
+}
+#gallery-nav > li > a {
+  display: block;
+  text-decoration: none;
+  padding: 0 15px;
+  height: 32px;
+  padding-top: 7px;
+}
+
+.bottom {
+  display: flex;
+  flex-direction: column;
+  padding: 1em 1em 0 1em;
+}
+
+@media screen and (max-width: 1000px) {
+  .bottom {
+    padding: 2em 1em 0 1em;
+  }
+}
+
+.bottom .about {
+  padding: 1rem 2em 3rem 2em;
+}
+
+@media screen and (max-width: 1000px) {
+  .bottom .about {
+    padding: 0rem 0em 1rem 0em;
+  }
+}
+
+.bottom .about li + li {
+  margin-top: 0.5em;
+}
+
+p {
+  text-align: left;
+  color: #eee;
+}
+
+h2 {
+  margin-bottom: 0;
+  font-size: 26px;
+  display: inline-block;
+  font-family: "Comfortaa", sans-serif;
+  line-height: 1.1;
+}
+
+h2:after {
+  content: " ";
+  display: block;
+  background-color: var(--secondary);
+  height: 0.15em;
+  width: 100%;
+  margin-top: 0.1em;
+  margin-left: 0.3em;
+}
+
+@media screen and (max-width: 1000px) {
+  h2 {
+    margin: 0;
+  }
+}
+
+* + h2 {
+  margin: 2rem 0 0 0;
+}
+
+h2 + * {
+  margin: 1rem 0 0 0;
+}
+
+h3 {
+  font-size: 20px;
+}
+
+pre {
+  background-color: #141000;
+  padding: 1rem;
+}
+
+a {
+  text-decoration: underline;
+  text-decoration-color: var(--primary100);
+  color: #fff;
+}
+
+a:hover {
+  color: var(--primary100);
+}
+
+b {
+  font-weight: 800;
+}
+
+ul {
+  padding: 0 0 0 1em;
+}
+
+@media screen and (max-width: 1000px) {
+  ul {
+    padding: 0;
+  }
+}
+
+li {
+  list-style: none;
+}
+
+li i {
+  font-size: 0.9em;
+}
+
+#footer {
+  margin: 1rem 0 0 0;
+  font-size: 0.8rem;
+  opacity: 60%;
+}
+
+.atlist {
+  display: inline;
+  list-style: none;
+}
+
+.atlist li {
+  display: inline;
+}
+
+.atlist li:after {
+  content: ", ";
+}
+
+.atlist li:last-child:after {
+  content: ".";
 }
diff --git a/hswaw/site/static/led.js b/hswaw/site/static/led.js
new file mode 100644
index 0000000..61580f0
--- /dev/null
+++ b/hswaw/site/static/led.js
@@ -0,0 +1,72 @@
+import { animations } from "./animations.js";
+
+class CanvasRenderer {
+    static WIDTH = 1024;
+    static HEIGHT = 1024;
+
+    constructor() {
+        const ledDiv = document.querySelector("#leds");
+        let canvas = document.createElement("canvas");
+        canvas.style.width = "100%";
+        canvas.style.height = "100%";
+        canvas.width = CanvasRenderer.WIDTH;
+        canvas.height = CanvasRenderer.HEIGHT;
+        ledDiv.appendChild(canvas);
+        ledDiv.style.backgroundColor = "#00000000";
+        let context = canvas.getContext('2d');
+
+        this.canvas = canvas;
+        this.context = context;
+    }
+
+    render(animation) {
+        const canvas = this.canvas;
+        const context = this.context;
+        const leds = animation.leds;
+        const nx = animation.nx;
+        const ny = animation.ny;
+
+        const xoff = CanvasRenderer.WIDTH / (nx + 1);
+        const yoff = CanvasRenderer.HEIGHT / (ny + 1);
+        const d = xoff * 0.7;
+
+        context.clearRect(0, 0, canvas.width, canvas.height);
+        for (let x = 0; x < nx; x++) {
+            for (let y = 0; y < ny; y++) {
+                const cx = (x + 1) * xoff
+                const cy = (y + 1) * yoff
+
+                const rgb = leds[x][y];
+                const r = Math.max(rgb[0] * 256, 0x1a);
+                const g = Math.max(rgb[1] * 256, 0x16);
+                const b = Math.max(rgb[2] * 256, 0x22);
+                const color = `rgba(${r}, ${g}, ${b})`;
+
+                context.beginPath();
+                context.arc(cx, cy, d/2, 0, 2 * Math.PI, false);
+                context.fillStyle = color;
+                context.fill();
+            }
+        }
+    }
+}
+
+window.addEventListener("load", () => {
+    const animationClass = animations[Math.floor(Math.random() * animations.length)];
+    console.log(`Picked LED animation: ${animationClass.name}`);
+
+    let renderer = new CanvasRenderer();
+    let animation = new animationClass(16, 16);
+
+    let step = (hrts) => {
+        // Run animation logic.
+        animation.draw(hrts / 1000);
+
+        // Draw LEDs.
+        renderer.render(animation);
+
+        // Schedule next frame.
+        window.requestAnimationFrame(step);
+    }
+    window.requestAnimationFrame(step);
+});
diff --git a/hswaw/site/static/neon-syrenka.svg b/hswaw/site/static/neon-syrenka.svg
new file mode 100644
index 0000000..72fd114
--- /dev/null
+++ b/hswaw/site/static/neon-syrenka.svg
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   width="210mm"
+   height="297mm"
+   viewBox="0 0 210 297"
+   version="1.1"
+   id="svg4661">
+  <defs
+     id="defs4655" />
+  <metadata
+     id="metadata4658">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1">
+    <path
+       d="m 192.769,218.96976 c -5.05369,5.54002 -13.21138,7.72603 -15.07391,11.71992 -8.93948,19.18306 -14.72499,27.8945 -26.11805,36.04284 -9.64327,6.90351 -22.29975,14.53301 -41.53166,16.23734 -15.446242,1.37282 -25.325723,-0.57701 -39.714462,-6.04376 -6.480056,-2.46405 -22.834899,-13.20676 -26.884639,-18.57343 -5.571452,-7.38519 -10.615892,-12.18879 -13.206788,-21.36792 -2.821194,-10.00279 -5.421339,-19.68678 -5.638876,-30.29335 -0.109881,-5.44001 0.0998,-10.1715 0.66777,-16.12451 0.872511,-2.20812 2.29533,-4.31735 4.19867,-5.7844 0.191249,-0.14535 0.05911,-0.0685 0.178383,-1.75434 0.158215,-2.25464 0.853873,-2.82237 0.972586,-3.40408 0.172123,-0.84577 -0.391016,-9.71658 0.486293,-13.61509 1.831152,-8.12622 3.93113,-13.91058 5.712245,-21.07235 0.09528,-0.38629 0.145349,-0.67361 0.122052,-0.80975 -5.28065,-6.14384 -7.902911,-4.01716 -8.389169,-20.69545 l 0.77713,-0.83183 c 2.681617,-1.21338 9.757291,-4.34405 12.556369,-6.07518 m 35.328539,27.98405 -2.649,2.08593 c -6.162515,1.59968 -13.342888,-1.74971 -15.074,-8.26583 m -27.425629,49.82076 c 2.708323,4.40811 4.644244,8.17164 7.534114,12.40171 1.231988,1.79975 5.603999,8.5125 8.026109,8.5125 1.44031,0 2.767714,-0.0546 3.953211,-0.33625 1.809072,-0.42815 12.079396,-9.49789 13.710468,-18.87828 0.618949,-3.80424 3.563481,-8.6486 7.294461,-9.96554 0.994701,-0.35482 2.958476,-1.06331 4.135802,-0.48629 l 3.499465,0.73523 m 76.205196,48.32704 c -1.82187,5.47137 -8.10181,8.90686 -14.56907,11.03351 -7.91682,2.60365 -15.7103,4.04393 -23.82608,1.39493 -3.27727,-1.07266 -6.44052,-2.65836 -9.47581,-4.54421 -6.21245,-3.85894 -11.311553,-9.56187 -14.142066,-16.31064 -0.817917,-1.94869 -2.240702,-7.512 -3.222607,-10.95211 M 44.719333,176.46544 c 0.78638,-2.26282 1.876531,-3.3715 0.04138,-5.40739 m 51.420433,-57.08382 c -1.272777,-0.58634 -10.870671,-4.88507 -11.429116,-5.34808 -2.576883,-2.15461 -1.136642,-7.1397 -0.240626,-9.725895 m 49.661436,14.765665 c 4.04042,-1.80442 10.71127,-3.94969 16.32459,-7.8761 7.04778,-4.9351 12.84724,-12.065468 14.34221,-17.832369 m -1.45891,-11.669905 c -0.93653,1.526348 -7.12105,4.811733 -8.99758,5.588887 -10.62518,4.404589 -21.3819,-0.303737 -29.42199,-7.293245 -5.79368,-5.039805 -9.56535,-10.329718 -15.56029,-15.805751 -5.13986,-4.689616 -12.623864,-6.98497 -19.454113,-7.294434 -1.682257,-0.07685 -4.789698,-0.245389 -7.90757,0.137177 -3.144578,0.381583 -6.303203,1.316937 -8.021554,2.012633 -2.262748,0.917941 -4.871065,2.262811 -5.95301,2.958511 -1.917214,1.222721 -4.212579,3.871729 -5.693642,5.590072 l -2.195255,2.975921 -0.490953,3.313318 c -1.108719,2.358181 -1.408875,7.607397 -1.699712,10.456516 -0.277137,2.703688 0.05911,6.117056 0.927242,8.720721 1.458879,4.385958 5.757554,10.70662 9.307092,13.433628 2.117295,-0.44561 3.576243,-0.91793 5.657514,-1.612455 1.027283,-0.34554 1.64964,-0.51424 3.808905,-1.37745 m 12.606442,92.198225 c 2.267442,0 4.103289,1.83582 4.103289,4.10326 0,2.26744 -1.835847,4.10443 -4.103289,4.10443 -2.268625,0 -4.104402,-1.83699 -4.104402,-4.10443 0,-2.26744 1.835777,-4.10326 4.104402,-4.10326 z m 24.148394,-13.25679 c 2.26856,0 4.1044,1.83703 4.1044,4.10444 0,2.26278 -1.83584,4.10325 -4.1044,4.10325 -2.26744,0 -4.10329,-1.84047 -4.10329,-4.10325 0,-2.26741 1.83585,-4.10444 4.10329,-4.10444 z m 24.14954,-13.26023 c 2.26397,0 4.10444,1.84047 4.10444,4.10325 0,2.26863 -1.84047,4.10444 -4.10444,4.10444 -2.26748,0 -4.10325,-1.83581 -4.10325,-4.10444 0,-2.26278 1.83577,-4.10325 4.10325,-4.10325 z m -0.15821,-27.69901 c 2.26744,0 4.10329,1.83581 4.10329,4.10444 0,2.26274 -1.83585,4.10322 -4.10329,4.10322 -2.26393,0 -4.1044,-1.84048 -4.1044,-4.10322 0,-2.26863 1.84047,-4.10444 4.1044,-4.10444 z m -23.5178,-13.73258 c 2.26744,0 4.10322,1.83581 4.10322,4.10322 0,2.26747 -1.83578,4.10325 -4.10322,4.10325 -2.26393,0 -4.1044,-1.83578 -4.1044,-4.10325 0,-2.26741 1.84047,-4.10322 4.1044,-4.10322 z m -5.25853,17.64618 c -1.71251,0.20829 -2.79908,0.35837 -4.76289,2.68044 l -5.21194,9.0302 c -0.427076,0.87251 -0.481703,2.91313 -0.222544,3.76704 1.366974,2.24532 6.602214,12.12481 9.093034,12.09337 1.4682,-0.0209 12.22481,-0.0685 13.09277,-0.45489 1.34952,-0.60379 8.1122,-10.63797 8.0936,-12.32837 -0.0209,-1.81835 -2.02657,-5.71683 -3.80891,-8.60785 -1.24947,-2.02198 -2.24883,-4.13935 -2.91657,-5.23993 -0.43173,-0.71777 -1.18199,-0.94929 -1.73229,-0.96327 -2.93056,-0.0814 -10.87419,-0.0675 -11.62458,0.0243 z m -35.051614,0 c 4.435992,7.68882 5.994911,10.3832 7.10829,12.31091 0.849282,1.47637 0.653792,1.69509 0.253491,2.50828 -1.030795,1.78581 -4.707113,8.153 -6.907062,11.96541 l 0.0313,1.6276 c 1.258763,2.60831 3.494771,6.05308 6.648703,11.5198 0.545649,0.94936 0.354957,2.38146 0.118922,2.64441 -0.500202,0.55956 -4.798982,8.36239 -5.26316,9.12092 -1.444934,2.35003 -1.921908,3.06321 -1.408875,4.45811 l 6.922153,11.98401 c 0.0033,0.0104 0.394598,0.0769 1.485688,0.95864 4.762888,-0.34077 9.552551,-0.35485 14.2968,-0.7178 l 1.21342,-1.21808 c 1.967284,-2.65832 4.880384,-7.76674 6.367224,-10.339 0.44457,-0.21768 1.58103,-1.54032 2.39419,-1.68108 4.41741,-0.7725 12.76587,0.4863 15.19734,-1.21808 0.79457,-0.55841 1.44026,-1.48099 1.98588,-2.49895 1.34028,-2.50824 2.55366,-5.07703 3.89851,-7.58061 0.89111,-1.65899 1.07729,-2.52688 4.58145,-2.44426 3.07127,0.0769 6.15309,0.2222 9.15232,0.20342 1.92194,-0.0139 3.43548,-0.33625 4.42663,-1.64505 1.97195,-2.59903 4.50815,-6.59407 5.65759,-9.48394 1.92187,-3.29932 0.69103,-5.59468 -1.14131,-8.13902 -1.84513,-2.55828 -3.25282,-4.62679 -4.63958,-7.47128 -0.70383,-1.4449 -0.5131,-2.00449 1.24138,-4.52671 0.39411,-0.56307 5.27126,-7.54803 5.33872,-9.24308 l -0.33173,-1.56824 -6.47074,-12.40632 c -0.58637,-1.01797 -0.35033,-0.89466 -1.52289,-1.09012 -4.53485,-0.75039 -9.07089,-0.48633 -13.85592,-0.51769 -1.6543,-0.0104 -1.5496,0.19055 -2.47215,-1.29604 -1.49497,-2.4082 -3.74493,-7.37586 -5.27596,-10.04349 -1.06334,-1.84511 -0.82713,-1.73576 -2.70836,-1.74855 -3.895,-0.0264 -12.71576,-0.1593 -14.48763,0.003 l -6.858164,10.59841 c -0.636371,0.71315 -1.2355,2.68509 -4.121858,2.92593 -0.553751,0.0452 -4.276595,0.0139 -7.025705,0.064 -1.42275,0.0278 -2.73628,0 -4.143973,0.0243 -0.818996,0.0139 -1.818356,0.33625 -2.281386,1.13546 -1.772978,3.07249 -4.981572,8.62997 -6.417223,11.11613 z M 64.901407,97.927532 c 0,1.782323 -0.231237,4.598898 -1.944024,5.590078 l -2.672298,0.73178 -1.458843,-0.24539 c -2.913133,-0.22689 -3.640225,0.61777 -5.594715,-0.48627 -0.922548,-0.52236 -13.405722,-18.973638 -14.587707,-21.882086 -1.177326,-2.889848 -0.05459,-4.971143 0.727092,-7.538731 0.927243,-3.039934 4.436027,-17.460086 6.812829,-27.384931 0.427075,-1.663637 1.535656,-3.185363 3.266733,-4.486009 0.418834,-0.25363 0.800427,-0.544467 1.149438,-0.86323 l 55.934418,-7.962201 c 2.07192,3.563419 5.99373,5.894853 10.39714,5.730825 3.29935,-0.117392 10.9068,-1.926603 12.92409,-4.231228 l 0.17664,-2.536199 -0.15821,-1.581034 -2.30931,0.34077 -3.54481,0.504931 -1.06334,-1.441457 -3.17361,-7.056271 1.41816,-3.380856 0.70849,-1.694984 3.54484,-0.504861 2.14066,-0.277137 -0.1676,-1.595012 -0.81907,-2.109228 c -1.95797,-1.330845 -4.34405,-2.071916 -6.88839,-1.9765 -5.48535,0.199941 -14.611,4.508074 -15.5871,9.680512 l -53.839134,7.660894 c -0.586368,-0.739923 -1.354144,-1.349519 -2.217405,-1.848608 -1.494972,-0.864443 -4.780309,-0.595687 -8.611365,0.508408 -1.354178,0.390912 -2.004493,1.945171 -2.426839,3.227232 l -18.33729,2.612976 c -1.809038,0.253491 -3.080632,1.948649 -2.822377,3.757703 l 1.237726,5.575333 c 0.25975,1.809068 1.95456,3.081817 3.763597,2.827032 L 38.488804,43.08552 c 0.168647,0.449069 0.218371,0.880672 0.02086,1.290191 -6.198538,12.815848 -10.143543,20.518612 -14.09206,35.878799 l 0.477043,1.976595 0.622392,2.562939 c 1.849756,3.468048 3.208594,7.058246 4.61747,10.698473 2.422145,6.265993 5.135196,12.201573 8.025066,18.236063 l 0.727057,4.13585 m 158.866028,82.85042 c -0.99115,6.30324 -20.78157,13.10205 -26.45303,12.12481 -5.66798,-0.97259 -16.56546,-16.02913 -15.56957,-22.34979"
+       style="fill:none;stroke:#d947ad;stroke-width:10.4317;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path899" />
+    <path
+       d="m 192.76932,218.96976 c -5.05373,5.54002 -13.21142,7.72603 -15.07395,11.71992 -8.93948,19.18306 -14.72503,27.8945 -26.11805,36.04284 -9.6433,6.90351 -22.29978,14.53301 -41.5317,16.23734 -15.446239,1.37282 -25.32572,-0.57701 -39.714424,-6.04376 -6.480091,-2.46405 -22.834924,-13.20676 -26.88467,-18.57343 -5.571438,-7.38519 -10.615865,-12.18879 -13.206774,-21.36792 -2.821191,-10.00279 -5.421322,-19.68678 -5.638873,-30.29335 -0.109741,-5.44001 0.09994,-10.1715 0.667753,-16.12451 0.872532,-2.20812 2.295362,-4.31735 4.198684,-5.7844 0.19111,-0.14535 0.05925,-0.0685 0.178244,-1.75434 0.158145,-2.25464 0.853883,-2.82237 0.972575,-3.40408 0.172054,-0.84577 -0.391009,-9.71658 0.486314,-13.61509 1.831149,-8.12622 3.931106,-13.91058 5.712231,-21.07235 0.09531,-0.38629 0.14528,-0.67361 0.122087,-0.80975 -5.280629,-6.14384 -7.902911,-4.01716 -8.389172,-20.69545 l 0.777122,-0.83183 c 2.681631,-1.21338 9.757295,-4.34405 12.55638,-6.07518 m 35.32856,27.98405 -2.649035,2.08593 c -6.16248,1.59968 -13.342888,-1.74971 -15.073979,-8.26583 m -27.425636,49.82076 c 2.708316,4.40811 4.644244,8.17164 7.534107,12.40171 1.232012,1.79975 5.603995,8.5125 8.026137,8.5125 1.440293,0 2.767714,-0.0546 3.953208,-0.33625 1.809047,-0.42815 12.079399,-9.49789 13.710436,-18.87828 0.618984,-3.80424 3.563516,-8.6486 7.294462,-9.96554 0.994735,-0.35482 2.958475,-1.06331 4.135836,-0.48629 l 3.499465,0.73523 m 76.205202,48.32704 c -1.82187,5.47137 -8.10181,8.90686 -14.56907,11.03351 -7.91682,2.60365 -15.71034,4.04393 -23.82609,1.39493 -3.27727,-1.07266 -6.44052,-2.65836 -9.47581,-4.54421 -6.21248,-3.85894 -11.311549,-9.56187 -14.142062,-16.31064 -0.817918,-1.94869 -2.240702,-7.512 -3.222607,-10.95211 M 44.719483,176.46544 c 0.786397,-2.26282 1.876534,-3.3715 0.04121,-5.40739 M 96.181151,113.9743 c -1.272777,-0.58634 -10.870671,-4.88507 -11.429116,-5.34808 -2.576883,-2.15461 -1.136642,-7.1397 -0.240626,-9.725895 m 49.661431,14.765665 c 4.04042,-1.80442 10.71127,-3.94969 16.32459,-7.8761 7.04778,-4.9351 12.84724,-12.065469 14.34218,-17.83237 m -1.45887,-11.669904 c -0.93654,1.526347 -7.12105,4.811733 -8.99758,5.588887 -10.62522,4.404589 -21.3819,-0.303738 -29.42202,-7.293245 -5.79365,-5.039806 -9.56532,-10.329719 -15.5603,-15.805752 -5.13982,-4.689615 -12.623863,-6.984969 -19.454113,-7.294434 -1.682257,-0.07685 -4.789663,-0.245389 -7.907535,0.137178 -3.144613,0.381582 -6.303238,1.316937 -8.021554,2.012633 -2.262748,0.917941 -4.871065,2.262811 -5.95301,2.95851 -1.917249,1.222722 -4.212614,3.871729 -5.693642,5.590073 l -2.195266,2.975921 -0.490948,3.313318 c -1.108734,2.358181 -1.408869,7.607396 -1.69973,10.456516 -0.277172,2.703687 0.05925,6.117056 0.927242,8.72072 1.458889,4.385958 5.757578,10.706621 9.307081,13.433629 2.11733,-0.44561 3.576278,-0.91793 5.657549,-1.612455 1.027283,-0.34554 1.64964,-0.51424 3.808905,-1.37745 m 12.606442,92.198225 c 2.267442,0 4.103289,1.83581 4.103289,4.10326 0,2.26744 -1.835847,4.10443 -4.103289,4.10443 -2.268625,0 -4.104402,-1.83699 -4.104402,-4.10443 0,-2.26745 1.835777,-4.10326 4.104402,-4.10326 z M 114.44066,177.0331 c 2.26856,0 4.1044,1.83703 4.1044,4.10444 0,2.26278 -1.83584,4.10325 -4.1044,4.10325 -2.26744,0 -4.10329,-1.84047 -4.10329,-4.10325 0,-2.26741 1.83585,-4.10444 4.10329,-4.10444 z m 24.14953,-13.26023 c 2.26393,0 4.1044,1.84047 4.1044,4.10325 0,2.26863 -1.84047,4.10444 -4.1044,4.10444 -2.26748,0 -4.10329,-1.83581 -4.10329,-4.10444 0,-2.26278 1.83582,-4.10325 4.10329,-4.10325 z m -0.15821,-27.69901 c 2.26744,0 4.10329,1.83581 4.10329,4.10444 0,2.26274 -1.83585,4.10321 -4.10329,4.10321 -2.26393,0 -4.1044,-1.84047 -4.1044,-4.10321 0,-2.26863 1.84047,-4.10444 4.1044,-4.10444 z m -23.51779,-13.73258 c 2.26741,0 4.10321,1.83581 4.10321,4.10322 0,2.26747 -1.8358,4.10325 -4.10321,4.10325 -2.26396,0 -4.10444,-1.83578 -4.10444,-4.10325 0,-2.26741 1.84048,-4.10322 4.10444,-4.10322 z m -5.25854,17.64618 c -1.7125,0.20829 -2.79912,0.35837 -4.76289,2.68044 l -5.21197,9.0302 c -0.427075,0.87251 -0.481668,2.91313 -0.222544,3.76704 1.366984,2.24532 6.602254,12.12481 9.093034,12.09337 1.46823,-0.0209 12.22485,-0.0685 13.09277,-0.45489 1.34952,-0.60379 8.11223,-10.63798 8.09363,-12.32837 -0.0209,-1.81835 -2.02661,-5.71683 -3.8089,-8.60785 -1.24947,-2.02198 -2.24883,-4.13935 -2.9166,-5.23993 -0.43171,-0.71778 -1.18199,-0.94929 -1.7323,-0.96327 -2.93055,-0.0814 -10.87415,-0.0675 -11.62454,0.0243 z m -35.051609,0 c 4.435957,7.68882 5.994911,10.3832 7.10829,12.31091 0.849282,1.47637 0.653792,1.69509 0.253491,2.50828 -1.030795,1.78581 -4.707113,8.15299 -6.907062,11.96541 l 0.0313,1.6276 c 1.258763,2.60831 3.494771,6.05308 6.648703,11.5198 0.545649,0.94936 0.354922,2.38146 0.118922,2.64441 -0.500237,0.55956 -4.799017,8.36239 -5.26316,9.12092 -1.444934,2.35003 -1.921943,3.06321 -1.408875,4.45811 l 6.922153,11.98401 c 0.0033,0.0104 0.394598,0.0769 1.485653,0.95864 4.762888,-0.34077 9.552586,-0.35485 14.296835,-0.7178 l 1.213385,-1.21808 c 1.967324,-2.65832 4.880424,-7.76674 6.367224,-10.339 0.4446,-0.21768 1.58103,-1.54032 2.39422,-1.68108 4.41742,-0.7725 12.76584,0.4863 15.1973,-1.21808 0.79459,-0.55841 1.44031,-1.48099 1.98593,-2.49895 1.34023,-2.50825 2.55365,-5.07703 3.89848,-7.58061 0.89114,-1.65899 1.07732,-2.52688 4.58144,-2.44426 3.07131,0.0768 6.15313,0.2222 9.15236,0.20342 1.9219,-0.0139 3.43544,-0.33625 4.42663,-1.64505 1.97195,-2.59903 4.50815,-6.59407 5.65759,-9.48394 1.92187,-3.29932 0.69103,-5.59468 -1.14131,-8.13902 -1.84516,-2.55828 -3.25285,-4.62679 -4.63961,-7.47128 -0.7038,-1.4449 -0.51311,-2.00449 1.24141,-4.52671 0.39407,-0.56307 5.27122,-7.54803 5.33872,-9.24308 l -0.33173,-1.56824 -6.47077,-12.40632 c -0.58634,-1.01797 -0.3503,-0.89466 -1.52286,-1.09012 -4.53489,-0.75039 -9.07089,-0.48633 -13.85592,-0.51769 -1.6543,-0.0104 -1.54964,0.19055 -2.47219,-1.29604 -1.49493,-2.4082 -3.74492,-7.37586 -5.27592,-10.04349 -1.06334,-1.84511 -0.82717,-1.73576 -2.70839,-1.74855 -3.89497,-0.0264 -12.71577,-0.1593 -14.4876,0.003 l -6.858169,10.59841 c -0.636371,0.71315 -1.235535,2.68509 -4.121858,2.92593 -0.553751,0.0452 -4.276595,0.0139 -7.025705,0.064 -1.422785,0.0278 -2.73628,0 -4.143973,0.0243 -0.818996,0.0139 -1.818356,0.33625 -2.281386,1.13546 -1.773013,3.07249 -4.981607,8.62997 -6.417257,11.11613 z M 64.901407,97.927601 c 0,1.782324 -0.231237,4.598899 -1.944024,5.590079 l -2.672305,0.73177 -1.45884,-0.24538 c -2.913143,-0.22689 -3.640245,0.61777 -5.594722,-0.48627 -0.922555,-0.52237 -13.405731,-18.973638 -14.58772,-21.882086 -1.177305,-2.889849 -0.05459,-4.971144 0.727099,-7.538732 0.927242,-3.039933 4.43602,-17.460086 6.812828,-27.384931 0.427069,-1.663636 1.535649,-3.185362 3.266751,-4.486009 0.418827,-0.25363 0.80041,-0.544467 1.149432,-0.863229 L 106.5343,33.400612 c 2.07195,3.563419 5.99373,5.894853 10.39714,5.730824 3.29935,-0.117392 10.90683,-1.926603 12.92412,-4.231227 l 0.17664,-2.536199 -0.15821,-1.581034 -2.30934,0.34077 -3.54478,0.504931 -1.06334,-1.441458 -3.17361,-7.05627 1.41812,-3.380857 0.70853,-1.694983 3.5448,-0.504862 2.14067,-0.277136 -0.16726,-1.595013 -0.81906,-2.109227 c -1.95797,-1.330846 -4.34406,-2.071916 -6.88839,-1.976501 -5.48536,0.199942 -14.61101,4.508075 -15.58707,9.680512 l -53.83915,7.660895 c -0.586358,-0.739924 -1.354158,-1.34952 -2.217416,-1.848609 -1.494951,-0.864443 -4.780302,-0.595687 -8.611364,0.508408 -1.354158,0.390912 -2.0045,1.945172 -2.426833,3.227232 l -18.337283,2.612977 c -1.809044,0.253491 -3.080624,1.948648 -2.822373,3.757703 l 1.237733,5.575332 c 0.259646,1.809069 1.954528,3.081817 3.763576,2.827032 L 38.489145,43.08559 c 0.168473,0.449069 0.218441,0.880671 0.02052,1.29019 C 32.311116,57.191628 28.366101,64.894392 24.41758,80.254579 l 0.477044,1.976596 0.62242,2.562938 c 1.849745,3.468049 3.20859,7.058246 4.617459,10.698474 2.422142,6.265993 5.135196,12.201573 8.025055,18.236063 l 0.727051,4.13585 M 197.7527,200.71492 c -0.99116,6.30324 -20.78161,13.10205 -26.4531,12.12481 -5.66798,-0.97259 -16.56543,-16.02913 -15.56958,-22.34979"
+       style="fill:none;stroke:#f6d3e9;stroke-width:3.47725;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3779" />
+  </g>
+</svg>
diff --git a/hswaw/site/static/serwerownia.jpg b/hswaw/site/static/serwerownia.jpg
new file mode 100644
index 0000000..04f7ec5
--- /dev/null
+++ b/hswaw/site/static/serwerownia.jpg
Binary files differ
diff --git a/hswaw/site/static/space.jpg b/hswaw/site/static/space.jpg
new file mode 100644
index 0000000..ef32f6f
--- /dev/null
+++ b/hswaw/site/static/space.jpg
Binary files differ
diff --git a/hswaw/site/static/tokarka.jpg b/hswaw/site/static/tokarka.jpg
new file mode 100644
index 0000000..10db3f8
--- /dev/null
+++ b/hswaw/site/static/tokarka.jpg
Binary files differ
diff --git a/hswaw/site/templates/index.html b/hswaw/site/templates/index.html
index 48f0acd..5b8bb62 100644
--- a/hswaw/site/templates/index.html
+++ b/hswaw/site/templates/index.html
@@ -2,59 +2,85 @@
 <meta charset="utf-8">
 <!-- https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-omission -->
 <title>Warszawski Hackerspace</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-<link rel="preconnect" href="https://fonts.gstatic.com">
+<meta name="viewport" content="width=device-width, initial-scale=1" />
 <link rel="stylesheet" href="/static/site/landing.css"/>
 <link rel="stylesheet" href="/static/leaflet/leaflet.css"/>
-<link href="https://fonts.googleapis.com/css2?family=Allerta&family=Lato&display=swap" rel="stylesheet">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&family=Comfortaa&display=swap" rel="stylesheet">
 <style>
 </style>
+<div id="ledsFloater">
+<div id="ledsWrapper">
+    <div id="leds">
+    </div>
+</div>
+</div>
 <div id="page">
-    <div class="top">
-        <div class="logo">
-            <img src="/static/site/syrenka.png" />
+    <header id="top">
+        <div id="logo">
+            <img src="/static/site/neon-syrenka.svg" />
+        </div>
+        <div class="type">
             <h1>Warszawski Hackerspace</h1>
         </div>
-        <div class="mapcontainer">
-            <h2>Gdzie jesteśmy?</h2>
-            <div id="map"></div>
-            <pre>Warszawski Hackerspace
-ul. Wolność 2A
-01-018 Warszawa 
-52°14'29.8"N 20°59'5.5"E</pre>
-        </div>
-    </div>
-    <div class="covid">
-        <span>Na okres pandemii Hackerspace jest <b>zamknięty</b>, więcej informacji: <a href="">projekt covid-19</a></span>
-    </div>
+    </header>
+    <nav id="quicklinks">
+        <ul>
+            <li><a href="https://wiki.hackerspace.pl/">Wiki</a></li>
+            <li><a href="https://profile.hackerspace.pl/">Konto</a></li>
+            <li><a href="https://wiki.hackerspace.pl/partners">Partnerzy</a></li>
+            <li><a href="https://wiki.hackerspace.pl/kontakt">Kontakt</a></li>
+            {{ if eq .AtError nil }}
+            {{ $count := len .AtStatus.Users }}
+            <li>
+                <a href="https://at.hackerspace.pl">Osób w spejsie: <b>{{ $count }}</b></a>
+            </li>
+            {{ end }}
+        </ul>
+    </nav>
     <div class="bottom">
         <div class="about">
-            <h2>Czym jest Hackerspace?</h2>
+            <h2>Czym jest Warszawski Hackerspace?</h2>
             <p>
-              Przestrzeń stworzona i utrzymywana przez grupę kreatywnych osób, które łączy fascynacja ogólno pojętym tworzeniem w duchu <a href="https://pl.wikipedia.org/wiki/Spo%C5%82eczno%C5%9B%C4%87_haker%C3%B3w">kultury hackerskiej</a>.
+              Przestrzeń stworzona i utrzymywana przez grupę kreatywnych osób, które łączy fascynacja ogólno pojętym tworzeniem w duchu <a href="https://pl.wikipedia.org/wiki/Spo%C5%82eczno%C5%9B%C4%87_haker%C3%B3w">kultury hackerskiej</a>. Razem utrzymujemy przestrzeń na ul. Wolność 2A, gdzie mamy między innymi:
+              <ul>
+                  <li><b>Warsztat ciężki</b>, ze sprzętem takim jak ploter laserowy, frezarka kolumnowa CNC, tokarka, spawarki i ramię robotyczne KUKA,</li>
+                  <li><b>Warsztat elektroniczny</b>, z oscyloskopami, stacjami lutowniczymi i masą części elektronicznych,</li>
+                  <li><b>Przestrzeń socjalną</b>, pełną stołów i kanap do hakowania nad projektami software'owymi,</li>
+                  <li><b>Serwerownię</b>, utrzymująca infrastrukturę spejsu i naszego mikro-ISP <a href="https://bgp.wtf">bgp.wtf</a>.</li>
+              </ul>
             </p>
             <p>
-              <b>Hackerspace nie zna barier.</b> Jeśli masz ciekawy pomysł i szukasz ludzi chętnych do współpracy lub po prostu potrzebujesz miejsca i sprzętu - <a href="">zapraszamy</a>!
+              <b>Hackerspace nie zna barier.</b> Jeśli masz ciekawy pomysł i szukasz ludzi chętnych do współpracy, lub po prostu potrzebujesz miejsca i sprzętu - <a href="https://wiki.hackerspace.pl/jak-dolaczyc">zapraszamy</a>! Utrzymujemy się w całosci z wolontariatu naszych członków, <a href="https://wiki.hackerspace.pl/finanse">darowizn i składek</a> oraz drobnej aktywności komercyjnej.
             </p>
-        </div>
-        <div class="blog">
-            <h2>Blog</h2>
-            <p>
-                Najnowsze wpisy z naszego <a href="https://blog.hackerspace.pl">bloga</a>:
-                <ul>
-                    {{ range .Entries }}
-                    <li><a href="{{ .Link.Href }}">{{ .Title }}</a> <i>{{ .UpdatedHuman }}, {{ .Author }}</i></li>
-                    {{ else }}
-                    <li><i>Ups, nie udało się załadować wpisów.</i></li>
-                    {{ end }}
+            <section id="gallery">
+                <div class="img-wrapper">
+                    <img src="/static/site/space.jpg" />
+                </div>
+                <ul id="gallery-nav">
+                    <li data-name="space" class="active"><a href="#">Spejs</a></li>
+                    <li data-name="frezarka"><a href="#">Frezarka</a></li>
+                    <li data-name="tokarka"><a href="#">Tokarka</a></li>
+                    <li data-name="kuka"><a href="#">KUKA</a></li>
+                    <li data-name="serwerownia"><a href="#">Serwerownia</a></li>
                 </ul>
+            </section>
+            <h2>Czy mogę odwiedzić spejs? Jak do was dołączyć?</h2>
             <p>
+              Nasze cotygodniowe otwarte spotkania są w tej chwili zawieszone z powodu pandemii. Mimo tego, <b>dalej jesteśmy otwarci na nowych członków</b> i zainteresowanych - tylko w mniejszej skali i po wcześniejszym umówieniu się. Więcej informacji znajdziesz na <a href="https://wiki.hackerspace.pl/jak-dolaczyc">wiki.hackerspace.pl/jak-dolaczyc</a>.
+            </p>
+            <h2>Gdzie jest Hackerspace?</h2>
+            <div id="map"></div>
+            <p>
+                Stowarzyszenie Warszawski Hackerspace, ul. Wolność 2A, 01-018 Warszawa.
+            </p>
+            <h2>Gdzie was znaleźć w Internecie?</h2>
+            <p>
+              Jeśli nalegasz, mamy rzadko aktualizowane konta na <a href="https://twitter.com/hackerspacepl">Twitterze</a> i <a href="https://www.facebook.com/hackerspacepl">Facebooku</a>. Lepiej jednak kontaktować się z nami <a href="https://wiki.hackerspace.pl/kontakt">przez IRC, Matrixa lub mejlowo</a>.
+            </p>
         </div>
     </div>
-
-    <div id="footer">
-        <span>&copy; 2021 <a href="https://cs.hackerspace.pl/hscloud/-/tree/hswaw/site">Autorzy Strony</a>. Ten utwór jest dostępny na <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">licencji Creative Commons Uznanie autorstwa 4.0 Międzynarodowe</a>.</span>
-    </div>
 </div>
 
 <script>
@@ -71,5 +97,16 @@
 
     L.marker([52.24158, 20.9848]).addTo(map);
 }
+document.querySelectorAll('#gallery-nav > li > a').forEach(link => {
+    link.onclick = e => {
+        e.preventDefault()
+        document.querySelector(`#gallery-nav > li.active`).classList.remove('active')
+        const li = e.currentTarget.parentNode
+        li.classList.add('active')
+        const name = li.dataset.name
+        document.querySelector('#gallery img').src = `/static/site/${name}.jpg`
+    }
+})
 </script>
 <script src="/static/leaflet/leaflet.js" crossorigin="" onload="loadMap()"></script>
+<script src="/static/site/led.js" crossorigin="" type="module" ></script>
diff --git a/hswaw/site/views.go b/hswaw/site/views.go
index 59f948e..6109a55 100644
--- a/hswaw/site/views.go
+++ b/hswaw/site/views.go
@@ -1,12 +1,15 @@
 package main
 
 import (
+	"encoding/json"
 	"fmt"
 	"html/template"
 	"net/http"
+	"strings"
 
 	"github.com/golang/glog"
 
+	"code.hackerspace.pl/hscloud/hswaw/site/calendar"
 	"code.hackerspace.pl/hscloud/hswaw/site/templates"
 )
 
@@ -53,7 +56,55 @@
 
 // handleIndex handles rendering the main page at /.
 func (s *service) handleIndex(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+
+	atStatus, atError := getAt(ctx)
+
 	render(w, tmplIndex, map[string]interface{}{
-		"Entries": s.getFeeds(),
+		"Entries":  s.getFeeds(),
+		"Events":   s.getEvents(),
+		"AtStatus": atStatus,
+		"AtError":  atError,
 	})
 }
+
+func (s *service) handleJSONEvents(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(s.getEvents())
+}
+
+// handleEvent is a fallback HTML-only event render view.
+// TODO(q3k): make this pretty by either making a template or redirecting to a
+// pretty viewer.
+func (s *service) handleEvent(w http.ResponseWriter, r *http.Request) {
+	parts := strings.Split(r.URL.Path, "/")
+	uid := parts[len(parts)-1]
+
+	events := s.getEvents()
+	var event *calendar.UpcomingEvent
+	for _, ev := range events {
+		if ev.UID == uid {
+			event = ev
+			break
+		}
+	}
+	if event == nil {
+		http.NotFound(w, r)
+		return
+	}
+
+	render(w, template.Must(template.New("event").Parse(`<!DOCTYPE html>
+	<meta charset="utf-8">
+	<title>Event details: {{ .Summary }}</title>
+	<body>
+	<i>this interface intentionally left ugly...</i><br/>
+	<b>summary:</b> {{ .Summary }}<br />
+	<b>date:</b> {{ .WarsawDate }}<br />
+	<pre>{{ .Description }}</pre>`)), event)
+}
+
+func (s *service) handleSpaceAPI(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(generateSpaceAPIResponse(ctx))
+}
diff --git a/kube/kube.libsonnet b/kube/kube.libsonnet
index 8d7254a..7e69720 100644
--- a/kube/kube.libsonnet
+++ b/kube/kube.libsonnet
@@ -46,6 +46,7 @@
 
     // Make OpenAPI v3 schema specification less painful.
     OpenAPI:: {
+        local openapi = self,
         Validation(obj):: {
             openAPIV3Schema: obj.render,
         },
@@ -54,69 +55,85 @@
             required:: true,
         },
 
-        Dict:: {
-            local dict = self,
+        renderable:: {
             required:: false,
+            render:: {},
+        },
+        local renderable = self.renderable,
+
+        lift(f, keys):: {
+            [if f[k] != null then k]: f[k]
+            for k in keys
+        },
+        local lift = self.lift,
+
+        parametrized(type, keys=[]):: (
+            local keysp = keys + [
+                "description",
+                "nullable",
+                "x-kubernetes-preserve-unknown-fields",
+            ];
+            renderable {
+                render:: lift(self, keysp) {
+                    type: type,
+                },
+            } + {
+                [k]: null
+                for k in keysp
+            }
+        ),
+        local parametrized = self.parametrized,
+
+        Any:: renderable,
+        Boolean:: parametrized("boolean"),
+        Integer:: parametrized("integer", ["minimum", "maximum", "format"]),
+        Number:: parametrized("number"),
+        String:: parametrized("string", ["pattern"]),
+
+        Dict:: renderable {
+            local d = self,
 
             local requiredList = [
-                k for k in std.filter(function(k) dict[k].required, std.objectFields(dict))
+                k for k in std.filter(function(k) d[k].required, std.objectFields(d))
             ],
 
-            render:: {
+            render+: {
                 properties: {
-                    [k]: dict[k].render
-                    for k in std.objectFields(dict)
+                    [k]: d[k].render
+                    for k in std.objectFields(d)
                 },
-            } + (if std.length(requiredList) > 0 then {
-                required: requiredList,
-            } else {}),
+                [if std.length(requiredList) > 0 then 'required']: requiredList,
+            },
+        },
+        Object(props={}):: parametrized("object") {
+            local requiredList = [
+                k for k in std.filter(function(k) props[k].required, std.objectFields(props))
+            ],
+
+            render+: {
+                [if std.length(std.objectFields(props)) > 0 then "properties"]: {
+                    [k]: props[k].render
+                    for k in std.objectFields(props)
+                },
+                [if std.length(requiredList) > 0 then 'required']: requiredList,
+            },
         },
 
-        Array(items):: {
-            required:: false,
-            render:: {
-                type: "array",
+        Array(items):: parametrized("array") {
+            render+: {
                 items: items.render,
             },
         },
 
-        Integer:: {
-            local integer = self,
-            required:: false,
-            render:: {
-                type: "integer",
-            } + (if integer.minimum != null then {
-                minimum: integer.minimum,
-            } else {}) + (if integer.maximum != null then {
-                maximum: integer.maximum,
-            } else {}),
-
-            minimum:: null,
-            maximum:: null,
-        },
-
-        String:: {
-            local string = self,
-            required:: false,
-            render:: {
-                type: "string",
-            } + (if string.pattern != null then {
-                pattern: string.pattern,
-            } else {}),
-
-            pattern:: null,
-        },
-
-        Boolean:: {
-            required:: false,
-            render:: {
-                type: "boolean",
+        Enum(items):: parametrized("string") {
+            render+: {
+                enum: items,
             },
         },
 
-        Any:: {
-            required:: false,
-            render:: {},
+        KubeAny(nullable=false):: self.Object() {
+            [if nullable then "nullable"]: true,
+            "x-kubernetes-preserve-unknown-fields": true,
         },
     },
 }
diff --git a/nix/readtree.nix b/nix/readtree.nix
deleted file mode 100644
index 066d326..0000000
--- a/nix/readtree.nix
+++ /dev/null
@@ -1,96 +0,0 @@
-# The MIT License (MIT)
-# 
-# Copyright (c) 2019 Vincent Ambo
-# 
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-# 
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-# 
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-{ ... }:
-
-args: initPath:
-
-let
-  inherit (builtins)
-    attrNames
-    baseNameOf
-    filter
-    hasAttr
-    head
-    isAttrs
-    length
-    listToAttrs
-    map
-    match
-    readDir
-    substring;
-
-  argsWithPath = parts:
-    let meta.locatedAt = parts;
-    in meta // (if isAttrs args then args else args meta);
-
-  readDirVisible = path:
-    let
-      children = readDir path;
-      isVisible = f: f == ".skip-subtree" || (substring 0 1 f) != ".";
-      names = filter isVisible (attrNames children);
-    in listToAttrs (map (name: {
-      inherit name;
-      value = children.${name};
-    }) names);
-
-  # The marker is added to every set that was imported directly by
-  # readTree.
-  importWithMark = path: parts:
-    let imported = import path (argsWithPath parts);
-    in if (isAttrs imported)
-      then imported // { __readTree = true; }
-      else imported;
-
-  nixFileName = file:
-    let res = match "(.*)\.nix" file;
-    in if res == null then null else head res;
-
-  readTree = path: parts:
-    let
-      dir = readDirVisible path;
-      self = importWithMark path parts;
-      joinChild = c: path + ("/" + c);
-
-      # Import subdirectories of the current one, unless the special
-      # `.skip-subtree` file exists which makes readTree ignore the
-      # children.
-      #
-      # This file can optionally contain information on why the tree
-      # should be ignored, but its content is not inspected by
-      # readTree
-      filterDir = f: dir."${f}" == "directory";
-      children = if hasAttr ".skip-subtree" dir then [] else map (c: {
-        name = c;
-        value = readTree (joinChild c) (parts ++ [ c ]);
-      }) (filter filterDir (attrNames dir));
-
-      # Import Nix files
-      nixFiles = filter (f: f != null) (map nixFileName (attrNames dir));
-      nixChildren = map (c: let p = joinChild (c + ".nix"); in {
-        name = c;
-        value = importWithMark p (parts ++ [ c ]);
-      }) nixFiles;
-    in if dir ? "default.nix"
-      then (if isAttrs self then self // (listToAttrs children) else self)
-      else listToAttrs (nixChildren ++ children);
-in readTree initPath [ (baseNameOf initPath) ]
diff --git a/nix/readtree/LICENSE b/nix/readtree/LICENSE
new file mode 100644
index 0000000..bdc72a2
--- /dev/null
+++ b/nix/readtree/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Vincent Ambo
+Copyright (c) 2020-2021 The TVL Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/nix/readtree/README.md b/nix/readtree/README.md
new file mode 100644
index 0000000..138abbe
--- /dev/null
+++ b/nix/readtree/README.md
@@ -0,0 +1,84 @@
+readTree
+========
+
+This is a Nix program that builds up an attribute set tree for a large
+repository based on the filesystem layout.
+
+It is in fact the tool that lays out the attribute set of this repository.
+
+As an example, consider a root (`.`) of a repository and a layout such as:
+
+```
+.
+├── third_party
+│   ├── default.nix
+│   └── rustpkgs
+│       ├── aho-corasick.nix
+│       └── serde.nix
+└── tools
+    ├── cheddar
+    │   └── default.nix
+    └── roquefort.nix
+```
+
+When `readTree` is called on that tree, it will construct an attribute set with
+this shape:
+
+```nix
+{
+    tools = {
+        cheddar = ...;
+        roquefort = ...;
+    };
+
+    third_party = {
+        # the `default.nix` of this folder might have had arbitrary other
+        # attributes here, such as this:
+        favouriteColour = "orange";
+
+        rustpkgs = {
+            aho-corasick = ...;
+            serde = ...;
+        };
+    };
+}
+```
+
+Every imported Nix file that yields an attribute set will have a `__readTree =
+true;` attribute merged into it.
+
+## Traversal logic
+
+`readTree` will follow any subdirectories of a tree and import all Nix files,
+with some exceptions:
+
+* A folder can declare that its children are off-limit by containing a
+  `.skip-subtree` file. Since the content of the file is not checked, it can be
+  useful to leave a note for a human in the file.
+* If a folder contains a `default.nix` file, no *sibling* Nix files will be
+  imported - however children are traversed as normal.
+* If a folder contains a `default.nix` it is loaded and, if it evaluates to a
+  set, *merged* with the children. If it evaluates to anything else the children
+  are *not traversed*.
+* The `default.nix` of the top-level folder on which readTree is
+  called is **not** read to avoid infinite recursion (as, presumably,
+  this file is where readTree itself is called).
+
+Traversal is lazy, `readTree` will only build up the tree as requested. This
+currently has the downside that directories with no importable files end up in
+the tree as empty nodes (`{}`).
+
+## Import structure
+
+`readTree` is called with two parameters: The arguments to pass to all imports,
+and the initial path at which to start the traversal.
+
+The package headers in this repository follow the form `{ pkgs, ... }:` where
+`pkgs` is a fixed-point of the entire package tree (see the `default.nix` at the
+root of the depot).
+
+In theory `readTree` can pass arguments of different shapes, but I have found
+this to be a good solution for the most part.
+
+Note that `readTree` does not currently make functions overridable, though it is
+feasible that it could do that in the future.
diff --git a/nix/readtree/default.nix b/nix/readtree/default.nix
new file mode 100644
index 0000000..633915f
--- /dev/null
+++ b/nix/readtree/default.nix
@@ -0,0 +1,116 @@
+# Copyright (c) 2019 Vincent Ambo
+# Copyright (c) 2020-2021 The TVL Authors
+# SPDX-License-Identifier: MIT
+#
+# Provides a function to automatically read a a filesystem structure
+# into a Nix attribute set.
+#
+# Optionally accepts an argument `argsFilter` on import, which is a
+# function that receives the current tree location (as a list of
+# strings) and the argument set and can arbitrarily modify it.
+{ argsFilter ? (x: _parts: x)
+, ... }:
+
+let
+  inherit (builtins)
+    attrNames
+    baseNameOf
+    concatStringsSep
+    filter
+    hasAttr
+    head
+    isAttrs
+    length
+    listToAttrs
+    map
+    match
+    readDir
+    substring;
+
+  assertMsg = pred: msg:
+    if pred
+    then true
+    else builtins.trace msg false;
+
+  argsWithPath = args: parts:
+    let meta.locatedAt = parts;
+    in meta // (if isAttrs args then args else args meta);
+
+  readDirVisible = path:
+    let
+      children = readDir path;
+      isVisible = f: f == ".skip-subtree" || (substring 0 1 f) != ".";
+      names = filter isVisible (attrNames children);
+    in listToAttrs (map (name: {
+      inherit name;
+      value = children.${name};
+    }) names);
+
+  # Create a mark containing the location of this attribute.
+  marker = parts: {
+    __readTree = parts;
+  };
+
+  # The marker is added to every set that was imported directly by
+  # readTree.
+  importWithMark = args: path: parts:
+    let
+      importedFile = import path;
+      pathType = builtins.typeOf importedFile;
+      imported =
+        assert assertMsg
+          (pathType == "lambda")
+          "readTree: trying to import ${toString path}, but it’s a ${pathType}, you need to make it a function like { depot, pkgs, ... }";
+        importedFile (argsFilter (argsWithPath args parts) parts);
+    in if (isAttrs imported)
+      then imported // (marker parts)
+      else imported;
+
+  nixFileName = file:
+    let res = match "(.*)\\.nix" file;
+    in if res == null then null else head res;
+
+  readTree = { args, initPath, rootDir, parts }:
+    let
+      dir = readDirVisible initPath;
+      joinChild = c: initPath + ("/" + c);
+
+      self = if rootDir
+        then { __readTree = []; }
+        else importWithMark args initPath parts;
+
+      # Import subdirectories of the current one, unless the special
+      # `.skip-subtree` file exists which makes readTree ignore the
+      # children.
+      #
+      # This file can optionally contain information on why the tree
+      # should be ignored, but its content is not inspected by
+      # readTree
+      filterDir = f: dir."${f}" == "directory";
+      children = if hasAttr ".skip-subtree" dir then [] else map (c: {
+        name = c;
+        value = readTree {
+          args = args;
+          initPath = (joinChild c);
+          rootDir = false;
+          parts = (parts ++ [ c ]);
+        };
+      }) (filter filterDir (attrNames dir));
+
+      # Import Nix files
+      nixFiles = filter (f: f != null) (map nixFileName (attrNames dir));
+      nixChildren = map (c: let p = joinChild (c + ".nix"); in {
+        name = c;
+        value = importWithMark args p (parts ++ [ c ]);
+      }) nixFiles;
+    in if dir ? "default.nix"
+      then (if isAttrs self then self // (listToAttrs children) else self)
+      else (listToAttrs (nixChildren ++ children) // (marker parts));
+
+in {
+  __functor = _: args: initPath: readTree {
+    inherit args initPath;
+    rootDir = true;
+    parts = [];
+  };
+}
diff --git a/ops/README.md b/ops/README.md
new file mode 100644
index 0000000..d31f767
--- /dev/null
+++ b/ops/README.md
@@ -0,0 +1,23 @@
+Operations
+===
+
+Deploying NixOS machines
+---
+
+Machine configurations are in `ops/machines.nix`.
+
+Wrapper script to show all available machines and provision a single machine:
+
+     $ $(nix-build -A ops.provision)
+     Available machines:
+      - bc01n01.hswaw.net
+      - bc01n02.hswaw.net
+      - dcr01s22.hswaw.net
+      - dcr01s24.hswaw.net
+      - edge01.waw.bgp.wtf
+
+     $ $(nix-build -A ops.provision) edge01.waw.bgp.wtf
+
+This can be slow, as it evaluates/builds all machines' configs. If you just want to deploy one machine and possible iterate faster:
+
+    $ $(nix-build -A 'ops.machines."edge01.waw.bgp.wtf".config.passthru.hscloud.provision')
diff --git a/ops/ceph/0000-fix-SPDK-build-env.patch b/ops/ceph/0000-fix-SPDK-build-env.patch
new file mode 100644
index 0000000..a117408
--- /dev/null
+++ b/ops/ceph/0000-fix-SPDK-build-env.patch
@@ -0,0 +1,11 @@
+--- a/cmake/modules/BuildSPDK.cmake
++++ b/cmake/modules/BuildSPDK.cmake
+@@ -35,7 +35,7 @@ macro(build_spdk)
+     # unset $CFLAGS, otherwise it will interfere with how SPDK sets
+     # its include directory.
+     # unset $LDFLAGS, otherwise SPDK will fail to mock some functions.
+-    BUILD_COMMAND env -i PATH=$ENV{PATH} CC=${CMAKE_C_COMPILER} ${make_cmd} EXTRA_CFLAGS="${spdk_CFLAGS}"
++    BUILD_COMMAND env -i PATH=$ENV{PATH} CC=${CMAKE_C_COMPILER} ${make_cmd} EXTRA_CFLAGS="${spdk_CFLAGS}" C_OPT="-mssse3"
+     BUILD_IN_SOURCE 1
+     INSTALL_COMMAND "true")
+   unset(make_cmd)
diff --git a/ops/ceph/COPYING b/ops/ceph/COPYING
new file mode 100644
index 0000000..fe46c6a
--- /dev/null
+++ b/ops/ceph/COPYING
@@ -0,0 +1,20 @@
+Copyright (c) 2003-2021 Eelco Dolstra and the Nixpkgs/NixOS contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/ops/ceph/README.md b/ops/ceph/README.md
new file mode 100644
index 0000000..1a25652
--- /dev/null
+++ b/ops/ceph/README.md
@@ -0,0 +1,3 @@
+Ceph 16.4 backport from nixpkgs @ 2021-09-10.
+
+To be removed once nixpkgs on hscloud nodes is bumped past this version being available upstream.
diff --git a/ops/ceph/default.nix b/ops/ceph/default.nix
new file mode 100644
index 0000000..0ccc96c
--- /dev/null
+++ b/ops/ceph/default.nix
@@ -0,0 +1,254 @@
+{ lib, stdenv, runCommand, fetchurl
+, ensureNewerSourcesHook
+, cmake, pkg-config
+, which, git
+, boost
+, libxml2, zlib, lz4
+, openldap, lttng-ust
+, babeltrace, gperf
+, gtest
+, cunit, snappy
+, makeWrapper
+, leveldb, oathToolkit
+, libnl, libcap_ng
+, rdkafka
+, nixosTests
+, cryptsetup
+, sqlite
+, lua
+, icu
+, bzip2
+, doxygen
+, graphviz
+, fmt
+, python3
+
+# Optional Dependencies
+, yasm ? null, fcgi ? null, expat ? null
+, curl ? null, fuse ? null
+, libedit ? null, libatomic_ops ? null
+, libs3 ? null
+
+# Mallocs
+, jemalloc ? null, gperftools ? null
+
+# Crypto Dependencies
+, cryptopp ? null
+, nss ? null, nspr ? null
+
+# Linux Only Dependencies
+, linuxHeaders, util-linux, libuuid, udev, keyutils, rdma-core, rabbitmq-c
+, libaio ? null, libxfs ? null, zfs ? null, liburing ? null
+, ...
+}:
+
+# We must have one crypto library
+assert cryptopp != null || (nss != null && nspr != null);
+
+let
+  shouldUsePkg = pkg: if pkg != null && pkg.meta.available then pkg else null;
+
+  optYasm = shouldUsePkg yasm;
+  optFcgi = shouldUsePkg fcgi;
+  optExpat = shouldUsePkg expat;
+  optCurl = shouldUsePkg curl;
+  optFuse = shouldUsePkg fuse;
+  optLibedit = shouldUsePkg libedit;
+  optLibatomic_ops = shouldUsePkg libatomic_ops;
+  optLibs3 = shouldUsePkg libs3;
+
+  optJemalloc = shouldUsePkg jemalloc;
+  optGperftools = shouldUsePkg gperftools;
+
+  optCryptopp = shouldUsePkg cryptopp;
+  optNss = shouldUsePkg nss;
+  optNspr = shouldUsePkg nspr;
+
+  optLibaio = shouldUsePkg libaio;
+  optLibxfs = shouldUsePkg libxfs;
+  optZfs = shouldUsePkg zfs;
+
+  hasRadosgw = optFcgi != null && optExpat != null && optCurl != null && optLibedit != null;
+
+
+  # Malloc implementation (can be jemalloc, tcmalloc or null)
+  malloc = if optJemalloc != null then optJemalloc else optGperftools;
+
+  # We prefer nss over cryptopp
+  cryptoStr = if optNss != null && optNspr != null then "nss" else
+    if optCryptopp != null then "cryptopp" else "none";
+
+  cryptoLibsMap = {
+    nss = [ optNss optNspr ];
+    cryptopp = [ optCryptopp ];
+    none = [ ];
+  };
+
+  getMeta = description: with lib; {
+     homepage = "https://ceph.com/";
+     inherit description;
+     license = with licenses; [ lgpl21 gpl2 bsd3 mit publicDomain ];
+     maintainers = with maintainers; [ adev ak johanot krav ];
+     platforms = [ "x86_64-linux" "aarch64-linux" ];
+   };
+
+  ceph-common = python.pkgs.buildPythonPackage rec{
+    pname = "ceph-common";
+    inherit src version;
+
+    sourceRoot = "ceph-${version}/src/python-common";
+
+    checkInputs = [ python.pkgs.pytest ];
+    propagatedBuildInputs = with python.pkgs; [ pyyaml six ];
+
+    meta = getMeta "Ceph common module for code shared by manager modules";
+  };
+
+  python = python3.override {
+    packageOverrides = self: super: {
+      # scipy > 1.3 breaks diskprediction_local, leading to mgr hang on startup
+      # Bump once these issues are resolved:
+      # https://tracker.ceph.com/issues/42764 https://tracker.ceph.com/issues/45147
+      scipy = super.scipy.overridePythonAttrs (oldAttrs: rec {
+        version = "1.3.3";
+        src = oldAttrs.src.override {
+          inherit version;
+          sha256 = "02iqb7ws7fw5fd1a83hx705pzrw1imj7z0bphjsl4bfvw254xgv4";
+        };
+        doCheck = false;
+      });
+    };
+  };
+
+  ceph-python-env = python.withPackages (ps: [
+    ps.sphinx
+    ps.flask
+    ps.cython
+    ps.setuptools
+    ps.virtualenv
+    # Libraries needed by the python tools
+    ps.Mako
+    ceph-common
+    ps.cherrypy
+    ps.cmd2
+    ps.colorama
+    ps.python-dateutil
+    ps.jsonpatch
+    ps.pecan
+    ps.prettytable
+    ps.pyopenssl
+    ps.pyjwt
+    ps.webob
+    ps.bcrypt
+    ps.scipy
+    ps.six
+    ps.pyyaml
+  ]);
+  sitePackages = ceph-python-env.python.sitePackages;
+
+  version = "16.2.4";
+  src = fetchurl {
+    url = "http://download.ceph.com/tarballs/ceph-${version}.tar.gz";
+    sha256 = "sha256-J6FVK7feNN8cGO5BSDlfRGACAzchmRUSWR+a4ZgeWy0=";
+  };
+in rec {
+  ceph = stdenv.mkDerivation {
+    pname = "ceph";
+    inherit src version;
+
+    patches = [
+      ./0000-fix-SPDK-build-env.patch
+    ];
+
+    nativeBuildInputs = [
+      cmake
+      pkg-config which git python.pkgs.wrapPython makeWrapper
+      python.pkgs.python # for the toPythonPath function
+      (ensureNewerSourcesHook { year = "1980"; })
+      python
+      fmt
+      # for building docs/man-pages presumably
+      doxygen
+      graphviz
+    ];
+
+    buildInputs = cryptoLibsMap.${cryptoStr} ++ [
+      boost ceph-python-env libxml2 optYasm optLibatomic_ops optLibs3
+      malloc zlib openldap lttng-ust babeltrace gperf gtest cunit
+      snappy lz4 oathToolkit leveldb libnl libcap_ng rdkafka
+      cryptsetup sqlite lua icu bzip2
+    ] ++ lib.optionals stdenv.isLinux [
+      linuxHeaders util-linux libuuid udev keyutils liburing optLibaio optLibxfs optZfs
+      # ceph 14
+      rdma-core rabbitmq-c
+    ] ++ lib.optionals hasRadosgw [
+      optFcgi optExpat optCurl optFuse optLibedit
+    ];
+
+    pythonPath = [ ceph-python-env "${placeholder "out"}/${ceph-python-env.sitePackages}" ];
+
+    preConfigure =''
+      substituteInPlace src/common/module.c --replace "/sbin/modinfo"  "modinfo"
+      substituteInPlace src/common/module.c --replace "/sbin/modprobe" "modprobe"
+      substituteInPlace src/common/module.c --replace "/bin/grep" "grep"
+
+      # for pybind/rgw to find internal dep
+      export LD_LIBRARY_PATH="$PWD/build/lib''${LD_LIBRARY_PATH:+:}$LD_LIBRARY_PATH"
+      # install target needs to be in PYTHONPATH for "*.pth support" check to succeed
+      # set PYTHONPATH, so the build system doesn't silently skip installing ceph-volume and others
+      export PYTHONPATH=${ceph-python-env}/${sitePackages}:$lib/${sitePackages}:$out/${sitePackages}
+      patchShebangs src/script src/spdk src/test src/tools
+    '';
+
+    cmakeFlags = [
+      "-DWITH_SYSTEM_ROCKSDB=OFF"  # breaks Bluestore
+      "-DCMAKE_INSTALL_DATADIR=${placeholder "lib"}/lib"
+
+      "-DWITH_SYSTEM_BOOST=ON"
+      "-DWITH_SYSTEM_GTEST=ON"
+      "-DMGR_PYTHON_VERSION=${ceph-python-env.python.pythonVersion}"
+      "-DWITH_SYSTEMD=OFF"
+      "-DWITH_TESTS=OFF"
+      "-DWITH_CEPHFS_SHELL=ON"
+      # TODO breaks with sandbox, tries to download stuff with npm
+      "-DWITH_MGR_DASHBOARD_FRONTEND=OFF"
+      # WITH_XFS has been set default ON from Ceph 16, keeping it optional in nixpkgs for now
+      ''-DWITH_XFS=${if optLibxfs != null then "ON" else "OFF"}''
+    ] ++ lib.optional stdenv.isLinux "-DWITH_SYSTEM_LIBURING=ON";
+
+    postFixup = ''
+      wrapPythonPrograms
+      wrapProgram $out/bin/ceph-mgr --prefix PYTHONPATH ":" "$(toPythonPath ${placeholder "out"}):$(toPythonPath ${ceph-python-env})"
+
+      # Test that ceph-volume exists since the build system has a tendency to
+      # silently drop it with misconfigurations.
+      test -f $out/bin/ceph-volume
+    '';
+
+    outputs = [ "out" "lib" "dev" "doc" "man" ];
+
+    doCheck = false; # uses pip to install things from the internet
+
+    # Takes 7+h to build with 2 cores.
+    requiredSystemFeatures = [ "big-parallel" ];
+
+    meta = getMeta "Distributed storage system";
+
+    passthru.version = version;
+    passthru.tests = { inherit (nixosTests) ceph-single-node ceph-multi-node ceph-single-node-bluestore; };
+  };
+
+  ceph-client = runCommand "ceph-client-${version}" {
+      meta = getMeta "Tools needed to mount Ceph's RADOS Block Devices";
+    } ''
+      mkdir -p $out/{bin,etc,${sitePackages},share/bash-completion/completions}
+      cp -r ${ceph}/bin/{ceph,.ceph-wrapped,rados,rbd,rbdmap} $out/bin
+      cp -r ${ceph}/bin/ceph-{authtool,conf,dencoder,rbdnamer,syn} $out/bin
+      cp -r ${ceph}/bin/rbd-replay* $out/bin
+      cp -r ${ceph}/${sitePackages} $out/${sitePackages}
+      cp -r ${ceph}/etc/bash_completion.d $out/share/bash-completion/completions
+      # wrapPythonPrograms modifies .ceph-wrapped, so lets just update its paths
+      substituteInPlace $out/bin/ceph          --replace ${ceph} $out
+      substituteInPlace $out/bin/.ceph-wrapped --replace ${ceph} $out
+   '';
+}
diff --git a/ops/machines.nix b/ops/machines.nix
index 0e63228..9a54c56 100644
--- a/ops/machines.nix
+++ b/ops/machines.nix
@@ -3,60 +3,135 @@
 # This allows to have a common attrset of machines that can be deployed
 # in the same way.
 #
-# Currently building/deployment is still done in a half-assed way:
-#
-#    machine=edge01.waw.bgp.wtf
-#    d=$(nix-build -A 'ops.machines."'$machine'"'.toplevel)
-#
-# To then deploy derivation $d on $machine:
-#
-#    nix-copy-closure --to root@$machine $d
-#    ssh root@$machine $d/bin/switch-to-configuration dry-activate
-#    ssh root@$machine $d/bin/switch-to-configuration test
-#    ssh root@$machine nix-env -p /nix/var/nix/profiles/system --set $d
-#    ssh root@$machine $d/bin/switch-to-configuration boot
-#
-# TODO(q3k): merge this with //cluster/clustercfg - this should be unified!
+# For information about building/deploying machines see //ops/README.md.
 
 { hscloud, pkgs, ... }:
 
 let
+  # nixpkgs for cluster machines (.hswaw.net). Currently pinned to an old
+  # nixpkgs because NixOS modules for kubernetes changed enough that it's not
+  # super easy to use them as is.
+  #
+  # TODO(q3k): fix this: use an old nixpkgs for Kube modules while using
+  # hscloud nixpkgs for everything else.
+  nixpkgsCluster = import (pkgs.fetchFromGitHub {
+    owner = "nixos";
+    repo = "nixpkgs-channels";
+    rev = "44ad80ab1036c5cc83ada4bfa451dac9939f2a10";
+    sha256 = "1b61nzvy0d46cspy07szkc0rggacxiqg9v1py27pkqpj7rvawfsk";
+  }) {
+    overlays = [
+      (self: super: rec {
+        # Use a newer version of Ceph (16, Pacific, EOL 2023-06-01) than in
+        # this nixpkgs (15, Octopus, EOL 2022-06-01).
+        #
+        # This is to:
+        #  1. Fix a bug in which ceph-volume lvm create fails due to a rocksdb
+        #     mismatch (https://tracker.ceph.com/issues/49815)
+        #  2. At the time of deployment not start out with an ancient version
+        #     of Ceph.
+        #
+        # Once we unpin nixpkgsCluster past a version that contains this Ceph,
+        # this can be unoverlayed.
+        inherit (super.callPackages ./ceph {
+          boost = super.boost17x.override { enablePython = true; python = super.python3; };
+          lua = super.lua5_4;
+        }) ceph ceph-client;
+        ceph-lib = ceph.lib;
+      })
+    ];
+  };
+
+  # edge01 still lives on an old nixpkgs checkout.
+  #
+  # TODO(b/3): unpin and deploy.
+  nixpkgsBgpwtf = import (pkgs.fetchFromGitHub {
+    owner = "nixos";
+    repo = "nixpkgs-channels";
+    rev = "c59ea8b8a0e7f927e7291c14ea6cd1bd3a16ff38";
+    sha256 = "1ak7jqx94fjhc68xh1lh35kh3w3ndbadprrb762qgvcfb8351x8v";
+  }) {};
+
   # Stopgap measure to import //cluster/nix machine definitions into new
-  # //ops/machines infrastructure.
+  # //ops/ infrastructure.
+  #
   # TODO(q3k): inject defs-cluster-k0.nix / defs-machines.nix content via
   # nixos options instead of having module definitions loading it themselves,
   # deduplicate list of machines below with defs-machines.nix somehow.
-  mkClusterMachine = name: pkgs.nixos ({ config, pkgs, ... }: {
+  clusterMachineConfig = name: [({ config, pkgs, ...}: {
     # The hostname is used by //cluster/nix machinery to load the appropriate
     # config from defs-machines into defs-cluster-k0.
     networking.hostName = name;
     imports = [
       ../cluster/nix/modules/base.nix
       ../cluster/nix/modules/kubernetes.nix
+      ../cluster/nix/modules/ceph.nix
     ];
-  });
+  })];
 
+  # mkMachine builds NixOS modules into a NixOS derivation, and injects
+  # passthru.hscloud.provision which deploys that configuration over SSH to a
+  # production machine.
   mkMachine = pkgs: paths: pkgs.nixos ({ config, pkgs, ... }: {
     imports = paths;
+
+    config = let
+      name = config.networking.hostName;
+      domain = if (config.networking ? domain) && config.networking.domain != null then config.networking.domain else "hswaw.net";
+      fqdn = name + "." + domain;
+      toplevel = config.system.build.toplevel;
+
+      runProvision = ''
+        #!/bin/sh
+        set -eu
+        remote=root@${fqdn}
+        echo "Configuration for ${fqdn} is ${toplevel}"
+        nix copy -s --to ssh://$remote ${toplevel}
+
+        running="$(ssh $remote readlink -f /nix/var/nix/profiles/system)"
+        if [ "$running" == "${toplevel}" ]; then
+          echo "${fqdn} already running ${toplevel}."
+        else
+          echo "/etc/systemd/system diff:"
+          ssh $remote diff -ur /var/run/current-system/etc/systemd/system ${toplevel}/etc/systemd/system || true
+          echo ""
+          echo ""
+          echo "dry-activate diff:"
+          ssh $remote ${toplevel}/bin/switch-to-configuration dry-activate
+          read -p "Do you want to switch to this configuration? " -n 1 -r
+          echo
+          if ! [[ $REPLY =~ ^[Yy]$ ]]; then
+            exit 1
+          fi
+
+          echo -ne "\n\nswitch-to-configuration test...\n"
+          ssh $remote ${toplevel}/bin/switch-to-configuration test
+        fi
+
+        echo -ne "\n\n"
+        read -p "Do you want to set this configuration as boot? " -n 1 -r
+        echo
+        if ! [[ $REPLY =~ ^[Yy]$ ]]; then
+            exit 1
+        fi
+
+        echo -ne "\n\nsetting system profile...\n"
+        ssh $remote nix-env -p /nix/var/nix/profiles/system --set ${toplevel}
+
+        echo -ne "\n\nswitch-to-configuration boot...\n"
+        ssh $remote ${toplevel}/bin/switch-to-configuration boot
+      '';
+    in {
+      passthru.hscloud.provision = pkgs.writeScript "provision-${fqdn}" runProvision;
+    };
   });
-
 in {
-  "bc01n01.hswaw.net" = mkClusterMachine "bc01n01";
-  "bc01n02.hswaw.net" = mkClusterMachine "bc01n02";
-  "bc01n03.hswaw.net" = mkClusterMachine "bc01n03";
-  "dcr01s22.hswaw.net" = mkClusterMachine "dcr01s22";
-  "dcr01s24.hswaw.net" = mkClusterMachine "dcr01s24";
+  "bc01n01.hswaw.net"  = mkMachine nixpkgsCluster (clusterMachineConfig "bc01n01");
+  "bc01n02.hswaw.net"  = mkMachine nixpkgsCluster (clusterMachineConfig "bc01n02");
+  "dcr01s22.hswaw.net" = mkMachine nixpkgsCluster (clusterMachineConfig "dcr01s22");
+  "dcr01s24.hswaw.net" = mkMachine nixpkgsCluster (clusterMachineConfig "dcr01s24");
 
-  # edge01 still lives on an old nixpkgs checkout.
-  # TODO(b/3): unpin and deploy.
-  "edge01.waw.bgp.wtf" = mkMachine (
-    import (pkgs.fetchFromGitHub {
-      owner = "nixos";
-      repo = "nixpkgs-channels";
-      rev = "c59ea8b8a0e7f927e7291c14ea6cd1bd3a16ff38";
-      sha256 = "1ak7jqx94fjhc68xh1lh35kh3w3ndbadprrb762qgvcfb8351x8v";
-    }) {}
-  ) [
+  "edge01.waw.bgp.wtf" = mkMachine nixpkgsBgpwtf [
     ../bgpwtf/machines/edge01.waw.bgp.wtf.nix
     ../bgpwtf/machines/edge01.waw.bgp.wtf-hardware.nix
   ];
diff --git a/ops/provision.nix b/ops/provision.nix
new file mode 100644
index 0000000..76054c4
--- /dev/null
+++ b/ops/provision.nix
@@ -0,0 +1,74 @@
+# Top-level wrapper script for calling per-machine provisioners.
+#
+# Given ops.machines."edge01.waw.bgp.wtf".config.passthru.hscloud.provision,
+# this script allows to run it by doing:
+#   $ $(nix-build -A ops.provision) edge01.waw.bgp.wtf
+# Or, to first list all available machines by doing:
+#   $ $(nix-build -A ops.provision)
+#
+# The main logic of the provisioner script is in machines.nix.
+
+{ hscloud, pkgs, lib, ... }:
+
+with lib; with builtins;
+
+let
+
+  # All machines from ops.machines, keyed by FQDN.
+  machines = filterAttrs (n: _: n != "__readTree") hscloud.ops.machines;
+  # Machines' provisioner scripts, keyed by machine FQDN.
+  machineProvisioners = mapAttrs (_: v: v.config.passthru.hscloud.provision) machines;
+  # List of machine FQDNs.
+  machineNames = attrNames machines;
+
+  # User-friendly list of machines by FQDN.
+  machineList = concatStringsSep "\n"
+    (map
+      (name: "  - ${name}")
+      machineNames);
+
+  # Derivation containing bin/provision-FQDN symlinks to machines' provisioners.
+  forest = pkgs.linkFarm "provision-forest"
+    (mapAttrsToList
+      (fqdn: p: { name = "bin/provision-${fqdn}"; path = p; })
+      machineProvisioners);
+in
+
+pkgs.writeScript "provision" ''
+  #!/bin/sh
+  name="$1"
+
+  usage() {
+    echo >&2 "Usage: $0 machine|machine.hswaw.net"
+    echo >&2 "Available machines:"
+    echo >&2 "${machineList}"
+  }
+
+  if [ -z "$name" ]; then
+    usage
+    exit 1
+  fi
+
+  provisioner="${forest}/bin/provision-$name"
+  if [ ! -e "$provisioner" ]; then
+    name="$name.hswaw.net"
+    provisioner="${forest}/bin/provision-$name"
+  fi
+  if [ ! -e "$provisioner" ]; then
+    usage
+    exit 1
+  fi
+  # :^)
+  echo -ne "\e[34mh \e[31ms \e[33mc l \e[34mo \e[32mu \e[31md \e[0m"
+  echo ""
+  echo "Starting provisioner for $name..."
+  echo ""
+  echo "Too slow to evaluate? Equivalent faster command line that rebuilds just one node:"
+  echo "  \$(nix-build -A 'ops.machines.\"$name\".config.passthru.hscloud.provision')"
+  echo ""
+  echo "Or, if you want to deploy the same configuration on different machines, just run"
+  echo "this script again without re-evaluating nix:"
+  echo "  $0 $name"
+  echo ""
+  exec "$provisioner"
+''
diff --git a/personal/arsenicum/test.md b/personal/arsenicum/test.md
new file mode 100644
index 0000000..064b118
--- /dev/null
+++ b/personal/arsenicum/test.md
@@ -0,0 +1 @@
+test23
\ No newline at end of file
diff --git a/personal/q3k/b/32/BUILD.bazel b/personal/q3k/b/32/BUILD.bazel
new file mode 100644
index 0000000..ae285a1
--- /dev/null
+++ b/personal/q3k/b/32/BUILD.bazel
@@ -0,0 +1,10 @@
+load("@rules_python//python:defs.bzl", "py_binary")
+load("@pydeps//:requirements.bzl", "requirement")
+
+py_binary(
+    name = "cleanup",
+    srcs = ["cleanup.py"],
+    deps = [
+        requirement("psycopg2"),
+    ],
+)
diff --git a/personal/q3k/b/32/cleanup.py b/personal/q3k/b/32/cleanup.py
new file mode 100644
index 0000000..3ded775
--- /dev/null
+++ b/personal/q3k/b/32/cleanup.py
@@ -0,0 +1,150 @@
+# Script to attempt to clean up our owncloud database (b/32) after The Postgres
+# Fuckup (b/30).
+#
+# Think of it as a one-shot fsck, documented in the form of the code that q3k@
+# used to recover from this kerfuffle.
+#
+# SECURITY: It's full of manual SQL query crafting without parametrization.
+# Don't attempt to use it for anything else other than this one-shot usecase.
+#
+# You will need to tunnel to the postgreses running on Boston:
+#    $ ssh \
+#        -L15432:127.0.0.1:5432 \
+#        -L15433:127.0.0.1:5433 \
+#        hackerspace.pl
+
+from datetime import datetime
+import os
+
+import psycopg2
+
+
+incident_start = 1611529200 # when pg12 started to run
+incident_end = 1611788400 # when we rolled back to pg9
+
+
+OWNCLOUD_PASSWORD = os.environ.get("OWNCLOUD_PASSWORD").strip()
+if not OWNCLOUD_PASSWORD:
+    # Get it from boston, /var/www/owncloud/config/config.php.
+    raise Exception("OWNCLOUD_PASSWORD must be set to owncloud postgres password")
+
+
+conn9 = psycopg2.connect(host="localhost", port=15432, user="owncloud", password=OWNCLOUD_PASSWORD, dbname="owncloud")
+conn12 = psycopg2.connect(host="localhost", port=15433, user="owncloud", password=OWNCLOUD_PASSWORD, dbname="owncloud")
+
+
+def idset(conn, table, keyname="id"):
+    """Return a set of IDs from a given table, one per row."""
+    cur = conn.cursor()
+    cur.execute(f"SELECT {keyname} FROM oc_{table}")
+    res = cur.fetchall()
+    cur.close()
+    return set([r[0] for r in res])
+
+
+def valset(conn, table, keys):
+    """Return a set of concatenated values for the given keys in a table, one per row."""
+    keynames = ", ".join(keys)
+    cur = conn.cursor()
+    cur.execute(f"SELECT {keynames} FROM oc_{table}")
+    res = cur.fetchall()
+    cur.close()
+    res = [';;;'.join([str(elem) for elem in r]) for r in res]
+    return set(res)
+
+
+# Check accounts difference.
+#
+# RESULT: Thankfully, no accounts have been accidentally roled back.
+accounts12 = idset(conn12, "accounts", keyname="uid")
+accounts9 = idset(conn9, "accounts", keyname="uid")
+print("Accounts missing in 9:", accounts12 - accounts9)
+assert (accounts12 - accounts9) == set()
+
+
+def account_by_uid(conn, uid):
+    """Return SSO UID for a given Owncloud UID."""
+    cur = conn.cursor()
+    cur.execute(f"SELECT ldap_dn FROM oc_ldap_user_mapping WHERE owncloud_name = '{uid}'")
+    dn, = cur.fetchone()
+    cur.close()
+    part = dn.split(',')[0]
+    assert part.startswith('uid=')
+    return part[4:]
+
+
+def storage_owner_by_id(conn, id_):
+    """Return SSO UID for a given storage numerical ID."""
+    cur = conn.cursor()
+    cur.execute(f"SELECT id FROM oc_storages WHERE numeric_id = '{id_}'")
+    oid, = cur.fetchone()
+    cur.close()
+    if oid == 'object::store:amazon::nextcloud':
+        return "S3"
+    assert oid.startswith('object::user:')
+    userid = oid[13:]
+    assert len(userid) > 0
+    if userid == "gallery":
+        return "GALLERY"
+    return account_by_uid(conn, userid)
+
+
+# Check shares table. This table contains the intent of sharing some file with someone else.
+#
+# RESULT: we only have things that have been removed after rollback to PG9,
+# nothing was created in PG12 and lost.
+shareids12 = idset(conn12, "share")
+shareids9 = idset(conn9, "share")
+print("Shares missing in 9:", len(shareids12 - shareids9))
+cur12 = conn12.cursor()
+for id_ in list(shareids12-shareids9):
+    cur12.execute(f"SELECT uid_owner, file_target, stime, share_with FROM oc_share WHERE id = {id_}")
+    uid_owner, file_target, stime, share_with = cur12.fetchone()
+    account = account_by_uid(conn12, uid_owner)
+    stime_human = datetime.utcfromtimestamp(stime).strftime('%Y-%m-%d %H:%M:%S')
+    print(f"Missing share {id_} {file_target} owned by {account}..")
+    if stime < incident_start or stime > incident_end:
+        print(f"  Skipping, created at {stime_human}")
+        continue
+    raise Exception("Unhandled.")
+cur12.close()
+
+
+# Check mounts table. This contains root file storages for each user, but also
+# incoming shares 'mounted' into a user's account.
+# From what I cen tell, storage_id/root_id are the source path that's being
+# mounted (root_id being the fileid inside an oc_filecache, and storage_id
+# being the storage in which that file is kept), while user_id/mount_point are
+# the mount destination (ie. path into which this is mounted for a user's
+# view).
+#
+# RESULT: we only have share-mounts missing for a handful of users. We choose
+# to ignore it, as we assume next time these users log in they will get the
+# mounts again.
+# TODO(q3k): verify this
+mounts12 = valset(conn12, "mounts", ["storage_id", "root_id", "user_id", "mount_point"])
+mounts9 = valset(conn9, "mounts", ["storage_id", "root_id", "user_id", "mount_point"])
+print("Mounts missing in 9:", len(mounts12 - mounts9))
+# Mounts that appearify normally whenever you log into owncloud, as they are the result of shares':
+mount_names_ok = set(["2020-03-26_covid_templar", "camera", "Public Shaming", "przylbice.md", "Test.txt", "covid"])
+# Mounts that used to be from a share that existed, but has been since deleted in PG9.
+mount_names_ok |= set(["Covid-instrukcje", "Chaos_modele_covid", "Covid_proces_presspack"])
+mounts_sorted = []
+for m in list(mounts12 - mounts9):
+    storage_id, root_id, user_id, mount_point = m.split(';;;')
+    mounts_sorted.append((storage_id, root_id, user_id, mount_point))
+mounts_sorted = sorted(mounts_sorted, key=lambda el: el[2])
+for storage_id, root_id, user_id, mount_point in mounts_sorted:
+    assert mount_point.startswith("/" + user_id + "/")
+    mount_point = mount_point[len(user_id)+1:]
+    account = account_by_uid(conn12, user_id)
+    print(f"Missing mount {mount_point}, storage ID {storage_id}, owned by {account}..")
+    storage_owner = storage_owner_by_id(conn12, storage_id)
+    print(f"  Storage owner: {storage_owner}")
+
+    parts = mount_point.split('/')
+    if len(parts) == 4 and parts[0] == '' and parts[1] == 'files' and parts[2] in mount_names_ok and parts[3] == '':
+        print("  Skipping, known okay")
+        continue
+    raise Exception("Unhandled")
+
diff --git a/personal/q3k/b/32/secrets/cipher/log.txt b/personal/q3k/b/32/secrets/cipher/log.txt
new file mode 100644
index 0000000..1546b07
--- /dev/null
+++ b/personal/q3k/b/32/secrets/cipher/log.txt
@@ -0,0 +1,58 @@
+-----BEGIN PGP MESSAGE-----
+
+hQEMAzhuiT4RC8VbAQf9Ecn9tDsi84AKCyPySLpGFBj5Fp8Rc/9b3RA5f4614tqE
+2LKn5UQP7Ejg6LzCZEBlplu4CFBM5fVe+sx0pZNTVdi+qOPFYB4ruV0TLLjacaAY
+6hyD7mzmMEWVhHYHJpqCwUV8Vx7vN/6SG91nObCuEjbfYrAXjwdiXvDiBumH6d9O
+N5CTh5EYVqazZD4SvSXYPyG/iv6/1nrxlfYA/LxD+ULVPhjKBboNTjfVtjAhQVvD
+ZRMl4/rP1/WXRXz7svGaENTEG6TQ95ZrnSYPZQD+amWKumRiCdneN6I3N4s2F9dJ
++FP5WJ214rUAx/rusf+Gt5v2FPyfNw6QJR41WFrjhIUBDANcG2tp6fXqvgEH/0pJ
+Hlwe6F1ltWm5gdhUUJ5+/tJQH3e4cq0EI+26dENE9633pVA8ERFY3zmcTcxOZ04k
+K9r8+ifiagdIvWldNLKXrHaNxZWM+r4fJ1RTo0x/gskid8e9otdxo9kR8t3yh9SZ
+DHRSlCKr2+/H167h89dPWtJh5meUPWAlw2Zmcl2GDue5zjDVrHEkZ8fbxOwf9E/o
+J93oaux9Ijfrp8tP7lO90qAoaXvTAMeI7hnkWHnX2akcVh03U4FgDqLpZSlVCRP/
+mIFXRnaLuZEFdmrO+raZUi58t8xaadTZy9hsmCtlsgSKIwgA8zsF2CmJCRvZD95w
+Q70vLg91E7Bfyr1duhKFAgwDodoT8VqRl4UBEACoBXEXQaxVvZ3vClxUkxrq468/
+YC6NyTXWOt9KvpuGUWCtbFIQQ4CSfRu8UpvPasppY5shLE4L+0f79OV8aQfV4mF+
+EdHZ0zCWuVIBcYh+iiAsz5zYpbB7vSW+J+DG7ZH0pCVJZ8CSTJhK42BmyDh0a6ut
+glHqLQaeC6dPur5To+C+Ozl8v6GduJDzZQ5ERCaML0nhauH0yoEe3My52BlEZ9MK
+mjJlm3Q3VCYTfT8M08ZLTART6zMotYTemE/u+U8rVVPPDY+7kgy8yt44QAoUzE49
+shr5llK9GBjDeo4URf//0KvLs95H4TdGVYMfELcFyqyCs0YJ2vee2KvoyuXPGmkO
+6kuASSgGn5zFqdX13P0y4QIC5OxWnX22CxMdXpvG18RqCW2qyMT3wscqTCYuIHyc
+yPZWfi+MM1tDB+CdQbFLus5gWPUSZ8kK+sur/5lb0ToamWvYHhNTKMOBh8MCODAy
+F6lxrUIc81uojF8VKbgK+lOyQUtL4R3+1Wbk+9TyBYUJNjMi/S2mpL1bOUkKZkpb
+hmqloSPP7W8xa9JuxFDgyPLWpmGSJkDz+6MPiAPfPBgmczUORNuGReck4SDYlZib
+SRWrhs54zkmlZPmFCEoqbLqyFfQiNqLhQ8J/ya//HZuXAUNd9612P2a+idAAE6gx
+pz54H+jeH9o8Db01BYUCDAPiA8lOXOuz7wEP/1Uw8k0t4kPrkD1hZ0p2KFklyM6O
+Ri3j1Y2IcYcQZ23OKMx5qo90/aLBzYSRtA/NHWuqcaDjudyFJ12lSTNyQX0sFnUP
+nbig6smYzOu3XnkTnRBrOe4YN5YJiyUFTsK5wPcUcArCuLASvRCxzkwHyTQTnW6Y
+r92oArKEzXzE8sYFMPpRYpV29sQgqXUBEq0bA9codN1Z1m5N3aGvMiYyimq4jXoq
+Va+Tsry5KON2S0/h8UZsLnY/USSXjWdhb266tU9MLgY2EIK9DaTfU6mxsMTLysVQ
+RhBmtHQhzczkfueMYa7KigbXJNxvEjUlR22RVRiH9F3lhhismsW1xtgfI/IlGmQx
+6uhCFMDIsgZh39kWRP4vUxzWTvnPSD76omBcdjVTKGDEd8vqEwIBeORg9E6NoN+Q
+8HR6Fb6y0pAk30VO1mP3xC0Li9q13ips1p1w+Xu9WOPFVEwJaSFn1oaEWTuQttn3
+gPdng35LjYLnch588exe1bhoj6WiUaCclZF5yjewMCGowlvCO05QEyivRj2YGeHS
+D+oH+dv/Ex0PIJR/8P+clAcB/u3qQl2W40pPjHOmMGb8Gi+GSVU5HX0lENdmMrfw
+QfZuqKh08j8gVK77yX0UjtNhN0XVu5toCncgSFLxZSkQd4opZTonmdji0vwSapx2
+FbPI2PVqRRWjknuj0ukBu3zMhA+90PvWjmWxaSVBs0FDOvOApYUf7QwYuVpk/hkN
+GFqrarltId1Xy2UQSLHdKgb1fj4OhnVSApK9cKxD0mjJ87QibQFLeR2KCIOX4/EJ
+qAuC7xV8VTtzFQqOYF1RrXN8NbU/htMAecKYX3mj3S9FXgOUHEj/fYdFHuFhVhdw
+onJZ1CReuQX6nGU6c7bCKj7mvt/QISq7eiPT2zqt+f0X+Bz6ODkk/5QHyu2mQUTt
+PQOayDWpi5vcNsiMj8DLR7nLjErHau/joIyRMopssYzdDb5d2tsofIjoL9VzYbUu
+PTs/VJCIJ7XvRt1SA0pyQ9JUrvNhskz095CVuPF1LoA8HlcHWCUhiT19bWegsYIu
+rFqglNCrNuN2t0mQpeA9108EB8m5bmaEK6tlepwhqno3S35KnYGT+FZTo8A3xt38
+sOATC71bTPyLSNklXK+7t4YPD5nulINNLDqoxUe7ruCwZRkiWZHbp0+AON/jk5CZ
+O9pNUPA524+sLZzY1XGumifv+f7H4vExOhAMWsPAVfwBdwW4//wIeafV65RMsEUD
+SzMUfdNQVvf6w1YP3MpDvCHOLlCTrMf8fKKQAkT6Dj/nGHt5bVA10IYY/jaIdXj0
+VsONFWWaqKxmh9kVGmHdjvZ/tnDFXRPXS+3ddA/jqfVt2JzebHBDeropYYmDXMEe
+wA4Nbl5qjSoxFsmVMNROIXMrMv66LVG3kVxuHUix147qix8JTOWCFlgaBBvp16Dv
+8k52poapN6QnMnR12QWxZyroZEeTV4RvNCB/QdoVHEcx8/XBmE3psSceUh9GNU6S
+BZsb+68KosvRV9bFDcg0DJ+Qyp7k25F4+ItdSxIceW0phAQft9GJafehvnXaQj9A
+vRLuLTtM37QL8YX7vs5DKJAzG7RCgmrUfbVS6BnWhLAaUWTFgQXHd9gtw2XCkb3y
+GUCztuO3t5zRwxTlvXWt1KBdLvIm4xrmU/yfuUOK2eLmwdH+rqouVUW1fRrTXhZD
+OGyvSLIB/kujiukjIJ4idBImzmJcqMewdEdiYw/zsns3RZrfFop1IBZ8fpdAXY4m
+S0j9Yhy9qqyZ+h9ZYFG5vK6xdevaeqIGGpc7Zk7xTZjXrA+kbrjZyJa6LMUF9dFH
+MJx4tytpu546euLWP2t9OEu7T+0b9sOrQCrNNIS+qDPN37FNzvxOwCf+i5xr00nI
+7ifX1c5tA/K6b/IhDVcACqfnUAcgPg+S2qkRtleATzKE0g2ISozbw2/LNb/Th3L1
+/fH2VmHZyTHJg06PpLsaqnNFpiwIS3Kb2RKtgo3ZNLg+S6QVUd90wEMEX+cdgg==
+=aO5Z
+-----END PGP MESSAGE-----
diff --git a/personal/q3k/minecraft/Dockerfile-paper b/personal/q3k/minecraft/Dockerfile-paper
index d9b6c67..9f4db69 100644
--- a/personal/q3k/minecraft/Dockerfile-paper
+++ b/personal/q3k/minecraft/Dockerfile-paper
@@ -4,7 +4,7 @@
     export DEBIAN_FRONTEND=noninteractive ;\
     apt-get -y update ;\
     apt-get -y upgrade ;\
-    apt-get -y install git openjdk-8-jre-headless wget
+    apt-get -y install git openjdk-8-jre-headless wget unzip
 
 RUN set -e -x ;\
     export DEBIAN_FRONTEND=noninteractive ;\
@@ -26,7 +26,7 @@
 
 USER minecraft
 WORKDIR /home/minecraft
-ARG VERSION=1.16.4
+ARG VERSION=1.16.5
 
 RUN set -e -x ;\
     wget --quiet https://papermc.io/api/v1/paper/${VERSION}/latest/download ;\
diff --git a/personal/q3k/minecraft/Dockerfile-vanilla-1.16.5 b/personal/q3k/minecraft/Dockerfile-vanilla-1.16.5
new file mode 100644
index 0000000..59bcdef
--- /dev/null
+++ b/personal/q3k/minecraft/Dockerfile-vanilla-1.16.5
@@ -0,0 +1,16 @@
+FROM ubuntu:20.04
+
+RUN set -e -x ;\
+    export DEBIAN_FRONTEND=noninteractive ;\
+    apt-get -y update ;\
+    apt-get -y upgrade ;\
+    apt-get -y install git openjdk-8-jre-headless wget unzip
+
+RUN set -e -x ;\
+    useradd -rm minecraft
+
+USER minecraft
+WORKDIR /home/minecraft
+
+RUN set -e -x ;\
+    wget --quiet -O server.jar https://launcher.mojang.com/v1/objects/1b557e7b033b583cd9f66746b7a9ab1ec1673ced/server.jar
diff --git a/personal/q3k/minecraft/prod.jsonnet b/personal/q3k/minecraft/prod.jsonnet
index 6c4bd4b..c7493e0 100644
--- a/personal/q3k/minecraft/prod.jsonnet
+++ b/personal/q3k/minecraft/prod.jsonnet
@@ -7,12 +7,16 @@
         "spigot-1.16.1": "registry.k0.hswaw.net/q3k/minecraft:spigot-1.16.1-r2",
         "paper-1.16.1": "registry.k0.hswaw.net/q3k/minecraft:paper-1.16.1-r2",
         "paper-1.16.4": "registry.k0.hswaw.net/q3k/minecraft:paper-1.16.4-r1",
+        "paper-1.16.5": "registry.k0.hswaw.net/q3k/minecraft:paper-1.16.5-r2",
+        "vanilla-1.16.5": "registry.k0.hswaw.net/enleth/minectaft:vanilla-1.16.5",
     },
     server(name, version):: {
         local server = self,
         name:: name,
         version:: version,
         image:: minecraft.versions[server.version],
+        worldedit:: true,
+        overviewer:: true,
 
         metadata:: {
             namespace: "minecraft",
@@ -70,20 +74,25 @@
 
         worldguardConfig:: defaultWorldguardConfig,
 
+        startSteps:: [
+        ] + (if server.worldedit then [
+            "mkdir -p plugins/WorldGuard",
+            "cp /home/minecraft/worldedit-*.jar plugins",
+            "cp /home/minecraft/worldguard-*.jar plugins",
+            "cp /home/minecraft/config/worldguard_config.yaml plugins/WorldGuard/config.yml",
+        ] else []),
+
         startsh:: |||
             #!/usr/bin/env bash
             cd /home/minecraft/world
             cp /home/minecraft/config/server.properties .
             cp /home/minecraft/server.jar .
-            mkdir -p plugins/WorldGuard
-            cp /home/minecraft/worldedit-*.jar plugins
-            cp /home/minecraft/worldguard-*.jar plugins
-            cp /home/minecraft/config/worldguard_config.yaml plugins/WorldGuard/config.yml
             echo "eula=true" > eula.txt
+            %s
 
             bash /home/minecraft/config/overviewer.sh &
             exec java -Xmx4G -Xms4G -jar server.jar
-        |||,
+        ||| % [std.join("\n", server.startSteps)],
 
         overviewersh:: |||
             #!/usr/bin/env bash
@@ -102,11 +111,13 @@
             data: {
                 local properties = std.join("\n", ["%s=%s" % [k, std.toString(server.properties[k])] for k in std.objectFields(server.properties)]),
                 "server.properties": std.base64(properties),
+                "start.sh": std.base64(server.startsh),
+            } + (if server.worldedit then {
                 local worldguardConfig = std.manifestYamlDoc(server.worldguardConfig),
                 "worldguard_config.yaml": std.base64(worldguardConfig),
-                "start.sh": std.base64(server.startsh),
+            } else {} )+ (if server.overviewer then {
                 "overviewer.sh": std.base64(server.overviewersh),
-            },
+            } else {}),
         },
 
         worldVolume: kube.PersistentVolumeClaim(server.componentName("world")) {
@@ -156,6 +167,17 @@
                                     },
                                 },
                             },
+                            bridge: kube.Container("bridge") {
+                                image: "registry.k0.hswaw.net/q3k/minecraft-hscloud-bridge:20200518c",
+                                command: [
+                                    "/personal/q3k/minecraft/plugin/hscloud/bridge/bridge",
+                                    "-plugin", "127.0.0.1:2137",
+                                ],
+                                ports_: {
+                                    bridge: { containerPort: 8081 },
+                                },
+                            },
+                        } + (if server.overviewer then {
                             overviewer: kube.Container("overviewer") {
                                 image: "halverneus/static-file-server:v1.8.0",
                                 env_: {
@@ -168,17 +190,7 @@
                                     web: { containerPort: 8080 },
                                 },
                             },
-                            bridge: kube.Container("bridge") {
-                                image: "registry.k0.hswaw.net/q3k/minecraft-hscloud-bridge:20200518c",
-                                command: [
-                                    "/personal/q3k/minecraft/plugin/hscloud/bridge/bridge",
-                                    "-plugin", "127.0.0.1:2137",
-                                ],
-                                ports_: {
-                                    bridge: { containerPort: 8081 },
-                                },
-                            },
-                        },
+                        } else {}),
                     },
                 },
             },
@@ -202,17 +214,47 @@
 
     ns: kube.Namespace("minecraft"),
 
+    admins: minecraft.ns.Contain(kube.RoleBinding("admins")) {
+        roleRef: {
+            apiGroup: "rbac.authorization.k8s.io",
+            kind: "ClusterRole",
+            name: "system:admin-namespace",
+        },
+        subjects: [
+            kube.User("enleth@hackerspace.pl"),
+        ],
+    },
+
     q3k: {
-        survival: minecraft.server("q3k-survival", "paper-1.16.4") {
+        "nova-arcana": minecraft.server("q3k-nova-arcana", "vanilla-1.16.5") {
+            overviewer: false,
+            worldedit: false,
             properties+: {
-                motd: "wypierdol z polski kropka pe el",
-                "enforce-whitelist": true,
+                motd: "Nova Arcana V1.1.4",
+                //"enforce-whitelist": true,
+                "enable-rcon": "true",
+                "rcon.password": "dupa.8",
+                "enable-command-block": true,
             },
-            worldguardConfig+: {
-                mobs+: {
-                    "block-creeper-block-damage": true,
-                },
-            },
+            startSteps+: [
+                |||
+                    if [ ! -e world/map-installed.txt ]; then
+                        set -e -x
+                        mkdir -p world
+                        cd world
+                        rm -rf *
+                        wget https://object.ceph-waw3.hswaw.net/q3k-personal/f1a73ad0518a2629a5bed072a7de4e4534a3c89705d6cea2f203a05cccd01634.zip -O map.zip
+                        unzip -o map.zip
+                        mv Untold*/* .
+                        rm -rf Untold*
+                        rm map.zip
+                        touch map-installed.txt
+                        ls -la
+                        cd ..
+                        set +e +x
+                    fi
+                |||
+            ],
         },
     },
 }
diff --git a/personal/q3k/ppsa.jsonnet b/personal/q3k/ppsa.jsonnet
new file mode 100644
index 0000000..46eda70
--- /dev/null
+++ b/personal/q3k/ppsa.jsonnet
@@ -0,0 +1,61 @@
+local kube = import "../../kube/kube.libsonnet";
+
+{
+    local top = self,
+    ns: kube.Namespace("personal-q3k"),
+
+    deploy: top.ns.Contain(kube.Deployment("ppsa-jsonapi")) {
+        spec+: {
+            template+: {
+                spec+: {
+                    containers_: {
+                        default: kube.Container("default") {
+                            image: "registry.k0.hswaw.net/q3k/ppsa-jsonapi:1615508489",
+                            ports_: {
+                                http: { containerPort: 8080 },
+                            },
+                            resources: {
+                                requests: {
+                                    cpu: "10m",
+                                    memory: "64M",
+                                },
+                                limits: {
+                                    cpu: "100m",
+                                    memory: "256M",
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    },
+    svc: top.ns.Contain(kube.Service("ppsa-jsonapi")) {
+        target_pod:: top.deploy.spec.template,
+    },
+    ingress: top.ns.Contain(kube.Ingress("ppsa-jsonapi")) {
+        metadata+: {
+            annotations+: {
+                "kubernetes.io/tls-acme": "true",
+                "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
+                "nginx.ingress.kubernetes.io/proxy-body-size": "0",
+            },
+        },
+        spec+: {
+            tls: [
+                { hosts: [ "ppsa.app.q3k.org"], secretName: "ppsa-jsonapi-tls", },
+            ],
+            rules: [
+                {
+                    host: "ppsa.app.q3k.org",
+                    http: {
+                        paths: [
+                            { path: "/", backend: top.svc.name_port },
+                        ],
+                    },
+                },
+            ],
+        },
+    },
+
+}
diff --git a/personal/q3k/rc3.jsonnet b/personal/q3k/rc3.jsonnet
new file mode 100644
index 0000000..879e291
--- /dev/null
+++ b/personal/q3k/rc3.jsonnet
@@ -0,0 +1,60 @@
+local kube = import "../../kube/kube.libsonnet";
+
+{
+    local rc3 = self,
+    deploy: kube.Deployment("rc3-data") {
+        metadata+: {
+            namespace: "personal-q3k",
+        },
+        spec+: {
+            template+: {
+                spec+: {
+                    containers_: {
+                        default: kube.Container("default") {
+                            image: "registry.k0.hswaw.net/q3k/rc3-data:1610640062",
+                            ports_: {
+                                http: { containerPort: 8080 },
+                            },
+                        },
+                    },
+                    securityContext: {
+                        // nginx:nginx
+                        runAsUser: 101,
+                        runAsGroup: 101,
+                    },
+                },
+            },
+        },
+    },
+    svc: kube.Service("rc3-data") {
+        metadata+: {
+            namespace: "personal-q3k",
+        },
+        target_pod:: rc3.deploy.spec.template,
+    },
+    ingress: kube.Ingress("rc3-data") {
+        metadata+: {
+            namespace: "personal-q3k",
+            annotations+: {
+                "kubernetes.io/tls-acme": "true",
+                "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
+                "nginx.ingress.kubernetes.io/proxy-body-size": "0",
+            },
+        },
+        spec+: {
+            tls: [
+                { hosts: [ "rc3-data.q3k.org"], secretName: "rc3-data-tls", },
+            ],
+            rules: [
+                {
+                    host: "rc3-data.q3k.org",
+                    http: {
+                        paths: [
+                            { path: "/", backend: rc3.svc.name_port },
+                        ],
+                    },
+                },
+            ],
+        },
+    },
+}
diff --git a/shell.nix b/shell.nix
index 1b504cf..e52c741 100644
--- a/shell.nix
+++ b/shell.nix
@@ -4,7 +4,7 @@
 
   hscloud = import ./default.nix {};
 
-in with hscloud.config.pkgs; let
+in with hscloud.pkgs; let
 
   wrapper = pkgs.writeScript "wrapper.sh"
   ''
@@ -12,6 +12,13 @@
     source ${toString ./.}/env.sh
     ${toString ./.}/tools/install.sh
 
+    # Fancy colorful PS1 to make people notice easily they're in hscloud.
+    PS1='\[\033]0;\u/hscloud:\w\007\]'
+    if type -P dircolors >/dev/null ; then
+      PS1+='\[\033[01;35m\]\u/hscloud\[\033[01;34m\] \w \$\[\033[00m\] '
+    fi
+    export PS1
+
     exec bash "$@"
   '';
 
@@ -27,6 +34,9 @@
     gcc binutils
     pwgen
     tmate
+    git
+    which
+    nettools
   ];
   multiPkgs = pkgs: [
     (pkgs.runCommand "protocols" {}
diff --git a/third_party/go/repositories.bzl b/third_party/go/repositories.bzl
index d4355a6..dc12312 100644
--- a/third_party/go/repositories.bzl
+++ b/third_party/go/repositories.bzl
@@ -5,11 +5,13 @@
         name = "org_golang_x_net",
         commit = "d3edc9973b7eb1fb302b0ff2c62357091cea9a30",
         importpath = "golang.org/x/net",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "io_k8s_kubernetes",
         importpath = "k8s.io/kubernetes",
+        build_naming_convention = "go_default_library",
         patch_args = ["-p1"],
         patches = ["//third_party/go/kubernetes:build.patch"],
         sum = "h1:V6ohBHSxTkrPRyfVp8tbdEsgi9nfVN49xlUVkQseass=",
@@ -20,11 +22,13 @@
         name = "io_k8s_repo_infra",
         commit = "df02ded38f9506e5bbcbf21702034b4fef815f2f",
         importpath = "k8s.io/repo-infra",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_bitnami_kubecfg",
         importpath = "github.com/bitnami/kubecfg",
+        build_naming_convention = "go_default_library",
         vcs = "git",
         commit = "5070ed28ed12016b0ca75dcfd257f567f581c095",
         remote = "https://github.com/q3k/kubecfg",
@@ -34,6 +38,7 @@
     go_repository(
         name = "com_github_projectcalico_calicoctl",
         importpath = "github.com/projectcalico/calicoctl",
+        build_naming_convention = "go_default_library",
         # This fork implements explicit Bazel rules
         remote = "https://github.com/q3k/calicoctl",
         vcs = "git",
@@ -45,83 +50,99 @@
         name = "com_github_shirou_gopsutil",
         commit = "2cbc9195c892b304060269ef280375236d2fcac9",
         importpath = "github.com/shirou/gopsutil",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_cloudflare_cfssl",
         commit = "768cd563887febaad559b511aaa5964823ccb4ab",
         importpath = "github.com/cloudflare/cfssl",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_mattn_go_sqlite3",
         commit = "5994cc52dfa89a4ee21ac891b06fbc1ea02c52d3",
         importpath = "github.com/mattn/go-sqlite3",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_sebastiaanklippert_go_wkhtmltopdf",
         commit = "72a7793efd38728796273861bb27d590cc33d9d4",
         importpath = "github.com/sebastiaanklippert/go-wkhtmltopdf",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_ziutek_telnet",
         commit = "c3b780dc415b28894076b4ec975ea3ea69e3980f",
         importpath = "github.com/ziutek/telnet",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_digitalocean_go_netbox",
         commit = "29433ec527e78486ea0a5758817ab672d977f90e",
         importpath = "github.com/digitalocean/go-netbox",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_cenkalti_backoff",
         commit = "4b4cebaf850ec58f1bb1fec5bdebdf8501c2bc3f",
         importpath = "github.com/cenkalti/backoff",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
-        name = "ml_vbom_util",
-        commit = "db5cfe13f5cc",
-        importpath = "vbom.ml/util",
+        name = "ml_vbom_util_sortorder",
+        commit = "26fad50c6b32a3064c09ed089865c16f2f3615f6",
+        importpath = "vbom.ml/util/sortorder",
+        build_naming_convention = "go_default_library",
+        remote = "https://github.com/fvbommel/sortorder",
+        vcs = "git",
     )
 
     go_repository(
         name = "com_github_go_openapi_strfmt",
         importpath = "github.com/go-openapi/strfmt",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.0",
     )
 
     go_repository(
         name = "com_github_go_openapi_swag",
         importpath = "github.com/go-openapi/swag",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.5",
     )
 
     go_repository(
         name = "com_github_go_openapi_errors",
         importpath = "github.com/go-openapi/errors",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.2",
     )
 
     go_repository(
         name = "com_github_go_openapi_runtime",
         importpath = "github.com/go-openapi/runtime",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.0",
     )
 
     go_repository(
         name = "com_github_go_openapi_validate",
         importpath = "github.com/go-openapi/validate",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.2",
     )
 
     go_repository(
         name = "com_github_mitchellh_mapstructure",
         importpath = "github.com/mitchellh/mapstructure",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.2",
     )
 
@@ -129,11 +150,13 @@
         name = "org_mongodb_go_mongo_driver",
         commit = "9ec4480161a76f5267d56fc836b7f6d357fd9209",
         importpath = "go.mongodb.org/mongo-driver",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_yaml_v2",
         importpath = "gopkg.in/yaml.v2",
+        build_naming_convention = "go_default_library",
         tag = "v2.2.4",
     )
 
@@ -141,11 +164,13 @@
         name = "com_github_asaskevich_govalidator",
         commit = "f61b66f89f4a",
         importpath = "github.com/asaskevich/govalidator",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_go_openapi_spec",
         importpath = "github.com/go-openapi/spec",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.2",
     )
 
@@ -153,35 +178,41 @@
         name = "com_github_mailru_easyjson",
         commit = "b2ccc519800e",
         importpath = "github.com/mailru/easyjson",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_go_openapi_analysis",
         importpath = "github.com/go-openapi/analysis",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.2",
     )
 
     go_repository(
         name = "com_github_go_openapi_jsonpointer",
         importpath = "github.com/go-openapi/jsonpointer",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.3",
     )
 
     go_repository(
         name = "com_github_go_openapi_loads",
         importpath = "github.com/go-openapi/loads",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.2",
     )
 
     go_repository(
         name = "com_github_go_openapi_jsonreference",
         importpath = "github.com/go-openapi/jsonreference",
+        build_naming_convention = "go_default_library",
         tag = "v0.19.2",
     )
 
     go_repository(
         name = "com_github_puerkitobio_purell",
         importpath = "github.com/PuerkitoBio/purell",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.1",
     )
 
@@ -189,17 +220,20 @@
         name = "com_github_puerkitobio_urlesc",
         commit = "de5bf2ad4578",
         importpath = "github.com/PuerkitoBio/urlesc",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_abbot_go_http_auth",
         commit = "860ed7f246ff5abfdbd5c7ce618fd37b49fd3d86",
         importpath = "github.com/abbot/go-http-auth",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_urfave_cli",
         importpath = "github.com/urfave/cli",
+        build_naming_convention = "go_default_library",
         tag = "v1.20.0",
     )
 
@@ -207,23 +241,27 @@
         name = "org_golang_x_crypto",
         commit = "60c769a6c586",
         importpath = "golang.org/x/crypto",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_x_oauth2",
         commit = "0f29369cfe45",
         importpath = "golang.org/x/oauth2",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_djherbis_atime",
         commit = "2d569978378562c466df74eda2d82900f435c5f4",
         importpath = "github.com/djherbis/atime",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_google_cloud_go",
         importpath = "cloud.google.com/go",
+        build_naming_convention = "go_default_library",
         tag = "v0.38.0",
     )
 
@@ -231,17 +269,20 @@
         name = "com_github_stackexchange_wmi",
         commit = "cbe66965904dbe8a6cd589e2298e5d8b986bd7dd",
         importpath = "github.com/stackexchange/wmi",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_go_ole_go_ole",
         commit = "938323a72016e9cf84fa5fba7635089efb0ad87f",
         importpath = "github.com/go-ole/go-ole",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_dustin_go_humanize",
         importpath = "github.com/dustin/go-humanize",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
@@ -252,6 +293,7 @@
             "-known_import=github.com/googleapis/gnostic",
         ],
         importpath = "k8s.io/client-go",
+        build_naming_convention = "go_default_library",
         sum = "h1:ctqR1nQ52NUs6LpI0w+a5U+xjYwflFwA13OJKcicMxg=",
         version = "v0.19.3",
     )
@@ -260,6 +302,7 @@
         name = "io_k8s_apimachinery",
         build_file_proto_mode = "disable",
         importpath = "k8s.io/apimachinery",
+        build_naming_convention = "go_default_library",
         patch_args = ["-p1"],
         patches = ["//third_party/go/k8s-apimachinery:fix-kubernetes-bug-87675.patch"],
         sum = "h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc=",
@@ -269,6 +312,7 @@
     go_repository(
         name = "io_k8s_klog",
         importpath = "k8s.io/klog",
+        build_naming_convention = "go_default_library",
         sum = "h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=",
         version = "v1.0.0",
     )
@@ -277,6 +321,7 @@
         name = "io_k8s_utils",
         commit = "e782cd3c129f",
         importpath = "k8s.io/utils",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
@@ -284,6 +329,7 @@
         build_file_generation = "on",
         build_file_proto_mode = "disable",
         importpath = "github.com/googleapis/gnostic",
+        build_naming_convention = "go_default_library",
         sum = "h1:2qsuRm+bzgwSIKikigPASa2GhW8H2Dn4Qq7UxD8K/48=",
         version = "v0.5.3",
     )
@@ -292,6 +338,7 @@
         name = "io_k8s_api",
         build_file_proto_mode = "disable",
         importpath = "k8s.io/api",
+        build_naming_convention = "go_default_library",
         sum = "h1:GN6ntFnv44Vptj/b+OnMW7FmzkpDoIDLZRvKX3XH9aU=",
         version = "v0.19.3",
     )
@@ -300,35 +347,41 @@
         name = "org_golang_x_time",
         commit = "9d24e82272b4",
         importpath = "golang.org/x/time",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_google_gofuzz",
         importpath = "github.com/google/gofuzz",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "io_k8s_sigs_yaml",
         importpath = "sigs.k8s.io/yaml",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.0",
     )
 
     go_repository(
         name = "com_github_modern_go_reflect2",
         importpath = "github.com/modern-go/reflect2",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.1",
     )
 
     go_repository(
         name = "com_github_davecgh_go_spew",
         importpath = "github.com/davecgh/go-spew",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.1",
     )
 
     go_repository(
         name = "com_github_json_iterator_go",
         importpath = "github.com/json-iterator/go",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.8",
     )
 
@@ -336,11 +389,13 @@
         name = "com_github_modern_go_concurrent",
         commit = "bacd9c7ef1dd",
         importpath = "github.com/modern-go/concurrent",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_inf_v0",
         importpath = "gopkg.in/inf.v0",
+        build_naming_convention = "go_default_library",
         tag = "v0.9.1",
     )
 
@@ -348,11 +403,13 @@
         name = "com_github_cloudflare_cfrpki",
         commit = "adece784464315db69299ba75e9287c60cd95c69",
         importpath = "github.com/cloudflare/cfrpki",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_prometheus_client_golang",
         importpath = "github.com/prometheus/client_golang",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
@@ -360,35 +417,41 @@
         name = "com_github_rs_cors",
         commit = "db0fe48135e83b5812a5a31be0eea66984b1b521",
         importpath = "github.com/rs/cors",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_cloudflare_gortr",
         commit = "95270606e8853d9b93f5be46d656d08ec0a4ef09",
         importpath = "github.com/cloudflare/gortr",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_gorilla_mux",
         importpath = "github.com/gorilla/mux",
+        build_naming_convention = "go_default_library",
         tag = "v1.6.2",
     )
 
     go_repository(
         name = "com_github_sirupsen_logrus",
         importpath = "github.com/sirupsen/logrus",
+        build_naming_convention = "go_default_library",
         tag = "v1.4.2",
     )
 
     go_repository(
         name = "com_github_prometheus_common",
         importpath = "github.com/prometheus/common",
+        build_naming_convention = "go_default_library",
         tag = "v0.4.1",
     )
 
     go_repository(
         name = "com_github_beorn7_perks",
         importpath = "github.com/beorn7/perks",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
@@ -396,17 +459,20 @@
         name = "com_github_prometheus_client_model",
         commit = "fd36f4220a90",
         importpath = "github.com/prometheus/client_model",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_prometheus_procfs",
         importpath = "github.com/prometheus/procfs",
+        build_naming_convention = "go_default_library",
         tag = "v0.0.2",
     )
 
     go_repository(
         name = "com_github_matttproud_golang_protobuf_extensions",
         importpath = "github.com/matttproud/golang_protobuf_extensions",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.1",
     )
 
@@ -414,24 +480,28 @@
         name = "com_github_jmoiron_sqlx",
         commit = "38398a30ed8516ffda617a04c822de09df8a3ec5",
         importpath = "github.com/jmoiron/sqlx",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_lib_pq",
         commit = "3427c32cb71afc948325f299f040e53c1dd78979",
         importpath = "github.com/lib/pq",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_gchaincl_sqlhooks",
         commit = "1932c8dd22f2283687586008bf2d58c2c5c014d0",
         importpath = "github.com/gchaincl/sqlhooks",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_golang_migrate_migrate_v4",
         commit = "e93eaeb3fe21ce2ccc1365277a01863e6bc84d9c",
         importpath = "github.com/golang-migrate/migrate/v4",
+        build_naming_convention = "go_default_library",
         remote = "https://github.com/golang-migrate/migrate",
         vcs = "git",
     )
@@ -440,89 +510,104 @@
         name = "com_github_hashicorp_go_multierror",
         commit = "bdca7bb83f603b80ef756bb953fe1dafa9cd00a2",
         importpath = "github.com/hashicorp/go-multierror",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_hashicorp_errwrap",
         commit = "8a6fb523712970c966eefc6b39ed2c5e74880354",
         importpath = "github.com/hashicorp/errwrap",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_cockroachdb_cockroach_go",
         commit = "e0a95dfd547cc9c3ebaaba1a12c2afe4bf621ac5",
         importpath = "github.com/cockroachdb/cockroach-go",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_jackc_pgx",
         commit = "6954c15ad0bd3c9aa6dd1b190732b020379beb28",
         importpath = "github.com/jackc/pgx",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_golang_collections_go_datastructures",
         commit = "59788d5eb2591d3497ffb8fafed2f16fe00e7775",
         importpath = "github.com/golang-collections/go-datastructures",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_go_test_deep",
         commit = "cf67d735e69b4a4d50cdf571a92b0144786080f7",
         importpath = "github.com/go-test/deep",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_sethvargo_go_password",
         commit = "68ac5879751a7105834296859f8c1bf70b064675",
         importpath = "github.com/sethvargo/go-password",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_ldap_v3",
         commit = "9f0d712775a0973b7824a1585a86a4ea1d5263d9",
         importpath = "gopkg.in/ldap.v3",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_asn1_ber_v1",
         commit = "f715ec2f112d1e4195b827ad68cf44017a3ef2b1",
         importpath = "gopkg.in/asn1-ber.v1",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_q3k_cursedjsonrpc",
         commit = "304f0561c9162a2696f3ae7c96f3404324177ab8",
         importpath = "github.com/q3k/cursedjsonrpc",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_q3k_cursedjson",
         commit = "af0e3abb1bcef7197b3b9f91d7d094e6528a2d05",
         importpath = "github.com/q3k/cursedjson",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "co_honnef_go_tools",
         commit = "ea95bdfd59fc",
         importpath = "honnef.co/go/tools",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_azure_go_ansiterm",
         commit = "d6e3b3328b78",
         importpath = "github.com/Azure/go-ansiterm",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_azure_go_autorest",
         importpath = "github.com/Azure/go-autorest",
+        build_naming_convention = "go_default_library",
         tag = "v11.5.0",
     )
 
     go_repository(
         name = "com_github_client9_misspell",
         importpath = "github.com/client9/misspell",
+        build_naming_convention = "go_default_library",
         tag = "v0.3.4",
     )
 
@@ -530,17 +615,20 @@
         name = "com_github_containerd_continuity",
         commit = "7f53d412b9eb",
         importpath = "github.com/containerd/continuity",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_coreos_clair",
         commit = "44ae4bc9590a",
         importpath = "github.com/coreos/clair",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_dgrijalva_jwt_go",
         importpath = "github.com/dgrijalva/jwt-go",
+        build_naming_convention = "go_default_library",
         tag = "v3.2.0",
     )
 
@@ -548,12 +636,14 @@
         name = "com_github_docker_cli",
         commit = "54c19e67f69c",
         importpath = "github.com/docker/cli",
+        build_naming_convention = "go_default_library",
         build_extra_args = ["-exclude=vendor"],
     )
 
     go_repository(
         name = "com_github_docker_distribution",
         importpath = "github.com/docker/distribution",
+        build_naming_convention = "go_default_library",
         tag = "v2.7.1",
         build_extra_args = ["-exclude=vendor"],
     )
@@ -562,18 +652,21 @@
         name = "com_github_docker_docker",
         commit = "be7ac8be2ae0",
         importpath = "github.com/docker/docker",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_docker_docker_ce",
         commit = "f53bd8bb8e43",
         importpath = "github.com/docker/docker-ce",
+        build_naming_convention = "go_default_library",
         build_extra_args = ["-exclude=components/cli/vendor"],
     )
 
     go_repository(
         name = "com_github_docker_docker_credential_helpers",
         importpath = "github.com/docker/docker-credential-helpers",
+        build_naming_convention = "go_default_library",
         tag = "v0.6.1",
     )
 
@@ -581,17 +674,20 @@
         name = "com_github_docker_go_connections",
         commit = "97c2040d34df",
         importpath = "github.com/docker/go-connections",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_docker_go_metrics",
         commit = "399ea8c73916",
         importpath = "github.com/docker/go-metrics",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_docker_go_units",
         importpath = "github.com/docker/go-units",
+        build_naming_convention = "go_default_library",
         tag = "v0.3.3",
     )
 
@@ -599,17 +695,20 @@
         name = "com_github_docker_libtrust",
         commit = "aabc10ec26b7",
         importpath = "github.com/docker/libtrust",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_elazarl_go_bindata_assetfs",
         commit = "38087fe4dafb",
         importpath = "github.com/elazarl/go-bindata-assetfs",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_evanphx_json_patch",
         importpath = "github.com/evanphx/json-patch",
+        build_naming_convention = "go_default_library",
         tag = "v4.2.0",
     )
 
@@ -617,11 +716,13 @@
         name = "com_github_fernet_fernet_go",
         commit = "9eac43b88a5e",
         importpath = "github.com/fernet/fernet-go",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_fsnotify_fsnotify",
         importpath = "github.com/fsnotify/fsnotify",
+        build_naming_convention = "go_default_library",
         tag = "v1.4.7",
     )
 
@@ -629,18 +730,21 @@
         name = "com_github_genuinetools_pkg",
         commit = "1c141f661797",
         importpath = "github.com/genuinetools/pkg",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_genuinetools_reg",
         commit = "d959057b30da",
         importpath = "github.com/genuinetools/reg",
+        build_naming_convention = "go_default_library",
         build_extra_args = ["-exclude=vendor"],
     )
 
     go_repository(
         name = "com_github_ghodss_yaml",
         importpath = "github.com/ghodss/yaml",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
@@ -648,41 +752,48 @@
         name = "com_github_golang_glog",
         commit = "23def4e6c14b",
         importpath = "github.com/golang/glog",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_golang_mock",
         importpath = "github.com/golang/mock",
+        build_naming_convention = "go_default_library",
         tag = "v1.2.0",
     )
 
     go_repository(
         name = "com_github_google_btree",
         importpath = "github.com/google/btree",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "com_github_google_go_cmp",
         importpath = "github.com/google/go-cmp",
+        build_naming_convention = "go_default_library",
         tag = "v0.3.0",
     )
 
     go_repository(
         name = "com_github_google_go_jsonnet",
         importpath = "github.com/google/go-jsonnet",
+        build_naming_convention = "go_default_library",
         tag = "v0.12.1",
     )
 
     go_repository(
         name = "com_github_gophercloud_gophercloud",
         importpath = "github.com/gophercloud/gophercloud",
+        build_naming_convention = "go_default_library",
         tag = "v0.1.0",
     )
 
     go_repository(
         name = "com_github_gorilla_context",
         importpath = "github.com/gorilla/context",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.1",
     )
 
@@ -690,11 +801,13 @@
         name = "com_github_gregjones_httpcache",
         commit = "9cad4c3443a7",
         importpath = "github.com/gregjones/httpcache",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_grpc_ecosystem_grpc_gateway",
         importpath = "github.com/grpc-ecosystem/grpc-gateway",
+        build_naming_convention = "go_default_library",
         sum = "h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=",
         version = "v1.16.0",
     )
@@ -702,60 +815,70 @@
     go_repository(
         name = "com_github_hpcloud_tail",
         importpath = "github.com/hpcloud/tail",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "com_github_imdario_mergo",
         importpath = "github.com/imdario/mergo",
+        build_naming_convention = "go_default_library",
         tag = "v0.3.5",
     )
 
     go_repository(
         name = "com_github_inconshreveable_mousetrap",
         importpath = "github.com/inconshreveable/mousetrap",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "com_github_kisielk_gotool",
         importpath = "github.com/kisielk/gotool",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "com_github_kr_pretty",
         importpath = "github.com/kr/pretty",
+        build_naming_convention = "go_default_library",
         tag = "v0.1.0",
     )
 
     go_repository(
         name = "com_github_kr_pty",
         importpath = "github.com/kr/pty",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.5",
     )
 
     go_repository(
         name = "com_github_kr_text",
         importpath = "github.com/kr/text",
+        build_naming_convention = "go_default_library",
         tag = "v0.1.0",
     )
 
     go_repository(
         name = "com_github_mattn_go_isatty",
         importpath = "github.com/mattn/go-isatty",
+        build_naming_convention = "go_default_library",
         tag = "v0.0.4",
     )
 
     go_repository(
         name = "com_github_microsoft_go_winio",
         importpath = "github.com/Microsoft/go-winio",
+        build_naming_convention = "go_default_library",
         tag = "v0.4.11",
     )
 
     go_repository(
         name = "com_github_mitchellh_go_wordwrap",
         importpath = "github.com/mitchellh/go-wordwrap",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
@@ -763,65 +886,76 @@
         name = "com_github_nvveen_gotty",
         commit = "cd527374f1e5",
         importpath = "github.com/Nvveen/Gotty",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_onsi_ginkgo",
         importpath = "github.com/onsi/ginkgo",
+        build_naming_convention = "go_default_library",
         tag = "v1.10.1",
     )
 
     go_repository(
         name = "com_github_onsi_gomega",
         importpath = "github.com/onsi/gomega",
+        build_naming_convention = "go_default_library",
         tag = "v1.7.0",
     )
 
     go_repository(
         name = "com_github_opencontainers_go_digest",
         importpath = "github.com/opencontainers/go-digest",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0-rc1",
     )
 
     go_repository(
         name = "com_github_opencontainers_image_spec",
         importpath = "github.com/opencontainers/image-spec",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.1",
     )
 
     go_repository(
         name = "com_github_opencontainers_runc",
         importpath = "github.com/opencontainers/runc",
+        build_naming_convention = "go_default_library",
         tag = "v0.1.1",
     )
 
     go_repository(
         name = "com_github_openzipkin_zipkin_go",
         importpath = "github.com/openzipkin/zipkin-go",
+        build_naming_convention = "go_default_library",
         tag = "v0.1.3",
     )
 
     go_repository(
         name = "com_github_peterbourgon_diskv",
         importpath = "github.com/peterbourgon/diskv",
+        build_naming_convention = "go_default_library",
         tag = "v2.0.1",
     )
 
     go_repository(
         name = "com_github_peterhellberg_link",
         importpath = "github.com/peterhellberg/link",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "com_github_pkg_errors",
         importpath = "github.com/pkg/errors",
+        build_naming_convention = "go_default_library",
         tag = "v0.8.1",
     )
 
     go_repository(
         name = "com_github_pmezard_go_difflib",
         importpath = "github.com/pmezard/go-difflib",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
@@ -829,17 +963,20 @@
         name = "com_github_sergi_go_diff",
         commit = "feef008d51ad",
         importpath = "github.com/sergi/go-diff",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_shurcool_httpfs",
         commit = "809beceb2371",
         importpath = "github.com/shurcooL/httpfs",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_spf13_cobra",
         importpath = "github.com/spf13/cobra",
+        build_naming_convention = "go_default_library",
         sum = "h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=",
         version = "v1.0.0",
     )
@@ -847,24 +984,28 @@
     go_repository(
         name = "com_github_spf13_pflag",
         importpath = "github.com/spf13/pflag",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.5",
     )
 
     go_repository(
         name = "com_github_stretchr_objx",
         importpath = "github.com/stretchr/objx",
+        build_naming_convention = "go_default_library",
         tag = "v0.2.0",
     )
 
     go_repository(
         name = "com_github_stretchr_testify",
         importpath = "github.com/stretchr/testify",
+        build_naming_convention = "go_default_library",
         tag = "v1.3.0",
     )
 
     go_repository(
         name = "in_gopkg_airbrake_gobrake_v2",
         importpath = "gopkg.in/airbrake/gobrake.v2",
+        build_naming_convention = "go_default_library",
         tag = "v2.0.9",
     )
 
@@ -872,17 +1013,20 @@
         name = "in_gopkg_check_v1",
         commit = "788fd7840127",
         importpath = "gopkg.in/check.v1",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_fsnotify_v1",
         importpath = "gopkg.in/fsnotify.v1",
+        build_naming_convention = "go_default_library",
         tag = "v1.4.7",
     )
 
     go_repository(
         name = "in_gopkg_gemnasium_logrus_airbrake_hook_v2",
         importpath = "gopkg.in/gemnasium/logrus-airbrake-hook.v2",
+        build_naming_convention = "go_default_library",
         tag = "v2.1.2",
     )
 
@@ -890,12 +1034,14 @@
         name = "in_gopkg_tomb_v1",
         commit = "dd632973f1e7",
         importpath = "gopkg.in/tomb.v1",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "io_k8s_apiextensions_apiserver",
         build_file_proto_mode = "disable",
         importpath = "k8s.io/apiextensions-apiserver",
+        build_naming_convention = "go_default_library",
         sum = "h1:WZxBypSHW4SdXHbdPTS/Jy7L2la6Niggs8BuU5o+avo=",
         version = "v0.19.3",
     )
@@ -904,6 +1050,7 @@
         name = "io_k8s_kube_openapi",
         build_extra_args = ["-known_import=github.com/googleapis/gnostic"],
         importpath = "k8s.io/kube-openapi",
+        build_naming_convention = "go_default_library",
         sum = "h1:mNpvQf4lkIHNOXCoM+Veu/UXwA56Yx1J7hY1Tvcs/oM=",
         version = "v0.0.0-20200923155610-8b5066479488",
     )
@@ -911,12 +1058,14 @@
     go_repository(
         name = "io_opencensus_go",
         importpath = "go.opencensus.io",
+        build_naming_convention = "go_default_library",
         tag = "v0.21.0",
     )
 
     go_repository(
         name = "io_opencensus_go_contrib_exporter_ocagent",
         importpath = "contrib.go.opencensus.io/exporter/ocagent",
+        build_naming_convention = "go_default_library",
         sum = "h1:BEfdCTXfMV30tLZD8c9n64V/tIZX5+9sXiuFLnrr1k8=",
         version = "v0.7.0",
     )
@@ -925,23 +1074,27 @@
         name = "org_apache_git_thrift_git",
         commit = "9b75e4fe745a",
         importpath = "git.apache.org/thrift.git",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_google_api",
         importpath = "google.golang.org/api",
+        build_naming_convention = "go_default_library",
         tag = "v0.4.0",
     )
 
     go_repository(
         name = "org_golang_google_appengine",
         importpath = "google.golang.org/appengine",
+        build_naming_convention = "go_default_library",
         tag = "v1.5.0",
     )
 
     go_repository(
         name = "org_golang_google_grpc",
         importpath = "google.golang.org/grpc",
+        build_naming_convention = "go_default_library",
         tag = "v1.29.1",
     )
 
@@ -949,29 +1102,34 @@
         name = "org_golang_x_lint",
         commit = "d0100b6bd8b3",
         importpath = "golang.org/x/lint",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_x_sync",
         commit = "112230192c58",
         importpath = "golang.org/x/sync",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_x_sys",
         commit = "c7b8b68b1456",
         importpath = "golang.org/x/sys",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_x_text",
         importpath = "golang.org/x/text",
+        build_naming_convention = "go_default_library",
         tag = "v0.3.2",
     )
 
     go_repository(
         name = "tools_gotest",
         importpath = "gotest.tools",
+        build_naming_convention = "go_default_library",
         tag = "v2.2.0",
     )
 
@@ -979,35 +1137,41 @@
         name = "com_github_alecthomas_template",
         commit = "a0175ee3bccc",
         importpath = "github.com/alecthomas/template",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_alecthomas_units",
         commit = "2efee857e7cf",
         importpath = "github.com/alecthomas/units",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_armon_consul_api",
         commit = "eb2c6b5be1b6",
         importpath = "github.com/armon/consul-api",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_bgentry_speakeasy",
         importpath = "github.com/bgentry/speakeasy",
+        build_naming_convention = "go_default_library",
         tag = "v0.1.0",
     )
 
     go_repository(
         name = "com_github_blang_semver",
         importpath = "github.com/blang/semver",
+        build_naming_convention = "go_default_library",
         tag = "v3.5.0",
     )
 
     go_repository(
         name = "com_github_burntsushi_toml",
         importpath = "github.com/BurntSushi/toml",
+        build_naming_convention = "go_default_library",
         tag = "v0.3.1",
     )
 
@@ -1015,41 +1179,48 @@
         name = "com_github_burntsushi_xgb",
         commit = "27f122750802",
         importpath = "github.com/BurntSushi/xgb",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_chai2010_gettext_go",
         commit = "c6fed771bfd5",
         importpath = "github.com/chai2010/gettext-go",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_cockroachdb_datadriven",
         commit = "80d97fb3cbaa",
         importpath = "github.com/cockroachdb/datadriven",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_coreos_etcd",
         importpath = "github.com/coreos/etcd",
+        build_naming_convention = "go_default_library",
         tag = "v3.3.10",
     )
 
     go_repository(
         name = "com_github_coreos_go_etcd",
         importpath = "github.com/coreos/go-etcd",
+        build_naming_convention = "go_default_library",
         tag = "v2.0.0",
     )
 
     go_repository(
         name = "com_github_coreos_go_oidc",
         importpath = "github.com/coreos/go-oidc",
+        build_naming_convention = "go_default_library",
         tag = "v2.1.0",
     )
 
     go_repository(
         name = "com_github_coreos_go_semver",
         importpath = "github.com/coreos/go-semver",
+        build_naming_convention = "go_default_library",
         tag = "v0.3.0",
     )
 
@@ -1057,23 +1228,27 @@
         name = "com_github_coreos_go_systemd",
         commit = "95778dfbb74e",
         importpath = "github.com/coreos/go-systemd",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_coreos_pkg",
         commit = "97fdf19511ea",
         importpath = "github.com/coreos/pkg",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_cpuguy83_go_md2man",
         importpath = "github.com/cpuguy83/go-md2man",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.10",
     )
 
     go_repository(
         name = "com_github_creack_pty",
         importpath = "github.com/creack/pty",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.7",
     )
 
@@ -1081,23 +1256,27 @@
         name = "com_github_daviddengcn_go_colortext",
         commit = "511bcaf42ccd",
         importpath = "github.com/daviddengcn/go-colortext",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_docker_spdystream",
         commit = "449fdfce4d96",
         importpath = "github.com/docker/spdystream",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_elazarl_goproxy",
         commit = "c4fc26588b6e",
         importpath = "github.com/elazarl/goproxy",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_emicklei_go_restful",
         importpath = "github.com/emicklei/go-restful",
+        build_naming_convention = "go_default_library",
         tag = "v2.9.5",
     )
 
@@ -1105,17 +1284,20 @@
         name = "com_github_exponent_io_jsonpath",
         commit = "d6023ce2651d",
         importpath = "github.com/exponent-io/jsonpath",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_fatih_camelcase",
         importpath = "github.com/fatih/camelcase",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "com_github_fatih_color",
         importpath = "github.com/fatih/color",
+        build_naming_convention = "go_default_library",
         tag = "v1.7.0",
     )
 
@@ -1123,23 +1305,27 @@
         name = "com_github_globalsign_mgo",
         commit = "eeefdecb41b8",
         importpath = "github.com/globalsign/mgo",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_go_kit_kit",
         importpath = "github.com/go-kit/kit",
+        build_naming_convention = "go_default_library",
         tag = "v0.8.0",
     )
 
     go_repository(
         name = "com_github_go_logfmt_logfmt",
         importpath = "github.com/go-logfmt/logfmt",
+        build_naming_convention = "go_default_library",
         tag = "v0.3.0",
     )
 
     go_repository(
         name = "com_github_go_logr_logr",
         importpath = "github.com/go-logr/logr",
+        build_naming_convention = "go_default_library",
         sum = "h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=",
         version = "v0.2.0",
     )
@@ -1147,6 +1333,7 @@
     go_repository(
         name = "com_github_go_stack_stack",
         importpath = "github.com/go-stack/stack",
+        build_naming_convention = "go_default_library",
         tag = "v1.8.0",
     )
 
@@ -1154,29 +1341,34 @@
         name = "com_github_golang_groupcache",
         commit = "02826c3e7903",
         importpath = "github.com/golang/groupcache",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_golangplus_bytes",
         commit = "45c989fe5450",
         importpath = "github.com/golangplus/bytes",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_golangplus_fmt",
         commit = "2a5d6d7d2995",
         importpath = "github.com/golangplus/fmt",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_golangplus_testing",
         commit = "af21d9c3145e",
         importpath = "github.com/golangplus/testing",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_google_martian",
         importpath = "github.com/google/martian",
+        build_naming_convention = "go_default_library",
         tag = "v2.1.0",
     )
 
@@ -1184,23 +1376,27 @@
         name = "com_github_google_pprof",
         commit = "3ea8567a2e57",
         importpath = "github.com/google/pprof",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_google_uuid",
         importpath = "github.com/google/uuid",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.1",
     )
 
     go_repository(
         name = "com_github_googleapis_gax_go_v2",
         importpath = "github.com/googleapis/gax-go/v2",
+        build_naming_convention = "go_default_library",
         tag = "v2.0.4",
     )
 
     go_repository(
         name = "com_github_gorilla_websocket",
         importpath = "github.com/gorilla/websocket",
+        build_naming_convention = "go_default_library",
         tag = "v1.4.0",
     )
 
@@ -1208,29 +1404,34 @@
         name = "com_github_grpc_ecosystem_go_grpc_middleware",
         commit = "f849b5445de4",
         importpath = "github.com/grpc-ecosystem/go-grpc-middleware",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_grpc_ecosystem_go_grpc_prometheus",
         importpath = "github.com/grpc-ecosystem/go-grpc-prometheus",
+        build_naming_convention = "go_default_library",
         tag = "v1.2.0",
     )
 
     go_repository(
         name = "com_github_hashicorp_golang_lru",
         importpath = "github.com/hashicorp/golang-lru",
+        build_naming_convention = "go_default_library",
         tag = "v0.5.1",
     )
 
     go_repository(
         name = "com_github_hashicorp_hcl",
         importpath = "github.com/hashicorp/hcl",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "com_github_jonboulle_clockwork",
         importpath = "github.com/jonboulle/clockwork",
+        build_naming_convention = "go_default_library",
         tag = "v0.1.0",
     )
 
@@ -1238,23 +1439,27 @@
         name = "com_github_jstemmer_go_junit_report",
         commit = "af01ea7f8024",
         importpath = "github.com/jstemmer/go-junit-report",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_julienschmidt_httprouter",
         importpath = "github.com/julienschmidt/httprouter",
+        build_naming_convention = "go_default_library",
         tag = "v1.2.0",
     )
 
     go_repository(
         name = "com_github_kisielk_errcheck",
         importpath = "github.com/kisielk/errcheck",
+        build_naming_convention = "go_default_library",
         tag = "v1.2.0",
     )
 
     go_repository(
         name = "com_github_konsorten_go_windows_terminal_sequences",
         importpath = "github.com/konsorten/go-windows-terminal-sequences",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.1",
     )
 
@@ -1262,23 +1467,27 @@
         name = "com_github_kr_logfmt",
         commit = "b84e30acd515",
         importpath = "github.com/kr/logfmt",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_liggitt_tabwriter",
         commit = "89fcab3d43de",
         importpath = "github.com/liggitt/tabwriter",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_lithammer_dedent",
         importpath = "github.com/lithammer/dedent",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.0",
     )
 
     go_repository(
         name = "com_github_magiconair_properties",
         importpath = "github.com/magiconair/properties",
+        build_naming_convention = "go_default_library",
         tag = "v1.8.0",
     )
 
@@ -1286,29 +1495,34 @@
         name = "com_github_makenowjust_heredoc",
         commit = "bb23615498cd",
         importpath = "github.com/MakeNowJust/heredoc",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_mattn_go_colorable",
         importpath = "github.com/mattn/go-colorable",
+        build_naming_convention = "go_default_library",
         tag = "v0.0.9",
     )
 
     go_repository(
         name = "com_github_mattn_go_runewidth",
         importpath = "github.com/mattn/go-runewidth",
+        build_naming_convention = "go_default_library",
         tag = "v0.0.2",
     )
 
     go_repository(
         name = "com_github_mitchellh_go_homedir",
         importpath = "github.com/mitchellh/go-homedir",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.0",
     )
 
     go_repository(
         name = "com_github_morikuni_aec",
         importpath = "github.com/morikuni/aec",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
@@ -1316,35 +1530,41 @@
         name = "com_github_munnerz_goautoneg",
         commit = "a7dc8b61c822",
         importpath = "github.com/munnerz/goautoneg",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_mwitkow_go_conntrack",
         commit = "cc309e4a2223",
         importpath = "github.com/mwitkow/go-conntrack",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_mxk_go_flowrate",
         commit = "cca7078d478f",
         importpath = "github.com/mxk/go-flowrate",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_nytimes_gziphandler",
         commit = "56545f4a5d46",
         importpath = "github.com/NYTimes/gziphandler",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_olekukonko_tablewriter",
         commit = "a0225b3f23b5",
         importpath = "github.com/olekukonko/tablewriter",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_pelletier_go_toml",
         importpath = "github.com/pelletier/go-toml",
+        build_naming_convention = "go_default_library",
         tag = "v1.2.0",
     )
 
@@ -1352,53 +1572,62 @@
         name = "com_github_pquerna_cachecontrol",
         commit = "0dec1b30a021",
         importpath = "github.com/pquerna/cachecontrol",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_remyoudompheng_bigfft",
         commit = "52369c62f446",
         importpath = "github.com/remyoudompheng/bigfft",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_rogpeppe_fastuuid",
         commit = "6724a57986af",
         importpath = "github.com/rogpeppe/fastuuid",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_russross_blackfriday",
         importpath = "github.com/russross/blackfriday",
+        build_naming_convention = "go_default_library",
         tag = "v1.5.2",
     )
 
     go_repository(
         name = "com_github_soheilhy_cmux",
         importpath = "github.com/soheilhy/cmux",
+        build_naming_convention = "go_default_library",
         tag = "v0.1.4",
     )
 
     go_repository(
         name = "com_github_spf13_afero",
         importpath = "github.com/spf13/afero",
+        build_naming_convention = "go_default_library",
         tag = "v1.2.2",
     )
 
     go_repository(
         name = "com_github_spf13_cast",
         importpath = "github.com/spf13/cast",
+        build_naming_convention = "go_default_library",
         tag = "v1.3.0",
     )
 
     go_repository(
         name = "com_github_spf13_jwalterweatherman",
         importpath = "github.com/spf13/jwalterweatherman",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "com_github_spf13_viper",
         importpath = "github.com/spf13/viper",
+        build_naming_convention = "go_default_library",
         tag = "v1.3.2",
     )
 
@@ -1406,65 +1635,76 @@
         name = "com_github_tmc_grpc_websocket_proxy",
         commit = "89b8d40f7ca8",
         importpath = "github.com/tmc/grpc-websocket-proxy",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_ugorji_go_codec",
         commit = "d75b2dcb6bc8",
         importpath = "github.com/ugorji/go/codec",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_xiang90_probing",
         commit = "43a291ad63a2",
         importpath = "github.com/xiang90/probing",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_xlab_handysort",
         commit = "fb3537ed64a1",
         importpath = "github.com/xlab/handysort",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_xordataexchange_crypt",
         commit = "b2862e3d0a77",
         importpath = "github.com/xordataexchange/crypt",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_alecthomas_kingpin_v2",
         importpath = "gopkg.in/alecthomas/kingpin.v2",
+        build_naming_convention = "go_default_library",
         tag = "v2.2.6",
     )
 
     go_repository(
         name = "in_gopkg_cheggaaa_pb_v1",
         importpath = "gopkg.in/cheggaaa/pb.v1",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.25",
     )
 
     go_repository(
         name = "in_gopkg_natefinch_lumberjack_v2",
         importpath = "gopkg.in/natefinch/lumberjack.v2",
+        build_naming_convention = "go_default_library",
         tag = "v2.0.0",
     )
 
     go_repository(
         name = "in_gopkg_resty_v1",
         importpath = "gopkg.in/resty.v1",
+        build_naming_convention = "go_default_library",
         tag = "v1.12.0",
     )
 
     go_repository(
         name = "in_gopkg_square_go_jose_v2",
         importpath = "gopkg.in/square/go-jose.v2",
+        build_naming_convention = "go_default_library",
         tag = "v2.2.2",
     )
 
     go_repository(
         name = "io_etcd_go_bbolt",
         importpath = "go.etcd.io/bbolt",
+        build_naming_convention = "go_default_library",
         tag = "v1.3.3",
     )
 
@@ -1472,11 +1712,13 @@
         name = "io_etcd_go_etcd",
         commit = "3cf2f69b5738",
         importpath = "go.etcd.io/etcd",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "io_k8s_apiserver",
         importpath = "k8s.io/apiserver",
+        build_naming_convention = "go_default_library",
         sum = "h1:H7KUbLD74rh8NOPMLBJPSEG3Djqcv6Zxn5Ud0AL5u/k=",
         version = "v0.19.3",
     )
@@ -1484,6 +1726,7 @@
     go_repository(
         name = "io_k8s_cli_runtime",
         importpath = "k8s.io/cli-runtime",
+        build_naming_convention = "go_default_library",
         sum = "h1:vZUTphJIvlh7+867cXiLmyzoCAuQdukbPLIad6eEajQ=",
         version = "v0.19.3",
     )
@@ -1491,6 +1734,7 @@
     go_repository(
         name = "io_k8s_code_generator",
         importpath = "k8s.io/code-generator",
+        build_naming_convention = "go_default_library",
         sum = "h1:fTrTpJ8PZog5oo6MmeZtveo89emjQZHiw0ieybz1RSs=",
         version = "v0.19.3",
     )
@@ -1498,6 +1742,7 @@
     go_repository(
         name = "io_k8s_component_base",
         importpath = "k8s.io/component-base",
+        build_naming_convention = "go_default_library",
         sum = "h1:c+DzDNAQFlaoyX+yv8YuWi8xmlQvvY5DnJGbaz5U74o=",
         version = "v0.19.3",
     )
@@ -1506,11 +1751,13 @@
         name = "io_k8s_gengo",
         commit = "26a664648505",
         importpath = "k8s.io/gengo",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "io_k8s_kubectl",
         importpath = "k8s.io/kubectl",
+        build_naming_convention = "go_default_library",
         sum = "h1:T8IHHpg+uRIfn34wqJ8wHG5bbH+VV5FNPtJ+jKcho1U=",
         version = "v0.19.3",
     )
@@ -1519,6 +1766,7 @@
         name = "io_k8s_metrics",
         build_file_proto_mode = "disable",
         importpath = "k8s.io/metrics",
+        build_naming_convention = "go_default_library",
         sum = "h1:p/goUqtdCslX76mSNowzZkNxiKzNRQW4bUP02U34+QQ=",
         version = "v0.19.3",
     )
@@ -1526,6 +1774,7 @@
     go_repository(
         name = "io_k8s_sigs_kustomize",
         importpath = "sigs.k8s.io/kustomize",
+        build_naming_convention = "go_default_library",
         tag = "v2.0.3",
     )
 
@@ -1533,89 +1782,104 @@
         name = "io_k8s_sigs_structured_merge_diff",
         commit = "b1b620dd3f06",
         importpath = "sigs.k8s.io/structured-merge-diff",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_x_exp",
         commit = "4b39c73a6495",
         importpath = "golang.org/x/exp",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_x_image",
         commit = "0694c2d4d067",
         importpath = "golang.org/x/image",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_x_mobile",
         commit = "d3739f865fa6",
         importpath = "golang.org/x/mobile",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_golang_x_xerrors",
         commit = "a985d3407aa7",
         importpath = "golang.org/x/xerrors",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_gonum_v1_gonum",
         commit = "3d26580ed485",
         importpath = "gonum.org/v1/gonum",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_gonum_v1_netlib",
         commit = "76723241ea4e",
         importpath = "gonum.org/v1/netlib",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "org_modernc_cc",
         importpath = "modernc.org/cc",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "org_modernc_golex",
         importpath = "modernc.org/golex",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "org_modernc_mathutil",
         importpath = "modernc.org/mathutil",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "org_modernc_strutil",
         importpath = "modernc.org/strutil",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "org_modernc_xc",
         importpath = "modernc.org/xc",
+        build_naming_convention = "go_default_library",
         tag = "v1.0.0",
     )
 
     go_repository(
         name = "org_uber_go_atomic",
         importpath = "go.uber.org/atomic",
+        build_naming_convention = "go_default_library",
         tag = "v1.3.2",
     )
 
     go_repository(
         name = "org_uber_go_multierr",
         importpath = "go.uber.org/multierr",
+        build_naming_convention = "go_default_library",
         tag = "v1.1.0",
     )
 
     go_repository(
         name = "org_uber_go_zap",
         importpath = "go.uber.org/zap",
+        build_naming_convention = "go_default_library",
         tag = "v1.10.0",
     )
 
@@ -1623,18 +1887,21 @@
         name = "com_github_dgraph_io_ristretto",
         commit = "83508260cb49a2c3261c2774c991870fd18b5a1b",
         importpath = "github.com/dgraph-io/ristretto",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_cespare_xxhash",
         commit = "d7df74196a9e781ede915320c11c378c1b2f3a1f",
         importpath = "github.com/cespare/xxhash",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_ulule_limiter_v3",
         commit = "6911899e37a5788df86f770b3f85c1c3eb0313d5",
         importpath = "github.com/ulule/limiter/v3",
+        build_naming_convention = "go_default_library",
         remote = "https://github.com/ulule/limiter",
         vcs = "git",
     )
@@ -1643,36 +1910,42 @@
         name = "com_github_go_telegram_bot_api_telegram_bot_api",
         commit = "b33efeebc78563cfeddf19563781cffb16aaabdf",
         importpath = "github.com/go-telegram-bot-api/telegram-bot-api",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_technoweenie_multipartstreamer",
         commit = "a90a01d73ae432e2611d178c18367fbaa13e0154",
         importpath = "github.com/technoweenie/multipartstreamer",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_irc_v3",
         commit = "d07dcb9293789fdc99c797d3499a5799bc343b86",
         importpath = "gopkg.in/irc.v3",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_russross_blackfriday_v2",
         commit = "d3b5b032dc8e8927d31a5071b56e14c89f045135",
         importpath = "gopkg.in/russross/blackfriday.v2",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_shurcool_sanitized_anchor_name",
         commit = "7bfe4c7ecddb3666a94b053b422cdd8f5aaa3615",
         importpath = "github.com/shurcooL/sanitized_anchor_name",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_go_git_go_billy_v5",
         commit = "d7a8afccaed297c30f8dff5724dbe422b491dd0d",
         importpath = "github.com/go-git/go-billy/v5",
+        build_naming_convention = "go_default_library",
         remote = "https://github.com/go-git/go-billy",
         vcs = "git",
     )
@@ -1681,6 +1954,7 @@
         name = "com_github_go_git_go_git_v5",
         commit = "3127ad9a44a2ee935502816065dfe39f494f583d",
         importpath = "github.com/go-git/go-git/v5",
+        build_naming_convention = "go_default_library",
         remote = "https://github.com/go-git/go-git",
         vcs = "git",
         build_extra_args = [
@@ -1692,53 +1966,62 @@
         name = "com_github_go_git_gcfg",
         commit = "22f18f9a74d34e3b1a7d59cfa33043bc50ebe376",
         importpath = "github.com/go-git/gcfg",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "in_gopkg_warnings_v0",
         commit = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b",
         importpath = "gopkg.in/warnings.v0",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_emirpasic_gods",
         commit = "80e934ed68b9084f386ae25f74f839aaecfb54d8",
         importpath = "github.com/emirpasic/gods",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_jbenet_go_context",
         commit = "d14ea06fba99483203c19d92cfcd13ebe73135f4",
         importpath = "github.com/jbenet/go-context",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_kevinburke_ssh_config",
         commit = "01f96b0aa0cdcaa93f9495f89bbc6cb5a992ce6e",
         importpath = "github.com/kevinburke/ssh_config",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_xanzy_ssh_agent",
         commit = "6a3e2ff9e7c564f36873c2e36413f634534f1c44",
         importpath = "github.com/xanzy/ssh-agent",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_gabriel_vasile_mimetype",
         commit = "06500030e7d26826f68caa5ca7d98c315c4caa28",
         importpath = "github.com/gabriel-vasile/mimetype",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_kevinburke_go_bindata",
         commit = "a606d617e1d1546a2342de6fc4ed95c78e171d68",
         importpath = "github.com/kevinburke/go-bindata",
+        build_naming_convention = "go_default_library",
     )
 
     go_repository(
         name = "com_github_yuin_goldmark",
         importpath = "github.com/yuin/goldmark",
+        build_naming_convention = "go_default_library",
         sum = "h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=",
         version = "v1.2.1",
     )
@@ -1746,6 +2029,7 @@
     go_repository(
         name = "com_github_jessevdk_go_flags",
         importpath = "github.com/jessevdk/go-flags",
+        build_naming_convention = "go_default_library",
         sum = "h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=",
         version = "v1.4.0",
     )
@@ -1756,36 +2040,42 @@
     go_repository(
         name = "com_github_piranha_gostatic",
         importpath = "github.com/piranha/gostatic",
+        build_naming_convention = "go_default_library",
         sum = "h1:GfShSQ+2DojR7GRI5wPByszs93zHXW2zOT0SuHadW6A=",
         version = "v0.0.0-20200923134324-eb52cbb4fb83",
     )
     go_repository(
         name = "io_k8s_klog_v2",
         importpath = "k8s.io/klog/v2",
+        build_naming_convention = "go_default_library",
         sum = "h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=",
         version = "v2.4.0",
     )
     go_repository(
         name = "io_k8s_sigs_structured_merge_diff_v4",
         importpath = "sigs.k8s.io/structured-merge-diff/v4",
+        build_naming_convention = "go_default_library",
         sum = "h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA=",
         version = "v4.0.1",
     )
     go_repository(
         name = "in_gopkg_yaml_v3",
         importpath = "gopkg.in/yaml.v3",
+        build_naming_convention = "go_default_library",
         sum = "h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=",
         version = "v3.0.0-20200615113413-eeeca48fe776",
     )
     go_repository(
         name = "com_github_moby_term",
         importpath = "github.com/moby/term",
+        build_naming_convention = "go_default_library",
         sum = "h1:K6V0Kwa5efKo60sqbTk1FOBbltdyX9Klw2a9+lKhA18=",
         version = "v0.0.0-20201101162038-25d840ce174a",
     )
     go_repository(
         name = "com_github_census_instrumentation_opencensus_proto",
         importpath = "github.com/census-instrumentation/opencensus-proto",
+        build_naming_convention = "go_default_library",
         sum = "h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk=",
         version = "v0.3.0",
         build_extra_args = ["-exclude=src"],
@@ -1793,36 +2083,71 @@
     go_repository(
         name = "com_github_minio_minio_go_v7",
         importpath = "github.com/minio/minio-go/v7",
+        build_naming_convention = "go_default_library",
         sum = "h1:1oUKe4EOPUEhw2qnPQaPsJ0lmVTYLFu03SiItauXs94=",
         version = "v7.0.10",
     )
     go_repository(
         name = "in_gopkg_ini_v1",
         importpath = "gopkg.in/ini.v1",
+        build_naming_convention = "go_default_library",
         sum = "h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=",
         version = "v1.62.0",
     )
     go_repository(
         name = "com_github_minio_md5_simd",
         importpath = "github.com/minio/md5-simd",
+        build_naming_convention = "go_default_library",
         sum = "h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=",
         version = "v1.1.2",
     )
     go_repository(
         name = "com_github_minio_sha256_simd",
         importpath = "github.com/minio/sha256-simd",
+        build_naming_convention = "go_default_library",
         sum = "h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=",
         version = "v1.0.0",
     )
     go_repository(
         name = "com_github_klauspost_cpuid_v2",
         importpath = "github.com/klauspost/cpuid/v2",
+        build_naming_convention = "go_default_library",
         sum = "h1:qnfhwbFriwDIX51QncuNU5mEMf+6KE3t7O8V2KQl3Dg=",
         version = "v2.0.5",
     )
     go_repository(
         name = "com_github_rs_xid",
         importpath = "github.com/rs/xid",
+        build_naming_convention = "go_default_library",
         sum = "h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=",
         version = "v1.2.1",
     )
+    go_repository(
+        name = "com_github_gorilla_sessions",
+        importpath = "github.com/gorilla/sessions",
+        build_naming_convention = "go_default_library",
+        sum = "h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=",
+        version = "v1.2.1",
+    )
+    go_repository(
+        name = "com_github_boltdb_bolt",
+        importpath = "github.com/boltdb/bolt",
+        build_naming_convention = "go_default_library",
+        sum = "h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=",
+        version = "v1.3.1",
+    )
+    go_repository(
+        name = "com_github_gorilla_securecookie",
+        importpath = "github.com/gorilla/securecookie",
+        build_naming_convention = "go_default_library",
+        sum = "h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=",
+        version = "v1.1.1",
+    )
+    go_repository(
+        name = "com_github_arran4_golang_ical",
+        importpath = "github.com/arran4/golang-ical",
+        build_naming_convention = "go_default_library",
+        sum = "h1:oOgavmDMGCnNtwZwNoXuK3jCcpF3I96Do9/5qPeSCr8=",
+        version = "v0.0.0-20210601225245-48fd351b08e7",
+    )
+
diff --git a/third_party/py/requirements.txt b/third_party/py/requirements.txt
index 1a50c93..df0da45 100644
--- a/third_party/py/requirements.txt
+++ b/third_party/py/requirements.txt
@@ -15,7 +15,6 @@
 fabric==2.4.0
 Flask==1.1.1
 Flask-Login==0.4.1
-Flask-OAuthlib==0.9.5
 Flask-SQLAlchemy==2.4.0
 Flask-WTF==0.14.2
 future==0.17.1
@@ -27,13 +26,13 @@
 itsdangerous==1.1.0
 Jinja2==2.10.1
 MarkupSafe==1.1.1
-oauthlib==2.1.0
+oauthlib==3.1.1
 paramiko==2.4.2
 psycopg2==2.8.5
 pyasn1==0.4.5
 pycparser==2.19
-PyNaCl==1.3.0
 pyelftools==0.26
+PyNaCl==1.3.0
 python-dateutil==2.8.0
 pytz==2019.1
 requests==2.22.0
@@ -44,3 +43,5 @@
 urllib3==1.25.3
 Werkzeug==0.15.5
 WTForms==2.2.1
+zope.event==4.5.0
+zope.interface==5.4.0
diff --git a/tools/secretstore.py b/tools/secretstore.py
index b0d2cfe..767a0fc 100644
--- a/tools/secretstore.py
+++ b/tools/secretstore.py
@@ -49,6 +49,10 @@
     "0879F9FCA1C836677BB808C870FD60197E195C26", # implr
 ]
 
+# Currently, Patryk's GPG key is expired. This hacks around that by pretending
+# it's January 2021.
+# TODO(q3k/patryk): remove this once Patryk updates his key.
+systime = '20210101T000000'
 
 _logger_name = __name__
 if _logger_name == '__main__':
@@ -61,7 +65,15 @@
 
 
 def encrypt(src, dst):
-    cmd = ['gpg' , '--encrypt', '--armor', '--batch', '--yes', '--output', dst]
+    cmd = [
+        'gpg' , 
+        '--encrypt',
+        '--faked-system-time', systime,
+        '--trust-model', 'always',
+        '--armor',
+        '--batch', '--yes',
+        '--output', dst,
+    ]
     for k in keys:
         cmd.append('--recipient')
         cmd.append(k)
@@ -80,7 +92,7 @@
     Returns the encryption key ID for a given GPG fingerprint (eg. one from the
     'keys' list.
     """
-    cmd = ['gpg', '-k', '--keyid-format', 'long', fp]
+    cmd = ['gpg', '-k', '--faked-system-time', systime, '--keyid-format', 'long', fp]
     res = subprocess.check_output(cmd).decode()
 
     # Sample output: