| /* |
| * 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 |
| } |