683 lines
17 KiB
Go
683 lines
17 KiB
Go
// +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")
|
|
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:
|
|
values, err := common.DoSysctrl("kernel.random.boot_id")
|
|
if err == nil && len(values) == 1 && values[0] != "" {
|
|
ret.HostID = strings.ToLower(values[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 = field[1]
|
|
case "VERSION":
|
|
version = 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("/usr/bin/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":
|
|
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 tempearture 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
|
|
}
|