From d954d7c851cd13b3477aaf63ce2f5c98a33afe80 Mon Sep 17 00:00:00 2001 From: Martin Geno Date: Sun, 7 May 2017 03:37:30 +0200 Subject: [PATCH] [TASK] add node model --- cmd/freifunkmanager/main.go | 11 ++- config/main.go | 2 + lib/worker/worker.go | 41 +++++++++ lib/worker/worker_test.go | 24 +++++ runtime/node.go | 170 ++++++++++++++++++++++++++++++++++++ runtime/node_test.go | 35 ++++++++ runtime/nodes.go | 86 ++++++++++++++---- 7 files changed, 352 insertions(+), 17 deletions(-) create mode 100644 lib/worker/worker.go create mode 100644 lib/worker/worker_test.go create mode 100644 runtime/node.go create mode 100644 runtime/node_test.go diff --git a/cmd/freifunkmanager/main.go b/cmd/freifunkmanager/main.go index b24799e..48ab40b 100644 --- a/cmd/freifunkmanager/main.go +++ b/cmd/freifunkmanager/main.go @@ -6,6 +6,7 @@ import ( "os" "os/signal" "syscall" + "time" "github.com/NYTimes/gziphandler" goji "goji.io" @@ -13,6 +14,7 @@ import ( configPackage "github.com/FreifunkBremen/freifunkmanager/config" "github.com/FreifunkBremen/freifunkmanager/lib/log" + "github.com/FreifunkBremen/freifunkmanager/lib/worker" "github.com/FreifunkBremen/freifunkmanager/runtime" "github.com/FreifunkBremen/freifunkmanager/ssh" "github.com/FreifunkBremen/freifunkmanager/yanic" @@ -34,7 +36,12 @@ func main() { log.Log.Info("starting...") sshmanager := ssh.NewManager(config.SSHPrivateKey) - nodes := runtime.NewNodes(config.SSHInterface, sshmanager) + nodes := runtime.NewNodes(config.StatePath, config.SSHInterface, sshmanager) + nodesUpdateWorker := worker.NewWorker(time.Duration(3)*time.Minute, nodes.Updater) + nodesSaveWorker := worker.NewWorker(time.Duration(3)*time.Minute, nodes.Saver) + + nodesUpdateWorker.Start() + nodesSaveWorker.Start() if config.Yanic.Enable { yanicDialer := yanic.Dial(config.Yanic.Type, config.Yanic.Address) @@ -69,6 +76,8 @@ func main() { if config.Yanic.Enable { yanicDialer.Close() } + nodesSaveWorker.Close() + nodesUpdateWorker.Close() sshmanager.Close() log.Log.Info("stop recieve:", sig) diff --git a/config/main.go b/config/main.go index ad70c1b..3036320 100644 --- a/config/main.go +++ b/config/main.go @@ -10,6 +10,8 @@ import ( //config file of this daemon (for more the config_example.conf in git repository) type Config struct { + // prevent crashes + StatePath string `toml:"state_path"` // address on which the api and static content webserver runs WebserverBind string `toml:"webserver_bind"` diff --git a/lib/worker/worker.go b/lib/worker/worker.go new file mode 100644 index 0000000..286f90d --- /dev/null +++ b/lib/worker/worker.go @@ -0,0 +1,41 @@ +// A little lib for cronjobs to run it in background +package worker + +import "time" + +// a struct which handle the job +type Worker struct { + every time.Duration + run func() + quit chan struct{} +} + +// create a new Worker with timestamp run every and his function +func NewWorker(every time.Duration, f func()) (w *Worker) { + w = &Worker{ + every: every, + run: f, + quit: make(chan struct{}), + } + return +} + +// start the worker +// please us it as a goroutine: go w.Start() +func (w *Worker) Start() { + ticker := time.NewTicker(w.every) + for { + select { + case <-ticker.C: + w.run() + case <-w.quit: + ticker.Stop() + return + } + } +} + +// stop the worker +func (w *Worker) Close() { + close(w.quit) +} diff --git a/lib/worker/worker_test.go b/lib/worker/worker_test.go new file mode 100644 index 0000000..c72f47d --- /dev/null +++ b/lib/worker/worker_test.go @@ -0,0 +1,24 @@ +package worker + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestWorker(t *testing.T) { + assert := assert.New(t) + + runtime := 0 + + w := NewWorker(time.Duration(5)*time.Millisecond, func() { + runtime = runtime + 1 + }) + go w.Start() + time.Sleep(time.Duration(18) * time.Millisecond) + w.Close() + + assert.Equal(3, runtime) + time.Sleep(time.Duration(8) * time.Millisecond) +} diff --git a/runtime/node.go b/runtime/node.go new file mode 100644 index 0000000..93a5858 --- /dev/null +++ b/runtime/node.go @@ -0,0 +1,170 @@ +package runtime + +import ( + "bytes" + "fmt" + "net" + "time" + + "github.com/FreifunkBremen/freifunkmanager/ssh" + "github.com/FreifunkBremen/yanic/data" + yanicRuntime "github.com/FreifunkBremen/yanic/runtime" +) + +const ( + SSHUpdateHostname = "uci set system.@system[0].hostname='%s';uci commit system;echo $(uci get system.@system[0].hostname) > /proc/sys/kernel/hostname" + SSHUpdateOwner = "uci set gluon-node-info.@owner[0].contact='%s';uci commit gluon-node-info;" + SSHUpdateLocation = "uci set gluon-node-info.@location[0].latitude='%d';uci set gluon-node-info.@location[0].longitude='%d';uci set gluon-node-info.@location[0].share_location=1;uci commit gluon-node-info;" +) + +type Node struct { + Lastseen time.Time `json:"lastseen"` + NodeID string `json:"node_id"` + Hostname string `json:"hostname"` + Location data.Location `json:"location"` + Wireless data.Wireless `json:"wireless"` + Owner string `json:"owner"` + Address net.IP `json:"address"` +} + +func NewNode(node *yanicRuntime.Node) *Node { + if nodeinfo := node.Nodeinfo; nodeinfo != nil { + node := &Node{ + Hostname: nodeinfo.Hostname, + NodeID: nodeinfo.NodeID, + Address: node.Address, + } + if owner := nodeinfo.Owner; owner != nil { + node.Owner = owner.Contact + } + if location := nodeinfo.Location; location != nil { + node.Location = *location + } + if wireless := nodeinfo.Wireless; wireless != nil { + node.Wireless = *wireless + } + return node + } + return nil +} + +func (n *Node) SSHUpdate(ssh *ssh.Manager, iface string, oldnode *Node) { + addr := n.GetAddress(iface) + if n.Hostname != oldnode.Hostname { + ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateHostname, n.Hostname)) + } + if n.Owner != oldnode.Owner { + ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateOwner, n.Owner)) + } + if !locationEqual(&n.Location, &oldnode.Location) { + ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateLocation, n.Location.Latitude, n.Location.Longtitude)) + } +} +func (n *Node) SSHSet(ssh *ssh.Manager, iface string) { + n.SSHUpdate(ssh, iface, nil) +} +func (n *Node) GetAddress(iface string) net.TCPAddr { + return net.TCPAddr{IP: n.Address, Port: 22, Zone: iface} +} +func (n *Node) Update(node *yanicRuntime.Node) { + if nodeinfo := node.Nodeinfo; nodeinfo != nil { + n.Hostname = nodeinfo.Hostname + n.NodeID = nodeinfo.NodeID + n.Address = node.Address + + if owner := nodeinfo.Owner; owner != nil { + n.Owner = owner.Contact + } + if location := nodeinfo.Location; location != nil { + n.Location = *location + } + if wireless := nodeinfo.Wireless; wireless != nil { + n.Wireless = *wireless + } + } +} +func (n *Node) IsEqual(node *Node) bool { + if n.NodeID != node.NodeID { + return false + } + if !bytes.Equal(n.Address, node.Address) { + return false + } + if n.Hostname != node.Hostname { + return false + } + if n.Owner != node.Owner { + return false + } + if !locationEqual(&n.Location, &node.Location) { + return false + } + if !wirelessEqual(&n.Wireless, &node.Wireless) { + return false + } + return true +} +func (n *Node) IsEqualNode(node *yanicRuntime.Node) bool { + nodeinfo := node.Nodeinfo + if nodeinfo == nil { + return false + } + owner := nodeinfo.Owner + if owner == nil { + return false + } + if n.NodeID != nodeinfo.NodeID { + return false + } + if !bytes.Equal(n.Address, node.Address) { + return false + } + if n.Hostname != nodeinfo.Hostname { + return false + } + if n.Owner != owner.Contact { + return false + } + if !locationEqual(&n.Location, nodeinfo.Location) { + return false + } + if !wirelessEqual(&n.Wireless, nodeinfo.Wireless) { + return false + } + return true +} + +func locationEqual(a, b *data.Location) bool { + if a == nil || b == nil { + return false + } + if a.Latitude != b.Latitude { + return false + } + if a.Longtitude != b.Longtitude { + return false + } + if a.Altitude != b.Altitude { + return false + } + return true +} + +func wirelessEqual(a, b *data.Wireless) bool { + if a == nil || b == nil { + return false + } + if a.Channel24 != b.Channel24 { + return false + } + if a.Channel5 != b.Channel5 { + return false + } + if a.TxPower24 != b.TxPower24 { + return false + } + if a.TxPower5 != b.TxPower5 { + return false + } + return true +} diff --git a/runtime/node_test.go b/runtime/node_test.go new file mode 100644 index 0000000..ff6c9b9 --- /dev/null +++ b/runtime/node_test.go @@ -0,0 +1,35 @@ +package runtime + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/FreifunkBremen/yanic/data" + yanicRuntime "github.com/FreifunkBremen/yanic/runtime" +) + +func TestNode(t *testing.T) { + assert := assert.New(t) + node1 := &yanicRuntime.Node{} + n1 := NewNode(node1) + assert.Nil(n1) + + node1.Nodeinfo = &data.NodeInfo{ + Owner: &data.Owner{Contact: "blub"}, + Wireless: &data.Wireless{}, + Location: &data.Location{Altitude: 13}, + } + n1 = NewNode(node1) + assert.NotNil(n1) + assert.Equal(float64(13), n1.Location.Altitude) + + n2 := NewNode(node1) + assert.True(n2.IsEqual(n1)) + assert.True(n2.IsEqualNode(node1)) + + node1.Nodeinfo.Owner.Contact = "blub2" + assert.False(n2.IsEqualNode(node1)) + n2.Update(node1) + assert.False(n2.IsEqual(n1)) +} diff --git a/runtime/nodes.go b/runtime/nodes.go index b8efc2c..8bf2d4e 100644 --- a/runtime/nodes.go +++ b/runtime/nodes.go @@ -1,7 +1,9 @@ package runtime import ( - "net" + "encoding/json" + "os" + "time" yanic "github.com/FreifunkBremen/yanic/runtime" @@ -10,33 +12,85 @@ import ( ) type Nodes struct { - node map[string]struct{} - ssh *ssh.Manager - iface string + List map[string]*Node `json:"nodes"` + ToUpdate map[string]struct{} + ssh *ssh.Manager + statePath string + iface string } -func NewNodes(iface string, mgmt *ssh.Manager) *Nodes { - return &Nodes{ - node: make(map[string]struct{}), - ssh: mgmt, - iface: iface, +func NewNodes(path string, iface string, mgmt *ssh.Manager) *Nodes { + nodes := &Nodes{ + List: make(map[string]*Node), + ToUpdate: make(map[string]struct{}), + ssh: mgmt, + statePath: path, + iface: iface, } + nodes.load() + return nodes } -func (nodes *Nodes) AddNode(node *yanic.Node) { - logger := log.Log.WithField("method", "AddNode").WithField("node_id", node.Nodeinfo.NodeID) - // session := nodes.ssh.ConnectTo(node.Address) - if _, ok := nodes.node[node.Address.String()]; ok { +func (nodes *Nodes) AddNode(n *yanic.Node) { + node := NewNode(n) + if node == nil { + return + } + logger := log.Log.WithField("method", "AddNode").WithField("node_id", node.NodeID) + + if cNode := nodes.List[node.NodeID]; cNode != nil { + cNode.Lastseen = time.Now() + if _, ok := nodes.ToUpdate[node.NodeID]; ok { + if nodes.List[node.NodeID].IsEqual(node) { + delete(nodes.ToUpdate, node.NodeID) + } + } else { + nodes.List[node.NodeID] = node + } logger.Debugf("know already these node") return } - address := net.TCPAddr{IP: node.Address, Port: 22, Zone: nodes.iface} - result, err := nodes.ssh.RunOn(address, "uptime") + node.Lastseen = time.Now() + // session := nodes.ssh.ConnectTo(node.Address) + result, err := nodes.ssh.RunOn(node.GetAddress(nodes.iface), "uptime") if err != nil { logger.Error("init ssh command not run") return } uptime := ssh.SSHResultToString(result) logger.Infof("new node with uptime: %s", uptime) - nodes.node[node.Address.String()] = struct{}{} + + nodes.List[node.NodeID] = node +} + +func (nodes *Nodes) UpdateNode(node *Node) { + if n, ok := nodes.List[node.NodeID]; ok { + go node.SSHUpdate(nodes.ssh, nodes.iface, n) + } + nodes.List[node.NodeID] = node + nodes.ToUpdate[node.NodeID] = struct{}{} +} + +func (nodes *Nodes) Updater() { + for nodeid := range nodes.ToUpdate { + if node := nodes.List[nodeid]; node != nil { + go node.SSHSet(nodes.ssh, nodes.iface) + } + } +} + +func (nodes *Nodes) load() { + if f, err := os.Open(nodes.statePath); err == nil { // transform data to legacy meshviewer + if err = json.NewDecoder(f).Decode(nodes); err == nil { + log.Log.Info("loaded", len(nodes.List), "nodes") + } else { + log.Log.Error("failed to unmarshal nodes:", err) + } + } else { + log.Log.Error("failed to load cached nodes:", err) + } +} + +func (nodes *Nodes) Saver() { + yanic.SaveJSON(nodes, nodes.statePath) }