go/svc/leasifier, proto/hswaw -> hswaw/

Continued from https://gerrit.hackerspace.pl/c/hscloud/+/73 .

Change-Id: Ie761c2af588e06739de94fa1eff4f715d1f9b145
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()
+
+}