diff options
author | Arthur de Kerhor <arthurdekerhor@gmail.com> | 2021-03-03 08:49:15 -0800 |
---|---|---|
committer | Dave Barach <openvpp@barachs.net> | 2021-03-24 12:16:43 +0000 |
commit | 1f13f8fd8a5f5f68e6e03825115c64383f17a1b5 (patch) | |
tree | 966eb8209ed495c3a544aeaa32f223f5e67214ae /extras/vpp_stats_fs/stats_fs.go | |
parent | 4d2726ece83f391a7cadb1314415ef23104f9ffe (diff) |
misc: fuse fs for the stats segment
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
Diffstat (limited to 'extras/vpp_stats_fs/stats_fs.go')
-rw-r--r-- | extras/vpp_stats_fs/stats_fs.go | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/extras/vpp_stats_fs/stats_fs.go b/extras/vpp_stats_fs/stats_fs.go new file mode 100644 index 00000000000..a9b8ae77633 --- /dev/null +++ b/extras/vpp_stats_fs/stats_fs.go @@ -0,0 +1,207 @@ +/* + * 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 +} |