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 }