From d774d6ab45188267ada8bd5e3d6a9c4a991cec40 Mon Sep 17 00:00:00 2001 From: Christian Rebischke Date: Mon, 16 Jul 2018 21:55:31 +0200 Subject: [PATCH] This commit adds support for CIFS to procfs Supported are any client statistics in SMB1 and SMB2 Signed-off-by: Christian Rebischke --- cifs/cifs.go | 78 +++++++ cifs/parse_cifs.go | 228 +++++++++++++++++++++ cifs/parse_cifs_test.go | 444 ++++++++++++++++++++++++++++++++++++++++ fs.go | 12 ++ 4 files changed, 762 insertions(+) create mode 100644 cifs/cifs.go create mode 100644 cifs/parse_cifs.go create mode 100644 cifs/parse_cifs_test.go diff --git a/cifs/cifs.go b/cifs/cifs.go new file mode 100644 index 000000000..2e79174dc --- /dev/null +++ b/cifs/cifs.go @@ -0,0 +1,78 @@ +// Copyright 2018 The Prometheus Authors +// 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. + +// Package cifs implements parsing of /proc/fs/cifs/Stats +// Fields are documented in https://www.kernel.org/doc/readme/Documentation-filesystems-cifs-README + +package cifs + +import "regexp" + +// model for the SMB1 statistics +type SMB1Stats struct { + SessionIDs SessionIDs + Stats map[string]uint64 +} + +// model for SMB2 statistics +type SMB2Stats struct { + SessionIDs SessionIDs + Stats map[string]map[string]uint64 +} + +// model for the Share sessionID "number) \\server\share" +type SessionIDs struct { + SessionID uint64 + Server string + Share string +} + +// model for the CIFS header statistics +type ClientStats struct { + Header map[string]uint64 + SMB1Stats []*SMB1Stats + SMB2Stats []*SMB2Stats +} + +// Array with fixed regex for parsing the SMB stats header +var regexpHeaders = [...]*regexp.Regexp{ + regexp.MustCompile(`CIFS Session: (?P\d+)`), + regexp.MustCompile(`Share \(unique mount targets\): (?P\d+)`), + regexp.MustCompile(`SMB Request/Response Buffer: (?P\d+) Pool size: (?P\d+)`), + regexp.MustCompile(`SMB Small Req/Resp Buffer: (?P\d+) Pool size: (?P\d+)`), + regexp.MustCompile(`Operations \(MIDs\): (?P\d+)`), + regexp.MustCompile(`(?P\d+) session (?P\d+) share reconnects`), + regexp.MustCompile(`Total vfs operations: (?P\d+) maximum at one time: (?P\d+)`), +} + +// Array with fixed regex for parsing SMB1 +var regexpSMB1s = [...]*regexp.Regexp{ + regexp.MustCompile(`(?P\d+)\) \\\\(?P[A-Za-z1-9-.]+)(?P.+)`), + regexp.MustCompile(`SMBs: (?P\d+) Oplocks breaks: (?P\d+)`), + regexp.MustCompile(`Reads: (?P\d+) Bytes: (?P\d+)`), + regexp.MustCompile(`Writes: (?P\d+) Bytes: (?P\d+)`), + regexp.MustCompile(`Flushes: (?P\d+)`), + regexp.MustCompile(`Locks: (?P\d+) HardLinks: (?P\d+) Symlinks: (?P\d+)`), + regexp.MustCompile(`Opens: (?P\d+) Closes: (?P\d+) Deletes: (?P\d+)`), + regexp.MustCompile(`Posix Opens: (?P\d+) Posix Mkdirs: (?P\d+)`), + regexp.MustCompile(`Mkdirs: (?P\d+) Rmdirs: (?P\d+)`), + regexp.MustCompile(`Renames: (?P\d+) T2 Renames (?P\d+)`), + regexp.MustCompile(`FindFirst: (?P\d+) FNext (?P\d+) FClose (?P\d+)`), +} + +// Array with fixed regex for parsing SMB2 +var regexpSMB2s = [...]*regexp.Regexp{ + regexp.MustCompile(`(?P\d+)\) \\\\(?P[A-Za-z1-9-.]+)(?P.+)`), + regexp.MustCompile(`SMBs: (?P\d+)`), + regexp.MustCompile(`(?P.*): (?P\d+) sent (?P\d+) failed`), +} diff --git a/cifs/parse_cifs.go b/cifs/parse_cifs.go new file mode 100644 index 000000000..ec183a3fd --- /dev/null +++ b/cifs/parse_cifs.go @@ -0,0 +1,228 @@ +// Copyright 2018 The Prometheus Authors +// 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. + +package cifs + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strconv" + "strings" +) + +// ParseClientStats returns stats read from /proc/fs/cifs/Stats +func ParseClientStats(r io.Reader) (*ClientStats, error) { + stats := &ClientStats{} + stats.Header = make(map[string]uint64) + scanner := bufio.NewScanner(r) + // Parse header + for scanner.Scan() { + line := scanner.Text() + for _, regexpHeader := range regexpHeaders { + match := regexpHeader.FindStringSubmatch(line) + if 0 == len(match) { + continue + } + for index, name := range regexpHeader.SubexpNames() { + if 0 == index || "" == name { + continue + } + value, err := strconv.ParseUint(match[index], 10, 64) + if nil != err { + continue + } + stats.Header[name] = value + } + break + } + if strings.HasPrefix(line, "Total vfs") { + break + } + } + // Parse Shares + var tmpSMB1Stats *SMB1Stats + var tmpSMB2Stats *SMB2Stats + var tmpSessionIDs *SessionIDs + // The legacy variable sets the current context. True for SMB1, False for SMB2 + legacy := true + for scanner.Scan() { + line := scanner.Text() + if legacy { + // This part manages the parsing of SMB1 + for _, regexpSMB1 := range regexpSMB1s { + match := regexpSMB1.FindStringSubmatch(line) + if 0 == len(match) { + // Check for SMB1 Line: "SMBs: 9 Oplocks breaks: 0" + // If this Check fails we change to SMB2 Statistics + if strings.HasPrefix(line, "SMBs:") && !(strings.Contains(line, "breaks")) { + legacy = false + tmpSMB2Stats = &SMB2Stats{ + Stats: make(map[string]map[string]uint64), + } + stats.SMB2Stats = append(stats.SMB2Stats, tmpSMB2Stats) + re := regexp.MustCompile("[0-9]+") + find_smb := re.FindAllString(line, 1) + tmpSMB2Stats.Stats["smbs"] = make(map[string]uint64) + value, err := strconv.ParseUint(find_smb[0], 10, 64) + if nil != err { + continue + } + tmpSMB2Stats.Stats["smbs"]["smbs"] = value + break + } + continue + } + for index, name := range regexpSMB1.SubexpNames() { + if 0 == index || "" == name { + continue + } + switch name { + case "sessionID": + value, err := strconv.ParseUint(match[index], 10, 64) + if nil != err { + continue + } + tmpSessionIDs = &SessionIDs{ + SessionID: value, + } + case "server": + if "" != match[index] { + tmpSessionIDs.Server = match[index] + } + case "share": + if "" != match[index] { + tmpSessionIDs.Share = match[index] + } + case "smbs": + tmpSMB1Stats = &SMB1Stats{ + Stats: make(map[string]uint64), + } + stats.SMB1Stats = append(stats.SMB1Stats, tmpSMB1Stats) + value, err := strconv.ParseUint(match[index], 10, 64) + if nil != err { + continue + } + tmpSMB1Stats.Stats[name] = value + default: + value, err := strconv.ParseUint(match[index], 10, 64) + if nil != err { + continue + } + if 0 == tmpSMB1Stats.SessionIDs.SessionID { + tmpSMB1Stats.SessionIDs.SessionID = tmpSessionIDs.SessionID + tmpSMB1Stats.SessionIDs.Server = tmpSessionIDs.Server + tmpSMB1Stats.SessionIDs.Share = tmpSessionIDs.Share + + } + tmpSMB1Stats.Stats[name] = value + } + } + break + } + } else { + // This part manages the parsing of SMB2 Shares + var keyword string + for _, regexpSMB2 := range regexpSMB2s { + match := regexpSMB2.FindStringSubmatch(line) + if 0 == len(match) { + // Check for SMB2 Line: "SMBs: 9" + // If this Check fails we change to SMB1 Statistics + if strings.HasPrefix(line, "SMBs:") && strings.Contains(line, "breaks") { + legacy = true + tmpSMB1Stats = &SMB1Stats{ + Stats: make(map[string]uint64), + } + stats.SMB1Stats = append(stats.SMB1Stats, tmpSMB1Stats) + re := regexp.MustCompile("[0-9]+") + find_smb := re.FindAllString(line, 2) + smbs, err := strconv.ParseUint(find_smb[0], 10, 64) + if nil != err { + continue + } + breaks, err := strconv.ParseUint(find_smb[1], 10, 64) + if nil != err { + continue + } + tmpSMB1Stats.Stats["smbs"] = smbs + tmpSMB1Stats.Stats["breaks"] = breaks + + break + } + continue + } + for index, name := range regexpSMB2.SubexpNames() { + if 0 == index || "" == name { + continue + } + switch name { + case "sessionID": + value, err := strconv.ParseUint(match[index], 10, 64) + if nil != err { + continue + } + tmpSessionIDs = &SessionIDs{ + SessionID: value, + } + case "server": + if "" != match[index] { + tmpSessionIDs.Server = match[index] + } + case "share": + if "" != match[index] { + tmpSessionIDs.Share = match[index] + } + case "smbs": + tmpSMB2Stats = &SMB2Stats{ + Stats: make(map[string]map[string]uint64), + } + stats.SMB2Stats = append(stats.SMB2Stats, tmpSMB2Stats) + value, err := strconv.ParseUint(match[index], 10, 64) + if nil != err { + continue + } + tmpSMB2Stats.Stats[name] = make(map[string]uint64) + tmpSMB2Stats.Stats[name][name] = value + + default: + value, err := strconv.ParseUint(match[index], 10, 64) + if nil != err { + keyword = match[index] + tmpSMB2Stats.Stats[keyword] = make(map[string]uint64) + continue + } + if 0 == tmpSMB2Stats.SessionIDs.SessionID { + tmpSMB2Stats.SessionIDs.SessionID = tmpSessionIDs.SessionID + tmpSMB2Stats.SessionIDs.Server = tmpSessionIDs.Server + tmpSMB2Stats.SessionIDs.Share = tmpSessionIDs.Share + + } + tmpSMB2Stats.Stats[keyword][name] = value + } + } + break + } + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error scanning SMB file: %s", err) + } + + if 0 == len(stats.Header) { + // We should never have an empty Header. Otherwise the file is invalid + return nil, fmt.Errorf("error scanning SMB file: header is empty") + } + return stats, nil +} diff --git a/cifs/parse_cifs_test.go b/cifs/parse_cifs_test.go new file mode 100644 index 000000000..6958010fe --- /dev/null +++ b/cifs/parse_cifs_test.go @@ -0,0 +1,444 @@ +// Copyright 2018 The Prometheus Authors +// 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. + +package cifs_test + +import ( + "reflect" + "strings" + "testing" + + "github.com/prometheus/procfs/cifs" +) + +func TestNewCifsRPCStats(t *testing.T) { + tests := []struct { + name string + content string + stats *cifs.ClientStats + invalid bool + }{ + { + name: "invalid file", + content: "invalid", + invalid: true, + }, { + name: "SMB1 statistics", + content: `Resources in use +CIFS Session: 1 +Share (unique mount targets): 2 +SMB Request/Response Buffer: 1 Pool size: 5 +SMB Small Req/Resp Buffer: 1 Pool size: 30 +Operations (MIDs): 0 + +0 session 0 share reconnects +Total vfs operations: 16 maximum at one time: 2 + +1) \\server\share +SMBs: 9 Oplocks breaks: 0 +Reads: 0 Bytes: 0 +Writes: 0 Bytes: 0 +Flushes: 0 +Locks: 0 HardLinks: 0 Symlinks: 0 +Opens: 0 Closes: 0 Deletes: 0 +Posix Opens: 0 Posix Mkdirs: 0 +Mkdirs: 0 Rmdirs: 0 +Renames: 0 T2 Renames 0 +FindFirst: 1 FNext 0 FClose 0`, + stats: &cifs.ClientStats{ + Header: map[string]uint64{ + "operations": 0, + "sessionCount": 0, + "sessions": 1, + "shareReconnects": 0, + "shares": 2, + "smbBuffer": 1, + "smbPoolSize": 5, + "smbSmallBuffer": 1, + "smbSmallPoolSize": 30, + "totalMaxOperations": 2, + "totalOperations": 16, + }, + SMB1Stats: []*cifs.SMB1Stats{ + &cifs.SMB1Stats{ + SessionIDs: cifs.SessionIDs{ + SessionID: 1, + Server: "server", + Share: "\\share", + }, + Stats: map[string]uint64{ + "breaks": 0, + "closes": 0, + "deletes": 0, + "fClose": 0, + "fNext": 0, + "findFirst": 1, + "flushes": 0, + "hardlinks": 0, + "locks": 0, + "mkdirs": 0, + "opens": 0, + "posixMkdirs": 0, + "posixOpens": 0, + "reads": 0, + "readsBytes": 0, + "renames": 0, + "rmdirs": 0, + "smbs": 9, + "symlinks": 0, + "t2Renames": 0, + "writes": 0, + "writesBytes": 0, + }, + }, + }, + }, + }, { + name: "SMB2 statistics", + content: `Resources in use +CIFS Session: 2 +Share (unique mount targets): 4 +SMB Request/Response Buffer: 2 Pool size: 6 +SMB Small Req/Resp Buffer: 2 Pool size: 30 +Operations (MIDs): 0 + +0 session 0 share reconnects +Total vfs operations: 90 maximum at one time: 2 + +1) \\server\share1 +SMBs: 20 +Negotiates: 0 sent 0 failed +SessionSetups: 0 sent 0 failed +Logoffs: 0 sent 0 failed +TreeConnects: 0 sent 0 failed +TreeDisconnects: 0 sent 0 failed +Creates: 0 sent 2 failed +Closes: 0 sent 0 failed +Flushes: 0 sent 0 failed +Reads: 0 sent 0 failed +Writes: 0 sent 0 failed +Locks: 0 sent 0 failed +IOCTLs: 0 sent 0 failed +Cancels: 0 sent 0 failed +Echos: 0 sent 0 failed +QueryDirectories: 0 sent 0 failed +ChangeNotifies: 0 sent 0 failed +QueryInfos: 0 sent 0 failed +SetInfos: 0 sent 0 failed +OplockBreaks: 0 sent 0 failed`, + stats: &cifs.ClientStats{ + Header: map[string]uint64{ + "operations": 0, + "sessionCount": 0, + "sessions": 2, + "shareReconnects": 0, + "shares": 4, + "smbBuffer": 2, + "smbPoolSize": 6, + "smbSmallBuffer": 2, + "smbSmallPoolSize": 30, + "totalMaxOperations": 2, + "totalOperations": 90, + }, + SMB2Stats: []*cifs.SMB2Stats{ + &cifs.SMB2Stats{ + SessionIDs: cifs.SessionIDs{ + SessionID: 1, + Server: "server", + Share: "\\share1", + }, + Stats: map[string]map[string]uint64{ + "Cancels": { + "failed": 0, + "sent": 0, + }, + "ChangeNotifies": { + "failed": 0, + "sent": 0, + }, + "Closes": { + "failed": 0, + "sent": 0, + }, + "Creates": { + "failed": 2, + "sent": 0, + }, + "Echos": { + "failed": 0, + "sent": 0, + }, + "Flushes": { + "failed": 0, + "sent": 0, + }, + "IOCTLs": { + "failed": 0, + "sent": 0, + }, + "Locks": { + "failed": 0, + "sent": 0, + }, + "Logoffs": { + "failed": 0, + "sent": 0, + }, + "Negotiates": { + "failed": 0, + "sent": 0, + }, + "OplockBreaks": { + "failed": 0, + "sent": 0, + }, + "QueryDirectories": { + "failed": 0, + "sent": 0, + }, + "QueryInfos": { + "failed": 0, + "sent": 0, + }, + "Reads": { + "failed": 0, + "sent": 0, + }, + "SessionSetups": { + "failed": 0, + "sent": 0, + }, + "SetInfos": { + "failed": 0, + "sent": 0, + }, + "TreeConnects": { + "failed": 0, + "sent": 0, + }, + "TreeDisconnects": { + "failed": 0, + "sent": 0, + }, + "Writes": { + "failed": 0, + "sent": 0, + }, + "smbs": { + "smbs": 20, + }, + }, + }, + }, + }, + }, { + name: "Mixed statistics (SMB1 then SMB2)", + content: `Resources in use +CIFS Session: 1 +Share (unique mount targets): 2 +SMB Request/Response Buffer: 1 Pool size: 5 +SMB Small Req/Resp Buffer: 1 Pool size: 30 +Operations (MIDs): 0 + +0 session 0 share reconnects +Total vfs operations: 16 maximum at one time: 2 + +1) \\server1\share1 +SMBs: 9 Oplocks breaks: 0 +Reads: 0 Bytes: 0 +Writes: 0 Bytes: 0 +Flushes: 0 +Locks: 0 HardLinks: 0 Symlinks: 0 +Opens: 0 Closes: 0 Deletes: 0 +Posix Opens: 0 Posix Mkdirs: 0 +Mkdirs: 0 Rmdirs: 0 +Renames: 0 T2 Renames 0 +FindFirst: 1 FNext 0 FClose 0 + +2) \\server2\share2 +SMBs: 20 +Negotiates: 0 sent 0 failed +SessionSetups: 0 sent 0 failed +Logoffs: 0 sent 0 failed +TreeConnects: 0 sent 0 failed +TreeDisconnects: 0 sent 0 failed +Creates: 0 sent 2 failed +Closes: 0 sent 0 failed +Flushes: 0 sent 0 failed +Reads: 0 sent 0 failed +Writes: 0 sent 0 failed +Locks: 0 sent 0 failed +IOCTLs: 0 sent 0 failed +Cancels: 0 sent 0 failed +Echos: 0 sent 0 failed +QueryDirectories: 0 sent 0 failed +ChangeNotifies: 0 sent 0 failed +QueryInfos: 0 sent 0 failed +SetInfos: 0 sent 0 failed +OplockBreaks: 0 sent 0 failed`, + stats: &cifs.ClientStats{ + Header: map[string]uint64{ + "operations": 0, + "sessionCount": 0, + "sessions": 1, + "shareReconnects": 0, + "shares": 2, + "smbBuffer": 1, + "smbPoolSize": 5, + "smbSmallBuffer": 1, + "smbSmallPoolSize": 30, + "totalMaxOperations": 2, + "totalOperations": 16, + }, + SMB1Stats: []*cifs.SMB1Stats{ + &cifs.SMB1Stats{ + SessionIDs: cifs.SessionIDs{ + SessionID: 1, + Server: "server1", + Share: "\\share1", + }, + Stats: map[string]uint64{ + "breaks": 0, + "closes": 0, + "deletes": 0, + "fClose": 0, + "fNext": 0, + "findFirst": 1, + "flushes": 0, + "hardlinks": 0, + "locks": 0, + "mkdirs": 0, + "opens": 0, + "posixMkdirs": 0, + "posixOpens": 0, + "reads": 0, + "readsBytes": 0, + "renames": 0, + "rmdirs": 0, + "smbs": 9, + "symlinks": 0, + "t2Renames": 0, + "writes": 0, + "writesBytes": 0, + }, + }, + }, + SMB2Stats: []*cifs.SMB2Stats{ + &cifs.SMB2Stats{ + SessionIDs: cifs.SessionIDs{ + SessionID: 2, + Server: "server2", + Share: "\\share2", + }, + Stats: map[string]map[string]uint64{ + "Cancels": { + "failed": 0, + "sent": 0, + }, + "ChangeNotifies": { + "failed": 0, + "sent": 0, + }, + "Closes": { + "failed": 0, + "sent": 0, + }, + "Creates": { + "failed": 2, + "sent": 0, + }, + "Echos": { + "failed": 0, + "sent": 0, + }, + "Flushes": { + "failed": 0, + "sent": 0, + }, + "IOCTLs": { + "failed": 0, + "sent": 0, + }, + "Locks": { + "failed": 0, + "sent": 0, + }, + "Logoffs": { + "failed": 0, + "sent": 0, + }, + "Negotiates": { + "failed": 0, + "sent": 0, + }, + "OplockBreaks": { + "failed": 0, + "sent": 0, + }, + "QueryDirectories": { + "failed": 0, + "sent": 0, + }, + "QueryInfos": { + "failed": 0, + "sent": 0, + }, + "Reads": { + "failed": 0, + "sent": 0, + }, + "SessionSetups": { + "failed": 0, + "sent": 0, + }, + "SetInfos": { + "failed": 0, + "sent": 0, + }, + "TreeConnects": { + "failed": 0, + "sent": 0, + }, + "TreeDisconnects": { + "failed": 0, + "sent": 0, + }, + "Writes": { + "failed": 0, + "sent": 0, + }, + "smbs": { + "smbs": 20, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stats, err := cifs.ParseClientStats(strings.NewReader(tt.content)) + + if tt.invalid && nil == err { + t.Fatal("expected an error, but none occured") + } + if !tt.invalid && nil != err { + t.Fatalf("unexpected error: %v", err) + } + if want, have := tt.stats, stats; !reflect.DeepEqual(want, have) { + t.Fatalf("unexpected CIFS Stats:\nwant:\n%v\nhave:\n%v", want, have) + } + }) + } +} diff --git a/fs.go b/fs.go index b6c6b2ce1..fa9149860 100644 --- a/fs.go +++ b/fs.go @@ -18,6 +18,7 @@ import ( "os" "path" + "github.com/prometheus/procfs/cifs" "github.com/prometheus/procfs/nfs" "github.com/prometheus/procfs/xfs" ) @@ -80,3 +81,14 @@ func (fs FS) NFSdServerRPCStats() (*nfs.ServerRPCStats, error) { return nfs.ParseServerRPCStats(f) } + +// CIFSClientStats retrieves CIFS client statistics for SMB1 and SMB2 +func (fs FS) CIFSClientStats() (*cifs.ClientStats, error) { + f, err := os.Open(fs.Path("fs/cifs/Stats")) + if err != nil { + return nil, err + } + defer f.Close() + + return cifs.ParseClientStats(f) +}