Skip to content
This repository was archived by the owner on Oct 27, 2022. It is now read-only.

Added monitoring function for link022 #81

Merged
merged 27 commits into from
Aug 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eca8ad2
Add a single radio ap config file for test on emulator
zhangtianyang May 31, 2018
d076f55
Update certificates for test and add a new script for generating test…
zhangtianyang May 31, 2018
74a2e4a
Merge remote-tracking branch 'upstream/master'
zhangtianyang Jul 10, 2018
721111f
add a function to fetch AP state periodic
zhangtianyang Jul 12, 2018
5dcf857
implemented collector which can convert AP state to Prometheus metrics
zhangtianyang Jul 16, 2018
aaa2316
add a HTTP server to expose metrics
zhangtianyang Jul 16, 2018
831e057
1. Delete use_model in getRequest to separate each leaf node into
zhangtianyang Jul 17, 2018
07cee6a
add ctrl_interface in hostapd.conf
zhangtianyang Jul 18, 2018
b0d4901
1. delete useless debug message and config
zhangtianyang Jul 18, 2018
f46f0e9
delete unnecessary code
zhangtianyang Jul 19, 2018
2f57ece
Add unit test for prometheus exporter
zhangtianyang Jul 23, 2018
b9a7af5
use a query friendly way process array type
zhangtianyang Jul 30, 2018
6bae6f6
add device states to LINK022's gNMI server
zhangtianyang Aug 10, 2018
dc45900
Merge remote-tracking branch 'upstream/master'
zhangtianyang Aug 10, 2018
59454a2
used a new way to implement type conversion
zhangtianyang Aug 14, 2018
821e5b8
add support to JSON data
zhangtianyang Aug 14, 2018
a5f639f
added support for JSON-IETF data.
zhangtianyang Aug 14, 2018
c485fcd
solve leafref when create new node
zhangtianyang Aug 15, 2018
302c35a
fixed a bug in calculating cpu usage
zhangtianyang Aug 15, 2018
d3e53a6
added ap info monitoring.
zhangtianyang Aug 15, 2018
3e80b56
fixed a bug in test which caused
zhangtianyang Aug 15, 2018
066c0ff
added README for prometheus exporter
zhangtianyang Aug 15, 2018
c93a11a
Add prometheus configuring instruction in prometheus exporter README.
zhangtianyang Aug 16, 2018
4f367fd
fixed issues reported by Go Report Card
zhangtianyang Aug 16, 2018
dca750e
Now getting hostname from os instead of hard-coding.
zhangtianyang Aug 17, 2018
44cdde0
added license headers.
zhangtianyang Aug 17, 2018
e5a46fe
corrected typos
zhangtianyang Aug 17, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ limitations under the License.
package main

import (
ctx "context"
"flag"
"fmt"
"net"
Expand All @@ -28,6 +29,7 @@ import (
"github.com/google/link022/agent/context"
"github.com/google/link022/agent/controller"
"github.com/google/link022/agent/gnmi"
"github.com/google/link022/agent/monitoring"
"github.com/google/link022/agent/syscmd"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
Expand Down Expand Up @@ -86,6 +88,10 @@ func main() {
log.Exitf("Failed to create the GNMI server. Error: %v.", err)
}

// Start a goroutine to collect states periodically
backgroundContext := ctx.Background()
go monitoring.UpdateDeviceStatus(backgroundContext, gnmiServer)

// Start the GNMI server.
var opts []grpc.ServerOption
if *controllerAddr == "" {
Expand Down
43 changes: 43 additions & 0 deletions agent/gnmi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@ limitations under the License.
package gnmi

import (
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"reflect"
"strings"

"github.com/google/gnxi/gnmi"
"github.com/google/link022/generated/ocstruct"

log "github.com/golang/glog"
pb "github.com/openconfig/gnmi/proto/gnmi"
"github.com/openconfig/ygot/ygot"
"github.com/openconfig/ygot/ytypes"
)

const (
Expand All @@ -48,6 +53,8 @@ type Server struct {
*gnmi.Server
}

type serverStateOperator func(path *pb.Path, val interface{}, config ygot.ValidatedGoStruct) error

// NewServer creates a GNMI server.
func NewServer() (*Server, error) {
// Load existing config
Expand Down Expand Up @@ -92,3 +99,39 @@ func loadExistingConfigContent() ([]byte, error) {
log.Info("Loaded existing configuration.")
return existingConfigContent, nil
}

// GNXIStateOptGenerator decorate a given function to a gNXI state operator function
func GNXIStateOptGenerator(path *pb.Path, val interface{}, stateOpt serverStateOperator) func(config ygot.ValidatedGoStruct) error {
fp := func(config ygot.ValidatedGoStruct) error {
return stateOpt(path, val, config)
}
return fp
}

// InternalUpdateState update state node in Server config. When updating,
// call server's InternalUpdate method and send this function as parameter.
// The type of val must exactly matchs node's type.
func InternalUpdateState(path *pb.Path, val interface{}, config ygot.ValidatedGoStruct) error {
checkStateNode := false
for _, i := range path.GetElem() {
if strings.Compare(i.GetName(), "state") == 0 {
checkStateNode = true
break
}
}
if !checkStateNode {
log.Error("failed update state: target node is not state node")
return errors.New("target node is not state node")
}

node, _, err := ytypes.GetOrCreateNode(ocstruct.SchemaTree["Device"], config, path)
if err != nil {
return fmt.Errorf("failed retrive target node: %v", err)
}

if reflect.ValueOf(node).Kind() != reflect.Ptr {
return fmt.Errorf("type of node is %v, not go struct pointer", reflect.ValueOf(node).Kind())
}
reflect.ValueOf(node).Elem().Set(reflect.ValueOf(val))
return nil
}
256 changes: 256 additions & 0 deletions agent/monitoring/monitoring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
/* Copyright 2017 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package monitoring

import (
ctx "context"
"errors"
"fmt"
"io/ioutil"
"os"
"regexp"
"strconv"
"strings"
"time"

log "github.com/golang/glog"
"github.com/google/gnxi/utils/xpath"
"github.com/google/link022/agent/context"
"github.com/google/link022/agent/gnmi"
"github.com/google/link022/agent/syscmd"
)

const (
statesUpdateDelay = 15 * time.Second
systemClockTick = 100
physicalMemoryPath = "/access-points/access-point[hostname=$hostname]/system/memory/state/physical"
cpuUsagePath = "/access-points/access-point[hostname=$hostname]/system/cpus/cpu[index=$index]/state/total/instant"
channelPath = "/access-points/access-point[hostname=$hostname]/radios/radio[id=$id]/state/channel"
widthPath = "/access-points/access-point[hostname=$hostname]/radios/radio[id=$id]/state/channel-width"
frequencyPath = "/access-points/access-point[hostname=$hostname]/radios/radio[id=$id]/state/operating-frequency"
txpowerPath = "/access-points/access-point[hostname=$hostname]/radios/radio[id=$id]/state/transmit-power"
selfMemPath = "/access-points/access-point[hostname=$hostname]/system/processes/process[pid=$pid]/state/memory-usage"
selfCPUPath = "/access-points/access-point[hostname=$hostname]/system/processes/process[pid=$pid]/state/cpu-utilization"
)

var cmdRunner = syscmd.Runner()

// UpdateDeviceStatus periodically collect AP device stats
// and update their corresponding nodes in OpenConfig Model tree.
func UpdateDeviceStatus(bkgdContext ctx.Context, gnmiServer *gnmi.Server) {
deviceConfig := context.GetDeviceConfig()
hostName := deviceConfig.Hostname
wLANINTFName := deviceConfig.WLANINTFName
for {
select {
case <-bkgdContext.Done():
return
case <-time.After(statesUpdateDelay):
}

if err := updateMemoryInfo(gnmiServer, hostName); err != nil {
log.Errorf("Error in updating memory info: %v", err)
}
if err := updateCPUInfo(gnmiServer, hostName); err != nil {
log.Errorf("Error in updating CPU info: %v", err)
}
if err := updateAPInfo(gnmiServer, hostName, wLANINTFName); err != nil {
log.Errorf("Error in updating AP info: %v", err)
}
}
}

func updateMemoryInfo(s *gnmi.Server, hostName string) error {
b, err := ioutil.ReadFile("/proc/meminfo")
if err != nil {
return err
}
memStr := string(b)
reFree := regexp.MustCompile("MemTotal:\\s+(\\d+)")
match := reFree.FindStringSubmatch(memStr)
if len(match) != 2 {
return errors.New("No Memory Free info in /proc/meminfo")
}
strPath := strings.Replace(physicalMemoryPath, "$hostname", hostName, 1)
pbPath, err := xpath.ToGNMIPath(strPath)
if err != nil {
return err
}
physicalMemory, err := strconv.ParseInt(match[1], 10, 64)
if err != nil {
return err
}
stateOpt := gnmi.GNXIStateOptGenerator(pbPath, uint64(physicalMemory*1024), gnmi.InternalUpdateState)
if err = s.InternalUpdate(stateOpt); err != nil {
return err
}

pid := os.Getpid()
spid := fmt.Sprint(pid)
filePath := fmt.Sprintf("/proc/%v/status", pid)
b, err = ioutil.ReadFile(filePath)
if err != nil {
log.Errorf("failed open %v: %v", filePath, err)
return err
}
memStr = string(b)
reSelfMem := regexp.MustCompile("VmRSS:\\s+(\\d+)")
match = reSelfMem.FindStringSubmatch(memStr)
if len(match) != 2 {
return fmt.Errorf("No Memory info in: %v", filePath)
}
p := strings.Replace(selfMemPath, "$pid", spid, 1)
p = strings.Replace(p, "$hostname", hostName, 1)
pbPath, err = xpath.ToGNMIPath(p)
if err != nil {
return err
}
selfMemory, err := strconv.ParseInt(match[1], 10, 64)
if err != nil {
return err
}
stateOpt = gnmi.GNXIStateOptGenerator(pbPath, uint64(selfMemory*1024), gnmi.InternalUpdateState)
if err = s.InternalUpdate(stateOpt); err != nil {
log.Errorf("update state failed: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return err
}

return nil
}

func updateCPUInfo(s *gnmi.Server, hostName string) error {
pid := os.Getpid()
spid := fmt.Sprint(pid)
filePath := fmt.Sprintf("/proc/%v/stat", pid)
b0, err := ioutil.ReadFile(filePath)
if err != nil {
log.Errorf("failed open %v: %v", filePath, err)
return err
}
time.Sleep(1 * time.Second)
b1, err := ioutil.ReadFile(filePath)
if err != nil {
log.Errorf("failed open %v: %v", filePath, err)
return err
}
cpuStr0 := strings.Split(string(b0), " ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to use regex instead of doing spiting and using hard-coded index.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file simply contains 52 numbers. Their positions are fixed. So it's better to find them by index.

cpuStr1 := strings.Split(string(b1), " ")
if len(cpuStr0) < 14 || len(cpuStr1) < 14 {
return errors.New("cpu info not correct")
}
up0, err := strconv.ParseInt(cpuStr0[13], 10, 64)
if err != nil {
log.Errorf("failed convert string to int: %v", err)
return err
}
up1, err := strconv.ParseInt(cpuStr1[13], 10, 64)
if err != nil {
log.Errorf("failed convert string to int: %v", err)
return err
}
cpuinfo, err := ioutil.ReadFile("/proc/cpuinfo")
if err != nil {
log.Errorf("failed open %v: %v", "/proc/cpuinfo", err)
return err
}
cpuCount := strings.Count(string(cpuinfo), "processor")
cpuUtil := (up1 - up0) / (systemClockTick * int64(cpuCount))
p := strings.Replace(selfCPUPath, "$pid", spid, 1)
p = strings.Replace(p, "$hostname", hostName, 1)
pbPath, err := xpath.ToGNMIPath(p)
if err != nil {
return err
}
stateOpt := gnmi.GNXIStateOptGenerator(pbPath, uint8(cpuUtil), gnmi.InternalUpdateState)
if err = s.InternalUpdate(stateOpt); err != nil {
log.Errorf("update state failed: %v", err)
return err
}

return nil
}

func updateAPInfo(s *gnmi.Server, hostName string, wLANINTFName string) error {
apInfoString, err := cmdRunner.GetAPStates()
if err != nil {
return err
}
// If one interface has multiple ssid, match the first one
apRegex := regexp.MustCompile("Interface\\s([\\w-_]+)[\\S\\s]*?ssid\\s([\\w-_]+)[\\S\\s]*?channel\\s([\\d]+)[\\S\\s]*?width:\\s([\\d]+)[\\S\\s]*?txpower\\s([\\d]+)")
apInfos := apRegex.FindAllStringSubmatch(apInfoString, -1)
for _, apInfo := range apInfos {
wlanName := apInfo[1]
channelStr := apInfo[3]
widthStr := apInfo[4]
txpowerStr := apInfo[5]

// Because radio info is not in IW command's output.
// The radio id is hard-coded here
phyIDStr := "1"

if strings.Compare(wlanName, wLANINTFName) != 0 {
continue
}

p := strings.Replace(channelPath, "$id", phyIDStr, 1)
p = strings.Replace(p, "$hostname", hostName, 1)
pbPath, err := xpath.ToGNMIPath(p)
if err != nil {
return fmt.Errorf("convert %v to GNMI path failed: %v", p, err)
}
channel, err := strconv.ParseInt(channelStr, 10, 8)
if err != nil {
log.Errorf("failed convert string to int: %v", err)
return err
}
stateOpt := gnmi.GNXIStateOptGenerator(pbPath, uint8(channel), gnmi.InternalUpdateState)
if err = s.InternalUpdate(stateOpt); err != nil {
return fmt.Errorf("update state failed: %v", err)
}

p = strings.Replace(widthPath, "$id", phyIDStr, 1)
p = strings.Replace(p, "$hostname", hostName, 1)
pbPath, err = xpath.ToGNMIPath(p)
if err != nil {
return fmt.Errorf("convert %v to GNMI path failed: %v", p, err)
}
width, err := strconv.ParseInt(widthStr, 10, 8)
if err != nil {
return fmt.Errorf("failed convert string to int: %v", err)
}
stateOpt = gnmi.GNXIStateOptGenerator(pbPath, uint8(width), gnmi.InternalUpdateState)
if err = s.InternalUpdate(stateOpt); err != nil {
log.Errorf("update state failed: %v", err)
return err
}

p = strings.Replace(txpowerPath, "$id", phyIDStr, 1)
p = strings.Replace(p, "$hostname", hostName, 1)
pbPath, err = xpath.ToGNMIPath(p)
if err != nil {
return fmt.Errorf("convert %v to GNMI path failed: %v", p, err)
}
txpower, err := strconv.ParseInt(txpowerStr, 10, 8)
if err != nil {
return fmt.Errorf("failed convert string to int: %v", err)
}
stateOpt = gnmi.GNXIStateOptGenerator(pbPath, uint8(txpower), gnmi.InternalUpdateState)
if err = s.InternalUpdate(stateOpt); err != nil {
return fmt.Errorf("update state failed: %v", err)
}
}

return nil
}
Loading