Merge "app/gerrit: fix advertised address"
diff --git a/WORKSPACE b/WORKSPACE
index 58dc8e5..6450987 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -53,7 +53,7 @@
 
 git_repository(
     name = "com_apt_itude_rules_pip",
-    commit = "e5ed5e72bf5a7521244e1d2119821628bbf17263",
+    commit = "ce667087818553cdc4b1a2258fc53df917c4f87c",
     remote = "https://github.com/apt-itude/rules_pip.git",
 )
 
diff --git a/bgpwtf/cccampix/BUILD b/bgpwtf/cccampix/BUILD
new file mode 100644
index 0000000..0d9fd06
--- /dev/null
+++ b/bgpwtf/cccampix/BUILD
@@ -0,0 +1,9 @@
+py_binary(
+    name = "sync",
+    srcs = [
+        "sync.py",
+    ],
+    deps = [
+        "@pip36//requests",
+    ],
+)
diff --git a/go/svc/cmc-proxy/BUILD.bazel b/bgpwtf/cccampix/peeringdb/BUILD.bazel
similarity index 64%
copy from go/svc/cmc-proxy/BUILD.bazel
copy to bgpwtf/cccampix/peeringdb/BUILD.bazel
index 56f3495..22c90e3 100644
--- a/go/svc/cmc-proxy/BUILD.bazel
+++ b/bgpwtf/cccampix/peeringdb/BUILD.bazel
@@ -2,16 +2,13 @@
 
 go_library(
     name = "go_default_library",
-    srcs = [
-        "client.go",
-        "main.go",
-    ],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/cmc-proxy",
+    srcs = ["main.go"],
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/peeringdb",
     visibility = ["//visibility:private"],
     deps = [
+        "//bgpwtf/cccampix/peeringdb/schema:go_default_library",
+        "//bgpwtf/cccampix/proto:go_default_library",
         "//go/mirko:go_default_library",
-        "//go/svc/cmc-proxy/proto:go_default_library",
-        "@com_github_cenkalti_backoff//:go_default_library",
         "@com_github_golang_glog//:go_default_library",
         "@org_golang_google_grpc//codes:go_default_library",
         "@org_golang_google_grpc//status:go_default_library",
@@ -19,7 +16,7 @@
 )
 
 go_binary(
-    name = "cmc-proxy",
+    name = "peeringdb",
     embed = [":go_default_library"],
     visibility = ["//visibility:public"],
 )
diff --git a/bgpwtf/cccampix/peeringdb/README.md b/bgpwtf/cccampix/peeringdb/README.md
new file mode 100644
index 0000000..502760e
--- /dev/null
+++ b/bgpwtf/cccampix/peeringdb/README.md
@@ -0,0 +1,41 @@
+PeeringDBProxy
+==============
+
+Exposes PeeringDB data as gRPC.
+
+API defined in [ix.proto](../proto/ix.proto).
+
+Usage
+-----
+
+    $ bazel run //bgpwtf/cccampix/peeringdb:peeringdb -- -hspki_disable
+    $ grpcurl -plaintext -d '{"id": 2325}' 127.0.0.1:4200 ix.PeeringDBProxy.GetIXMembers
+    {
+      "members": [
+        {
+          "asn": 206924,
+          "ipv4": "185.230.223.195",
+          "name": "BENJOJONET"
+        },
+        {
+          "asn": 207080,
+          "ipv4": "185.230.223.194",
+          "ipv6": "fe80::8651:4050:1715:bc4f",
+          "name": "Basil Fillan"
+        },
+        {
+          "asn": 39192,
+          "ipv4": "185.230.223.198",
+          "ipv6": "fe80::3:9192:1",
+          "name": "JackNet"
+        },
+        {
+          "asn": 205271,
+          "ipv4": "185.230.223.199",
+          "ipv6": "fe80::20:5271:1",
+          "name": "Harry Reeder"
+        }
+      ]
+    }
+
+
diff --git a/bgpwtf/cccampix/peeringdb/main.go b/bgpwtf/cccampix/peeringdb/main.go
new file mode 100644
index 0000000..fa11651
--- /dev/null
+++ b/bgpwtf/cccampix/peeringdb/main.go
@@ -0,0 +1,97 @@
+package main
+
+import (
+	"context"
+	"flag"
+
+	"github.com/golang/glog"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
+	"code.hackerspace.pl/hscloud/bgpwtf/cccampix/peeringdb/schema"
+	pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
+	"code.hackerspace.pl/hscloud/go/mirko"
+)
+
+type service struct {
+}
+
+func (s *service) GetIXMembers(ctx context.Context, req *pb.GetIXMembersRequest) (*pb.GetIXMembersResponse, error) {
+	if req.Id == 0 {
+		return nil, status.Error(codes.InvalidArgument, "IX id must be given")
+	}
+
+	// First, get netixlans (membership info) for the given IX.
+	js := struct {
+		Data []schema.NetIXLan `json:"Data"`
+	}{}
+	err := schema.Get(ctx, &js, schema.NetIXLanInIXURL(req.Id))
+	if err != nil {
+		return nil, status.Errorf(codes.Unavailable, "PeeringDB query error: %v", err)
+	}
+
+	// Build set of seen Nets/ASs.
+	netids := make(map[int64]bool)
+	for _, netixlan := range js.Data {
+		netids[netixlan.NetID] = true
+	}
+
+	// Convert set to unique list.
+	nets := make([]int64, len(netids))
+	i := 0
+	for id, _ := range netids {
+		nets[i] = id
+		i += 1
+	}
+
+	// Request information about nets/ASNs:
+	js2 := struct {
+		Data []schema.Net `json:"Data"`
+	}{}
+	err = schema.Get(ctx, &js2, schema.NetURLMulti(nets))
+	if err != nil {
+		return nil, status.Errorf(codes.Unavailable, "PeeringDB query error: %v", err)
+	}
+
+	// Make map net id -> Net
+	netidsNet := make(map[int64]*schema.Net)
+	for _, net := range js2.Data {
+		net := net
+		netidsNet[net.ID] = &net
+	}
+
+	// Build joined response.
+
+	res := &pb.GetIXMembersResponse{
+		Members: make([]*pb.GetIXMembersResponse_Member, len(js.Data)),
+	}
+
+	for i, netixlan := range js.Data {
+		res.Members[i] = &pb.GetIXMembersResponse_Member{
+			Asn:  netixlan.ASN,
+			Ipv4: netixlan.IPv4,
+			Ipv6: netixlan.IPv6,
+			Name: netidsNet[netixlan.NetID].Name,
+		}
+	}
+
+	return res, nil
+}
+
+func main() {
+	flag.Parse()
+	mi := mirko.New()
+
+	if err := mi.Listen(); err != nil {
+		glog.Exitf("Listen failed: %v", err)
+	}
+
+	s := &service{}
+	pb.RegisterPeeringDBProxyServer(mi.GRPC(), s)
+
+	if err := mi.Serve(); err != nil {
+		glog.Exitf("Serve failed: %v", err)
+	}
+
+	<-mi.Done()
+}
diff --git a/bgpwtf/cccampix/peeringdb/schema/BUILD.bazel b/bgpwtf/cccampix/peeringdb/schema/BUILD.bazel
new file mode 100644
index 0000000..8eded08
--- /dev/null
+++ b/bgpwtf/cccampix/peeringdb/schema/BUILD.bazel
@@ -0,0 +1,11 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "schema.go",
+        "urls.go",
+    ],
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/peeringdb/schema",
+    visibility = ["//visibility:public"],
+)
diff --git a/bgpwtf/cccampix/peeringdb/schema/schema.go b/bgpwtf/cccampix/peeringdb/schema/schema.go
new file mode 100644
index 0000000..0d48aed
--- /dev/null
+++ b/bgpwtf/cccampix/peeringdb/schema/schema.go
@@ -0,0 +1,34 @@
+package schema
+
+// Partial definition from https://www.peeringdb.com/apidocs/
+
+type IX struct {
+	ID       int64   `json:"id"`
+	OrgID    int64   `json:"org_id"`
+	Name     string  `json:"name"`
+	IXLanSet []IXLan `json:"ixlan_set"`
+}
+
+type IXLan struct {
+	ID     int64  `json:"id"`
+	Name   string `json:"name"`
+	NetSet []Net  `json:"net_set"`
+}
+
+type Net struct {
+	ID    int64  `json:"id"`
+	OrgID int64  `json:"org_id"`
+	Name  string `json:"name"`
+	ASN   int64  `json:"asn"`
+}
+
+type NetIXLan struct {
+	ID    int64  `json:"id"`
+	NetID int64  `json:"net_id"`
+	IXID  int64  `json:"ix_id"`
+	Name  string `json:"name"`
+	Speed int64  `json:"speed"`
+	ASN   int64  `json:"asn"`
+	IPv4  string `json:"ipaddr4"`
+	IPv6  string `json:"ipaddr6"`
+}
diff --git a/bgpwtf/cccampix/peeringdb/schema/urls.go b/bgpwtf/cccampix/peeringdb/schema/urls.go
new file mode 100644
index 0000000..abe5f56
--- /dev/null
+++ b/bgpwtf/cccampix/peeringdb/schema/urls.go
@@ -0,0 +1,61 @@
+package schema
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+)
+
+func IXURL(id int64) string {
+	return fmt.Sprintf("https://peeringdb.com/api/ix/%d.json?depth=4", id)
+}
+
+func NetIXLanInIXURL(id int64) string {
+	return fmt.Sprintf("https://peeringdb.com/api/netixlan?ix_id__in=%d", id)
+}
+
+func NetURLMulti(ids []int64) string {
+	sid := make([]string, len(ids))
+	for i, id := range ids {
+		sid[i] = fmt.Sprintf("%d", id)
+	}
+	return fmt.Sprintf("https://peeringdb.com/api/net?id__in=%s", strings.Join(sid, ","))
+}
+
+func Get(ctx context.Context, obj interface{}, url string) error {
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return fmt.Errorf("http.NewRequest(GET, %q): %v", url, err)
+	}
+
+	req.Header.Add("User-Agent", "bgpwtf-cccampix-peeringdbproxy/1.0 (https://code.hackerspace.pl/hscloud/bgpwtf/cccampix/peeringdb)")
+
+	req = req.WithContext(ctx)
+
+	client := http.DefaultClient
+	res, err := client.Do(req)
+	if err != nil {
+		return fmt.Errorf("client.Do(%v): %v", req, err)
+	}
+
+	defer res.Body.Close()
+
+	if res.StatusCode != 200 {
+		return fmt.Errorf("got status code %d", res.StatusCode)
+	}
+
+	data, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return fmt.Errorf("could not read response: %v", err)
+	}
+
+	err = json.Unmarshal(data, &obj)
+	if err != nil {
+		return fmt.Errorf("could not parse response JSON: %v", err)
+	}
+
+	return nil
+}
diff --git a/bgpwtf/cccampix/proto/BUILD.bazel b/bgpwtf/cccampix/proto/BUILD.bazel
new file mode 100644
index 0000000..023cd8d
--- /dev/null
+++ b/bgpwtf/cccampix/proto/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "ix_proto",
+    srcs = ["ix.proto"],
+    visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+    name = "ix_go_proto",
+    compilers = ["@io_bazel_rules_go//proto:go_grpc"],
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto",
+    proto = ":ix_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    embed = [":ix_go_proto"],
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/bgpwtf/cccampix/proto/ix.proto b/bgpwtf/cccampix/proto/ix.proto
new file mode 100644
index 0000000..73bc4d1
--- /dev/null
+++ b/bgpwtf/cccampix/proto/ix.proto
@@ -0,0 +1,26 @@
+syntax = "proto3";
+package ix;
+
+message GetIXMembersRequest {
+    // IX ID from PeeringDB
+    int64 id = 1;
+}
+
+message GetIXMembersResponse {
+    message Member {
+        int64 asn = 1;
+        // Per PeeringDB, at least one of the following two address families
+        // will be set.
+        string ipv4 = 2;
+        string ipv6 = 3;
+        // AS/network name.
+        string name = 4;
+    };
+
+    repeated Member members = 1;
+}
+
+service PeeringDBProxy {
+    // GetIXMembers returns information about membership of a given PeeringDB IX.
+    rpc GetIXMembers(GetIXMembersRequest) returns (GetIXMembersResponse);
+}
diff --git a/bgpwtf/cccampix/sync.py b/bgpwtf/cccampix/sync.py
new file mode 100644
index 0000000..31c2d2e
--- /dev/null
+++ b/bgpwtf/cccampix/sync.py
@@ -0,0 +1,216 @@
+"""
+Updates IRR objects in RIPE for the CCCamp IX.
+
+This, given a mntner password and a list of ASNs taking part in the IXP will
+ensure the right IRR objects are present:
+
+ - an aut-num containing import/export RPSL rules
+ - an as-set containing all member ASs.
+
+
+"""
+
+import difflib
+import string
+import sys
+import time
+
+import requests
+
+
+class IRRObject:
+    """An IRR object from RIPE."""
+    TYPE = None
+    def __init__(self, fields=None):
+        self.fields = fields or []
+
+    def add(self, k, v):
+        self.fields.append((k, v))
+
+    def render(self):
+        """Render to IRR format."""
+        return '\n'.join('{:16s}{}'.format(k+":", v) for k, v in self.fields) + "\n"
+
+    @classmethod
+    def from_ripe(cls, v):
+        """Download this object from the RIPE REST API."""
+        if cls.TYPE is None:
+            raise Exception('cannot fetch untyped IRRObject')
+
+        r = requests.get('https://rest.db.ripe.net/ripe/{}/{}.json'.format(cls.TYPE, v))
+        d = r.json()
+
+        obj = d['objects']['object'][0]
+        assert obj['type'] == cls.TYPE
+        assert obj['primary-key']['attribute'][0]['value'] == v
+
+        attrs = obj['attributes']['attribute']
+
+        fields = []
+        for attr in attrs:
+            # Skip metadata.
+            if attr['name'] in ('created', 'last-modified'):
+                continue
+            fields.append((attr['name'], attr['value']))
+
+        return cls(fields)
+
+    def send_to_ripe(self, password):
+        """Update this object (or create new) in RIPE using the SyncUpdates API."""
+        res = self.render()
+        res += 'password:       {}\n'.format(password)
+
+        data = {
+            'DATA': res,
+        }
+        r = requests.post('http://syncupdates.db.ripe.net/', files=data)
+        res = r.text
+        if 'Modify SUCCEEDED' not in res:
+            print(res)
+            raise Exception("Unexpected result from RIPE syncupdates")
+
+
+banner = [
+    ('remarks', r".--------------------------------------."),
+    ('remarks', r"|   _                         _    __  |"),
+    ('remarks', r"|  | |__   __ _ _ ____      _| |_ / _| |"),
+    ('remarks', r"|  | '_ \ / _` | '_ \ \ /\ / / __| |_  |"),
+    ('remarks', r"|  | |_) | (_| | |_) \ V  V /| |_|  _| |"),
+    ('remarks', r"|  |_.__/ \__, | .__(_)_/\_/  \__|_|   |"),
+    ('remarks', r"|         |___/|_|                     |"),
+    ('remarks', r"|--------------------------------------|"),
+    ('remarks', r"|  CCCamp2019 Internet Exchange Point  |"),
+    ('remarks', r"'--------------------------------------'"),
+    ('remarks', r''),
+    ('remarks', r'// 21. - 25. August 2019'),
+    ('remarks', r'// Ziegeleipark Mildenberg, Zehdenick, Germany, Earth, Milky Way'),
+    ('remarks', r'// Join us: https://bgp.wtf/cccamp19'),
+    ('remarks', r''),
+]
+
+
+class IXPAutNum(IRRObject):
+    """An aut-num (AS) object."""
+    TYPE = 'aut-num'
+    @classmethod
+    def make_for_members(cls, members):
+        fields = [
+            ('aut-num', 'AS208521'),
+            ('as-name', 'BGPWTF-CCCAMP19-IX'),
+        ] + banner + [
+            ('remarks', '// Current members:'),
+        ]
+
+        for member in sorted(list(members)):
+            fields.append(('import', 'from {} accept {}'.format(member, member)))
+            fields.append(('export', 'to {} announce AS-CCCAMP19-IX'.format(member)))
+
+        fields += [
+            ('remarks', ''),
+            ('remarks', '// Abuse: noc@hackerspace.pl'),
+            ('org', 'ORG-SH103-RIPE'),
+            ('admin-c', 'HACK2-RIPE'),
+            ('tech-c', 'HACK2-RIPE'),
+            ('status', 'ASSIGNED'),
+            ('mnt-by', 'RIPE-NCC-END-MNT'),
+            ('mnt-by', 'BGPWTF-AUTOMATION'),
+            ('mnt-by', 'pl-hs-1-mnt'),
+            ('source', 'RIPE'),
+        ]
+
+        return cls(fields)
+
+
+class ASSet(IRRObject):
+    """An as-set object."""
+    TYPE = 'as-set'
+    @classmethod
+    def make_for_members(cls, members):
+        fields = [
+            ('as-set', 'AS-CCCAMP19-IX'),
+            ('admin-c', 'HACK2-RIPE'),
+        ] + banner + [
+            ('remarks', '// Current members:'),
+        ]
+
+        for member in sorted(list(members)):
+            fields.append(('members', member))
+
+        fields += [
+            ('remarks', ''),
+            ('remarks', '// Abuse: noc@hackerspace.pl'),
+            ('tech-c', 'HACK2-RIPE'),
+            ('mnt-by', 'BGPWTF-AUTOMATION'),
+            ('mnt-by', 'pl-hs-1-mnt'),
+            ('source', 'RIPE'),
+        ]
+
+        return cls(fields)
+
+
+def sync(want, got, password, force):
+    """Sync an object if there is a diff to its current state."""
+    wantr = want.render().split('\n')
+    gotr = got.render().split('\n')
+
+    d = list(difflib.unified_diff(gotr, wantr, fromfile='got', tofile='want'))
+    fields_diff = set()
+    for dd in d:
+        if dd.startswith('---'):
+            continue
+        if dd.startswith('+++'):
+            continue
+        if not dd.startswith('+') and not dd.startswith('-'):
+            continue
+
+        field = dd[1:].split(':')[0]
+        fields_diff.add(field)
+
+    # We ignore remarks field changes, because the RIPE API returns us them always
+    # mangled (with spaces missing in the ASCII art).
+    if list(fields_diff) == ['remarks'] and not force:
+        print('No changes.')
+        return
+
+    if force:
+        print('Forcing update')
+    else:
+        print('Diff:')
+        print('\n'.join(d))
+    want.send_to_ripe(password)
+    print('Updated.')
+
+
+def sync_autnum(members, password, force=False):
+    print('Syncing aut-num...')
+    want = IXPAutNum.make_for_members(members)
+    got = IXPAutNum.from_ripe('AS208521')
+    sync(want, got, password, force)
+
+
+
+def sync_asset(members, password, force=False):
+    print('Syncing as-set...')
+    want = ASSet.make_for_members(members)
+    got = ASSet.from_ripe('AS-CCCAMP19-IX')
+    sync(want, got, password, force)
+
+
+
+if __name__ == '__main__':
+    if len(sys.argv) != 3:
+        print("Usage: {} password AS1,AS2,AS3,...".format(sys.argv[0]))
+        sys.exit(1)
+
+    password = sys.argv[1]
+    members = [m.strip().upper() for m in sys.argv[2].split(',')]
+
+    for member in members:
+        if not member.startswith('AS'):
+            raise Exception('{} is not a valid ASN'.format(member))
+
+        if not all(c in string.digits for c in member[2:]):
+            raise Exception('{} is not a valid ASN'.format(member))
+
+    sync_autnum(members, password)
+    sync_asset(members, password)
diff --git a/go/svc/invoice/BUILD.bazel b/bgpwtf/invoice/BUILD.bazel
similarity index 88%
rename from go/svc/invoice/BUILD.bazel
rename to bgpwtf/invoice/BUILD.bazel
index 05fcd23..b2e2ee8 100644
--- a/go/svc/invoice/BUILD.bazel
+++ b/bgpwtf/invoice/BUILD.bazel
@@ -9,12 +9,12 @@
         "render.go",
         "statusz.go",
     ],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/invoice",
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/invoice",
     visibility = ["//visibility:private"],
     deps = [
         "//go/mirko:go_default_library",
         "//go/statusz:go_default_library",
-        "//go/svc/invoice/templates:go_default_library",
+        "//bgpwtf/invoice/templates:go_default_library",
         "//proto/invoice:go_default_library",
         "@com_github_golang_glog//:go_default_library",
         "@com_github_golang_protobuf//proto:go_default_library",
diff --git a/go/svc/invoice/calc.go b/bgpwtf/invoice/calc.go
similarity index 100%
rename from go/svc/invoice/calc.go
rename to bgpwtf/invoice/calc.go
diff --git a/go/svc/invoice/main.go b/bgpwtf/invoice/main.go
similarity index 100%
rename from go/svc/invoice/main.go
rename to bgpwtf/invoice/main.go
diff --git a/go/svc/invoice/model.go b/bgpwtf/invoice/model.go
similarity index 100%
rename from go/svc/invoice/model.go
rename to bgpwtf/invoice/model.go
diff --git a/go/svc/invoice/proto/BUILD.bazel b/bgpwtf/invoice/proto/BUILD.bazel
similarity index 73%
rename from go/svc/invoice/proto/BUILD.bazel
rename to bgpwtf/invoice/proto/BUILD.bazel
index 6c78fa3..511bf26 100644
--- a/go/svc/invoice/proto/BUILD.bazel
+++ b/bgpwtf/invoice/proto/BUILD.bazel
@@ -3,6 +3,6 @@
 go_library(
     name = "go_default_library",
     srcs = ["generate.go"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/invoice/proto",
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/invoice/proto",
     visibility = ["//visibility:public"],
 )
diff --git a/go/svc/invoice/proto/generate.go b/bgpwtf/invoice/proto/generate.go
similarity index 100%
rename from go/svc/invoice/proto/generate.go
rename to bgpwtf/invoice/proto/generate.go
diff --git a/go/svc/invoice/render.go b/bgpwtf/invoice/render.go
similarity index 98%
rename from go/svc/invoice/render.go
rename to bgpwtf/invoice/render.go
index 03dcd77..2353014 100644
--- a/go/svc/invoice/render.go
+++ b/bgpwtf/invoice/render.go
@@ -8,7 +8,7 @@
 
 	wkhtml "github.com/sebastiaanklippert/go-wkhtmltopdf"
 
-	"code.hackerspace.pl/hscloud/go/svc/invoice/templates"
+	"code.hackerspace.pl/hscloud/bgpwtf/invoice/templates"
 	pb "code.hackerspace.pl/hscloud/proto/invoice"
 )
 
diff --git a/go/svc/invoice/statusz.go b/bgpwtf/invoice/statusz.go
similarity index 100%
rename from go/svc/invoice/statusz.go
rename to bgpwtf/invoice/statusz.go
diff --git a/go/svc/invoice/templates/BUILD.bazel b/bgpwtf/invoice/templates/BUILD.bazel
similarity index 74%
rename from go/svc/invoice/templates/BUILD.bazel
rename to bgpwtf/invoice/templates/BUILD.bazel
index 7874687..4756da4 100644
--- a/go/svc/invoice/templates/BUILD.bazel
+++ b/bgpwtf/invoice/templates/BUILD.bazel
@@ -13,6 +13,6 @@
     srcs = [
         ":templates_bindata",  # keep
     ],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/invoice/templates",  # keep
-    visibility = ["//go/svc/invoice:__subpackages__"],
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/invoice/templates",  # keep
+    visibility = ["//bgpwtf/invoice:__subpackages__"],
 )
diff --git a/go/svc/invoice/templates/invoice_en.html b/bgpwtf/invoice/templates/invoice_en.html
similarity index 100%
rename from go/svc/invoice/templates/invoice_en.html
rename to bgpwtf/invoice/templates/invoice_en.html
diff --git a/go/svc/invoice/templates/invoice_pl.html b/bgpwtf/invoice/templates/invoice_pl.html
similarity index 100%
rename from go/svc/invoice/templates/invoice_pl.html
rename to bgpwtf/invoice/templates/invoice_pl.html
diff --git a/go/svc/speedtest/.gitignore b/bgpwtf/speedtest/.gitignore
similarity index 100%
rename from go/svc/speedtest/.gitignore
rename to bgpwtf/speedtest/.gitignore
diff --git a/go/svc/speedtest/BUILD b/bgpwtf/speedtest/BUILD.bazel
similarity index 79%
rename from go/svc/speedtest/BUILD
rename to bgpwtf/speedtest/BUILD.bazel
index 3425af9..cc431d0 100644
--- a/go/svc/speedtest/BUILD
+++ b/bgpwtf/speedtest/BUILD.bazel
@@ -12,14 +12,14 @@
 go_library(
     name = "static_go",
     srcs = [":static"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/speedtest/static",
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/speedtest/static",
     visibility = ["//visibility:public"],
 )
 
 container_image(
     name="latest",
     base="@prodimage-bionic//image",
-    files = ["//go/svc/speedtest/backend:backend"],
+    files = ["//bgpwtf/speedtest/backend:backend"],
     directory = "/hscloud",
     entrypoint = ["/hscloud/backend"],
 )
@@ -30,9 +30,9 @@
     outs = ["version.sh"],
     executable = True,
     cmd = """
-        local=bazel/go/svc/speedtest:latest
+        local=bazel/bgpwtf/speedtest:latest
         tag=$$(date +%s)
-        remote=registry.k0.hswaw.net/go/svc/speedtest:$$tag
+        remote=registry.k0.hswaw.net/bgpwtf/speedtest:$$tag
 
         docker tag $$local $$remote
         docker push $$remote
diff --git a/go/svc/speedtest/LICENSE b/bgpwtf/speedtest/LICENSE
similarity index 100%
rename from go/svc/speedtest/LICENSE
rename to bgpwtf/speedtest/LICENSE
diff --git a/go/svc/speedtest/README.md b/bgpwtf/speedtest/README.md
similarity index 100%
rename from go/svc/speedtest/README.md
rename to bgpwtf/speedtest/README.md
diff --git a/go/svc/speedtest/backend/BUILD.bazel b/bgpwtf/speedtest/backend/BUILD.bazel
similarity index 81%
rename from go/svc/speedtest/backend/BUILD.bazel
rename to bgpwtf/speedtest/backend/BUILD.bazel
index 28ecc88..54ea2f6 100644
--- a/go/svc/speedtest/backend/BUILD.bazel
+++ b/bgpwtf/speedtest/backend/BUILD.bazel
@@ -3,11 +3,11 @@
 go_library(
     name = "go_default_library",
     srcs = ["main.go"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/speedtest/backend",
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/speedtest/backend",
     visibility = ["//visibility:private"],
     deps = [
         "@com_github_golang_glog//:go_default_library",
-        "//go/svc/speedtest:static_go", # keep
+        "//bgpwtf/speedtest:static_go", # keep
     ],
 )
 
diff --git a/go/svc/speedtest/backend/main.go b/bgpwtf/speedtest/backend/main.go
similarity index 94%
rename from go/svc/speedtest/backend/main.go
rename to bgpwtf/speedtest/backend/main.go
index 80c1c8c..7d4aece 100644
--- a/go/svc/speedtest/backend/main.go
+++ b/bgpwtf/speedtest/backend/main.go
@@ -27,7 +27,7 @@
 
 	"github.com/golang/glog"
 
-	"code.hackerspace.pl/hscloud/go/svc/speedtest/static"
+	"code.hackerspace.pl/hscloud/bgpwtf/speedtest/static"
 )
 
 var (
@@ -131,13 +131,13 @@
 		json.NewEncoder(w).Encode(res)
 	})
 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		w.Write(static.Data["go/svc/speedtest/index.html"])
+		w.Write(static.Data["bgpwtf/speedtest/index.html"])
 	})
 	http.HandleFunc("/speedtest.js", func(w http.ResponseWriter, r *http.Request) {
-		w.Write(static.Data["go/svc/speedtest/speedtest.js"])
+		w.Write(static.Data["bgpwtf/speedtest/speedtest.js"])
 	})
 	http.HandleFunc("/speedtest_worker.js", func(w http.ResponseWriter, r *http.Request) {
-		w.Write(static.Data["go/svc/speedtest/speedtest_worker.js"])
+		w.Write(static.Data["bgpwtf/speedtest/speedtest_worker.js"])
 	})
 
 	glog.Infof("Starting up at %v", flagBind)
diff --git a/go/svc/speedtest/backend/main_test.go b/bgpwtf/speedtest/backend/main_test.go
similarity index 100%
rename from go/svc/speedtest/backend/main_test.go
rename to bgpwtf/speedtest/backend/main_test.go
diff --git a/go/svc/speedtest/index.html b/bgpwtf/speedtest/index.html
similarity index 100%
rename from go/svc/speedtest/index.html
rename to bgpwtf/speedtest/index.html
diff --git a/go/svc/speedtest/kube/prod.jsonnet b/bgpwtf/speedtest/kube/prod.jsonnet
similarity index 97%
rename from go/svc/speedtest/kube/prod.jsonnet
rename to bgpwtf/speedtest/kube/prod.jsonnet
index e53fd45..fc6e083 100644
--- a/go/svc/speedtest/kube/prod.jsonnet
+++ b/bgpwtf/speedtest/kube/prod.jsonnet
@@ -9,7 +9,7 @@
         domain: "speedtest.hackerspace.pl",
 
         tag: "1563032542",
-        image: "registry.k0.hswaw.net/go/svc/speedtest:" + cfg.tag,
+        image: "registry.k0.hswaw.net/bgpwtf/speedtest:" + cfg.tag,
 
         resources: {
             requests: {
diff --git a/go/svc/speedtest/speedtest.js b/bgpwtf/speedtest/speedtest.js
similarity index 100%
rename from go/svc/speedtest/speedtest.js
rename to bgpwtf/speedtest/speedtest.js
diff --git a/go/svc/speedtest/speedtest_worker.js b/bgpwtf/speedtest/speedtest_worker.js
similarity index 100%
rename from go/svc/speedtest/speedtest_worker.js
rename to bgpwtf/speedtest/speedtest_worker.js
diff --git a/dc/README.md b/dc/README.md
new file mode 100644
index 0000000..6ee4c21
--- /dev/null
+++ b/dc/README.md
@@ -0,0 +1,4 @@
+hscloud/dc
+==========
+
+Software and systems related to DC operations and provisioning.
diff --git a/go/svc/arista-proxy/BUILD.bazel b/dc/arista-proxy/BUILD.bazel
similarity index 83%
rename from go/svc/arista-proxy/BUILD.bazel
rename to dc/arista-proxy/BUILD.bazel
index 5699607..fb7ec6a 100644
--- a/go/svc/arista-proxy/BUILD.bazel
+++ b/dc/arista-proxy/BUILD.bazel
@@ -6,11 +6,11 @@
         "main.go",
         "service.go",
     ],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/arista-proxy",
+    importpath = "code.hackerspace.pl/hscloud/dc/arista-proxy",
     visibility = ["//visibility:private"],
     deps = [
+        "//dc/arista-proxy/proto:go_default_library",
         "//go/mirko:go_default_library",
-        "//go/svc/arista-proxy/proto:go_default_library",
         "@com_github_golang_glog//:go_default_library",
         "@com_github_ybbus_jsonrpc//:go_default_library",
         "@org_golang_google_grpc//codes:go_default_library",
diff --git a/go/svc/arista-proxy/README.md b/dc/arista-proxy/README.md
similarity index 100%
rename from go/svc/arista-proxy/README.md
rename to dc/arista-proxy/README.md
diff --git a/go/svc/arista-proxy/main.go b/dc/arista-proxy/main.go
similarity index 94%
rename from go/svc/arista-proxy/main.go
rename to dc/arista-proxy/main.go
index 1227cb1..ccd1046 100644
--- a/go/svc/arista-proxy/main.go
+++ b/dc/arista-proxy/main.go
@@ -8,7 +8,7 @@
 	"github.com/golang/glog"
 	"github.com/ybbus/jsonrpc"
 
-	pb "code.hackerspace.pl/hscloud/go/svc/arista-proxy/proto"
+	pb "code.hackerspace.pl/hscloud/dc/arista-proxy/proto"
 )
 
 var (
diff --git a/go/svc/arista-proxy/proto/.gitignore b/dc/arista-proxy/proto/.gitignore
similarity index 100%
rename from go/svc/arista-proxy/proto/.gitignore
rename to dc/arista-proxy/proto/.gitignore
diff --git a/go/svc/arista-proxy/proto/BUILD.bazel b/dc/arista-proxy/proto/BUILD.bazel
similarity index 78%
rename from go/svc/arista-proxy/proto/BUILD.bazel
rename to dc/arista-proxy/proto/BUILD.bazel
index af116e3..2df4f58 100644
--- a/go/svc/arista-proxy/proto/BUILD.bazel
+++ b/dc/arista-proxy/proto/BUILD.bazel
@@ -10,7 +10,7 @@
 go_proto_library(
     name = "proto_go_proto",
     compilers = ["@io_bazel_rules_go//proto:go_grpc"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/arista-proxy/proto",
+    importpath = "code.hackerspace.pl/hscloud/dc/arista-proxy/proto",
     proto = ":proto_proto",
     visibility = ["//visibility:public"],
 )
@@ -18,6 +18,6 @@
 go_library(
     name = "go_default_library",
     embed = [":proto_go_proto"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/arista-proxy/proto",
+    importpath = "code.hackerspace.pl/hscloud/dc/arista-proxy/proto",
     visibility = ["//visibility:public"],
 )
diff --git a/go/svc/arista-proxy/proto/arista.proto b/dc/arista-proxy/proto/arista.proto
similarity index 90%
rename from go/svc/arista-proxy/proto/arista.proto
rename to dc/arista-proxy/proto/arista.proto
index d306b43..2874f70 100644
--- a/go/svc/arista-proxy/proto/arista.proto
+++ b/dc/arista-proxy/proto/arista.proto
@@ -1,6 +1,6 @@
 syntax = "proto3";
 package proto;
-option go_package = "code.hackerspace.pl/hscloud/go/svc/arista-proxy/proto";
+option go_package = "code.hackerspace.pl/hscloud/dc/arista-proxy/proto";
 
 message ShowVersionRequest {
 };
diff --git a/go/svc/arista-proxy/service.go b/dc/arista-proxy/service.go
similarity index 97%
rename from go/svc/arista-proxy/service.go
rename to dc/arista-proxy/service.go
index d7e2a29..3144ff7 100644
--- a/go/svc/arista-proxy/service.go
+++ b/dc/arista-proxy/service.go
@@ -7,7 +7,7 @@
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
-	pb "code.hackerspace.pl/hscloud/go/svc/arista-proxy/proto"
+	pb "code.hackerspace.pl/hscloud/dc/arista-proxy/proto"
 )
 
 func (s *server) ShowVersion(ctx context.Context, req *pb.ShowVersionRequest) (*pb.ShowVersionResponse, error) {
diff --git a/go/svc/cmc-proxy/BUILD.bazel b/dc/cmc-proxy/BUILD.bazel
similarity index 83%
rename from go/svc/cmc-proxy/BUILD.bazel
rename to dc/cmc-proxy/BUILD.bazel
index 56f3495..b2f68ca 100644
--- a/go/svc/cmc-proxy/BUILD.bazel
+++ b/dc/cmc-proxy/BUILD.bazel
@@ -6,11 +6,11 @@
         "client.go",
         "main.go",
     ],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/cmc-proxy",
+    importpath = "code.hackerspace.pl/hscloud/dc/cmc-proxy",
     visibility = ["//visibility:private"],
     deps = [
+        "//dc/cmc-proxy/proto:go_default_library",
         "//go/mirko:go_default_library",
-        "//go/svc/cmc-proxy/proto:go_default_library",
         "@com_github_cenkalti_backoff//:go_default_library",
         "@com_github_golang_glog//:go_default_library",
         "@org_golang_google_grpc//codes:go_default_library",
diff --git a/go/svc/cmc-proxy/README.md b/dc/cmc-proxy/README.md
similarity index 100%
rename from go/svc/cmc-proxy/README.md
rename to dc/cmc-proxy/README.md
diff --git a/go/svc/cmc-proxy/client.go b/dc/cmc-proxy/client.go
similarity index 100%
rename from go/svc/cmc-proxy/client.go
rename to dc/cmc-proxy/client.go
diff --git a/go/svc/cmc-proxy/main.go b/dc/cmc-proxy/main.go
similarity index 96%
rename from go/svc/cmc-proxy/main.go
rename to dc/cmc-proxy/main.go
index 5ae09d0..dc0cfb4 100644
--- a/go/svc/cmc-proxy/main.go
+++ b/dc/cmc-proxy/main.go
@@ -9,7 +9,7 @@
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
-	pb "code.hackerspace.pl/hscloud/go/svc/cmc-proxy/proto"
+	pb "code.hackerspace.pl/hscloud/dc/cmc-proxy/proto"
 )
 
 var (
diff --git a/go/svc/cmc-proxy/proto/.gitignore b/dc/cmc-proxy/proto/.gitignore
similarity index 100%
rename from go/svc/cmc-proxy/proto/.gitignore
rename to dc/cmc-proxy/proto/.gitignore
diff --git a/go/svc/m6220-proxy/proto/BUILD.bazel b/dc/cmc-proxy/proto/BUILD.bazel
similarity index 78%
rename from go/svc/m6220-proxy/proto/BUILD.bazel
rename to dc/cmc-proxy/proto/BUILD.bazel
index 8cbed6b..14b0569 100644
--- a/go/svc/m6220-proxy/proto/BUILD.bazel
+++ b/dc/cmc-proxy/proto/BUILD.bazel
@@ -10,7 +10,7 @@
 go_proto_library(
     name = "proto_go_proto",
     compilers = ["@io_bazel_rules_go//proto:go_grpc"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/m6220-proxy/proto",
+    importpath = "code.hackerspace.pl/hscloud/dc/cmc-proxy/proto",
     proto = ":proto_proto",
     visibility = ["//visibility:public"],
 )
@@ -18,6 +18,6 @@
 go_library(
     name = "go_default_library",
     embed = [":proto_go_proto"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/m6220-proxy/proto",
+    importpath = "code.hackerspace.pl/hscloud/dc/cmc-proxy/proto",
     visibility = ["//visibility:public"],
 )
diff --git a/go/svc/cmc-proxy/proto/proxy.proto b/dc/cmc-proxy/proto/proxy.proto
similarity index 76%
rename from go/svc/cmc-proxy/proto/proxy.proto
rename to dc/cmc-proxy/proto/proxy.proto
index a231693..5afe6b9 100644
--- a/go/svc/cmc-proxy/proto/proxy.proto
+++ b/dc/cmc-proxy/proto/proxy.proto
@@ -1,6 +1,6 @@
 syntax = "proto3";
 package proto;
-option go_package = "code.hackerspace.pl/hscloud/go/svc/cmc-proxy/proto";
+option go_package = "code.hackerspace.pl/hscloud/dc/cmc-proxy/proto";
 
 message GetKVMDataRequest {
     int64 blade_num = 1;
diff --git a/go/svc/m6220-proxy/BUILD.bazel b/dc/m6220-proxy/BUILD.bazel
similarity index 85%
rename from go/svc/m6220-proxy/BUILD.bazel
rename to dc/m6220-proxy/BUILD.bazel
index 90bfb1e..50fa692 100644
--- a/go/svc/m6220-proxy/BUILD.bazel
+++ b/dc/m6220-proxy/BUILD.bazel
@@ -6,11 +6,11 @@
         "cli.go",
         "main.go",
     ],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/m6220-proxy",
+    importpath = "code.hackerspace.pl/hscloud/dc/m6220-proxy",
     visibility = ["//visibility:private"],
     deps = [
+        "//dc/m6220-proxy/proto:go_default_library",
         "//go/mirko:go_default_library",
-        "//go/svc/m6220-proxy/proto:go_default_library",
         "//proto/infra:go_default_library",
         "@com_github_golang_glog//:go_default_library",
         "@com_github_ziutek_telnet//:go_default_library",
diff --git a/go/svc/m6220-proxy/cli.go b/dc/m6220-proxy/cli.go
similarity index 100%
rename from go/svc/m6220-proxy/cli.go
rename to dc/m6220-proxy/cli.go
diff --git a/go/svc/m6220-proxy/main.go b/dc/m6220-proxy/main.go
similarity index 98%
rename from go/svc/m6220-proxy/main.go
rename to dc/m6220-proxy/main.go
index a2ca4db..6fd972d 100644
--- a/go/svc/m6220-proxy/main.go
+++ b/dc/m6220-proxy/main.go
@@ -14,7 +14,7 @@
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
-	pb "code.hackerspace.pl/hscloud/go/svc/m6220-proxy/proto"
+	pb "code.hackerspace.pl/hscloud/dc/m6220-proxy/proto"
 	ipb "code.hackerspace.pl/hscloud/proto/infra"
 )
 
diff --git a/go/svc/m6220-proxy/proto/.gitignore b/dc/m6220-proxy/proto/.gitignore
similarity index 100%
rename from go/svc/m6220-proxy/proto/.gitignore
rename to dc/m6220-proxy/proto/.gitignore
diff --git a/go/svc/m6220-proxy/proto/BUILD.bazel b/dc/m6220-proxy/proto/BUILD.bazel
similarity index 78%
copy from go/svc/m6220-proxy/proto/BUILD.bazel
copy to dc/m6220-proxy/proto/BUILD.bazel
index 8cbed6b..4e0ae7e 100644
--- a/go/svc/m6220-proxy/proto/BUILD.bazel
+++ b/dc/m6220-proxy/proto/BUILD.bazel
@@ -10,7 +10,7 @@
 go_proto_library(
     name = "proto_go_proto",
     compilers = ["@io_bazel_rules_go//proto:go_grpc"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/m6220-proxy/proto",
+    importpath = "code.hackerspace.pl/hscloud/dc/m6220-proxy/proto",
     proto = ":proto_proto",
     visibility = ["//visibility:public"],
 )
@@ -18,6 +18,6 @@
 go_library(
     name = "go_default_library",
     embed = [":proto_go_proto"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/m6220-proxy/proto",
+    importpath = "code.hackerspace.pl/hscloud/dc/m6220-proxy/proto",
     visibility = ["//visibility:public"],
 )
diff --git a/go/svc/m6220-proxy/proto/proxy.proto b/dc/m6220-proxy/proto/proxy.proto
similarity index 78%
rename from go/svc/m6220-proxy/proto/proxy.proto
rename to dc/m6220-proxy/proto/proxy.proto
index bc840ad..b8444c3 100644
--- a/go/svc/m6220-proxy/proto/proxy.proto
+++ b/dc/m6220-proxy/proto/proxy.proto
@@ -1,6 +1,6 @@
 syntax = "proto3";
 package proto;
-option go_package = "code.hackerspace.pl/hscloud/go/svc/m6220-proxy/proto";
+option go_package = "code.hackerspace.pl/hscloud/dc/m6220-proxy/proto";
 
 message RunCommandRequest {
     string command = 1;
diff --git a/go/svc/topo/.gitignore b/dc/topo/.gitignore
similarity index 100%
rename from go/svc/topo/.gitignore
rename to dc/topo/.gitignore
diff --git a/go/svc/topo/BUILD.bazel b/dc/topo/BUILD.bazel
similarity index 75%
rename from go/svc/topo/BUILD.bazel
rename to dc/topo/BUILD.bazel
index 93c92a5..c10300c 100644
--- a/go/svc/topo/BUILD.bazel
+++ b/dc/topo/BUILD.bazel
@@ -6,15 +6,15 @@
         "main.go",
         "service.go",
     ],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/topo",
+    importpath = "code.hackerspace.pl/hscloud/dc/topo",
     visibility = ["//visibility:private"],
     deps = [
+        "//dc/topo/assets:go_default_library",
+        "//dc/topo/graph:go_default_library",
+        "//dc/topo/proto:go_default_library",
+        "//dc/topo/state:go_default_library",
         "//go/mirko:go_default_library",
         "//go/statusz:go_default_library",
-        "//go/svc/topo/assets:go_default_library",
-        "//go/svc/topo/graph:go_default_library",
-        "//go/svc/topo/proto:go_default_library",
-        "//go/svc/topo/state:go_default_library",
         "//proto/infra:go_default_library",
         "@com_github_digitalocean_go_netbox//netbox:go_default_library",
         "@com_github_digitalocean_go_netbox//netbox/client:go_default_library",
diff --git a/go/svc/topo/assets/BUILD b/dc/topo/assets/BUILD.bazel
similarity index 73%
rename from go/svc/topo/assets/BUILD
rename to dc/topo/assets/BUILD.bazel
index f8d186b..401487b 100644
--- a/go/svc/topo/assets/BUILD
+++ b/dc/topo/assets/BUILD.bazel
@@ -12,6 +12,6 @@
 go_library(
     name = "go_default_library",
     srcs = [":assets"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/topo/assets",
-    visibility = ["//go/svc/topo:__pkg__"],
+    importpath = "code.hackerspace.pl/hscloud/dc/topo/assets",
+    visibility = ["//dc/topo:__pkg__"],
 )
diff --git a/go/svc/topo/assets/full.render.js b/dc/topo/assets/full.render.js
similarity index 100%
rename from go/svc/topo/assets/full.render.js
rename to dc/topo/assets/full.render.js
diff --git a/go/svc/topo/assets/viz.js b/dc/topo/assets/viz.js
similarity index 100%
rename from go/svc/topo/assets/viz.js
rename to dc/topo/assets/viz.js
diff --git a/go/svc/topo/graph/BUILD.bazel b/dc/topo/graph/BUILD.bazel
similarity index 80%
rename from go/svc/topo/graph/BUILD.bazel
rename to dc/topo/graph/BUILD.bazel
index 205b401..26a5fc2 100644
--- a/go/svc/topo/graph/BUILD.bazel
+++ b/dc/topo/graph/BUILD.bazel
@@ -3,10 +3,10 @@
 go_library(
     name = "go_default_library",
     srcs = ["graph.go"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/topo/graph",
+    importpath = "code.hackerspace.pl/hscloud/dc/topo/graph",
     visibility = ["//visibility:public"],
     deps = [
-        "//go/svc/topo/proto:go_default_library",
+        "//dc/topo/proto:go_default_library",
         "@com_github_digitalocean_go_netbox//netbox/client:go_default_library",
         "@com_github_digitalocean_go_netbox//netbox/client/dcim:go_default_library",
         "@com_github_digitalocean_go_netbox//netbox/models:go_default_library",
diff --git a/go/svc/topo/graph/graph.go b/dc/topo/graph/graph.go
similarity index 98%
rename from go/svc/topo/graph/graph.go
rename to dc/topo/graph/graph.go
index 72e69b2..4d31f39 100644
--- a/go/svc/topo/graph/graph.go
+++ b/dc/topo/graph/graph.go
@@ -10,7 +10,7 @@
 	"github.com/digitalocean/go-netbox/netbox/models"
 	"github.com/golang/glog"
 
-	pb "code.hackerspace.pl/hscloud/go/svc/topo/proto"
+	pb "code.hackerspace.pl/hscloud/dc/topo/proto"
 )
 
 type MachinePort struct {
diff --git a/go/svc/topo/main.go b/dc/topo/main.go
similarity index 91%
rename from go/svc/topo/main.go
rename to dc/topo/main.go
index 4efe878..fd1fb8f 100644
--- a/go/svc/topo/main.go
+++ b/dc/topo/main.go
@@ -13,9 +13,9 @@
 	"github.com/golang/glog"
 	"github.com/golang/protobuf/proto"
 
-	"code.hackerspace.pl/hscloud/go/svc/topo/graph"
-	pb "code.hackerspace.pl/hscloud/go/svc/topo/proto"
-	"code.hackerspace.pl/hscloud/go/svc/topo/state"
+	"code.hackerspace.pl/hscloud/dc/topo/graph"
+	pb "code.hackerspace.pl/hscloud/dc/topo/proto"
+	"code.hackerspace.pl/hscloud/dc/topo/state"
 )
 
 var (
diff --git a/go/svc/topo/proto/.gitignore b/dc/topo/proto/.gitignore
similarity index 100%
rename from go/svc/topo/proto/.gitignore
rename to dc/topo/proto/.gitignore
diff --git a/go/svc/topo/proto/BUILD.bazel b/dc/topo/proto/BUILD.bazel
similarity index 77%
rename from go/svc/topo/proto/BUILD.bazel
rename to dc/topo/proto/BUILD.bazel
index b56fa57..3caae5b 100644
--- a/go/svc/topo/proto/BUILD.bazel
+++ b/dc/topo/proto/BUILD.bazel
@@ -9,7 +9,7 @@
 
 go_proto_library(
     name = "proto_go_proto",
-    importpath = "code.hackerspace.pl/hscloud/go/svc/topo/proto",
+    importpath = "code.hackerspace.pl/hscloud/dc/topo/proto",
     proto = ":proto_proto",
     visibility = ["//visibility:public"],
 )
@@ -17,6 +17,6 @@
 go_library(
     name = "go_default_library",
     embed = [":proto_go_proto"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/topo/proto",
+    importpath = "code.hackerspace.pl/hscloud/dc/topo/proto",
     visibility = ["//visibility:public"],
 )
diff --git a/go/svc/topo/proto/topo.proto b/dc/topo/proto/topo.proto
similarity index 90%
rename from go/svc/topo/proto/topo.proto
rename to dc/topo/proto/topo.proto
index 3f6e404..0127bf8 100644
--- a/go/svc/topo/proto/topo.proto
+++ b/dc/topo/proto/topo.proto
@@ -1,7 +1,7 @@
 syntax = "proto3";
 
 package topo;
-option go_package = "code.hackerspace.pl/hscloud/go/svc/topo/proto";
+option go_package = "code.hackerspace.pl/hscloud/dc/topo/proto";
 
 message Config {
     repeated Switch switch = 1;
diff --git a/go/svc/topo/service.go b/dc/topo/service.go
similarity index 97%
rename from go/svc/topo/service.go
rename to dc/topo/service.go
index fbbee92..8919939 100644
--- a/go/svc/topo/service.go
+++ b/dc/topo/service.go
@@ -13,9 +13,9 @@
 	"code.hackerspace.pl/hscloud/go/statusz"
 	ipb "code.hackerspace.pl/hscloud/proto/infra"
 
-	"code.hackerspace.pl/hscloud/go/svc/topo/assets"
-	"code.hackerspace.pl/hscloud/go/svc/topo/graph"
-	"code.hackerspace.pl/hscloud/go/svc/topo/state"
+	"code.hackerspace.pl/hscloud/dc/topo/assets"
+	"code.hackerspace.pl/hscloud/dc/topo/graph"
+	"code.hackerspace.pl/hscloud/dc/topo/state"
 )
 
 type Service struct {
diff --git a/go/svc/topo/state/BUILD.bazel b/dc/topo/state/BUILD.bazel
similarity index 73%
rename from go/svc/topo/state/BUILD.bazel
rename to dc/topo/state/BUILD.bazel
index 680e087..892e302 100644
--- a/go/svc/topo/state/BUILD.bazel
+++ b/dc/topo/state/BUILD.bazel
@@ -3,11 +3,11 @@
 go_library(
     name = "go_default_library",
     srcs = ["state.go"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/topo/state",
+    importpath = "code.hackerspace.pl/hscloud/dc/topo/state",
     visibility = ["//visibility:public"],
     deps = [
+        "//dc/topo/proto:go_default_library",
         "//go/pki:go_default_library",
-        "//go/svc/topo/proto:go_default_library",
         "//proto/infra:go_default_library",
         "@org_golang_google_grpc//:go_default_library",
     ],
diff --git a/go/svc/topo/state/state.go b/dc/topo/state/state.go
similarity index 96%
rename from go/svc/topo/state/state.go
rename to dc/topo/state/state.go
index 08403e7..116a55a 100644
--- a/go/svc/topo/state/state.go
+++ b/dc/topo/state/state.go
@@ -10,7 +10,7 @@
 	"code.hackerspace.pl/hscloud/go/pki"
 	ipb "code.hackerspace.pl/hscloud/proto/infra"
 
-	pb "code.hackerspace.pl/hscloud/go/svc/topo/proto"
+	pb "code.hackerspace.pl/hscloud/dc/topo/proto"
 )
 
 type SwitchportState struct {
diff --git a/go/svc/cmc-proxy/proto/BUILD.bazel b/go/svc/cmc-proxy/proto/BUILD.bazel
deleted file mode 100644
index 605570c..0000000
--- a/go/svc/cmc-proxy/proto/BUILD.bazel
+++ /dev/null
@@ -1,23 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
-
-proto_library(
-    name = "proto_proto",
-    srcs = ["proxy.proto"],
-    visibility = ["//visibility:public"],
-)
-
-go_proto_library(
-    name = "proto_go_proto",
-    compilers = ["@io_bazel_rules_go//proto:go_grpc"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/cmc-proxy/proto",
-    proto = ":proto_proto",
-    visibility = ["//visibility:public"],
-)
-
-go_library(
-    name = "go_default_library",
-    embed = [":proto_go_proto"],
-    importpath = "code.hackerspace.pl/hscloud/go/svc/cmc-proxy/proto",
-    visibility = ["//visibility:public"],
-)
diff --git a/personal/q3k/BUILD b/personal/q3k/BUILD
new file mode 100644
index 0000000..44988b4
--- /dev/null
+++ b/personal/q3k/BUILD
@@ -0,0 +1,7 @@
+py_binary(
+    name = "django-admin",
+    srcs = ["django-admin.py"],
+    deps = [
+        "@pip36//django",
+    ]
+)
diff --git a/personal/q3k/django-admin.py b/personal/q3k/django-admin.py
new file mode 100644
index 0000000..8648efa
--- /dev/null
+++ b/personal/q3k/django-admin.py
@@ -0,0 +1,5 @@
+#!python
+from django.core import management
+
+if __name__ == "__main__":
+    management.execute_from_command_line()
diff --git a/pip/requirements-linux.txt b/pip/requirements-linux.txt
index ce36ac3..304c0c4 100644
--- a/pip/requirements-linux.txt
+++ b/pip/requirements-linux.txt
@@ -1,3 +1,4 @@
+# This file is generated code. DO NOT EDIT.
 asn1crypto==0.24.0 \
     --hash=sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87 \
     --hash=sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49
@@ -21,6 +22,9 @@
     --hash=sha256:efcaace6e2915434d84e865c44f0cfe34e802269378afbb39a4aa6381aaec78b \
     --hash=sha256:f4431e01f1a5fdea95c78758e24c9565651499d92024ff34663b1ab12c8a10e5 \
     --hash=sha256:fd21155abee7cd4c0ba8fad5138636f2531174ea79ad1751b25dc30d833e1723
+certifi==2019.6.16 \
+    --hash=sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939 \
+    --hash=sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695
 cffi==1.11.5 \
     --hash=sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743 \
     --hash=sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef \
@@ -54,6 +58,9 @@
     --hash=sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917 \
     --hash=sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f \
     --hash=sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb
+chardet==3.0.4 \
+    --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
+    --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
 cryptography==2.4.2 \
     --hash=sha256:05a6052c6a9f17ff78ba78f8e6eb1d777d25db3b763343a1ae89a7a8670386dd \
     --hash=sha256:0eb83a24c650a36f68e31a6d0a70f7ad9c358fa2506dc7b683398b92e354a038 \
@@ -74,6 +81,9 @@
     --hash=sha256:af12dfc9874ac27ebe57fc28c8df0e8afa11f2a1025566476b0d50cdb8884f70 \
     --hash=sha256:b4fc04326b2d259ddd59ed8ea20405d2e695486ab4c5e1e49b025c484845206e \
     --hash=sha256:da5b5dda4aa0d5e2b758cc8dfc67f8d4212e88ea9caad5f61ba132f948bab859
+django==2.2.3 \
+    --hash=sha256:4d23f61b26892bac785f07401bc38cbf8fa4cec993f400e9cd9ddf28fd51c0ea \
+    --hash=sha256:6e974d4b57e3b29e4882b244d40171d6a75202ab8d2402b8e8adbd182e25cf0c
 fabric==2.4.0 \
     --hash=sha256:93684ceaac92e0b78faae551297e29c48370cede12ff0f853cdebf67d4b87068 \
     --hash=sha256:98538f2f3f63cf52497a8d0b24d18424ae83fe67ac7611225c72afb9e67f2cf6
@@ -114,6 +124,20 @@
     --hash=sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715 \
     --hash=sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1 \
     --hash=sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0
+pytz==2019.1 \
+    --hash=sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda \
+    --hash=sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141
+requests==2.22.0 \
+    --hash=sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4 \
+    --hash=sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31
 six==1.12.0 \
     --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
     --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
+sqlparse==0.3.0 \
+    --hash=sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177 \
+    --hash=sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873
+urllib3==1.25.3 \
+    --hash=sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1 \
+    --hash=sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232
+uwsgi==2.0.18 \
+    --hash=sha256:4972ac538800fb2d421027f49b4a1869b66048839507ccf0aa2fda792d99f583
diff --git a/pip/requirements.in b/pip/requirements.in
index d99d87f..653f04d 100644
--- a/pip/requirements.in
+++ b/pip/requirements.in
@@ -1,7 +1,10 @@
 asn1crypto==0.24.0
 bcrypt==3.1.5
+certifi==2019.6.16
 cffi==1.11.5
+chardet==3.0.4
 cryptography==2.4.2
+Django==2.2.3
 fabric==2.4.0
 future==0.17.1
 idna==2.8
@@ -10,4 +13,10 @@
 pyasn1==0.4.5
 pycparser==2.19
 PyNaCl==1.3.0
+pytz==2019.1
+requests==2.22.0
 six==1.12.0
+six==1.12.0
+sqlparse==0.3.0
+urllib3==1.25.3
+uWSGI==2.0.18