diff --git a/mountinfo.go b/mountinfo.go new file mode 100644 index 000000000..61fa61887 --- /dev/null +++ b/mountinfo.go @@ -0,0 +1,178 @@ +// Copyright 2019 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 procfs + +import ( + "bufio" + "fmt" + "io" + "os" + "strconv" + "strings" +) + +var validOptionalFields = map[string]bool{ + "shared": true, + "master": true, + "propagate_from": true, + "unbindable": true, +} + +// A MountInfo is a type that describes the details, options +// for each mount, parsed from /proc/self/mountinfo. +// The fields described in each entry of /proc/self/mountinfo +// is described in the following man page. +// http://man7.org/linux/man-pages/man5/proc.5.html +type MountInfo struct { + // Unique Id for the mount + MountId int + // The Id of the parent mount + ParentId int + // The value of `st_dev` for the files on this FS + MajorMinorVer string + // The pathname of the directory in the FS that forms + // the root for this mount + Root string + // The pathname of the mount point relative to the root + MountPoint string + // Mount options + Options map[string]string + // Zero or more optional fields + OptionalFields map[string]string + // The Filesystem type + FSType string + // FS specific information or "none" + Source string + // Superblock options + SuperOptions map[string]string +} + +// Returns part of the mountinfo line, if it exists, else an empty string. +func getStringSliceElement(parts []string, idx int, defaultValue string) string { + if idx >= len(parts) { + return defaultValue + } + return parts[idx] +} + +// Reads each line of the mountinfo file, and returns a list of formatted MountInfo structs. +func parseMountInfo(r io.Reader) ([]*MountInfo, error) { + mounts := []*MountInfo{} + scanner := bufio.NewScanner(r) + for scanner.Scan() { + mountString := scanner.Text() + parsedMounts, err := parseMountInfoString(mountString) + if err != nil { + return nil, err + } + mounts = append(mounts, parsedMounts) + } + + err := scanner.Err() + return mounts, err +} + +// Parses a mountinfo file line, and converts it to a MountInfo struct. +// An important check here is to see if the hyphen separator, as if it does not exist, +// it means that the line is malformed. +func parseMountInfoString(mountString string) (*MountInfo, error) { + var err error + + // OptionalFields can be zero, hence these checks to ensure we do not populate the wrong values in the wrong spots + separatorIndex := strings.Index(mountString, "-") + if separatorIndex == -1 { + return nil, fmt.Errorf("no separator found in mountinfo string: %s", mountString) + } + beforeFields := strings.Fields(mountString[:separatorIndex]) + afterFields := strings.Fields(mountString[separatorIndex+1:]) + if (len(beforeFields) + len(afterFields)) < 7 { + return nil, fmt.Errorf("too few fields") + } + + mount := &MountInfo{ + MajorMinorVer: getStringSliceElement(beforeFields, 2, ""), + Root: getStringSliceElement(beforeFields, 3, ""), + MountPoint: getStringSliceElement(beforeFields, 4, ""), + Options: mountOptionsParser(getStringSliceElement(beforeFields, 5, "")), + OptionalFields: nil, + FSType: getStringSliceElement(afterFields, 0, ""), + Source: getStringSliceElement(afterFields, 1, ""), + SuperOptions: mountOptionsParser(getStringSliceElement(afterFields, 2, "")), + } + + mount.MountId, err = strconv.Atoi(getStringSliceElement(beforeFields, 0, "")) + if err != nil { + return nil, fmt.Errorf("failed to parse mount ID") + } + mount.ParentId, err = strconv.Atoi(getStringSliceElement(beforeFields, 1, "")) + if err != nil { + return nil, fmt.Errorf("failed to parse parent ID") + } + // Has optional fields, which is a space separated list of values. + // Example: shared:2 master:7 + if len(beforeFields) > 6 { + mount.OptionalFields = make(map[string]string) + optionalFields := beforeFields[6:] + for _, field := range optionalFields { + optionSplit := strings.Split(field, ":") + target, value := optionSplit[0], "" + if len(optionSplit) == 2 { + value = optionSplit[1] + } + // Checks if the 'keys' in the optional fields in the mountinfo line are acceptable. + // Allowed 'keys' are shared, master, propagate_from, unbindable. + if _, ok := validOptionalFields[target]; ok { + mount.OptionalFields[target] = value + } + } + } + return mount, nil +} + +// Parses the mount options, superblock options. +func mountOptionsParser(mountOptions string) map[string]string { + opts := make(map[string]string) + options := strings.Split(mountOptions, ",") + for _, opt := range options { + splitOption := strings.Split(opt, "=") + if len(splitOption) < 2 { + key := splitOption[0] + opts[key] = "" + } else { + key, value := splitOption[0], splitOption[1] + opts[key] = value + } + } + return opts +} + +// Retrieves mountinfo information from `/proc/self/mountinfo`. +func GetMounts() ([]*MountInfo, error) { + f, err := os.Open("/proc/self/mountinfo") + if err != nil { + return nil, err + } + defer f.Close() + return parseMountInfo(f) +} + +// Retrieves mountinfo information from a processes' `/proc//mountinfo`. +func GetProcMounts(pid int) ([]*MountInfo, error) { + f, err := os.Open(fmt.Sprintf("/proc/%d/mountinfo", pid)) + if err != nil { + return nil, err + } + defer f.Close() + return parseMountInfo(f) +} diff --git a/mountinfo_test.go b/mountinfo_test.go new file mode 100644 index 000000000..35659796d --- /dev/null +++ b/mountinfo_test.go @@ -0,0 +1,135 @@ +// Copyright 2019 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 procfs + +import ( + "reflect" + "testing" +) + +func TestMountInfo(t *testing.T) { + tests := []struct { + name string + s string + mount *MountInfo + invalid bool + }{ + { + name: "Regular sysfs mounted at /sys", + s: "16 21 0:16 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw", + invalid: false, + mount: &MountInfo{ + MountId: 16, + ParentId: 21, + MajorMinorVer: "0:16", + Root: "/", + MountPoint: "/sys", + Options: map[string]string{"rw": "", "nosuid": "", "nodev": "", "noexec": "", "relatime": ""}, + OptionalFields: map[string]string{"shared": "7"}, + FSType: "sysfs", + Source: "sysfs", + SuperOptions: map[string]string{"rw": ""}, + }, + }, + { + name: "Not enough information", + s: "hello", + invalid: true, + }, + { + name: "Tmpfs mounted at /run", + s: "225 20 0:39 / /run/user/112 rw,nosuid,nodev,relatime shared:177 - tmpfs tmpfs rw,size=405096k,mode=700,uid=112,gid=116", + mount: &MountInfo{ + MountId: 225, + ParentId: 20, + MajorMinorVer: "0:39", + Root: "/", + MountPoint: "/run/user/112", + Options: map[string]string{"rw": "", "nosuid": "", "nodev": "", "relatime": ""}, + OptionalFields: map[string]string{"shared": "177"}, + FSType: "tmpfs", + Source: "tmpfs", + SuperOptions: map[string]string{"rw": "", "size": "405096k", "mode": "700", "uid": "112", "gid": "116"}, + }, + invalid: false, + }, + { + name: "Tmpfs mounted at /run, but no optional values", + s: "225 20 0:39 / /run/user/112 rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=405096k,mode=700,uid=112,gid=116", + mount: &MountInfo{ + MountId: 225, + ParentId: 20, + MajorMinorVer: "0:39", + Root: "/", + MountPoint: "/run/user/112", + Options: map[string]string{"rw": "", "nosuid": "", "nodev": "", "relatime": ""}, + OptionalFields: nil, + FSType: "tmpfs", + Source: "tmpfs", + SuperOptions: map[string]string{"rw": "", "size": "405096k", "mode": "700", "uid": "112", "gid": "116"}, + }, + invalid: false, + }, + { + name: "Tmpfs mounted at /run, with multiple optional values", + s: "225 20 0:39 / /run/user/112 rw,nosuid,nodev,relatime shared:177 master:8 - tmpfs tmpfs rw,size=405096k,mode=700,uid=112,gid=116", + mount: &MountInfo{ + MountId: 225, + ParentId: 20, + MajorMinorVer: "0:39", + Root: "/", + MountPoint: "/run/user/112", + Options: map[string]string{"rw": "", "nosuid": "", "nodev": "", "relatime": ""}, + OptionalFields: map[string]string{"shared": "177", "master": "8"}, + FSType: "tmpfs", + Source: "tmpfs", + SuperOptions: map[string]string{"rw": "", "size": "405096k", "mode": "700", "uid": "112", "gid": "116"}, + }, + invalid: false, + }, + { + name: "Tmpfs mounted at /run, with a mixture of valid and invalid optional values", + s: "225 20 0:39 / /run/user/112 rw,nosuid,nodev,relatime shared:177 master:8 foo:bar - tmpfs tmpfs rw,size=405096k,mode=700,uid=112,gid=116", + mount: &MountInfo{ + MountId: 225, + ParentId: 20, + MajorMinorVer: "0:39", + Root: "/", + MountPoint: "/run/user/112", + Options: map[string]string{"rw": "", "nosuid": "", "nodev": "", "relatime": ""}, + OptionalFields: map[string]string{"shared": "177", "master": "8"}, + FSType: "tmpfs", + Source: "tmpfs", + SuperOptions: map[string]string{"rw": "", "size": "405096k", "mode": "700", "uid": "112", "gid": "116"}, + }, + invalid: false, + }, + } + + for i, test := range tests { + t.Logf("[%02d] test %q", i, test.name) + + mount, err := parseMountInfoString(test.s) + + if test.invalid && err == nil { + t.Error("expected an error, but none occurred") + } + if !test.invalid && err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := test.mount, mount; !reflect.DeepEqual(want, have) { + t.Errorf("mounts:\nwant:\n%+v\nhave:\n%+v", want, have) + } + } +} diff --git a/proc.go b/proc.go index 8a8430147..41c148d06 100644 --- a/proc.go +++ b/proc.go @@ -247,6 +247,20 @@ func (p Proc) MountStats() ([]*Mount, error) { return parseMountStats(f) } +// MountInfo retrieves mount information for mount points in a +// process's namespace. +// It supplies information missing in `/proc/self/mounts` and +// fixes various other problems with that file too. +func (p Proc) MountInfo() ([]*MountInfo, error) { + f, err := os.Open(p.path("mountinfo")) + if err != nil { + return nil, err + } + defer f.Close() + + return parseMountInfo(f) +} + func (p Proc) fileDescriptors() ([]string, error) { d, err := os.Open(p.path("fd")) if err != nil {