go/svc/leasifier, proto/hswaw -> hswaw/
Continued from https://gerrit.hackerspace.pl/c/hscloud/+/73 .
Change-Id: Ie761c2af588e06739de94fa1eff4f715d1f9b145
diff --git a/hswaw/leasifier/BUILD.bazel b/hswaw/leasifier/BUILD.bazel
new file mode 100644
index 0000000..1873ad8
--- /dev/null
+++ b/hswaw/leasifier/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "main.go",
+ "statusz.go",
+ ],
+ importpath = "code.hackerspace.pl/hscloud/hswaw/leasifier",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//go/mirko:go_default_library",
+ "//go/statusz:go_default_library",
+ "//hswaw/proto:go_default_library",
+ "@com_github_golang_glog//:go_default_library",
+ ],
+)
+
+go_binary(
+ name = "leasifier",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/hswaw/leasifier/README.md b/hswaw/leasifier/README.md
new file mode 100644
index 0000000..a88bbe3
--- /dev/null
+++ b/hswaw/leasifier/README.md
@@ -0,0 +1 @@
+Leasifier, a checkinator backend service.
diff --git a/hswaw/leasifier/main.go b/hswaw/leasifier/main.go
new file mode 100644
index 0000000..74ce88d
--- /dev/null
+++ b/hswaw/leasifier/main.go
@@ -0,0 +1,242 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "flag"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/golang/glog"
+
+ mirko "code.hackerspace.pl/hscloud/go/mirko"
+ hpb "code.hackerspace.pl/hscloud/hswaw/proto"
+)
+
+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
+ leaseRefreshString string
+ leaseRefresh time.Duration
+ leaseC chan chan []lease
+}
+
+func (s *service) work(ctx context.Context) {
+ leases := []lease{}
+
+ ticker := time.NewTicker(s.leaseRefresh)
+ 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
+ }
+ sort.Slice(l, func(i, j int) bool { return string([]byte(l[i].ip)) < string([]byte(l[j].ip)) })
+ 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{
+ Leases: make([]*hpb.LeasifierLease, len(leases)),
+ }
+
+ 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.StringVar(&s.leaseRefreshString, "lease_refresh", "1m", "How often to refresh leases")
+ flag.Parse()
+
+ d, err := time.ParseDuration(s.leaseRefreshString)
+ if err != nil {
+ glog.Exit(err)
+ }
+ s.leaseRefresh = d
+
+ m := mirko.New()
+ if err := m.Listen(); err != nil {
+ glog.Exitf("Could not listen: %v", err)
+ }
+
+ hpb.RegisterLeasifierServer(m.GRPC(), s)
+
+ s.setupStatusz(m)
+ 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/hswaw/leasifier/statusz.go b/hswaw/leasifier/statusz.go
new file mode 100644
index 0000000..7f34a37
--- /dev/null
+++ b/hswaw/leasifier/statusz.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+ "context"
+
+ mirko "code.hackerspace.pl/hscloud/go/mirko"
+ "code.hackerspace.pl/hscloud/go/statusz"
+)
+
+const statuszFragment = `
+ <style type="text/css">
+ .table td,th {
+ background-color: #eee;
+ padding: 0.2em 0.4em 0.2em 0.4em;
+ }
+ .table th {
+ background-color: #c0c0c0;
+ }
+ .table {
+ background-color: #fff;
+ border-spacing: 0.2em;
+ }
+ </style>
+ <div>
+ <b>Current leases:</b> {{ .Leases | len }}<br />
+ <table class="table">
+ <tr>
+ <th>IP Address</th>
+ <th>MAC Address</th>
+ <th>Start</th>
+ <th>End</th>
+ </tr>
+ {{range .Leases }}
+ <tr>
+ <td>{{ .IP }}</td>
+ <td>{{ .MAC }}</td>
+ <td>{{ .Start }}</td>
+ <td>{{ .End }}</td>
+ </tr>
+ {{end}}
+ </table>
+ </div>
+`
+
+type szLeases struct {
+ IP string
+ MAC string
+ Start string
+ End string
+}
+
+func (s *service) setupStatusz(m *mirko.Mirko) {
+ statusz.AddStatusPart("Leases", statuszFragment, func(ctx context.Context) interface{} {
+ c := make(chan []lease)
+ s.leaseC <- c
+ leases := <-c
+
+ ls := make([]szLeases, len(leases))
+
+ for i, l := range leases {
+ ls[i].IP = l.ip.String()
+ ls[i].MAC = l.hardware.String()
+ ls[i].Start = l.from.String()
+ ls[i].End = l.to.String()
+ }
+
+ return struct {
+ Leases []szLeases
+ }{
+ Leases: ls,
+ }
+ })
+}