providers/linux/os.go (244 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package linux
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/elastic/go-sysinfo/types"
)
const (
osRelease = "/etc/os-release"
lsbRelease = "/etc/lsb-release"
distribRelease = "/etc/*-release"
versionGrok = `(?P<version>(?P<major>[0-9]+)\.?(?P<minor>[0-9]+)?\.?(?P<patch>\w+)?)(?: \((?P<codename>[-\w ]+)\))?`
versionGrokSuse = `(?P<version>(?P<major>[0-9]+)(?:[.-]?(?:SP)?(?P<minor>[0-9]+))?(?:[.-](?P<patch>[0-9]+|\w+))?)(?: \((?P<codename>[-\w ]+)\))?`
)
var (
// distribReleaseRegexp parses the /etc/<distrib>-release file. See man lsb-release.
distribReleaseRegexp = regexp.MustCompile(`(?P<name>[\w]+).* ` + versionGrok)
// versionRegexp parses version numbers (e.g. 6 or 6.1 or 6.1.0 or 6.1.0_20150102).
versionRegexp = regexp.MustCompile(versionGrok)
// versionRegexpSuse parses version numbers for SUSE (e.g. 15-SP1).
versionRegexpSuse = regexp.MustCompile(versionGrokSuse)
)
// familyMap contains a mapping of family -> []platforms.
var familyMap = map[string][]string{
"alpine": {"alpine"},
"arch": {"arch", "antergos", "manjaro"},
"redhat": {
"redhat", "fedora", "centos", "scientific", "oraclelinux", "ol",
"amzn", "rhel", "almalinux", "openeuler", "rocky",
},
"debian": {"debian", "ubuntu", "raspbian", "linuxmint"},
"suse": {"suse", "sles", "opensuse"},
}
var platformToFamilyMap map[string]string
func init() {
platformToFamilyMap = map[string]string{}
for family, platformList := range familyMap {
for _, platform := range platformList {
platformToFamilyMap[platform] = family
}
}
}
// OperatingSystem returns OS info. This does not take an alternate hostfs.
// to get OS info from an alternate root path, use reader.os()
func OperatingSystem() (*types.OSInfo, error) {
return getOSInfo("")
}
func getOSInfo(baseDir string) (*types.OSInfo, error) {
osInfo, err := getOSRelease(baseDir)
if err != nil {
// Fallback
return findDistribRelease(baseDir)
}
// For the redhat family, enrich version info with data from
// /etc/[distrib]-release because the minor and patch info isn't always
// present in os-release.
if osInfo.Family != "redhat" {
return osInfo, nil
}
distInfo, err := findDistribRelease(baseDir)
if err != nil {
return osInfo, err
}
osInfo.Major = distInfo.Major
osInfo.Minor = distInfo.Minor
osInfo.Patch = distInfo.Patch
osInfo.Codename = distInfo.Codename
return osInfo, nil
}
func getOSRelease(baseDir string) (*types.OSInfo, error) {
lsbRel, _ := os.ReadFile(filepath.Join(baseDir, lsbRelease))
osRel, err := os.ReadFile(filepath.Join(baseDir, osRelease))
if err != nil {
return nil, err
}
if len(osRel) == 0 {
return nil, fmt.Errorf("%v is empty: %w", osRelease, err)
}
return parseOSRelease(append(lsbRel, osRel...))
}
func parseOSRelease(content []byte) (*types.OSInfo, error) {
fields := map[string]string{}
s := bufio.NewScanner(bytes.NewReader(content))
for s.Scan() {
line := bytes.TrimSpace(s.Bytes())
// Skip blank lines and comments.
if len(line) == 0 || bytes.HasPrefix(line, []byte("#")) {
continue
}
parts := bytes.SplitN(s.Bytes(), []byte("="), 2)
if len(parts) != 2 {
continue
}
key := string(bytes.TrimSpace(parts[0]))
val := string(bytes.TrimSpace(parts[1]))
fields[key] = val
// Trim quotes.
val, err := strconv.Unquote(val)
if err == nil {
fields[key] = strings.TrimSpace(val)
}
}
if s.Err() != nil {
return nil, s.Err()
}
return makeOSInfo(fields)
}
func makeOSInfo(osRelease map[string]string) (*types.OSInfo, error) {
os := &types.OSInfo{
Type: "linux",
Platform: firstOf(osRelease, "ID", "DISTRIB_ID"),
Name: firstOf(osRelease, "NAME", "PRETTY_NAME"),
Version: firstOf(osRelease, "VERSION", "VERSION_ID", "DISTRIB_RELEASE"),
Build: osRelease["BUILD_ID"],
Codename: firstOf(osRelease, "VERSION_CODENAME", "DISTRIB_CODENAME"),
}
if os.Codename == "" {
// Some OSes use their own CODENAME keys (e.g UBUNTU_CODENAME).
for k, v := range osRelease {
if strings.Contains(k, "CODENAME") {
os.Codename = v
break
}
}
}
if os.Platform == "" {
// Fallback to the first word of the Name field.
os.Platform, _, _ = strings.Cut(os.Name, " ")
}
os.Family = linuxFamily(os.Platform)
if os.Family == "" {
// ID_LIKE is a space-separated list of OS identifiers that this
// OS is similar to. Use this to figure out the Linux family.
for _, id := range strings.Fields(osRelease["ID_LIKE"]) {
os.Family = linuxFamily(id)
if os.Family != "" {
break
}
}
}
if osRelease["ID_LIKE"] == "suse" {
extractVersionDetails(os, os.Version, versionRegexpSuse)
} else if os.Version != "" {
extractVersionDetails(os, os.Version, versionRegexp)
}
return os, nil
}
func extractVersionDetails(os *types.OSInfo, version string, re *regexp.Regexp) {
keys := re.SubexpNames()
for i, match := range re.FindStringSubmatch(version) {
switch keys[i] {
case "major":
os.Major, _ = strconv.Atoi(match)
case "minor":
os.Minor, _ = strconv.Atoi(match)
case "patch":
os.Patch, _ = strconv.Atoi(match)
case "codename":
if os.Codename == "" {
os.Codename = match
}
}
}
}
func findDistribRelease(baseDir string) (*types.OSInfo, error) {
matches, err := filepath.Glob(filepath.Join(baseDir, distribRelease))
if err != nil {
return nil, err
}
var errs []error
for _, path := range matches {
if strings.HasSuffix(path, osRelease) || strings.HasSuffix(path, lsbRelease) {
continue
}
info, err := os.Stat(path)
if err != nil || info.IsDir() || info.Size() == 0 {
continue
}
osInfo, err := getDistribRelease(path)
if err != nil {
errs = append(errs, fmt.Errorf("in %s: %w", path, err))
continue
}
return osInfo, nil
}
return nil, fmt.Errorf("no valid /etc/<distrib>-release file found: %w", errors.Join(errs...))
}
func getDistribRelease(file string) (*types.OSInfo, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
parts := bytes.SplitN(data, []byte("\n"), 2)
if len(parts) != 2 {
return nil, fmt.Errorf("failed to parse %v", file)
}
// Use distrib as platform name.
var platform string
if parts := strings.SplitN(filepath.Base(file), "-", 2); len(parts) > 0 {
platform = strings.ToLower(parts[0])
}
return parseDistribRelease(platform, parts[0])
}
func parseDistribRelease(platform string, content []byte) (*types.OSInfo, error) {
var (
line = string(bytes.TrimSpace(content))
keys = distribReleaseRegexp.SubexpNames()
os = &types.OSInfo{
Type: "linux",
Platform: platform,
}
)
for i, m := range distribReleaseRegexp.FindStringSubmatch(line) {
switch keys[i] {
case "name":
os.Name = m
case "version":
os.Version = m
case "major":
os.Major, _ = strconv.Atoi(m)
case "minor":
os.Minor, _ = strconv.Atoi(m)
case "patch":
os.Patch, _ = strconv.Atoi(m)
case "codename":
os.Version += " (" + m + ")"
os.Codename = m
}
}
os.Family = linuxFamily(os.Platform)
return os, nil
}
// firstOf returns the first non-empty value found in the map while
// iterating over keys.
func firstOf(kv map[string]string, keys ...string) string {
for _, key := range keys {
if v := kv[key]; v != "" {
return v
}
}
return ""
}
// linuxFamily returns the linux distribution family associated to the OS platform.
// If there is no family associated then it returns an empty string.
func linuxFamily(platform string) string {
if platform == "" {
return ""
}
platform = strings.ToLower(platform)
// First try a direct lookup.
if family, found := platformToFamilyMap[platform]; found {
return family
}
// Try prefix matching (e.g. opensuse matches opensuse-tumpleweed).
for platformPrefix, family := range platformToFamilyMap {
if strings.HasPrefix(platform, platformPrefix) {
return family
}
}
return ""
}