github.com/isyscore/isc-gobase@v1.5.3-0.20231218061332-cbc7451899e9/system/host/host_linux.go (about)

     1  //go:build linux
     2  
     3  package host
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"encoding/binary"
     9  	"fmt"
    10  	"github.com/isyscore/isc-gobase/system/common"
    11  	"golang.org/x/sys/unix"
    12  	"io/ioutil"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"regexp"
    17  	"strconv"
    18  	"strings"
    19  )
    20  
    21  type LSB struct {
    22  	ID          string
    23  	Release     string
    24  	Codename    string
    25  	Description string
    26  }
    27  
    28  // from utmp.h
    29  const USER_PROCESS = 7
    30  
    31  func HostIDWithContext(ctx context.Context) (string, error) {
    32  	sysProductUUID := common.HostSys("class/dmi/id/product_uuid")
    33  	machineID := common.HostEtc("machine-id")
    34  	procSysKernelRandomBootID := common.HostProc("sys/kernel/random/boot_id")
    35  	switch {
    36  	// In order to read this file, needs to be supported by kernel/arch and run as root
    37  	// so having fallback is important
    38  	case common.PathExists(sysProductUUID):
    39  		lines, err := common.ReadLines(sysProductUUID)
    40  		if err == nil && len(lines) > 0 && lines[0] != "" {
    41  			return strings.ToLower(lines[0]), nil
    42  		}
    43  		fallthrough
    44  	// Fallback on GNU Linux systems with systemd, readable by everyone
    45  	case common.PathExists(machineID):
    46  		lines, err := common.ReadLines(machineID)
    47  		if err == nil && len(lines) > 0 && len(lines[0]) == 32 {
    48  			st := lines[0]
    49  			return fmt.Sprintf("%s-%s-%s-%s-%s", st[0:8], st[8:12], st[12:16], st[16:20], st[20:32]), nil
    50  		}
    51  		fallthrough
    52  	// Not stable between reboot, but better than nothing
    53  	default:
    54  		lines, err := common.ReadLines(procSysKernelRandomBootID)
    55  		if err == nil && len(lines) > 0 && lines[0] != "" {
    56  			return strings.ToLower(lines[0]), nil
    57  		}
    58  	}
    59  
    60  	return "", nil
    61  }
    62  
    63  func numProcs(ctx context.Context) (uint64, error) {
    64  	return common.NumProcs()
    65  }
    66  
    67  func BootTimeWithContext(ctx context.Context) (uint64, error) {
    68  	return common.BootTimeWithContext(ctx)
    69  }
    70  
    71  func UptimeWithContext(ctx context.Context) (uint64, error) {
    72  	sysinfo := &unix.Sysinfo_t{}
    73  	if err := unix.Sysinfo(sysinfo); err != nil {
    74  		return 0, err
    75  	}
    76  	return uint64(sysinfo.Uptime), nil
    77  }
    78  
    79  func UsersWithContext(ctx context.Context) ([]UserStat, error) {
    80  	utmpfile := common.HostVar("run/utmp")
    81  
    82  	file, err := os.Open(utmpfile)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	defer file.Close()
    87  
    88  	buf, err := ioutil.ReadAll(file)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	count := len(buf) / sizeOfUtmp
    94  
    95  	ret := make([]UserStat, 0, count)
    96  
    97  	for i := 0; i < count; i++ {
    98  		b := buf[i*sizeOfUtmp : (i+1)*sizeOfUtmp]
    99  
   100  		var u utmp
   101  		br := bytes.NewReader(b)
   102  		err := binary.Read(br, binary.LittleEndian, &u)
   103  		if err != nil {
   104  			continue
   105  		}
   106  		if u.Type != USER_PROCESS {
   107  			continue
   108  		}
   109  		user := UserStat{
   110  			User:     common.IntToString(u.User[:]),
   111  			Terminal: common.IntToString(u.Line[:]),
   112  			Host:     common.IntToString(u.Host[:]),
   113  			Started:  int(u.Tv.Sec),
   114  		}
   115  		ret = append(ret, user)
   116  	}
   117  
   118  	return ret, nil
   119  
   120  }
   121  
   122  func getLSB() (*LSB, error) {
   123  	ret := &LSB{}
   124  	if common.PathExists(common.HostEtc("lsb-release")) {
   125  		contents, err := common.ReadLines(common.HostEtc("lsb-release"))
   126  		if err != nil {
   127  			return ret, err // return empty
   128  		}
   129  		for _, line := range contents {
   130  			field := strings.Split(line, "=")
   131  			if len(field) < 2 {
   132  				continue
   133  			}
   134  			switch field[0] {
   135  			case "DISTRIB_ID":
   136  				ret.ID = field[1]
   137  			case "DISTRIB_RELEASE":
   138  				ret.Release = field[1]
   139  			case "DISTRIB_CODENAME":
   140  				ret.Codename = field[1]
   141  			case "DISTRIB_DESCRIPTION":
   142  				ret.Description = field[1]
   143  			}
   144  		}
   145  	} else if common.PathExists("/usr/bin/lsb_release") {
   146  		lsb_release, err := exec.LookPath("lsb_release")
   147  		if err != nil {
   148  			return ret, err
   149  		}
   150  		out, err := invoke.Command(lsb_release)
   151  		if err != nil {
   152  			return ret, err
   153  		}
   154  		for _, line := range strings.Split(string(out), "\n") {
   155  			field := strings.Split(line, ":")
   156  			if len(field) < 2 {
   157  				continue
   158  			}
   159  			switch field[0] {
   160  			case "Distributor ID":
   161  				ret.ID = field[1]
   162  			case "Release":
   163  				ret.Release = field[1]
   164  			case "Codename":
   165  				ret.Codename = field[1]
   166  			case "Description":
   167  				ret.Description = field[1]
   168  			}
   169  		}
   170  
   171  	}
   172  
   173  	return ret, nil
   174  }
   175  
   176  func PlatformInformationWithContext(ctx context.Context) (platform string, family string, version string, err error) {
   177  	lsb, err := getLSB()
   178  	if err != nil {
   179  		lsb = &LSB{}
   180  	}
   181  
   182  	if common.PathExists(common.HostEtc("oracle-release")) {
   183  		platform = "oracle"
   184  		contents, err := common.ReadLines(common.HostEtc("oracle-release"))
   185  		if err == nil {
   186  			version = getRedhatishVersion(contents)
   187  		}
   188  
   189  	} else if common.PathExists(common.HostEtc("enterprise-release")) {
   190  		platform = "oracle"
   191  		contents, err := common.ReadLines(common.HostEtc("enterprise-release"))
   192  		if err == nil {
   193  			version = getRedhatishVersion(contents)
   194  		}
   195  	} else if common.PathExists(common.HostEtc("slackware-version")) {
   196  		platform = "slackware"
   197  		contents, err := common.ReadLines(common.HostEtc("slackware-version"))
   198  		if err == nil {
   199  			version = getSlackwareVersion(contents)
   200  		}
   201  	} else if common.PathExists(common.HostEtc("debian_version")) {
   202  		if lsb.ID == "Ubuntu" {
   203  			platform = "ubuntu"
   204  			version = lsb.Release
   205  		} else if lsb.ID == "LinuxMint" {
   206  			platform = "linuxmint"
   207  			version = lsb.Release
   208  		} else {
   209  			if common.PathExists("/usr/bin/raspi-config") {
   210  				platform = "raspbian"
   211  			} else {
   212  				platform = "debian"
   213  			}
   214  			contents, err := common.ReadLines(common.HostEtc("debian_version"))
   215  			if err == nil && len(contents) > 0 && contents[0] != "" {
   216  				version = contents[0]
   217  			}
   218  		}
   219  	} else if common.PathExists(common.HostEtc("redhat-release")) {
   220  		contents, err := common.ReadLines(common.HostEtc("redhat-release"))
   221  		if err == nil {
   222  			version = getRedhatishVersion(contents)
   223  			platform = getRedhatishPlatform(contents)
   224  		}
   225  	} else if common.PathExists(common.HostEtc("system-release")) {
   226  		contents, err := common.ReadLines(common.HostEtc("system-release"))
   227  		if err == nil {
   228  			version = getRedhatishVersion(contents)
   229  			platform = getRedhatishPlatform(contents)
   230  		}
   231  	} else if common.PathExists(common.HostEtc("gentoo-release")) {
   232  		platform = "gentoo"
   233  		contents, err := common.ReadLines(common.HostEtc("gentoo-release"))
   234  		if err == nil {
   235  			version = getRedhatishVersion(contents)
   236  		}
   237  	} else if common.PathExists(common.HostEtc("SuSE-release")) {
   238  		contents, err := common.ReadLines(common.HostEtc("SuSE-release"))
   239  		if err == nil {
   240  			version = getSuseVersion(contents)
   241  			platform = getSusePlatform(contents)
   242  		}
   243  	} else if common.PathExists(common.HostEtc("arch-release")) {
   244  		platform = "arch"
   245  		version = lsb.Release
   246  	} else if common.PathExists(common.HostEtc("alpine-release")) {
   247  		platform = "alpine"
   248  		contents, err := common.ReadLines(common.HostEtc("alpine-release"))
   249  		if err == nil && len(contents) > 0 && contents[0] != "" {
   250  			version = contents[0]
   251  		}
   252  	} else if common.PathExists(common.HostEtc("os-release")) {
   253  		p, v, err := common.GetOSRelease()
   254  		if err == nil {
   255  			platform = p
   256  			version = v
   257  		}
   258  	} else if lsb.ID == "RedHat" {
   259  		platform = "redhat"
   260  		version = lsb.Release
   261  	} else if lsb.ID == "Amazon" {
   262  		platform = "amazon"
   263  		version = lsb.Release
   264  	} else if lsb.ID == "ScientificSL" {
   265  		platform = "scientific"
   266  		version = lsb.Release
   267  	} else if lsb.ID == "XenServer" {
   268  		platform = "xenserver"
   269  		version = lsb.Release
   270  	} else if lsb.ID != "" {
   271  		platform = strings.ToLower(lsb.ID)
   272  		version = lsb.Release
   273  	}
   274  
   275  	switch platform {
   276  	case "debian", "ubuntu", "linuxmint", "raspbian":
   277  		family = "debian"
   278  	case "fedora":
   279  		family = "fedora"
   280  	case "oracle", "centos", "redhat", "scientific", "enterpriseenterprise", "amazon", "xenserver", "cloudlinux", "ibm_powerkvm", "rocky":
   281  		family = "rhel"
   282  	case "suse", "opensuse", "opensuse-leap", "opensuse-tumbleweed", "opensuse-tumbleweed-kubic", "sles", "sled", "caasp":
   283  		family = "suse"
   284  	case "gentoo":
   285  		family = "gentoo"
   286  	case "slackware":
   287  		family = "slackware"
   288  	case "arch":
   289  		family = "arch"
   290  	case "exherbo":
   291  		family = "exherbo"
   292  	case "alpine":
   293  		family = "alpine"
   294  	case "coreos":
   295  		family = "coreos"
   296  	case "solus":
   297  		family = "solus"
   298  	}
   299  
   300  	return platform, family, version, nil
   301  
   302  }
   303  
   304  func KernelVersionWithContext(ctx context.Context) (version string, err error) {
   305  	var utsname unix.Utsname
   306  	err = unix.Uname(&utsname)
   307  	if err != nil {
   308  		return "", err
   309  	}
   310  	return string(utsname.Release[:bytes.IndexByte(utsname.Release[:], 0)]), nil
   311  }
   312  
   313  func getSlackwareVersion(contents []string) string {
   314  	c := strings.ToLower(strings.Join(contents, ""))
   315  	c = strings.Replace(c, "slackware ", "", 1)
   316  	return c
   317  }
   318  
   319  func getRedhatishVersion(contents []string) string {
   320  	c := strings.ToLower(strings.Join(contents, ""))
   321  
   322  	if strings.Contains(c, "rawhide") {
   323  		return "rawhide"
   324  	}
   325  	if matches := regexp.MustCompile(`release (\d[\d.]*)`).FindStringSubmatch(c); matches != nil {
   326  		return matches[1]
   327  	}
   328  	return ""
   329  }
   330  
   331  func getRedhatishPlatform(contents []string) string {
   332  	c := strings.ToLower(strings.Join(contents, ""))
   333  
   334  	if strings.Contains(c, "red hat") {
   335  		return "redhat"
   336  	}
   337  	f := strings.Split(c, " ")
   338  
   339  	return f[0]
   340  }
   341  
   342  func getSuseVersion(contents []string) string {
   343  	version := ""
   344  	for _, line := range contents {
   345  		if matches := regexp.MustCompile(`VERSION = ([\d.]+)`).FindStringSubmatch(line); matches != nil {
   346  			version = matches[1]
   347  		} else if matches := regexp.MustCompile(`PATCHLEVEL = ([\d]+)`).FindStringSubmatch(line); matches != nil {
   348  			version = version + "." + matches[1]
   349  		}
   350  	}
   351  	return version
   352  }
   353  
   354  func getSusePlatform(contents []string) string {
   355  	c := strings.ToLower(strings.Join(contents, ""))
   356  	if strings.Contains(c, "opensuse") {
   357  		return "opensuse"
   358  	}
   359  	return "suse"
   360  }
   361  
   362  func VirtualizationWithContext(ctx context.Context) (string, string, error) {
   363  	return common.VirtualizationWithContext(ctx)
   364  }
   365  
   366  func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) {
   367  	var temperatures []TemperatureStat
   368  	files, err := filepath.Glob(common.HostSys("/class/hwmon/hwmon*/temp*_*"))
   369  	if err != nil {
   370  		return temperatures, err
   371  	}
   372  	if len(files) == 0 {
   373  		// CentOS has an intermediate /device directory:
   374  		// https://github.com/giampaolo/psutil/issues/971
   375  		files, err = filepath.Glob(common.HostSys("/class/hwmon/hwmon*/device/temp*_*"))
   376  		if err != nil {
   377  			return temperatures, err
   378  		}
   379  	}
   380  	var warns Warnings
   381  
   382  	if len(files) == 0 { // handle distributions without hwmon, like raspbian #391, parse legacy thermal_zone files
   383  		files, err = filepath.Glob(common.HostSys("/class/thermal/thermal_zone*/"))
   384  		if err != nil {
   385  			return temperatures, err
   386  		}
   387  		for _, file := range files {
   388  			// Get the name of the temperature you are reading
   389  			name, err := ioutil.ReadFile(filepath.Join(file, "type"))
   390  			if err != nil {
   391  				warns.Add(err)
   392  				continue
   393  			}
   394  			// Get the temperature reading
   395  			current, err := ioutil.ReadFile(filepath.Join(file, "temp"))
   396  			if err != nil {
   397  				warns.Add(err)
   398  				continue
   399  			}
   400  			temperature, err := strconv.ParseInt(strings.TrimSpace(string(current)), 10, 64)
   401  			if err != nil {
   402  				warns.Add(err)
   403  				continue
   404  			}
   405  
   406  			temperatures = append(temperatures, TemperatureStat{
   407  				SensorKey:   strings.TrimSpace(string(name)),
   408  				Temperature: float64(temperature) / 1000.0,
   409  			})
   410  		}
   411  		return temperatures, warns.Reference()
   412  	}
   413  
   414  	// example directory
   415  	// device/           temp1_crit_alarm  temp2_crit_alarm  temp3_crit_alarm  temp4_crit_alarm  temp5_crit_alarm  temp6_crit_alarm  temp7_crit_alarm
   416  	// name              temp1_input       temp2_input       temp3_input       temp4_input       temp5_input       temp6_input       temp7_input
   417  	// power/            temp1_label       temp2_label       temp3_label       temp4_label       temp5_label       temp6_label       temp7_label
   418  	// subsystem/        temp1_max         temp2_max         temp3_max         temp4_max         temp5_max         temp6_max         temp7_max
   419  	// temp1_crit        temp2_crit        temp3_crit        temp4_crit        temp5_crit        temp6_crit        temp7_crit        uevent
   420  	for _, file := range files {
   421  		filename := strings.Split(filepath.Base(file), "_")
   422  		if filename[1] == "label" {
   423  			// Do not try to read the temperature of the label file
   424  			continue
   425  		}
   426  
   427  		// Get the label of the temperature you are reading
   428  		var label string
   429  		c, _ := ioutil.ReadFile(filepath.Join(filepath.Dir(file), filename[0]+"_label"))
   430  		if c != nil {
   431  			//format the label from "Core 0" to "core0_"
   432  			label = fmt.Sprintf("%s_", strings.Join(strings.Split(strings.TrimSpace(strings.ToLower(string(c))), " "), ""))
   433  		}
   434  
   435  		// Get the name of the temperature you are reading
   436  		name, err := ioutil.ReadFile(filepath.Join(filepath.Dir(file), "name"))
   437  		if err != nil {
   438  			warns.Add(err)
   439  			continue
   440  		}
   441  
   442  		// Get the temperature reading
   443  		current, err := ioutil.ReadFile(file)
   444  		if err != nil {
   445  			warns.Add(err)
   446  			continue
   447  		}
   448  		temperature, err := strconv.ParseFloat(strings.TrimSpace(string(current)), 64)
   449  		if err != nil {
   450  			warns.Add(err)
   451  			continue
   452  		}
   453  
   454  		tempName := strings.TrimSpace(strings.ToLower(string(strings.Join(filename[1:], ""))))
   455  		temperatures = append(temperatures, TemperatureStat{
   456  			SensorKey:   fmt.Sprintf("%s_%s%s", strings.TrimSpace(string(name)), label, tempName),
   457  			Temperature: temperature / 1000.0,
   458  		})
   459  	}
   460  	return temperatures, warns.Reference()
   461  }