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
This commit is contained in:
Arthur de Kerhor
2021-03-03 08:49:15 -08:00
committed by Dave Barach
parent 4d2726ece8
commit 1f13f8fd8a
5 changed files with 661 additions and 0 deletions

View File

@ -218,6 +218,7 @@ help:
@echo " docs - Build the Sphinx documentation" @echo " docs - Build the Sphinx documentation"
@echo " docs-venv - Build the virtual environment for the Sphinx docs" @echo " docs-venv - Build the virtual environment for the Sphinx docs"
@echo " docs-clean - Remove the generated files from the Sphinx docs" @echo " docs-clean - Remove the generated files from the Sphinx docs"
@echo " stats-fs-help - Help to build the stats segment file system"
@echo "" @echo ""
@echo "Make Arguments:" @echo "Make Arguments:"
@echo " V=[0|1] - set build verbosity level" @echo " V=[0|1] - set build verbosity level"
@ -657,6 +658,33 @@ featurelist: centos-pyyaml
checkfeaturelist: centos-pyyaml checkfeaturelist: centos-pyyaml
@build-root/scripts/fts.py --validate --all @build-root/scripts/fts.py --validate --all
# Build vpp_stats_fs
.PHONY: stats-fs-install
stats-fs-install:
@extras/vpp_stats_fs/install.sh install
.PHONY: stats-fs-start
stats-fs-start:
@extras/vpp_stats_fs/install.sh start
.PHONY: stats-fs-cleanup
stats-fs-cleanup:
@extras/vpp_stats_fs/install.sh cleanup
.PHONY: stats-fs-help
stats-fs-help:
@extras/vpp_stats_fs/install.sh help
.PHONY: stats-fs-force-unmount
stats-fs-force-unmount:
@extras/vpp_stats_fs/install.sh unmount
.PHONY: stats-fs-stop
stats-fs-stop:
@extras/vpp_stats_fs/install.sh stop
# #
# Build the documentation # Build the documentation
# #

61
extras/vpp_stats_fs/README.md Executable file
View File

@ -0,0 +1,61 @@
# VPP stats segment FUSE filesystem
The statfs binary allows to create a FUSE filesystem to expose and to browse the stats segment.
Is is leaned on the Go-FUSE library and requires Go-VPP stats bindings to work.
The binary mounts a filesystem on the local machine whith the data from the stats segments.
The counters can be opened and read as files (e.g. in a Unix shell).
Note that the value of a counter is determined when the corresponding file is opened (as for /proc/interrupts).
Directories regularly update their contents so that new counters get added to the filesystem.
## Prerequisites (for building)
**GoVPP** library (master branch)
**Go-FUSE** library
vpp, vppapi
## Building
Here, we add the Go librairies before building the binary
```bash
go mod init stats_fs
go get git.fd.io/govpp.git@master
go get git.fd.io/govpp.git/adapter/statsclient@master
go get github.com/hanwen/go-fuse/v2
go build
```
## Usage
The basic usage is:
```bash
sudo ./statfs <MOUNT_POINT> &
```
**Options:**
- debug \<true|false\> (default is false)
- socket \<statSocket\> (default is /run/vpp/stats.sock)
## Browsing the filesystem
You can browse the filesystem as a regular user.
Example:
```bash
cd /path/to/mountpoint
cd sys/node
ls -al
cat names
```
## Unmounting the file system
You can unmount the filesystem with the fusermount command.
```bash
sudo fusermount -u /path/to/mountpoint
```
To force the unmount even if the resource is busy, add the -z option:
```bash
sudo fusermount -uz /path/to/mountpoint
```

View File

@ -0,0 +1,91 @@
/*
* 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.
*/
/*
* Copyright 2016 the Go-FUSE Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
// This file is the main program driver to mount the stats segment filesystem.
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"runtime"
"strings"
"syscall"
"git.fd.io/govpp.git/adapter/statsclient"
"git.fd.io/govpp.git/core"
"github.com/hanwen/go-fuse/v2/fs"
)
func main() {
statsSocket := flag.String("socket", statsclient.DefaultSocketName, "Path to VPP stats socket")
debug := flag.Bool("debug", false, "print debugging messages.")
flag.Parse()
if flag.NArg() < 1 {
fmt.Fprintf(os.Stderr, "usage: %s MOUNTPOINT\n", os.Args[0])
os.Exit(2)
}
//Conection to the stat segment socket.
sc := statsclient.NewStatsClient(*statsSocket)
fmt.Printf("Waiting for the VPP socket to be available. Be sure a VPP instance is running.\n")
c, err := core.ConnectStats(sc)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to the stats socket: %v\n", err)
os.Exit(1)
}
defer c.Disconnect()
fmt.Printf("Connected to the socket\n")
//Creating the filesystem instance
root, err := NewStatsFileSystem(sc)
if err != nil {
fmt.Fprintf(os.Stderr, "NewStatsFileSystem failed: %v\n", err)
os.Exit(1)
}
//Mounting the filesystem.
opts := &fs.Options{}
opts.Debug = *debug
opts.AllowOther = true
server, err := fs.Mount(flag.Arg(0), root, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Mount fail: %v\n", err)
os.Exit(1)
}
sigs := make(chan os.Signal)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
fmt.Printf("Successfully mounted the file system in directory: %s\n", flag.Arg(0))
runtime.GC()
for {
go server.Wait()
<-sigs
fmt.Println("Unmounting...")
err := server.Unmount()
if err == nil || !strings.Contains(err.Error(), "Device or resource busy") {
break
}
fmt.Fprintf(os.Stderr, "Unmount fail: %v\n", err)
}
}

274
extras/vpp_stats_fs/install.sh Executable file
View File

@ -0,0 +1,274 @@
# 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.
#!/bin/bash
# A simple script that installs stats_fs, a Fuse file system
# for the stats segment
set -eo pipefail
OPT_ARG=${1:-}
STATS_FS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"/
VPP_DIR=$(pwd)/
BUILD_ROOT=${VPP_DIR}build-root/
BINARY_DIR=${BUILD_ROOT}install-vpp-native/vpp/bin/
DEBUG_DIR=${BUILD_ROOT}install-vpp_debug-native/vpp/bin/
RUN_DIR=/run/vpp/
GOROOT=${GOROOT:-}
GOPATH=${GOPATH:-}
[ -z "${GOROOT}" ] && GOROOT="${HOME}/.go" && PATH=$GOROOT/bin:$PATH
[ -z "${GOPATH}" ] && GOPATH="${HOME}/go" && PATH=$GOPATH/bin:$PATH
function install_tools() {
echo "Installing downloading tools"
apt-get update
apt-get install git wget curl -y
}
# Install latest GO version
function install_go() {
local TMP="/tmp"
echo "Installing latest GO"
if [[ -x "$(command -v go)" ]]; then
local installed_ver installed_ver_fmt
installed_ver=$(go version)
installed_ver_fmt=${installed_ver#"go version go"}
echo "Found installed version ${installed_ver_fmt}"
return
fi
mkdir -p "${GOROOT}"
mkdir -p "${GOPATH}/"{src,pkg,bin}
wget "https://dl.google.com/go/$(curl https://golang.org/VERSION?m=text).linux-amd64.tar.gz" -O "${TMP}/go.tar.gz"
tar -C "$GOROOT" --strip-components=1 -xzf "${TMP}/go.tar.gz"
rm -f "${TMP}/go.tar.gz"
# export path for current session to install vpp_stast_fs
export GOROOT=${GOROOT}
export PATH=$GOROOT/bin:$PATH
export GOPATH=$GOPATH
export PATH=$GOPATH/bin:$PATH
echo "Installed $(go version)"
}
function install_fuse() {
echo "Installing Fuse"
apt-get update
apt-get install fuse -y
}
function install_go_dep() {
echo "Installing Go dependencies"
if [[ ! -x "$(command -v go)" ]]; then
echo "GO is not installed"
exit 1
fi
if [ ! -e "go.mod" ]; then
go mod init stats_fs
fi
# master required
go get git.fd.io/govpp.git@master
go get git.fd.io/govpp.git/adapter/statsclient@master
go get github.com/hanwen/go-fuse/v2
}
# Resolve stats_fs dependencies and builds the binary
function build_statfs() {
echo "Installing statfs"
go build
if [ -d "${BINARY_DIR}" ]; then
mv stats_fs "${BINARY_DIR}"/stats_fs
elif [ -d "${DEBUG_DIR}" ]; then
mv stats_fs "${DEBUG_DIR}"/stats_fs
else
echo "${BINARY_DIR} and ${DEBUG_DIR} directories does not exist, the binary is installed at ${STATS_FS_DIR}stats_fs instead"
fi
}
function install_statfs() {
if [[ ! -x "$(command -v go)" ]]; then
install_tools
install_go
fi
if [[ ! -x "$(command -v fusermount)" ]]; then
install_fuse
fi
if [ ! -d "${STATS_FS_DIR}" ]; then
echo "${STATS_FS_DIR} directory does not exist"
exit 1
fi
cd "${STATS_FS_DIR}"
if [[ ! -x "$(command -v ${STATS_FS_DIR}stats_fs)" ]]; then
install_go_dep
build_statfs
else
echo "stats_fs already installed at path ${STATS_FS_DIR}stats_fs"
fi
}
# Starts the statfs binary
function start_statfs() {
EXE_DIR=$STATS_FS_DIR
if [ -d "${BINARY_DIR}" ]; then
EXE_DIR=$BINARY_DIR
elif [ -d "${DEBUG_DIR}" ]; then
EXE_DIR=$DEBUG_DIR
fi
mountpoint="${RUN_DIR}stats_fs_dir"
if [[ -x "$(command -v ${EXE_DIR}stats_fs)" ]] ; then
if [ ! -d "$mountpoint" ] ; then
mkdir "$mountpoint"
fi
nohup "${EXE_DIR}"stats_fs $mountpoint 0<&- &>/dev/null &
return
fi
echo "stats_fs is not installed, use 'make stats-fs-install' first"
}
function stop_statfs() {
EXE_DIR=$STATS_FS_DIR
if [ -d "${BINARY_DIR}" ]; then
EXE_DIR=$BINARY_DIR
elif [ -d "${DEBUG_DIR}" ]; then
EXE_DIR=$DEBUG_DIR
fi
if [[ ! $(pidof "${EXE_DIR}"stats_fs) ]]; then
echo "The service stats_fs is not running"
exit 1
fi
PID=$(pidof "${EXE_DIR}"stats_fs)
kill "$PID"
if [[ $(pidof "${EXE_DIR}"stats_fs) ]]; then
echo "Can't unmount the file system: Device or resource busy"
exit 1
fi
if [ -d "${RUN_DIR}stats_fs_dir" ] ; then
rm -df "${RUN_DIR}stats_fs_dir"
fi
}
function force_unmount() {
if (( $(mount | grep "${RUN_DIR}stats_fs_dir" | wc -l) == 1 )) ; then
fusermount -uz "${RUN_DIR}stats_fs_dir"
else
echo "The default directory ${RUN_DIR}stats_fs_dir is not mounted."
fi
if [ -d "${RUN_DIR}stats_fs_dir" ] ; then
rm -df "${RUN_DIR}stats_fs_dir"
fi
}
# Remove stats_fs Go module
function cleanup() {
echo "Cleaning up stats_fs"
if [ ! -d "${STATS_FS_DIR}" ]; then
echo "${STATS_FS_DIR} directory does not exist"
exit 1
fi
cd "${STATS_FS_DIR}"
if [ -e "go.mod" ]; then
rm -f go.mod
fi
if [ -e "go.sum" ]; then
rm -f go.sum
fi
if [ -e "stats_fs" ]; then
rm -f stats_fs
fi
if [ -d "${BINARY_DIR}" ]; then
if [ -e "${BINARY_DIR}stats_fs" ]; then
rm -f ${BINARY_DIR}stats_fs
fi
elif [ -d "${DEBUG_DIR}" ]; then
if [ -e "${DEBUG_DIR}stats_fs" ]; then
rm -f ${DEBUG_DIR}stats_fs
fi
fi
if [ -d "${RUN_DIR}stats_fs_dir" ] ; then
rm -df "${RUN_DIR}stats_fs_dir"
fi
}
# Show available commands
function help() {
cat <<__EOF__
Stats_fs installer
stats-fs-install - Installs requirements (Go, GoVPP, GoFUSE) and builds stats_fs
stats-fs-start - Launches the stats_fs binary and creates a mountpoint
stats-fs-cleanup - Removes stats_fs binary and deletes go module
stats-fs-stop - Stops the executable, unmounts the file system
and removes the mountpoint directory
stats-fs-force-unmount - Forces the unmount of the filesystem even if it is busy
__EOF__
}
# Resolve chosen option and call appropriate functions
function resolve_option() {
local option=$1
case ${option} in
"start")
start_statfs
;;
"install")
install_statfs
;;
"cleanup")
cleanup
;;
"unmount")
force_unmount
;;
"stop")
stop_statfs
;;
"help")
help
;;
*) echo invalid option ;;
esac
}
if [[ -n ${OPT_ARG} ]]; then
resolve_option "${OPT_ARG}"
else
PS3="--> "
options=("install" "cleanup" "help" "start" "unmount")
select option in "${options[@]}"; do
resolve_option "${option}"
break
done
fi

View File

@ -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
}