go/svc/(dc stuff) -> dc/

We want to start keeping codebases separated per 'team'/intent, to then
have simple OWNER files/trees to specify review rules.

This means dc/ stuff can all be OWNED by q3k, and review will only
involve a +1 for style/readability, instead  of a +2 for approval.

Change-Id: I05afbc4e1018944b841ec0d88cd24cc95bec8bf1
diff --git a/dc/topo/graph/graph.go b/dc/topo/graph/graph.go
new file mode 100644
index 0000000..4d31f39
--- /dev/null
+++ b/dc/topo/graph/graph.go
@@ -0,0 +1,213 @@
+package graph
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	"github.com/digitalocean/go-netbox/netbox/client"
+	"github.com/digitalocean/go-netbox/netbox/client/dcim"
+	"github.com/digitalocean/go-netbox/netbox/models"
+	"github.com/golang/glog"
+
+	pb "code.hackerspace.pl/hscloud/dc/topo/proto"
+)
+
+type MachinePort struct {
+	Machine  *Machine
+	OtherEnd *SwitchPort
+	Name     string
+}
+
+type SwitchPort struct {
+	Switch   *Switch
+	OtherEnd *MachinePort
+	Name     string
+}
+
+type Machine struct {
+	Name     string
+	Complete bool
+
+	Ports map[string]*MachinePort
+}
+
+type Switch struct {
+	Name     string
+	Complete bool
+
+	Ports map[string]*SwitchPort
+}
+
+type Graph struct {
+	Switches map[string]*Switch
+	Machines map[string]*Machine
+	Mu       sync.RWMutex
+}
+
+func New() *Graph {
+	return &Graph{
+		Switches: make(map[string]*Switch),
+		Machines: make(map[string]*Machine),
+	}
+}
+
+func (g *Graph) RemoveMachine(name string) {
+	glog.Infof("Removed machine %q", name)
+}
+
+func (g *Graph) RemoveSwitch(name string) {
+	glog.Infof("Removed switch %q", name)
+}
+
+func (g *Graph) LoadConfig(conf *pb.Config) error {
+	loadedMachines := make(map[string]bool)
+	loadedSwitches := make(map[string]bool)
+
+	// Add new machines and switches.
+	for _, machinepb := range conf.Machine {
+		if machinepb.Name == "" {
+			return fmt.Errorf("empty machine name")
+		}
+		if loadedMachines[machinepb.Name] {
+			return fmt.Errorf("duplicate machine name: %v", machinepb.Name)
+		}
+		machine, ok := g.Machines[machinepb.Name]
+		if !ok {
+			machine = &Machine{
+				Name:  machinepb.Name,
+				Ports: make(map[string]*MachinePort),
+			}
+			for _, portpb := range machinepb.ManagedPort {
+				machine.Ports[portpb.Name] = &MachinePort{
+					Name:    portpb.Name,
+					Machine: machine,
+				}
+			}
+			g.Machines[machinepb.Name] = machine
+			glog.Infof("Added machine %q with %d managed ports", machine.Name, len(machine.Ports))
+		}
+		machine.Complete = false
+		loadedMachines[machinepb.Name] = true
+	}
+	for _, switchpb := range conf.Switch {
+		if switchpb.Name == "" {
+			return fmt.Errorf("empty switch name")
+		}
+		if loadedSwitches[switchpb.Name] {
+			return fmt.Errorf("duplicate switch name: %v", switchpb.Name)
+		}
+		if loadedMachines[switchpb.Name] {
+			return fmt.Errorf("switch name collides with machine name: %v", switchpb.Name)
+		}
+		sw, ok := g.Switches[switchpb.Name]
+		if !ok {
+			sw = &Switch{
+				Name:  switchpb.Name,
+				Ports: make(map[string]*SwitchPort),
+			}
+			for _, portpb := range switchpb.ManagedPort {
+				sw.Ports[portpb.Name] = &SwitchPort{
+					Name:   portpb.Name,
+					Switch: sw,
+				}
+			}
+			g.Switches[switchpb.Name] = sw
+			glog.Infof("Added switch %q with %d managed ports", sw.Name, len(sw.Ports))
+		}
+		sw.Complete = false
+		loadedSwitches[switchpb.Name] = true
+	}
+
+	// Remove old machines and switches.
+	removeMachines := make(map[string]bool)
+	removeSwitches := make(map[string]bool)
+	for name, _ := range g.Switches {
+		if !loadedSwitches[name] {
+			removeSwitches[name] = true
+		}
+	}
+	for name, _ := range g.Machines {
+		if !loadedMachines[name] {
+			removeMachines[name] = true
+		}
+	}
+	for name, _ := range removeMachines {
+		g.RemoveMachine(name)
+	}
+	for name, _ := range removeSwitches {
+		g.RemoveSwitch(name)
+	}
+	return nil
+
+}
+
+func (g *Graph) FeedFromNetbox(ctx context.Context, nb *client.NetBox) error {
+	// Clear all connections first, because it's easier that way.
+	for _, machine := range g.Machines {
+		for _, port := range machine.Ports {
+			port.OtherEnd = nil
+		}
+	}
+	for _, sw := range g.Switches {
+		for _, port := range sw.Ports {
+			port.OtherEnd = nil
+		}
+	}
+
+	// Load new connections.
+	// Iterating over just machines should be fine if all connections are
+	// guaranteed to be between machines and switches (which is the model for
+	// now).
+	for _, machine := range g.Machines {
+		req := &dcim.DcimInterfaceConnectionsListParams{
+			Device:  &machine.Name,
+			Context: ctx,
+		}
+		res, err := nb.Dcim.DcimInterfaceConnectionsList(req, nil)
+		if err != nil {
+			return fmt.Errorf("while querying information about %q: %v", machine.Name, err)
+		}
+		for _, connection := range res.Payload.Results {
+			ia := connection.InterfaceA
+			ib := connection.InterfaceB
+			if ia == nil || ib == nil {
+				continue
+			}
+
+			// Find which way this thing actually connects.
+			var thisSide, otherSide *models.PeerInterface
+			if ia.Device.Name == machine.Name {
+				thisSide = ia
+				otherSide = ib
+			} else if ib.Device.Name == machine.Name {
+				thisSide = ib
+				otherSide = ia
+			} else {
+				glog.Warning("Netbox connectivity for %q reported a link without it involced..?", machine.Name)
+				continue
+			}
+
+			thisPort, ok := machine.Ports[*thisSide.Name]
+			if !ok {
+				continue
+			}
+			sw, ok := g.Switches[otherSide.Device.Name]
+			if !ok {
+				glog.Warningf("Machine %q port %q is managed but connected to unknown device %q", machine.Name, thisPort.Name, otherSide.Device.Name)
+				continue
+			}
+			otherPort, ok := sw.Ports[*otherSide.Name]
+			if !ok {
+				glog.Warningf("Machine %q port %q is managed but connected to unmanaged port %q on %q", machine.Name, thisPort.Name, otherSide.Name, sw.Name)
+				continue
+			}
+
+			// Connect the two together!
+			thisPort.OtherEnd = otherPort
+			otherPort.OtherEnd = thisPort
+			glog.Infof("Connected: %s/%s <-> %s/%s", machine.Name, thisPort.Name, sw.Name, otherPort.Name)
+		}
+	}
+	return nil
+}