From 626320d7f4c57ad4dca465623a300fbcb9c969b9 Mon Sep 17 00:00:00 2001 From: Tom Wilkie Date: Tue, 23 Jun 2015 10:50:30 +0000 Subject: [PATCH] Move probe host topology code into its own module - add tests - use constants for NodeMetadata keys - add host uptime and kernel version --- Makefile | 2 +- app/origin_host.go | 11 +-- probe/host/reporter.go | 128 ++++++++++++++++++++++++++++++ probe/host/reporter_test.go | 84 ++++++++++++++++++++ probe/{ => host}/system_darwin.go | 2 +- probe/{ => host}/system_linux.go | 5 +- probe/main.go | 40 +++------- render/detailed_node.go | 20 +++-- render/mapping.go | 3 +- render/theinternet.go | 3 +- render/theinternet_test.go | 3 +- 11 files changed, 250 insertions(+), 51 deletions(-) create mode 100644 probe/host/reporter.go create mode 100644 probe/host/reporter_test.go rename probe/{ => host}/system_darwin.go (97%) rename probe/{ => host}/system_linux.go (87%) diff --git a/Makefile b/Makefile index e259baa69..74ff21c96 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ $(SCOPE_EXPORT): $(APP_EXE) $(PROBE_EXE) docker/* $(APP_EXE): app/*.go render/*.go report/*.go xfer/*.go -$(PROBE_EXE): probe/*.go probe/tag/*.go probe/docker/*.go probe/endpoint/*.go probe/process/*.go report/*.go xfer/*.go +$(PROBE_EXE): probe/*.go probe/docker/*.go probe/endpoint/*.go probe/host/*.go probe/process/*.go probe/tag/*.go report/*.go xfer/*.go $(APP_EXE) $(PROBE_EXE): go get -tags netgo ./$(@D) diff --git a/app/origin_host.go b/app/origin_host.go index d1d4d3757..ff38b37cb 100644 --- a/app/origin_host.go +++ b/app/origin_host.go @@ -6,6 +6,7 @@ import ( "github.com/gorilla/mux" + "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/report" ) @@ -20,16 +21,16 @@ type OriginHost struct { } func getOriginHost(t report.Topology, nodeID string) (OriginHost, bool) { - host, ok := t.NodeMetadatas[nodeID] + h, ok := t.NodeMetadatas[nodeID] if !ok { return OriginHost{}, false } return OriginHost{ - Hostname: host["host_name"], - OS: host["os"], - Networks: strings.Split(host["local_networks"], " "), - Load: host["load"], + Hostname: h[host.HostName], + OS: h[host.OS], + Networks: strings.Split(h[host.LocalNetworks], " "), + Load: h[host.Load], }, true } diff --git a/probe/host/reporter.go b/probe/host/reporter.go new file mode 100644 index 000000000..96364eb10 --- /dev/null +++ b/probe/host/reporter.go @@ -0,0 +1,128 @@ +package host + +import ( + "fmt" + "io/ioutil" + "net" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "github.com/weaveworks/scope/probe/tag" + "github.com/weaveworks/scope/report" +) + +// Keys for use in NodeMetadata +const ( + Timestamp = "ts" + HostName = "host_name" + LocalNetworks = "local_networks" + OS = "os" + Load = "load" + KernelVersion = "kernel_version" + Uptime = "uptime" +) + +// Exposed for testing +const ( + ProcUptime = "/proc/uptime" + ProcLoad = "/proc/loadavg" +) + +// Exposed for testing +var ( + InterfaceAddrs = net.InterfaceAddrs + Now = func() string { return time.Now().UTC().Format(time.RFC3339Nano) } + ReadFile = ioutil.ReadFile + Uname = syscall.Uname +) + +type reporter struct { + hostID string + hostName string +} + +// NewReporter returns a Reporter which produces a report containing host +// topology for this host. +func NewReporter(hostID, hostName string) tag.Reporter { + return &reporter{ + hostID: hostID, + hostName: hostName, + } +} + +func getUptime() (time.Duration, error) { + var result time.Duration + + buf, err := ReadFile(ProcUptime) + if err != nil { + return result, err + } + + fields := strings.Fields(string(buf)) + if len(fields) != 2 { + return result, fmt.Errorf("invalid format: %s", string(buf)) + } + + uptime, err := strconv.ParseFloat(fields[0], 64) + if err != nil { + return result, err + } + + return time.Duration(uptime) * time.Second, nil +} + +func charsToString(ca [65]int8) string { + s := make([]byte, len(ca)) + var lens int + for ; lens < len(ca); lens++ { + if ca[lens] == 0 { + break + } + s[lens] = uint8(ca[lens]) + } + return string(s[0:lens]) +} + +func (r *reporter) Report() (report.Report, error) { + var ( + rep = report.MakeReport() + localCIDRs []string + utsname syscall.Utsname + ) + + localNets, err := InterfaceAddrs() + if err != nil { + return rep, err + } + for _, localNet := range localNets { + // Not all networks are IP networks. + if ipNet, ok := localNet.(*net.IPNet); ok { + localCIDRs = append(localCIDRs, ipNet.String()) + } + } + + if err := Uname(&utsname); err != nil { + return rep, err + } + kernel := fmt.Sprintf("%s %s", charsToString(utsname.Release), charsToString(utsname.Version)) + + uptime, err := getUptime() + if err != nil { + return rep, err + } + + rep.Host.NodeMetadatas[report.MakeHostNodeID(r.hostID)] = report.NodeMetadata{ + Timestamp: Now(), + HostName: r.hostName, + LocalNetworks: strings.Join(localCIDRs, " "), + OS: runtime.GOOS, + Load: getLoad(), + KernelVersion: kernel, + Uptime: uptime.String(), + } + + return rep, nil +} diff --git a/probe/host/reporter_test.go b/probe/host/reporter_test.go new file mode 100644 index 000000000..c8a6c03ba --- /dev/null +++ b/probe/host/reporter_test.go @@ -0,0 +1,84 @@ +package host_test + +import ( + "net" + "reflect" + "runtime" + "syscall" + "testing" + + "github.com/weaveworks/scope/probe/host" + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/test" +) + +const ( + procLoad = "0.59 0.36 0.29 1/200 12187" + procUptime = "1004143.23 1263220.30" + release = "release" + version = "version" + + network = "192.168.0.0/16" + hostid = "hostid" + now = "now" + hostname = "hostname" + load = "0.59 0.36 0.29" + uptime = "278h55m43s" + kernel = "release version" +) + +func string2c(s string) [65]int8 { + var result [65]int8 + for i, c := range s { + result[i] = int8(c) + } + return result +} + +func TestReporter(t *testing.T) { + oldInterfaceAddrs, oldNow, oldReadFile, oldUname := host.InterfaceAddrs, host.Now, host.ReadFile, host.Uname + defer func() { + host.InterfaceAddrs, host.Now, host.ReadFile, host.Uname = oldInterfaceAddrs, oldNow, oldReadFile, oldUname + }() + + host.InterfaceAddrs = func() ([]net.Addr, error) { + _, ipnet, _ := net.ParseCIDR(network) + return []net.Addr{ipnet}, nil + } + + host.Now = func() string { return now } + + host.ReadFile = func(filename string) ([]byte, error) { + switch filename { + case host.ProcUptime: + return []byte(procUptime), nil + case host.ProcLoad: + return []byte(procLoad), nil + default: + panic(filename) + } + } + + host.Uname = func(uts *syscall.Utsname) error { + uts.Release = string2c(release) + uts.Version = string2c(version) + return nil + } + + r := host.NewReporter(hostid, hostname) + have, _ := r.Report() + want := report.MakeReport() + want.Host.NodeMetadatas[report.MakeHostNodeID(hostid)] = report.NodeMetadata{ + host.Timestamp: now, + host.HostName: hostname, + host.LocalNetworks: network, + host.OS: runtime.GOOS, + host.Load: load, + host.Uptime: uptime, + host.KernelVersion: kernel, + } + + if !reflect.DeepEqual(want, have) { + t.Errorf("%s", test.Diff(want, have)) + } +} diff --git a/probe/system_darwin.go b/probe/host/system_darwin.go similarity index 97% rename from probe/system_darwin.go rename to probe/host/system_darwin.go index 81874c322..1c365bb77 100644 --- a/probe/system_darwin.go +++ b/probe/host/system_darwin.go @@ -1,4 +1,4 @@ -package main +package host import ( "fmt" diff --git a/probe/system_linux.go b/probe/host/system_linux.go similarity index 87% rename from probe/system_linux.go rename to probe/host/system_linux.go index 257abf125..5305055c0 100644 --- a/probe/system_linux.go +++ b/probe/host/system_linux.go @@ -1,14 +1,13 @@ -package main +package host import ( "fmt" - "io/ioutil" "strconv" "strings" ) func getLoad() string { - buf, err := ioutil.ReadFile("/proc/loadavg") + buf, err := ReadFile(ProcLoad) if err != nil { return "unknown" } diff --git a/probe/main.go b/probe/main.go index a73d503aa..59a44bd4a 100644 --- a/probe/main.go +++ b/probe/main.go @@ -3,19 +3,18 @@ package main import ( "flag" "log" - "net" "net/http" "os" "os/signal" "runtime" "strconv" - "strings" "syscall" "time" "github.com/weaveworks/procspy" "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/endpoint" + "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/probe/tag" "github.com/weaveworks/scope/report" @@ -80,8 +79,15 @@ func main() { weaveTagger *tag.WeaveTagger ) - taggers := []tag.Tagger{tag.NewTopologyTagger(), tag.NewOriginHostTagger(hostID)} - reporters := []tag.Reporter{endpoint.NewReporter(hostID, hostName, *spyProcs)} + taggers := []tag.Tagger{ + tag.NewTopologyTagger(), + tag.NewOriginHostTagger(hostID), + } + + reporters := []tag.Reporter{ + host.NewReporter(hostID, hostName), + endpoint.NewReporter(hostID, hostName, *spyProcs), + } if *dockerEnabled && runtime.GOOS == linux { if err = report.AddLocalBridge(*dockerBridge); err != nil { @@ -131,9 +137,6 @@ func main() { r = report.MakeReport() case <-spyTick: - // Do this every tick so it gets tagged by the OriginHostTagger - r.Host = hostTopology(hostID, hostName) - for _, reporter := range reporters { newReport, err := reporter.Report() if err != nil { @@ -157,29 +160,6 @@ func main() { log.Printf("%s", <-interrupt()) } -// hostTopology produces a host topology for this host. No need to do this -// more than once per published report. -func hostTopology(hostID, hostName string) report.Topology { - var localCIDRs []string - if localNets, err := net.InterfaceAddrs(); err == nil { - // Not all networks are IP networks. - for _, localNet := range localNets { - if ipNet, ok := localNet.(*net.IPNet); ok { - localCIDRs = append(localCIDRs, ipNet.String()) - } - } - } - t := report.NewTopology() - t.NodeMetadatas[report.MakeHostNodeID(hostID)] = report.NodeMetadata{ - "ts": time.Now().UTC().Format(time.RFC3339Nano), - "host_name": hostName, - "local_networks": strings.Join(localCIDRs, " "), - "os": runtime.GOOS, - "load": getLoad(), - } - return t -} - func interrupt() chan os.Signal { c := make(chan os.Signal) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) diff --git a/render/detailed_node.go b/render/detailed_node.go index 6856cab8e..80f718a89 100644 --- a/render/detailed_node.go +++ b/render/detailed_node.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/report" ) @@ -208,15 +209,18 @@ func containerImageOriginTable(nmd report.NodeMetadata) (Table, bool) { func hostOriginTable(nmd report.NodeMetadata) (Table, bool) { rows := []Row{} - if val, ok := nmd["host_name"]; ok { - rows = append(rows, Row{"Host name", val, ""}) - } - if val, ok := nmd["load"]; ok { - rows = append(rows, Row{"Load", val, ""}) - } - if val, ok := nmd["os"]; ok { - rows = append(rows, Row{"Operating system", val, ""}) + for _, tuple := range []struct{ key, human string }{ + {host.HostName, "Host name"}, + {host.Load, "Load"}, + {host.OS, "Operating system"}, + {host.KernelVersion, "Kernel version"}, + {host.Uptime, "Uptime"}, + } { + if val, ok := nmd[tuple.key]; ok { + rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""}) + } } + return Table{ Title: "Origin Host", Numeric: false, diff --git a/render/mapping.go b/render/mapping.go index 4582758ff..3b7190cb0 100644 --- a/render/mapping.go +++ b/render/mapping.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/report" ) @@ -123,7 +124,7 @@ func MapAddressIdentity(m report.NodeMetadata) (RenderableNode, bool) { func MapHostIdentity(m report.NodeMetadata) (RenderableNode, bool) { var ( id = MakeHostID(report.ExtractHostID(m)) - hostname = m["host_name"] + hostname = m[host.HostName] parts = strings.SplitN(hostname, ".", 2) major, minor, rank = "", "", "" ) diff --git a/render/theinternet.go b/render/theinternet.go index 2da92c3f5..f3ca414ca 100644 --- a/render/theinternet.go +++ b/render/theinternet.go @@ -4,6 +4,7 @@ import ( "net" "strings" + "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/report" ) @@ -18,7 +19,7 @@ func LocalNetworks(r report.Report) report.Networks { ) for _, md := range r.Host.NodeMetadatas { - val, ok := md["local_networks"] + val, ok := md[host.LocalNetworks] if !ok { continue } diff --git a/render/theinternet_test.go b/render/theinternet_test.go index d5f4538af..66b71ccc2 100644 --- a/render/theinternet_test.go +++ b/render/theinternet_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" "github.com/weaveworks/scope/test" @@ -14,7 +15,7 @@ func TestReportLocalNetworks(t *testing.T) { r := report.MakeReport() r.Merge(report.Report{Host: report.Topology{NodeMetadatas: report.NodeMetadatas{ "nonets": {}, - "foo": {"local_networks": "10.0.0.1/8 192.168.1.1/24 10.0.0.1/8 badnet/33"}, + "foo": {host.LocalNetworks: "10.0.0.1/8 192.168.1.1/24 10.0.0.1/8 badnet/33"}, }}}) want := report.Networks([]*net.IPNet{ mustParseCIDR("10.0.0.1/8"),