go: add bazel buildfiles, implement leasifier
diff --git a/go/mirko/BUILD.bazel b/go/mirko/BUILD.bazel
new file mode 100644
index 0000000..59ed755
--- /dev/null
+++ b/go/mirko/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["mirko.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/mirko",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/pki:go_default_library",
+        "//go/statusz:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+        "@org_golang_google_grpc//:go_default_library",
+        "@org_golang_google_grpc//reflection:go_default_library",
+        "@org_golang_x_net//trace:go_default_library",
+    ],
+)
diff --git a/go/mirko/mirko.go b/go/mirko/mirko.go
index 2e62c3d..01766db 100644
--- a/go/mirko/mirko.go
+++ b/go/mirko/mirko.go
@@ -38,17 +38,15 @@
 	httpServer *http.Server
 	httpMux    *http.ServeMux
 
-	ctx     context.Context
-	cancel  context.CancelFunc
-	waiters []chan bool
+	ctx    context.Context
+	cancel context.CancelFunc
 }
 
 func New() *Mirko {
 	ctx, cancel := context.WithCancel(context.Background())
 	return &Mirko{
-		ctx:     ctx,
-		cancel:  cancel,
-		waiters: []chan bool{},
+		ctx:    ctx,
+		cancel: cancel,
 	}
 }
 
@@ -161,10 +159,8 @@
 // Done() returns a channel that will emit a value when the service is
 // shut down. This should be used in the main() function instead of a select{}
 // call, to allow the background context to be canceled fully.
-func (m *Mirko) Done() chan bool {
-	c := make(chan bool, 1)
-	m.waiters = append(m.waiters, c)
-	return c
+func (m *Mirko) Done() <-chan struct{} {
+	return m.Context().Done()
 }
 
 // Serve starts serving HTTP and gRPC requests
@@ -187,10 +183,6 @@
 		select {
 		case <-signalCh:
 			m.cancel()
-			time.Sleep(time.Second)
-			for _, w := range m.waiters {
-				w <- true
-			}
 		}
 	}()
 
diff --git a/go/pki/BUILD.bazel b/go/pki/BUILD.bazel
new file mode 100644
index 0000000..0d3544f
--- /dev/null
+++ b/go/pki/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["grpc.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/pki",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_golang_glog//:go_default_library",
+        "@org_golang_google_grpc//:go_default_library",
+        "@org_golang_google_grpc//codes:go_default_library",
+        "@org_golang_google_grpc//credentials:go_default_library",
+        "@org_golang_google_grpc//peer:go_default_library",
+        "@org_golang_google_grpc//status:go_default_library",
+        "@org_golang_x_net//trace:go_default_library",
+    ],
+)
diff --git a/go/pki/grpc.go b/go/pki/grpc.go
index f014a34..6d8f173 100644
--- a/go/pki/grpc.go
+++ b/go/pki/grpc.go
@@ -37,6 +37,7 @@
 	flagCertificatePath string
 	flagKeyPath         string
 	flagPKIRealm        string
+	flagPKIDisable      bool
 
 	// Enable logging HSPKI info into traces
 	Trace = true
@@ -53,6 +54,7 @@
 	flag.StringVar(&flagCertificatePath, "hspki_tls_certificate_path", "pki/service.pem", "Path to PKI service certificate")
 	flag.StringVar(&flagKeyPath, "hspki_tls_key_path", "pki/service-key.pem", "Path to PKI service private key")
 	flag.StringVar(&flagPKIRealm, "hspki_realm", "svc.cluster.local", "PKI realm")
+	flag.BoolVar(&flagPKIDisable, "hspki_disable", false, "Disable PKI entirely (insecure!)")
 }
 
 func maybeTrace(ctx context.Context, f string, args ...interface{}) {
@@ -168,6 +170,10 @@
 	if !flag.Parsed() {
 		glog.Exitf("WithServerHSPKI called before flag.Parse!")
 	}
+	if flagPKIDisable {
+		return []grpc.ServerOption{}
+	}
+
 	serverCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath)
 	if err != nil {
 		glog.Exitf("WithServerHSPKI: cannot load service certificate/key: %v", err)
@@ -194,6 +200,13 @@
 }
 
 func WithClientHSPKI() grpc.DialOption {
+	if !flag.Parsed() {
+		glog.Exitf("WithServerHSPKI called before flag.Parse!")
+	}
+	if flagPKIDisable {
+		return grpc.WithInsecure()
+	}
+
 	certPool := x509.NewCertPool()
 	ca, err := ioutil.ReadFile(flagCAPath)
 	if err != nil {
diff --git a/go/proto/BUILD.bazel b/go/proto/BUILD.bazel
new file mode 100644
index 0000000..d2f24fb
--- /dev/null
+++ b/go/proto/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["generate.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/statusz/BUILD.bazel b/go/statusz/BUILD.bazel
new file mode 100644
index 0000000..951adff
--- /dev/null
+++ b/go/statusz/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["statusz.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/statusz",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_golang_glog//:go_default_library",
+        "@com_github_shirou_gopsutil//load:go_default_library",
+    ],
+)
diff --git a/go/svc/arista-proxy/BUILD.bazel b/go/svc/arista-proxy/BUILD.bazel
new file mode 100644
index 0000000..0d88e08
--- /dev/null
+++ b/go/svc/arista-proxy/BUILD.bazel
@@ -0,0 +1,41 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "proto_proto",
+    srcs = ["arista.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/arista-proxy",
+    proto = ":proto_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "main.go",
+        "service.go",
+    ],
+    embed = [":proto_go_proto"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/arista-proxy",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//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",
+        "@org_golang_google_grpc//status:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "arista-proxy",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/arista-proxy/proto/BUILD.bazel b/go/svc/arista-proxy/proto/BUILD.bazel
new file mode 100644
index 0000000..ae4ee9b
--- /dev/null
+++ b/go/svc/arista-proxy/proto/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["generate.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/arista-proxy/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/cmc-proxy/BUILD.bazel b/go/svc/cmc-proxy/BUILD.bazel
new file mode 100644
index 0000000..a142dc9
--- /dev/null
+++ b/go/svc/cmc-proxy/BUILD.bazel
@@ -0,0 +1,41 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "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",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "client.go",
+        "main.go",
+    ],
+    embed = [":proto_go_proto"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/cmc-proxy",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//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",
+    ],
+)
+
+go_binary(
+    name = "cmc-proxy",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/cmc-proxy/proto/BUILD.bazel b/go/svc/cmc-proxy/proto/BUILD.bazel
new file mode 100644
index 0000000..e924906
--- /dev/null
+++ b/go/svc/cmc-proxy/proto/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["generate.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/cmc-proxy/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/leasifier/BUILD.bazel b/go/svc/leasifier/BUILD.bazel
new file mode 100644
index 0000000..386f886
--- /dev/null
+++ b/go/svc/leasifier/BUILD.bazel
@@ -0,0 +1,21 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["main.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/leasifier",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//go/mirko:go_default_library",
+        "//proto/hswaw: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",
+    ],
+)
+
+go_binary(
+    name = "leasifier",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/leasifier/main.go b/go/svc/leasifier/main.go
new file mode 100644
index 0000000..9b2cf5a
--- /dev/null
+++ b/go/svc/leasifier/main.go
@@ -0,0 +1,228 @@
+package main
+
+import (
+	"bufio"
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"net"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/golang/glog"
+
+	mirko "code.hackerspace.pl/hscloud/go/mirko"
+	hpb "code.hackerspace.pl/hscloud/proto/hswaw"
+)
+
+type lease struct {
+	hardware net.HardwareAddr
+	ip       net.IP
+	from     time.Time
+	to       time.Time
+}
+
+const timeFmt = "2006/01/02 15:04:05"
+
+func parseLeases(f io.Reader, now time.Time) ([]lease, error) {
+	scanner := bufio.NewScanner(f)
+
+	leases := make(map[string]*lease)
+
+	var curAddr *net.IP
+	var curStart *time.Time
+	var curEnd *time.Time
+	var curHW *net.HardwareAddr
+
+	for scanner.Scan() {
+		l := scanner.Text()
+		l = strings.TrimSpace(l)
+		if len(l) < 1 {
+			continue
+		}
+
+		if strings.HasPrefix(l, "#") {
+			continue
+		}
+
+		parts := strings.Fields(l)
+		if len(parts) < 1 {
+			continue
+		}
+
+		if curAddr == nil {
+			switch parts[0] {
+			case "lease":
+				if len(parts) < 3 || parts[2] != "{" {
+					glog.Warningf("invalid lease line %q", l)
+					break
+				}
+
+				ip := net.ParseIP(parts[1])
+				if ip == nil {
+					glog.Warningf("invalid lease line %q: invalid ip")
+					break
+				}
+				curAddr = &ip
+			}
+		} else {
+			parts[len(parts)-1] = strings.TrimRight(parts[len(parts)-1], ";")
+
+			switch parts[0] {
+			case "starts":
+				fallthrough
+			case "ends":
+				if len(parts) != 4 {
+					glog.Warningf("invalid time line %q", l)
+					break
+				}
+				t, err := time.Parse(timeFmt, fmt.Sprintf("%s %s", parts[2], parts[3]))
+				if err != nil {
+					glog.Warningf("invalid time line %q: %v", l, err)
+					break
+				}
+				if parts[0] == "starts" {
+					curStart = &t
+				} else {
+					curEnd = &t
+				}
+			case "hardware":
+				if len(parts) < 2 {
+					glog.Warningf("invalid hardware line %q", l)
+					break
+				}
+				if parts[1] != "ethernet" {
+					break
+				}
+				if len(parts) != 3 {
+					glog.Warningf("invalid hardware ethernet line %q", l)
+					break
+				}
+				hw, err := net.ParseMAC(parts[2])
+				if err != nil {
+					glog.Warningf("invalid hardware ethernet line %q: %v", l, err)
+					break
+				}
+				curHW = &hw
+			case "}":
+				if curStart == nil || curEnd == nil || curHW == nil {
+					glog.V(2).Info("Invalid block for %q, not enough fields", curAddr.String())
+				} else if curEnd.Before(now) {
+					// skip.
+				} else {
+					leases[curHW.String()] = &lease{
+						hardware: *curHW,
+						ip:       *curAddr,
+						from:     *curStart,
+						to:       *curEnd,
+					}
+				}
+				curAddr = nil
+				curStart = nil
+				curEnd = nil
+				curHW = nil
+			}
+		}
+	}
+
+	ret := make([]lease, len(leases))
+	i := 0
+	for _, v := range leases {
+		ret[i] = *v
+		i += 1
+	}
+
+	return ret, nil
+}
+
+type service struct {
+	leaseFile string
+	leaseC    chan chan []lease
+}
+
+func (s *service) work(ctx context.Context) {
+	leases := []lease{}
+
+	ticker := time.NewTicker(30 * time.Second)
+	start := make(chan struct{}, 1)
+	start <- struct{}{}
+
+	work := func() {
+		glog.Infof("Parsing leases...")
+		f, err := os.Open(s.leaseFile)
+		if err != nil {
+			glog.Errorf("Could not open lease file: %v", err)
+			return
+		}
+		l, err := parseLeases(f, time.Now())
+		f.Close()
+		if err != nil {
+			glog.Errorf("Could not parse lease file: %v", err)
+			return
+		}
+		leases = l
+		glog.Infof("Got %d leases", len(leases))
+	}
+
+	glog.Infof("Worker started.")
+
+	for {
+		select {
+		case <-start:
+			work()
+		case <-ticker.C:
+			work()
+		case c := <-s.leaseC:
+			c <- leases
+		case <-ctx.Done():
+			glog.Infof("Worker quitting.")
+			close(start)
+			return
+		}
+	}
+}
+
+func (s *service) Leases(ctx context.Context, req *hpb.LeasifierLeasesRequest) (*hpb.LeasifierLeasesResponse, error) {
+	c := make(chan []lease)
+	s.leaseC <- c
+	leases := <-c
+
+	res := &hpb.LeasifierLeasesResponse{}
+
+	for i, l := range leases {
+		res.Leases[i] = &hpb.LeasifierLease{
+			PhysicalAddress: l.hardware.String(),
+			IpAddress:       l.ip.String(),
+		}
+	}
+
+	return res, nil
+}
+
+func main() {
+	s := &service{
+		leaseC: make(chan chan []lease),
+	}
+
+	flag.StringVar(&s.leaseFile, "lease_file", "/var/db/dhcpd.leases", "Location of leasefile")
+	flag.Parse()
+
+	m := mirko.New()
+	if err := m.Listen(); err != nil {
+		glog.Exitf("Could not listen: %v", err)
+	}
+
+	hpb.RegisterLeasifierServer(m.GRPC(), s)
+
+	go s.work(m.Context())
+
+	if err := m.Serve(); err != nil {
+		glog.Exitf("Could not run: %v", err)
+	}
+
+	glog.Info("Running.")
+	<-m.Done()
+
+}
diff --git a/go/svc/m6220-proxy/BUILD.bazel b/go/svc/m6220-proxy/BUILD.bazel
new file mode 100644
index 0000000..ee500a9
--- /dev/null
+++ b/go/svc/m6220-proxy/BUILD.bazel
@@ -0,0 +1,43 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "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/m6220-proxy",
+    proto = ":proto_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "cli.go",
+        "main.go",
+    ],
+    embed = [":proto_go_proto"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/m6220-proxy",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//go/mirko:go_default_library",
+        "//go/proto/infra:go_default_library",
+        "//go/svc/m6220-proxy/proto:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+        "@com_github_ziutek_telnet//:go_default_library",
+        "@org_golang_google_grpc//codes:go_default_library",
+        "@org_golang_google_grpc//status:go_default_library",
+        "@org_golang_x_net//trace:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "m6220-proxy",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/m6220-proxy/proto/BUILD.bazel b/go/svc/m6220-proxy/proto/BUILD.bazel
new file mode 100644
index 0000000..5fb4fac
--- /dev/null
+++ b/go/svc/m6220-proxy/proto/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["generate.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/m6220-proxy/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/topo/BUILD.bazel b/go/svc/topo/BUILD.bazel
new file mode 100644
index 0000000..5e4abff
--- /dev/null
+++ b/go/svc/topo/BUILD.bazel
@@ -0,0 +1,46 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "proto_proto",
+    srcs = ["config.proto"],
+    visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+    name = "proto_go_proto",
+    importpath = "code.hackerspace.pl/hscloud/go/svc/topo",
+    proto = ":proto_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "main.go",
+        "service.go",
+    ],
+    embed = [":proto_go_proto"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/topo",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//go/mirko:go_default_library",
+        "//go/proto/infra:go_default_library",
+        "//go/statusz:go_default_library",
+        "//go/svc/topo/graph:go_default_library",
+        "//go/svc/topo/proto:go_default_library",
+        "//go/svc/topo/state:go_default_library",
+        "@com_github_digitalocean_go_netbox//netbox:go_default_library",
+        "@com_github_digitalocean_go_netbox//netbox/client:go_default_library",
+        "@com_github_gobuffalo_packr//:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+        "@com_github_golang_protobuf//proto:go_default_library",
+        "@ml_vbom_util//sortorder:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "topo",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/topo/graph/BUILD.bazel b/go/svc/topo/graph/BUILD.bazel
new file mode 100644
index 0000000..205b401
--- /dev/null
+++ b/go/svc/topo/graph/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["graph.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/topo/graph",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/svc/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",
+        "@com_github_golang_glog//:go_default_library",
+    ],
+)
diff --git a/go/svc/topo/proto/BUILD.bazel b/go/svc/topo/proto/BUILD.bazel
new file mode 100644
index 0000000..01007dd
--- /dev/null
+++ b/go/svc/topo/proto/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["generate.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/topo/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/svc/topo/state/BUILD.bazel b/go/svc/topo/state/BUILD.bazel
new file mode 100644
index 0000000..81985d2
--- /dev/null
+++ b/go/svc/topo/state/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["state.go"],
+    importpath = "code.hackerspace.pl/hscloud/go/svc/topo/state",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/pki:go_default_library",
+        "//go/proto/infra:go_default_library",
+        "//go/svc/topo/proto:go_default_library",
+        "@org_golang_google_grpc//:go_default_library",
+    ],
+)