summaryrefslogtreecommitdiffstats
path: root/extras/vpp_stats_fs
diff options
context:
space:
mode:
authorArthur de Kerhor <arthurdekerhor@gmail.com>2021-03-03 08:49:15 -0800
committerDave Barach <openvpp@barachs.net>2021-03-24 12:16:43 +0000
commit1f13f8fd8a5f5f68e6e03825115c64383f17a1b5 (patch)
tree966eb8209ed495c3a544aeaa32f223f5e67214ae /extras/vpp_stats_fs
parent4d2726ece83f391a7cadb1314415ef23104f9ffe (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')
-rwxr-xr-xextras/vpp_stats_fs/README.md61
-rw-r--r--extras/vpp_stats_fs/cmd.go91
-rwxr-xr-xextras/vpp_stats_fs/install.sh274
-rw-r--r--extras/vpp_stats_fs/stats_fs.go207
4 files changed, 633 insertions, 0 deletions
diff --git a/extras/vpp_stats_fs/README.md b/extras/vpp_stats_fs/README.md
new file mode 100755
index 00000000000..3b0b09468a6
--- /dev/null
+++ b/extras/vpp_stats_fs/README.md
@@ -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
+``` \ No newline at end of file
diff --git a/extras/vpp_stats_fs/cmd.go b/extras/vpp_stats_fs/cmd.go
new file mode 100644
index 00000000000..826b011b00d
--- /dev/null
+++ b/extras/vpp_stats_fs/cmd.go
@@ -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)
+ }
+}
diff --git a/extras/vpp_stats_fs/install.sh b/extras/vpp_stats_fs/install.sh
new file mode 100755
index 00000000000..6249e63c6eb
--- /dev/null
+++ b/extras/vpp_stats_fs/install.sh
@@ -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
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
+}