| /* |
| * 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" |
| "path" |
| "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 { |
| stats, err := cl.PrepareDir(dirPath) |
| if err != nil { |
| LogMsg(fmt.Sprintf("Listing stats index failed: %v\n", err)) |
| return syscall.EAGAIN |
| } |
| |
| n.Operations().(*dirNode).epoch = stats.Epoch |
| |
| n.RmAllChildren() |
| |
| for _, entry := range stats.Entries { |
| localPath := strings.TrimPrefix(string(entry.Name), dirPath) |
| dirPath, base := filepath.Split(localPath) |
| |
| parent := n |
| for _, component := range strings.Split(dirPath, "/") { |
| if len(component) == 0 { |
| continue |
| } |
| child := parent.GetChild(component) |
| if child == nil { |
| child = parent.NewInode(ctx, &dirNode{client: cl, epoch: stats.Epoch}, |
| fs.StableAttr{Mode: fuse.S_IFDIR}) |
| parent.AddChild(component, child, true) |
| } else { |
| child.Operations().(*dirNode).epoch = stats.Epoch |
| } |
| |
| parent = child |
| } |
| |
| filename := strings.Replace(base, " ", "_", -1) |
| child := parent.GetChild(filename) |
| if child == nil { |
| child := parent.NewPersistentInode(ctx, &statNode{client: cl, index: entry.Index}, fs.StableAttr{}) |
| parent.AddChild(filename, child, true) |
| } |
| } |
| return 0 |
| } |
| |
| func getCounterContent(index uint32, client *statsclient.StatsClient) (content string, status syscall.Errno) { |
| content = "" |
| statsDir, err := client.PrepareDirOnIndex(index) |
| if err != nil { |
| LogMsg(fmt.Sprintf("Dumping stats on index failed: %v\n", err)) |
| return content, syscall.EAGAIN |
| } |
| if len(statsDir.Entries) != 1 { |
| return content, syscall.ENOENT |
| } |
| result := statsDir.Entries[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 |
| } |
| |
| //The dirNode structure represents directories |
| type dirNode struct { |
| fs.Inode |
| client *statsclient.StatsClient |
| epoch int64 |
| } |
| |
| var _ = (fs.NodeOpendirer)((*dirNode)(nil)) |
| var _ = (fs.NodeGetattrer)((*dirNode)(nil)) |
| var _ = (fs.NodeOnAdder)((*dirNode)(nil)) |
| |
| func (dn *dirNode) OnAdd(ctx context.Context) { |
| if dn.Inode.IsRoot() { |
| updateDir(ctx, &dn.Inode, dn.client, "/") |
| } |
| } |
| |
| func (dn *dirNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { |
| out.Mtime = uint64(time.Now().Unix()) |
| out.Atime = out.Mtime |
| out.Ctime = out.Mtime |
| return 0 |
| } |
| |
| func (dn *dirNode) Opendir(ctx context.Context) syscall.Errno { |
| var status syscall.Errno = syscall.F_OK |
| var sleepTime time.Duration = 10 * time.Millisecond |
| newEpoch, inProgress := dn.client.GetEpoch() |
| for inProgress { |
| newEpoch, inProgress = dn.client.GetEpoch() |
| time.Sleep(sleepTime) |
| sleepTime = sleepTime * 2 |
| } |
| |
| //We check that the directory epoch is up to date |
| if dn.epoch != newEpoch { |
| //directoryPath is the path to the current directory from root |
| directoryPath := path.Clean("/" + dn.Inode.Path(nil) + "/") |
| status = updateDir(ctx, &dn.Inode, dn.client, directoryPath) |
| } |
| return status |
| } |
| |
| //The statNode structure represents counters |
| type statNode struct { |
| fs.Inode |
| client *statsclient.StatsClient |
| index uint32 |
| } |
| |
| var _ = (fs.NodeOpener)((*statNode)(nil)) |
| var _ = (fs.NodeGetattrer)((*statNode)(nil)) |
| |
| func (fh *statNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { |
| out.Mtime = uint64(time.Now().Unix()) |
| out.Atime = out.Mtime |
| out.Ctime = out.Mtime |
| return 0 |
| } |
| |
| //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.index, sn.client) |
| if status == syscall.ENOENT { |
| _, parent := sn.Inode.Parent() |
| parent.RmChild(sn.Inode.Path(parent)) |
| |
| } |
| 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 &dirNode{client: sc}, nil |
| } |