misc: fuse fs for the stats segment 91/31491/15
authorArthur de Kerhor <arthurdekerhor@gmail.com>
Wed, 3 Mar 2021 16:49:15 +0000 (08:49 -0800)
committerDave Barach <openvpp@barachs.net>
Wed, 24 Mar 2021 12:16:43 +0000 (12:16 +0000)
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

Makefile
extras/vpp_stats_fs/README.md [new file with mode: 0755]
extras/vpp_stats_fs/cmd.go [new file with mode: 0644]
extras/vpp_stats_fs/install.sh [new file with mode: 0755]
extras/vpp_stats_fs/stats_fs.go [new file with mode: 0644]

index 05a912c..a1226a7 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -218,6 +218,7 @@ help:
        @echo " docs                 - Build the Sphinx documentation"
        @echo " docs-venv            - Build the virtual environment for 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 "Make Arguments:"
        @echo " V=[0|1]                  - set build verbosity level"
@@ -657,6 +658,33 @@ featurelist: centos-pyyaml
 checkfeaturelist: centos-pyyaml
        @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
 #
diff --git a/extras/vpp_stats_fs/README.md b/extras/vpp_stats_fs/README.md
new file mode 100755 (executable)
index 0000000..3b0b094
--- /dev/null
@@ -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 (file)
index 0000000..826b011
--- /dev/null
@@ -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 (executable)
index 0000000..6249e63
--- /dev/null
@@ -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 (file)
index 0000000..a9b8ae7
--- /dev/null
@@ -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
+}