From 1f13f8fd8a5f5f68e6e03825115c64383f17a1b5 Mon Sep 17 00:00:00 2001 From: Arthur de Kerhor Date: Wed, 3 Mar 2021 08:49:15 -0800 Subject: 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 Change-Id: I692f9ca5a65c1123b3cf28c761455eec36049791 --- extras/vpp_stats_fs/README.md | 61 +++++++++ extras/vpp_stats_fs/cmd.go | 91 +++++++++++++ extras/vpp_stats_fs/install.sh | 274 ++++++++++++++++++++++++++++++++++++++++ extras/vpp_stats_fs/stats_fs.go | 207 ++++++++++++++++++++++++++++++ 4 files changed, 633 insertions(+) create mode 100755 extras/vpp_stats_fs/README.md create mode 100644 extras/vpp_stats_fs/cmd.go create mode 100755 extras/vpp_stats_fs/install.sh create mode 100644 extras/vpp_stats_fs/stats_fs.go (limited to 'extras/vpp_stats_fs') 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 & +``` +**Options:** + - debug \ (default is false) + - socket \ (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 +} -- cgit 1.2.3-korg