|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux
|
|
|
|
|
|
|
|
package host
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/binary"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
|
|
|
|
"github.com/shirou/gopsutil/v4/internal/common"
|
|
|
|
)
|
|
|
|
|
|
|
|
type lsbStruct struct {
|
|
|
|
ID string
|
|
|
|
Release string
|
|
|
|
Codename string
|
|
|
|
Description string
|
|
|
|
}
|
|
|
|
|
host_linux: Skip everything that isn't a normal process.
host_darwin does the same filtering. Not doing this gives us some rather strange
entries that likely aren't what we want.
Before:
{"user":"reboot","terminal":"~","host":"3.10.0-327.4.5.el7.x86_64","started":1454378260}
{"user":"LOGIN","terminal":"ttyS0","host":"","started":1454378270}
{"user":"LOGIN","terminal":"tty1","host":"","started":1454378270}
{"user":"runlevel","terminal":"~","host":"3.10.0-327.4.5.el7.x86_64","started":1454378276}
{"user":"root","terminal":"pts/0","host":"vpn","started":1454404513}
After:
{"user":"root","terminal":"pts/0","host":"vpn","started":1454404513}
9 years ago
|
|
|
// from utmp.h
|
|
|
|
const (
|
|
|
|
user_PROCESS = 7
|
|
|
|
)
|
host_linux: Skip everything that isn't a normal process.
host_darwin does the same filtering. Not doing this gives us some rather strange
entries that likely aren't what we want.
Before:
{"user":"reboot","terminal":"~","host":"3.10.0-327.4.5.el7.x86_64","started":1454378260}
{"user":"LOGIN","terminal":"ttyS0","host":"","started":1454378270}
{"user":"LOGIN","terminal":"tty1","host":"","started":1454378270}
{"user":"runlevel","terminal":"~","host":"3.10.0-327.4.5.el7.x86_64","started":1454378276}
{"user":"root","terminal":"pts/0","host":"vpn","started":1454404513}
After:
{"user":"root","terminal":"pts/0","host":"vpn","started":1454404513}
9 years ago
|
|
|
|
|
|
|
func HostIDWithContext(ctx context.Context) (string, error) {
|
|
|
|
sysProductUUID := common.HostSysWithContext(ctx, "class/dmi/id/product_uuid")
|
|
|
|
machineID := common.HostEtcWithContext(ctx, "machine-id")
|
|
|
|
procSysKernelRandomBootID := common.HostProcWithContext(ctx, "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] != "" {
|
|
|
|
return strings.ToLower(lines[0]), nil
|
|
|
|
}
|
|
|
|
fallthrough
|
|
|
|
// Fallback on GNU Linux systems with systemd, readable by everyone
|
Have a real fixed HostID on Linux
On Linux, most golang programs do not run as root (or at least, they should not),
by default, the kernels uses strict permissions, so most userland programs cannot
read `/sys/class/dmi/id/product_uuid`. However, programs such as Consul are relying
on it to get fixed IDs, instead they have a different ID on each boot.
We propose to use `/etc/machine-id` as fallback https://www.freedesktop.org/software/systemd/man/machine-id.html
In order to fix this, this patch does the following:
- if `/sys/class/dmi/id/product_uuid` can be read, use it for HostID
- else if `/etc/machine-id` exists and has 32 chars, use it and add '-' to have the same format as product_uuid
- finally, if notthing works, use the `kernel.random.boot_id`
This will greatly increase the number of programs having correct behaviour when
those rely on having a fixed HostID.
This will fix the following issues:
- https://github.com/shirou/gopsutil/issues/350
- https://github.com/hashicorp/consul/issues/4741
6 years ago
|
|
|
case common.PathExists(machineID):
|
|
|
|
lines, err := common.ReadLines(machineID)
|
|
|
|
if err == nil && len(lines) > 0 && len(lines[0]) == 32 {
|
|
|
|
st := lines[0]
|
|
|
|
return fmt.Sprintf("%s-%s-%s-%s-%s", st[0:8], st[8:12], st[12:16], st[16:20], st[20:32]), nil
|
Have a real fixed HostID on Linux
On Linux, most golang programs do not run as root (or at least, they should not),
by default, the kernels uses strict permissions, so most userland programs cannot
read `/sys/class/dmi/id/product_uuid`. However, programs such as Consul are relying
on it to get fixed IDs, instead they have a different ID on each boot.
We propose to use `/etc/machine-id` as fallback https://www.freedesktop.org/software/systemd/man/machine-id.html
In order to fix this, this patch does the following:
- if `/sys/class/dmi/id/product_uuid` can be read, use it for HostID
- else if `/etc/machine-id` exists and has 32 chars, use it and add '-' to have the same format as product_uuid
- finally, if notthing works, use the `kernel.random.boot_id`
This will greatly increase the number of programs having correct behaviour when
those rely on having a fixed HostID.
This will fix the following issues:
- https://github.com/shirou/gopsutil/issues/350
- https://github.com/hashicorp/consul/issues/4741
6 years ago
|
|
|
}
|
|
|
|
fallthrough
|
|
|
|
// Not stable between reboot, but better than nothing
|
|
|
|
default:
|
|
|
|
lines, err := common.ReadLines(procSysKernelRandomBootID)
|
|
|
|
if err == nil && len(lines) > 0 && lines[0] != "" {
|
|
|
|
return strings.ToLower(lines[0]), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func numProcs(ctx context.Context) (uint64, error) {
|
|
|
|
return common.NumProcsWithContext(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
func BootTimeWithContext(ctx context.Context) (uint64, error) {
|
|
|
|
return common.BootTimeWithContext(ctx, enableBootTimeCache)
|
|
|
|
}
|
|
|
|
|
|
|
|
func UptimeWithContext(ctx context.Context) (uint64, error) {
|
|
|
|
sysinfo := &unix.Sysinfo_t{}
|
|
|
|
if err := unix.Sysinfo(sysinfo); err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return uint64(sysinfo.Uptime), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func UsersWithContext(ctx context.Context) ([]UserStat, error) {
|
|
|
|
utmpfile := common.HostVarWithContext(ctx, "run/utmp")
|
|
|
|
|
|
|
|
file, err := os.Open(utmpfile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
buf, err := io.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 {
|
host_linux: Skip everything that isn't a normal process.
host_darwin does the same filtering. Not doing this gives us some rather strange
entries that likely aren't what we want.
Before:
{"user":"reboot","terminal":"~","host":"3.10.0-327.4.5.el7.x86_64","started":1454378260}
{"user":"LOGIN","terminal":"ttyS0","host":"","started":1454378270}
{"user":"LOGIN","terminal":"tty1","host":"","started":1454378270}
{"user":"runlevel","terminal":"~","host":"3.10.0-327.4.5.el7.x86_64","started":1454378276}
{"user":"root","terminal":"pts/0","host":"vpn","started":1454404513}
After:
{"user":"root","terminal":"pts/0","host":"vpn","started":1454404513}
9 years ago
|
|
|
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 getlsbStruct(ctx context.Context) (*lsbStruct, error) {
|
|
|
|
ret := &lsbStruct{}
|
|
|
|
if common.PathExists(common.HostEtcWithContext(ctx, "lsb-release")) {
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "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 = strings.ReplaceAll(field[1], `"`, ``)
|
|
|
|
case "DISTRIB_RELEASE":
|
|
|
|
ret.Release = strings.ReplaceAll(field[1], `"`, ``)
|
|
|
|
case "DISTRIB_CODENAME":
|
|
|
|
ret.Codename = strings.ReplaceAll(field[1], `"`, ``)
|
|
|
|
case "DISTRIB_DESCRIPTION":
|
|
|
|
ret.Description = strings.ReplaceAll(field[1], `"`, ``)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if common.PathExists("/usr/bin/lsb_release") {
|
|
|
|
out, err := invoke.Command("/usr/bin/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 = strings.ReplaceAll(field[1], `"`, ``)
|
|
|
|
case "Release":
|
|
|
|
ret.Release = strings.ReplaceAll(field[1], `"`, ``)
|
|
|
|
case "Codename":
|
|
|
|
ret.Codename = strings.ReplaceAll(field[1], `"`, ``)
|
|
|
|
case "Description":
|
|
|
|
ret.Description = strings.ReplaceAll(field[1], `"`, ``)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func PlatformInformationWithContext(ctx context.Context) (platform string, family string, version string, err error) {
|
|
|
|
lsb, err := getlsbStruct(ctx)
|
|
|
|
if err != nil {
|
|
|
|
lsb = &lsbStruct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "oracle-release")) {
|
|
|
|
platform = "oracle"
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "oracle-release"))
|
|
|
|
if err == nil {
|
|
|
|
version = getRedhatishVersion(contents)
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "enterprise-release")) {
|
|
|
|
platform = "oracle"
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "enterprise-release"))
|
|
|
|
if err == nil {
|
|
|
|
version = getRedhatishVersion(contents)
|
|
|
|
}
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "slackware-version")) {
|
|
|
|
platform = "slackware"
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "slackware-version"))
|
|
|
|
if err == nil {
|
|
|
|
version = getSlackwareVersion(contents)
|
|
|
|
}
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "debian_version")) {
|
|
|
|
if lsb.ID == "Ubuntu" {
|
|
|
|
platform = "ubuntu"
|
|
|
|
version = lsb.Release
|
|
|
|
} else if lsb.ID == "LinuxMint" {
|
|
|
|
platform = "linuxmint"
|
|
|
|
version = lsb.Release
|
|
|
|
} else if lsb.ID == "Kylin" {
|
|
|
|
platform = "Kylin"
|
|
|
|
version = lsb.Release
|
|
|
|
} else if lsb.ID == `"Cumulus Linux"` {
|
|
|
|
platform = "cumuluslinux"
|
|
|
|
version = lsb.Release
|
|
|
|
} else if lsb.ID == "uos" {
|
|
|
|
platform = "uos"
|
|
|
|
version = lsb.Release
|
|
|
|
} else if lsb.ID == "Deepin" {
|
|
|
|
platform = "Deepin"
|
|
|
|
version = lsb.Release
|
|
|
|
} else {
|
|
|
|
if common.PathExistsWithContents("/usr/bin/raspi-config") {
|
|
|
|
platform = "raspbian"
|
|
|
|
} else {
|
|
|
|
platform = "debian"
|
|
|
|
}
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "debian_version"))
|
|
|
|
if err == nil && len(contents) > 0 && contents[0] != "" {
|
|
|
|
version = contents[0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "neokylin-release")) {
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "neokylin-release"))
|
|
|
|
if err == nil {
|
|
|
|
version = getRedhatishVersion(contents)
|
|
|
|
platform = getRedhatishPlatform(contents)
|
|
|
|
}
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "redhat-release")) {
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "redhat-release"))
|
|
|
|
if err == nil {
|
|
|
|
version = getRedhatishVersion(contents)
|
|
|
|
platform = getRedhatishPlatform(contents)
|
|
|
|
}
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "system-release")) {
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "system-release"))
|
|
|
|
if err == nil {
|
|
|
|
version = getRedhatishVersion(contents)
|
|
|
|
platform = getRedhatishPlatform(contents)
|
|
|
|
}
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "gentoo-release")) {
|
|
|
|
platform = "gentoo"
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "gentoo-release"))
|
|
|
|
if err == nil {
|
|
|
|
version = getRedhatishVersion(contents)
|
|
|
|
}
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "SuSE-release")) {
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "SuSE-release"))
|
|
|
|
if err == nil {
|
|
|
|
version = getSuseVersion(contents)
|
|
|
|
platform = getSusePlatform(contents)
|
|
|
|
}
|
|
|
|
// TODO: slackware detecion
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "arch-release")) {
|
|
|
|
platform = "arch"
|
|
|
|
version = lsb.Release
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "alpine-release")) {
|
|
|
|
platform = "alpine"
|
|
|
|
contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "alpine-release"))
|
|
|
|
if err == nil && len(contents) > 0 && contents[0] != "" {
|
|
|
|
version = contents[0]
|
|
|
|
}
|
|
|
|
} else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "os-release")) {
|
|
|
|
p, v, err := common.GetOSReleaseWithContext(ctx)
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
platform = strings.Trim(platform, `"`)
|
|
|
|
|
|
|
|
switch platform {
|
|
|
|
case "debian", "ubuntu", "linuxmint", "raspbian", "Kylin", "cumuluslinux", "uos", "Deepin":
|
|
|
|
family = "debian"
|
|
|
|
case "fedora":
|
|
|
|
family = "fedora"
|
|
|
|
case "oracle", "centos", "redhat", "scientific", "enterpriseenterprise", "amazon", "xenserver", "cloudlinux", "ibm_powerkvm", "rocky", "almalinux":
|
|
|
|
family = "rhel"
|
|
|
|
case "suse", "opensuse", "opensuse-leap", "opensuse-tumbleweed", "opensuse-tumbleweed-kubic", "sles", "sled", "caasp":
|
|
|
|
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"
|
|
|
|
case "solus":
|
|
|
|
family = "solus"
|
|
|
|
case "neokylin":
|
|
|
|
family = "neokylin"
|
|
|
|
case "anolis":
|
|
|
|
family = "anolis"
|
|
|
|
}
|
|
|
|
|
|
|
|
return platform, family, version, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func KernelVersionWithContext(ctx context.Context) (version string, err error) {
|
|
|
|
var utsname unix.Utsname
|
|
|
|
err = unix.Uname(&utsname)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return unix.ByteSliceToString(utsname.Release[:]), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getSlackwareVersion(contents []string) string {
|
|
|
|
c := strings.ToLower(strings.Join(contents, ""))
|
|
|
|
c = strings.Replace(c, "slackware ", "", 1)
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
var redhatishReleaseMatch = regexp.MustCompile(`release (\w[\d.]*)`)
|
|
|
|
|
|
|
|
func getRedhatishVersion(contents []string) string {
|
|
|
|
c := strings.ToLower(strings.Join(contents, ""))
|
|
|
|
|
|
|
|
if strings.Contains(c, "rawhide") {
|
|
|
|
return "rawhide"
|
|
|
|
}
|
|
|
|
if matches := redhatishReleaseMatch.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]
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
suseVersionMatch = regexp.MustCompile(`VERSION = ([\d.]+)`)
|
|
|
|
susePatchLevelMatch = regexp.MustCompile(`PATCHLEVEL = (\d+)`)
|
|
|
|
)
|
|
|
|
|
|
|
|
func getSuseVersion(contents []string) string {
|
|
|
|
version := ""
|
|
|
|
for _, line := range contents {
|
|
|
|
if matches := suseVersionMatch.FindStringSubmatch(line); matches != nil {
|
|
|
|
version = matches[1]
|
|
|
|
} else if matches = susePatchLevelMatch.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 VirtualizationWithContext(ctx context.Context) (string, string, error) {
|
|
|
|
return common.VirtualizationWithContext(ctx)
|
|
|
|
}
|