Skip to content

Commit

Permalink
This commit adds support for CIFS to procfs
Browse files Browse the repository at this point in the history
Supported are any client statistics in SMB1 and SMB2

Signed-off-by: Christian Rebischke <Chris.Rebischke@posteo.de>
  • Loading branch information
Christian Rebischke authored and Christian Rebischke committed Jul 16, 2018
1 parent ae68e2d commit d774d6a
Show file tree
Hide file tree
Showing 4 changed files with 762 additions and 0 deletions.
78 changes: 78 additions & 0 deletions cifs/cifs.go
Original file line number Diff line number Diff line change
@@ -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<sessions>\d+)`),
regexp.MustCompile(`Share \(unique mount targets\): (?P<shares>\d+)`),
regexp.MustCompile(`SMB Request/Response Buffer: (?P<smbBuffer>\d+) Pool size: (?P<smbPoolSize>\d+)`),
regexp.MustCompile(`SMB Small Req/Resp Buffer: (?P<smbSmallBuffer>\d+) Pool size: (?P<smbSmallPoolSize>\d+)`),
regexp.MustCompile(`Operations \(MIDs\): (?P<operations>\d+)`),
regexp.MustCompile(`(?P<sessionCount>\d+) session (?P<shareReconnects>\d+) share reconnects`),
regexp.MustCompile(`Total vfs operations: (?P<totalOperations>\d+) maximum at one time: (?P<totalMaxOperations>\d+)`),
}

// Array with fixed regex for parsing SMB1
var regexpSMB1s = [...]*regexp.Regexp{
regexp.MustCompile(`(?P<sessionID>\d+)\) \\\\(?P<server>[A-Za-z1-9-.]+)(?P<share>.+)`),
regexp.MustCompile(`SMBs: (?P<smbs>\d+) Oplocks breaks: (?P<breaks>\d+)`),
regexp.MustCompile(`Reads: (?P<reads>\d+) Bytes: (?P<readsBytes>\d+)`),
regexp.MustCompile(`Writes: (?P<writes>\d+) Bytes: (?P<writesBytes>\d+)`),
regexp.MustCompile(`Flushes: (?P<flushes>\d+)`),
regexp.MustCompile(`Locks: (?P<locks>\d+) HardLinks: (?P<hardlinks>\d+) Symlinks: (?P<symlinks>\d+)`),
regexp.MustCompile(`Opens: (?P<opens>\d+) Closes: (?P<closes>\d+) Deletes: (?P<deletes>\d+)`),
regexp.MustCompile(`Posix Opens: (?P<posixOpens>\d+) Posix Mkdirs: (?P<posixMkdirs>\d+)`),
regexp.MustCompile(`Mkdirs: (?P<mkdirs>\d+) Rmdirs: (?P<rmdirs>\d+)`),
regexp.MustCompile(`Renames: (?P<renames>\d+) T2 Renames (?P<t2Renames>\d+)`),
regexp.MustCompile(`FindFirst: (?P<findFirst>\d+) FNext (?P<fNext>\d+) FClose (?P<fClose>\d+)`),
}

// Array with fixed regex for parsing SMB2
var regexpSMB2s = [...]*regexp.Regexp{
regexp.MustCompile(`(?P<sessionID>\d+)\) \\\\(?P<server>[A-Za-z1-9-.]+)(?P<share>.+)`),
regexp.MustCompile(`SMBs: (?P<smbs>\d+)`),
regexp.MustCompile(`(?P<keyword>.*): (?P<sent>\d+) sent (?P<failed>\d+) failed`),
}
228 changes: 228 additions & 0 deletions cifs/parse_cifs.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit d774d6a

Please sign in to comment.