
This extra allows to mount a FUSE filesystem reflecting the state of the stats segment. Type: feature Signed-off-by: Arthur de Kerhor <arthurdekerhor@gmail.com> Change-Id: I692f9ca5a65c1123b3cf28c761455eec36049791
208 lines
6.0 KiB
Go
208 lines
6.0 KiB
Go
/*
|
|
* Copyright (c) 2021 Cisco Systems and/or its affiliates.
|
|
* 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:
|
|
*
|
|
* http://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.
|
|
*/
|
|
|
|
/*Go-FUSE allows us to define the behaviour of our filesystem by recoding any primitive function we need.
|
|
*The structure of the filesystem is constructed as a tree.
|
|
*Each type of nodes (root, directory, file) follows its own prmitives.
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/hanwen/go-fuse/v2/fs"
|
|
"github.com/hanwen/go-fuse/v2/fuse"
|
|
|
|
"git.fd.io/govpp.git/adapter"
|
|
"git.fd.io/govpp.git/adapter/statsclient"
|
|
)
|
|
|
|
func updateDir(ctx context.Context, n *fs.Inode, cl *statsclient.StatsClient, dirPath string) syscall.Errno {
|
|
list, err := cl.ListStats(dirPath)
|
|
if err != nil {
|
|
log.Println("list stats failed:", err)
|
|
return syscall.EAGAIN
|
|
}
|
|
|
|
if list == nil {
|
|
n.ForgetPersistent()
|
|
return syscall.ENOENT
|
|
}
|
|
|
|
for _, path := range list {
|
|
localPath := strings.TrimPrefix(path, dirPath)
|
|
dir, base := filepath.Split(localPath)
|
|
|
|
parent := n
|
|
for _, component := range strings.Split(dir, "/") {
|
|
if len(component) == 0 {
|
|
continue
|
|
}
|
|
child := parent.GetChild(component)
|
|
if child == nil {
|
|
child = parent.NewPersistentInode(ctx, &dirNode{client: cl, lastUpdate: time.Now()},
|
|
fs.StableAttr{Mode: fuse.S_IFDIR})
|
|
parent.AddChild(component, child, true)
|
|
}
|
|
|
|
parent = child
|
|
}
|
|
filename := strings.Replace(base, " ", "_", -1)
|
|
child := parent.GetChild(filename)
|
|
if child == nil {
|
|
child := parent.NewPersistentInode(ctx, &statNode{client: cl, path: path}, fs.StableAttr{})
|
|
parent.AddChild(filename, child, true)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func getCounterContent(path string, client *statsclient.StatsClient) (content string, status syscall.Errno) {
|
|
content = ""
|
|
//We add '$' because we deal with regexp here
|
|
res, err := client.DumpStats(path + "$")
|
|
if err != nil {
|
|
return content, syscall.EAGAIN
|
|
}
|
|
if res == nil {
|
|
return content, syscall.ENOENT
|
|
}
|
|
|
|
result := res[0]
|
|
if result.Data == nil {
|
|
return content, 0
|
|
}
|
|
|
|
switch result.Type {
|
|
case adapter.ScalarIndex:
|
|
stats := result.Data.(adapter.ScalarStat)
|
|
content = fmt.Sprintf("%.2f\n", stats)
|
|
case adapter.ErrorIndex:
|
|
stats := result.Data.(adapter.ErrorStat)
|
|
content = fmt.Sprintf("%-16s%s\n", "Index", "Count")
|
|
for i, value := range stats {
|
|
content += fmt.Sprintf("%-16d%d\n", i, value)
|
|
}
|
|
case adapter.SimpleCounterVector:
|
|
stats := result.Data.(adapter.SimpleCounterStat)
|
|
content = fmt.Sprintf("%-16s%-16s%s\n", "Thread", "Index", "Packets")
|
|
for i, vector := range stats {
|
|
for j, value := range vector {
|
|
content += fmt.Sprintf("%-16d%-16d%d\n", i, j, value)
|
|
}
|
|
}
|
|
case adapter.CombinedCounterVector:
|
|
stats := result.Data.(adapter.CombinedCounterStat)
|
|
content = fmt.Sprintf("%-16s%-16s%-16s%s\n", "Thread", "Index", "Packets", "Bytes")
|
|
for i, vector := range stats {
|
|
for j, value := range vector {
|
|
content += fmt.Sprintf("%-16d%-16d%-16d%d\n", i, j, value[0], value[1])
|
|
}
|
|
}
|
|
case adapter.NameVector:
|
|
stats := result.Data.(adapter.NameStat)
|
|
content = fmt.Sprintf("%-16s%s\n", "Index", "Name")
|
|
for i, value := range stats {
|
|
content += fmt.Sprintf("%-16d%s\n", i, string(value))
|
|
}
|
|
default:
|
|
content = fmt.Sprintf("Unknown stat type: %d\n", result.Type)
|
|
//For now, the empty type (file deleted) is not implemented in GoVPP
|
|
return content, syscall.ENOENT
|
|
}
|
|
return content, fs.OK
|
|
}
|
|
|
|
type rootNode struct {
|
|
fs.Inode
|
|
client *statsclient.StatsClient
|
|
lastUpdate time.Time
|
|
}
|
|
|
|
var _ = (fs.NodeOnAdder)((*rootNode)(nil))
|
|
|
|
func (root *rootNode) OnAdd(ctx context.Context) {
|
|
updateDir(ctx, &root.Inode, root.client, "/")
|
|
root.lastUpdate = time.Now()
|
|
}
|
|
|
|
//The dirNode structure represents directories
|
|
type dirNode struct {
|
|
fs.Inode
|
|
client *statsclient.StatsClient
|
|
lastUpdate time.Time
|
|
}
|
|
|
|
var _ = (fs.NodeOpendirer)((*dirNode)(nil))
|
|
|
|
func (dn *dirNode) Opendir(ctx context.Context) syscall.Errno {
|
|
//We do not update a directory more than once a second, as counters are rarely added/deleted.
|
|
if time.Now().Sub(dn.lastUpdate) < time.Second {
|
|
return 0
|
|
}
|
|
|
|
//directoryPath is the path to the current directory from root
|
|
directoryPath := "/" + dn.Inode.Path(nil) + "/"
|
|
status := updateDir(ctx, &dn.Inode, dn.client, directoryPath)
|
|
dn.lastUpdate = time.Now()
|
|
return status
|
|
}
|
|
|
|
//The statNode structure represents counters
|
|
type statNode struct {
|
|
fs.Inode
|
|
client *statsclient.StatsClient
|
|
path string
|
|
}
|
|
|
|
var _ = (fs.NodeOpener)((*statNode)(nil))
|
|
|
|
//When a file is opened, the correpsonding counter value is dumped and a file handle is created
|
|
func (sn *statNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
|
|
content, status := getCounterContent(sn.path, sn.client)
|
|
if status == syscall.ENOENT {
|
|
sn.Inode.ForgetPersistent()
|
|
}
|
|
return &statFH{data: []byte(content)}, fuse.FOPEN_DIRECT_IO, status
|
|
}
|
|
|
|
/* The statFH structure aims at dislaying the counters dynamically.
|
|
* It allows the Kernel to read data as I/O without having to specify files sizes, as they may evolve dynamically.
|
|
*/
|
|
type statFH struct {
|
|
data []byte
|
|
}
|
|
|
|
var _ = (fs.FileReader)((*statFH)(nil))
|
|
|
|
func (fh *statFH) Read(ctx context.Context, data []byte, off int64) (fuse.ReadResult, syscall.Errno) {
|
|
end := int(off) + len(data)
|
|
if end > len(fh.data) {
|
|
end = len(fh.data)
|
|
}
|
|
return fuse.ReadResultData(fh.data[off:end]), fs.OK
|
|
}
|
|
|
|
//NewStatsFileSystem creates the fs for the stat segment.
|
|
func NewStatsFileSystem(sc *statsclient.StatsClient) (root fs.InodeEmbedder, err error) {
|
|
return &rootNode{client: sc}, nil
|
|
}
|