| // +build linux |
| |
| package host |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/binary" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strconv" |
| "strings" |
| "sync/atomic" |
| "time" |
| |
| "github.com/shirou/gopsutil/internal/common" |
| ) |
| |
| type LSB struct { |
| ID string |
| Release string |
| Codename string |
| Description string |
| } |
| |
| // from utmp.h |
| const USER_PROCESS = 7 |
| |
| func Info() (*InfoStat, error) { |
| return InfoWithContext(context.Background()) |
| } |
| |
| func InfoWithContext(ctx context.Context) (*InfoStat, error) { |
| ret := &InfoStat{ |
| OS: runtime.GOOS, |
| } |
| |
| hostname, err := os.Hostname() |
| if err == nil { |
| ret.Hostname = hostname |
| } |
| |
| platform, family, version, err := PlatformInformation() |
| if err == nil { |
| ret.Platform = platform |
| ret.PlatformFamily = family |
| ret.PlatformVersion = version |
| } |
| kernelVersion, err := KernelVersion() |
| if err == nil { |
| ret.KernelVersion = kernelVersion |
| } |
| |
| system, role, err := Virtualization() |
| if err == nil { |
| ret.VirtualizationSystem = system |
| ret.VirtualizationRole = role |
| } |
| |
| boot, err := BootTime() |
| if err == nil { |
| ret.BootTime = boot |
| ret.Uptime = uptime(boot) |
| } |
| |
| if numProcs, err := common.NumProcs(); err == nil { |
| ret.Procs = numProcs |
| } |
| |
| sysProductUUID := common.HostSys("class/dmi/id/product_uuid") |
| machineID := common.HostEtc("machine-id") |
| procSysKernelRandomBootID := common.HostProc("sys/kernel/random/boot_id") |
| switch { |
| // In order to read this file, needs to be supported by kernel/arch and run as root |
| // so having fallback is important |
| case common.PathExists(sysProductUUID): |
| lines, err := common.ReadLines(sysProductUUID) |
| if err == nil && len(lines) > 0 && lines[0] != "" { |
| ret.HostID = strings.ToLower(lines[0]) |
| break |
| } |
| fallthrough |
| // Fallback on GNU Linux systems with systemd, readable by everyone |
| case common.PathExists(machineID): |
| lines, err := common.ReadLines(machineID) |
| if err == nil && len(lines) > 0 && len(lines[0]) == 32 { |
| st := lines[0] |
| ret.HostID = fmt.Sprintf("%s-%s-%s-%s-%s", st[0:8], st[8:12], st[12:16], st[16:20], st[20:32]) |
| break |
| } |
| fallthrough |
| // Not stable between reboot, but better than nothing |
| default: |
| lines, err := common.ReadLines(procSysKernelRandomBootID) |
| if err == nil && len(lines) > 0 && lines[0] != "" { |
| ret.HostID = strings.ToLower(lines[0]) |
| } |
| } |
| |
| return ret, nil |
| } |
| |
| // cachedBootTime must be accessed via atomic.Load/StoreUint64 |
| var cachedBootTime uint64 |
| |
| // BootTime returns the system boot time expressed in seconds since the epoch. |
| func BootTime() (uint64, error) { |
| return BootTimeWithContext(context.Background()) |
| } |
| |
| func BootTimeWithContext(ctx context.Context) (uint64, error) { |
| t := atomic.LoadUint64(&cachedBootTime) |
| if t != 0 { |
| return t, nil |
| } |
| |
| system, role, err := Virtualization() |
| if err != nil { |
| return 0, err |
| } |
| |
| statFile := "stat" |
| if system == "lxc" && role == "guest" { |
| // if lxc, /proc/uptime is used. |
| statFile = "uptime" |
| } else if system == "docker" && role == "guest" { |
| // also docker, guest |
| statFile = "uptime" |
| } |
| |
| filename := common.HostProc(statFile) |
| lines, err := common.ReadLines(filename) |
| if err != nil { |
| return 0, err |
| } |
| |
| if statFile == "stat" { |
| for _, line := range lines { |
| if strings.HasPrefix(line, "btime") { |
| f := strings.Fields(line) |
| if len(f) != 2 { |
| return 0, fmt.Errorf("wrong btime format") |
| } |
| b, err := strconv.ParseInt(f[1], 10, 64) |
| if err != nil { |
| return 0, err |
| } |
| t = uint64(b) |
| atomic.StoreUint64(&cachedBootTime, t) |
| return t, nil |
| } |
| } |
| } else if statFile == "uptime" { |
| if len(lines) != 1 { |
| return 0, fmt.Errorf("wrong uptime format") |
| } |
| f := strings.Fields(lines[0]) |
| b, err := strconv.ParseFloat(f[0], 64) |
| if err != nil { |
| return 0, err |
| } |
| t = uint64(time.Now().Unix()) - uint64(b) |
| atomic.StoreUint64(&cachedBootTime, t) |
| return t, nil |
| } |
| |
| return 0, fmt.Errorf("could not find btime") |
| } |
| |
| func uptime(boot uint64) uint64 { |
| return uint64(time.Now().Unix()) - boot |
| } |
| |
| func Uptime() (uint64, error) { |
| return UptimeWithContext(context.Background()) |
| } |
| |
| func UptimeWithContext(ctx context.Context) (uint64, error) { |
| boot, err := BootTime() |
| if err != nil { |
| return 0, err |
| } |
| return uptime(boot), nil |
| } |
| |
| func Users() ([]UserStat, error) { |
| return UsersWithContext(context.Background()) |
| } |
| |
| func UsersWithContext(ctx context.Context) ([]UserStat, error) { |
| utmpfile := common.HostVar("run/utmp") |
| |
| file, err := os.Open(utmpfile) |
| if err != nil { |
| return nil, err |
| } |
| defer file.Close() |
| |
| buf, err := ioutil.ReadAll(file) |
| if err != nil { |
| return nil, err |
| } |
| |
| count := len(buf) / sizeOfUtmp |
| |
| ret := make([]UserStat, 0, count) |
| |
| for i := 0; i < count; i++ { |
| b := buf[i*sizeOfUtmp : (i+1)*sizeOfUtmp] |
| |
| var u utmp |
| br := bytes.NewReader(b) |
| err := binary.Read(br, binary.LittleEndian, &u) |
| if err != nil { |
| continue |
| } |
| if u.Type != USER_PROCESS { |
| continue |
| } |
| user := UserStat{ |
| User: common.IntToString(u.User[:]), |
| Terminal: common.IntToString(u.Line[:]), |
| Host: common.IntToString(u.Host[:]), |
| Started: int(u.Tv.Sec), |
| } |
| ret = append(ret, user) |
| } |
| |
| return ret, nil |
| |
| } |
| |
| func getOSRelease() (platform string, version string, err error) { |
| contents, err := common.ReadLines(common.HostEtc("os-release")) |
| if err != nil { |
| return "", "", nil // return empty |
| } |
| for _, line := range contents { |
| field := strings.Split(line, "=") |
| if len(field) < 2 { |
| continue |
| } |
| switch field[0] { |
| case "ID": // use ID for lowercase |
| platform = strings.Trim(field[1], `"`) |
| case "VERSION": |
| version = strings.Trim(field[1], `"`) |
| } |
| } |
| return platform, version, nil |
| } |
| |
| func getLSB() (*LSB, error) { |
| ret := &LSB{} |
| if common.PathExists(common.HostEtc("lsb-release")) { |
| contents, err := common.ReadLines(common.HostEtc("lsb-release")) |
| if err != nil { |
| return ret, err // return empty |
| } |
| for _, line := range contents { |
| field := strings.Split(line, "=") |
| if len(field) < 2 { |
| continue |
| } |
| switch field[0] { |
| case "DISTRIB_ID": |
| ret.ID = field[1] |
| case "DISTRIB_RELEASE": |
| ret.Release = field[1] |
| case "DISTRIB_CODENAME": |
| ret.Codename = field[1] |
| case "DISTRIB_DESCRIPTION": |
| ret.Description = field[1] |
| } |
| } |
| } else if common.PathExists("/usr/bin/lsb_release") { |
| lsb_release, err := exec.LookPath("lsb_release") |
| if err != nil { |
| return ret, err |
| } |
| out, err := invoke.Command(lsb_release) |
| if err != nil { |
| return ret, err |
| } |
| for _, line := range strings.Split(string(out), "\n") { |
| field := strings.Split(line, ":") |
| if len(field) < 2 { |
| continue |
| } |
| switch field[0] { |
| case "Distributor ID": |
| ret.ID = field[1] |
| case "Release": |
| ret.Release = field[1] |
| case "Codename": |
| ret.Codename = field[1] |
| case "Description": |
| ret.Description = field[1] |
| } |
| } |
| |
| } |
| |
| return ret, nil |
| } |
| |
| func PlatformInformation() (platform string, family string, version string, err error) { |
| return PlatformInformationWithContext(context.Background()) |
| } |
| |
| func PlatformInformationWithContext(ctx context.Context) (platform string, family string, version string, err error) { |
| |
| lsb, err := getLSB() |
| if err != nil { |
| lsb = &LSB{} |
| } |
| |
| if common.PathExists(common.HostEtc("oracle-release")) { |
| platform = "oracle" |
| contents, err := common.ReadLines(common.HostEtc("oracle-release")) |
| if err == nil { |
| version = getRedhatishVersion(contents) |
| } |
| |
| } else if common.PathExists(common.HostEtc("enterprise-release")) { |
| platform = "oracle" |
| contents, err := common.ReadLines(common.HostEtc("enterprise-release")) |
| if err == nil { |
| version = getRedhatishVersion(contents) |
| } |
| } else if common.PathExists(common.HostEtc("slackware-version")) { |
| platform = "slackware" |
| contents, err := common.ReadLines(common.HostEtc("slackware-version")) |
| if err == nil { |
| version = getSlackwareVersion(contents) |
| } |
| } else if common.PathExists(common.HostEtc("debian_version")) { |
| if lsb.ID == "Ubuntu" { |
| platform = "ubuntu" |
| version = lsb.Release |
| } else if lsb.ID == "LinuxMint" { |
| platform = "linuxmint" |
| version = lsb.Release |
| } else { |
| if common.PathExists("/usr/bin/raspi-config") { |
| platform = "raspbian" |
| } else { |
| platform = "debian" |
| } |
| contents, err := common.ReadLines(common.HostEtc("debian_version")) |
| if err == nil { |
| version = contents[0] |
| } |
| } |
| } else if common.PathExists(common.HostEtc("redhat-release")) { |
| contents, err := common.ReadLines(common.HostEtc("redhat-release")) |
| if err == nil { |
| version = getRedhatishVersion(contents) |
| platform = getRedhatishPlatform(contents) |
| } |
| } else if common.PathExists(common.HostEtc("system-release")) { |
| contents, err := common.ReadLines(common.HostEtc("system-release")) |
| if err == nil { |
| version = getRedhatishVersion(contents) |
| platform = getRedhatishPlatform(contents) |
| } |
| } else if common.PathExists(common.HostEtc("gentoo-release")) { |
| platform = "gentoo" |
| contents, err := common.ReadLines(common.HostEtc("gentoo-release")) |
| if err == nil { |
| version = getRedhatishVersion(contents) |
| } |
| } else if common.PathExists(common.HostEtc("SuSE-release")) { |
| contents, err := common.ReadLines(common.HostEtc("SuSE-release")) |
| if err == nil { |
| version = getSuseVersion(contents) |
| platform = getSusePlatform(contents) |
| } |
| // TODO: slackware detecion |
| } else if common.PathExists(common.HostEtc("arch-release")) { |
| platform = "arch" |
| version = lsb.Release |
| } else if common.PathExists(common.HostEtc("alpine-release")) { |
| platform = "alpine" |
| contents, err := common.ReadLines(common.HostEtc("alpine-release")) |
| if err == nil && len(contents) > 0 { |
| version = contents[0] |
| } |
| } else if common.PathExists(common.HostEtc("os-release")) { |
| p, v, err := getOSRelease() |
| if err == nil { |
| platform = p |
| version = v |
| } |
| } else if lsb.ID == "RedHat" { |
| platform = "redhat" |
| version = lsb.Release |
| } else if lsb.ID == "Amazon" { |
| platform = "amazon" |
| version = lsb.Release |
| } else if lsb.ID == "ScientificSL" { |
| platform = "scientific" |
| version = lsb.Release |
| } else if lsb.ID == "XenServer" { |
| platform = "xenserver" |
| version = lsb.Release |
| } else if lsb.ID != "" { |
| platform = strings.ToLower(lsb.ID) |
| version = lsb.Release |
| } |
| |
| switch platform { |
| case "debian", "ubuntu", "linuxmint", "raspbian": |
| family = "debian" |
| case "fedora": |
| family = "fedora" |
| case "oracle", "centos", "redhat", "scientific", "enterpriseenterprise", "amazon", "xenserver", "cloudlinux", "ibm_powerkvm": |
| family = "rhel" |
| case "suse", "opensuse", "sles": |
| family = "suse" |
| case "gentoo": |
| family = "gentoo" |
| case "slackware": |
| family = "slackware" |
| case "arch": |
| family = "arch" |
| case "exherbo": |
| family = "exherbo" |
| case "alpine": |
| family = "alpine" |
| case "coreos": |
| family = "coreos" |
| } |
| |
| return platform, family, version, nil |
| |
| } |
| |
| func KernelVersion() (version string, err error) { |
| return KernelVersionWithContext(context.Background()) |
| } |
| |
| func KernelVersionWithContext(ctx context.Context) (version string, err error) { |
| filename := common.HostProc("sys/kernel/osrelease") |
| if common.PathExists(filename) { |
| contents, err := common.ReadLines(filename) |
| if err != nil { |
| return "", err |
| } |
| |
| if len(contents) > 0 { |
| version = contents[0] |
| } |
| } |
| |
| return version, nil |
| } |
| |
| func getSlackwareVersion(contents []string) string { |
| c := strings.ToLower(strings.Join(contents, "")) |
| c = strings.Replace(c, "slackware ", "", 1) |
| return c |
| } |
| |
| func getRedhatishVersion(contents []string) string { |
| c := strings.ToLower(strings.Join(contents, "")) |
| |
| if strings.Contains(c, "rawhide") { |
| return "rawhide" |
| } |
| if matches := regexp.MustCompile(`release (\d[\d.]*)`).FindStringSubmatch(c); matches != nil { |
| return matches[1] |
| } |
| return "" |
| } |
| |
| func getRedhatishPlatform(contents []string) string { |
| c := strings.ToLower(strings.Join(contents, "")) |
| |
| if strings.Contains(c, "red hat") { |
| return "redhat" |
| } |
| f := strings.Split(c, " ") |
| |
| return f[0] |
| } |
| |
| func getSuseVersion(contents []string) string { |
| version := "" |
| for _, line := range contents { |
| if matches := regexp.MustCompile(`VERSION = ([\d.]+)`).FindStringSubmatch(line); matches != nil { |
| version = matches[1] |
| } else if matches := regexp.MustCompile(`PATCHLEVEL = ([\d]+)`).FindStringSubmatch(line); matches != nil { |
| version = version + "." + matches[1] |
| } |
| } |
| return version |
| } |
| |
| func getSusePlatform(contents []string) string { |
| c := strings.ToLower(strings.Join(contents, "")) |
| if strings.Contains(c, "opensuse") { |
| return "opensuse" |
| } |
| return "suse" |
| } |
| |
| func Virtualization() (string, string, error) { |
| return VirtualizationWithContext(context.Background()) |
| } |
| |
| func VirtualizationWithContext(ctx context.Context) (string, string, error) { |
| var system string |
| var role string |
| |
| filename := common.HostProc("xen") |
| if common.PathExists(filename) { |
| system = "xen" |
| role = "guest" // assume guest |
| |
| if common.PathExists(filepath.Join(filename, "capabilities")) { |
| contents, err := common.ReadLines(filepath.Join(filename, "capabilities")) |
| if err == nil { |
| if common.StringsContains(contents, "control_d") { |
| role = "host" |
| } |
| } |
| } |
| } |
| |
| filename = common.HostProc("modules") |
| if common.PathExists(filename) { |
| contents, err := common.ReadLines(filename) |
| if err == nil { |
| if common.StringsContains(contents, "kvm") { |
| system = "kvm" |
| role = "host" |
| } else if common.StringsContains(contents, "vboxdrv") { |
| system = "vbox" |
| role = "host" |
| } else if common.StringsContains(contents, "vboxguest") { |
| system = "vbox" |
| role = "guest" |
| } else if common.StringsContains(contents, "vmware") { |
| system = "vmware" |
| role = "guest" |
| } |
| } |
| } |
| |
| filename = common.HostProc("cpuinfo") |
| if common.PathExists(filename) { |
| contents, err := common.ReadLines(filename) |
| if err == nil { |
| if common.StringsContains(contents, "QEMU Virtual CPU") || |
| common.StringsContains(contents, "Common KVM processor") || |
| common.StringsContains(contents, "Common 32-bit KVM processor") { |
| system = "kvm" |
| role = "guest" |
| } |
| } |
| } |
| |
| filename = common.HostProc() |
| if common.PathExists(filepath.Join(filename, "bc", "0")) { |
| system = "openvz" |
| role = "host" |
| } else if common.PathExists(filepath.Join(filename, "vz")) { |
| system = "openvz" |
| role = "guest" |
| } |
| |
| // not use dmidecode because it requires root |
| if common.PathExists(filepath.Join(filename, "self", "status")) { |
| contents, err := common.ReadLines(filepath.Join(filename, "self", "status")) |
| if err == nil { |
| |
| if common.StringsContains(contents, "s_context:") || |
| common.StringsContains(contents, "VxID:") { |
| system = "linux-vserver" |
| } |
| // TODO: guest or host |
| } |
| } |
| |
| if common.PathExists(filepath.Join(filename, "self", "cgroup")) { |
| contents, err := common.ReadLines(filepath.Join(filename, "self", "cgroup")) |
| if err == nil { |
| if common.StringsContains(contents, "lxc") { |
| system = "lxc" |
| role = "guest" |
| } else if common.StringsContains(contents, "docker") { |
| system = "docker" |
| role = "guest" |
| } else if common.StringsContains(contents, "machine-rkt") { |
| system = "rkt" |
| role = "guest" |
| } else if common.PathExists("/usr/bin/lxc-version") { |
| system = "lxc" |
| role = "host" |
| } |
| } |
| } |
| |
| if common.PathExists(common.HostEtc("os-release")) { |
| p, _, err := getOSRelease() |
| if err == nil && p == "coreos" { |
| system = "rkt" // Is it true? |
| role = "host" |
| } |
| } |
| return system, role, nil |
| } |
| |
| func SensorsTemperatures() ([]TemperatureStat, error) { |
| return SensorsTemperaturesWithContext(context.Background()) |
| } |
| |
| func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { |
| var temperatures []TemperatureStat |
| files, err := filepath.Glob(common.HostSys("/class/hwmon/hwmon*/temp*_*")) |
| if err != nil { |
| return temperatures, err |
| } |
| if len(files) == 0 { |
| // CentOS has an intermediate /device directory: |
| // https://github.com/giampaolo/psutil/issues/971 |
| files, err = filepath.Glob(common.HostSys("/class/hwmon/hwmon*/device/temp*_*")) |
| if err != nil { |
| return temperatures, err |
| } |
| } |
| |
| // example directory |
| // device/ temp1_crit_alarm temp2_crit_alarm temp3_crit_alarm temp4_crit_alarm temp5_crit_alarm temp6_crit_alarm temp7_crit_alarm |
| // name temp1_input temp2_input temp3_input temp4_input temp5_input temp6_input temp7_input |
| // power/ temp1_label temp2_label temp3_label temp4_label temp5_label temp6_label temp7_label |
| // subsystem/ temp1_max temp2_max temp3_max temp4_max temp5_max temp6_max temp7_max |
| // temp1_crit temp2_crit temp3_crit temp4_crit temp5_crit temp6_crit temp7_crit uevent |
| for _, file := range files { |
| filename := strings.Split(filepath.Base(file), "_") |
| if filename[1] == "label" { |
| // Do not try to read the temperature of the label file |
| continue |
| } |
| |
| // Get the label of the temperature you are reading |
| var label string |
| c, _ := ioutil.ReadFile(filepath.Join(filepath.Dir(file), filename[0]+"_label")) |
| if c != nil { |
| //format the label from "Core 0" to "core0_" |
| label = fmt.Sprintf("%s_", strings.Join(strings.Split(strings.TrimSpace(strings.ToLower(string(c))), " "), "")) |
| } |
| |
| // Get the name of the temperature you are reading |
| name, err := ioutil.ReadFile(filepath.Join(filepath.Dir(file), "name")) |
| if err != nil { |
| return temperatures, err |
| } |
| |
| // Get the temperature reading |
| current, err := ioutil.ReadFile(file) |
| if err != nil { |
| return temperatures, err |
| } |
| temperature, err := strconv.ParseFloat(strings.TrimSpace(string(current)), 64) |
| if err != nil { |
| continue |
| } |
| |
| tempName := strings.TrimSpace(strings.ToLower(string(strings.Join(filename[1:], "")))) |
| temperatures = append(temperatures, TemperatureStat{ |
| SensorKey: fmt.Sprintf("%s_%s%s", strings.TrimSpace(string(name)), label, tempName), |
| Temperature: temperature / 1000.0, |
| }) |
| } |
| return temperatures, nil |
| } |