From d363a1881ec256e1be5a48a90046ff16e84ddc02 Mon Sep 17 00:00:00 2001 From: Jean Rouge Date: Thu, 30 May 2019 09:51:41 -0700 Subject: [PATCH] Adding OS version info to the nodes' `Info` struct This is needed so that we can add OS version constraints in Swarmkit, which does require the engine to report its host's OS version (see https://github.com/docker/swarmkit/issues/2770). The OS version is parsed from the `os-release` file on Linux, and from the `ReleaseId` string value of the `SOFTWARE\Microsoft\Windows NT\CurrentVersion` registry key on Windows. Added unit tests when possible, as well as Prometheus metrics. Signed-off-by: Jean Rouge --- api/swagger.yaml | 11 ++ api/types/types.go | 1 + daemon/daemon.go | 1 + daemon/info.go | 23 ++- daemon/metrics.go | 6 +- .../operatingsystem/operatingsystem_linux.go | 41 +++-- ..._test.go => operatingsystem_linux_test.go} | 146 +++++++++++------- .../operatingsystem/operatingsystem_unix.go | 7 + .../operatingsystem_windows.go | 66 ++++---- 9 files changed, 207 insertions(+), 95 deletions(-) rename pkg/parsers/operatingsystem/{operatingsystem_unix_test.go => operatingsystem_linux_test.go} (64%) diff --git a/api/swagger.yaml b/api/swagger.yaml index c85c4ba9d0..dd431d2b23 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -3840,6 +3840,17 @@ definitions: or "Windows Server 2016 Datacenter" type: "string" example: "Alpine Linux v3.5" + OSVersion: + description: | + Version of the host's operating system + +


+ + > **Note**: The information returned in this field, including its + > very existence, and the formatting of values, should not be considered + > stable, and may change without notice. + type: "string" + example: "16.04" OSType: description: | Generic type of the operating system of the host, as returned by the diff --git a/api/types/types.go b/api/types/types.go index a39ffcb7be..b13d9c4c7d 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -177,6 +177,7 @@ type Info struct { NEventsListener int KernelVersion string OperatingSystem string + OSVersion string OSType string Architecture string IndexServerAddress string diff --git a/daemon/daemon.go b/daemon/daemon.go index e6a1fb1456..2c5624b4ca 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -1061,6 +1061,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S info.KernelVersion, info.OperatingSystem, info.OSType, + info.OSVersion, info.ID, ).Set(1) engineCpus.Set(float64(info.NCPU)) diff --git a/daemon/info.go b/daemon/info.go index 173cf96547..bfd8199edb 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -21,11 +21,14 @@ import ( "github.com/docker/docker/pkg/system" "github.com/docker/docker/registry" "github.com/docker/go-connections/sockets" + "github.com/docker/go-metrics" "github.com/sirupsen/logrus" ) // SystemInfo returns information about the host server the daemon is running on. func (daemon *Daemon) SystemInfo() (*types.Info, error) { + defer metrics.StartTimer(hostInfoFunctions.WithValues("system_info"))() + sysInfo := sysinfo.New(true) cRunning, cPaused, cStopped := stateCtr.get() @@ -49,6 +52,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) { NEventsListener: daemon.EventsService.SubscribersCount(), KernelVersion: kernelVersion(), OperatingSystem: operatingSystem(), + OSVersion: osVersion(), IndexServerAddress: registry.IndexServer, OSType: platform.OSType, Architecture: platform.Architecture, @@ -82,6 +86,8 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) { // SystemVersion returns version information about the daemon. func (daemon *Daemon) SystemVersion() types.Version { + defer metrics.StartTimer(hostInfoFunctions.WithValues("system_version"))() + kernelVersion := kernelVersion() v := types.Version{ @@ -240,8 +246,9 @@ func memInfo() *system.MemInfo { return memInfo } -func operatingSystem() string { - var operatingSystem string +func operatingSystem() (operatingSystem string) { + defer metrics.StartTimer(hostInfoFunctions.WithValues("operating_system"))() + if s, err := operatingsystem.GetOperatingSystem(); err != nil { logrus.Warnf("Could not get operating system name: %v", err) } else { @@ -256,9 +263,21 @@ func operatingSystem() string { operatingSystem += " (containerized)" } } + return operatingSystem } +func osVersion() (version string) { + defer metrics.StartTimer(hostInfoFunctions.WithValues("os_version"))() + + version, err := operatingsystem.GetOperatingSystemVersion() + if err != nil { + logrus.Warnf("Could not get operating system version: %v", err) + } + + return version +} + func maskCredentials(rawURL string) string { parsedURL, err := url.Parse(rawURL) if err != nil || parsedURL.User == nil { diff --git a/daemon/metrics.go b/daemon/metrics.go index ec633d6cd7..f2548b6dfa 100644 --- a/daemon/metrics.go +++ b/daemon/metrics.go @@ -17,6 +17,7 @@ const metricsPluginType = "MetricsCollector" var ( containerActions metrics.LabeledTimer networkActions metrics.LabeledTimer + hostInfoFunctions metrics.LabeledTimer engineInfo metrics.LabeledGauge engineCpus metrics.Gauge engineMemory metrics.Gauge @@ -38,6 +39,7 @@ func init() { } { containerActions.WithValues(a).Update(0) } + hostInfoFunctions = ns.NewLabeledTimer("host_info_functions", "The number of seconds it takes to call functions gathering info about the host", "function") networkActions = ns.NewLabeledTimer("network_actions", "The number of seconds it takes to process each network action", "action") engineInfo = ns.NewLabeledGauge("engine", "The information related to the engine and the OS it is running on", metrics.Unit("info"), @@ -45,8 +47,10 @@ func init() { "commit", "architecture", "graphdriver", - "kernel", "os", + "kernel", + "os", "os_type", + "os_version", "daemon_id", // ID is a randomly generated unique identifier (e.g. UUID4) ) engineCpus = ns.NewGauge("engine_cpus", "The number of cpus that the host system of the engine has", metrics.Unit("cpus")) diff --git a/pkg/parsers/operatingsystem/operatingsystem_linux.go b/pkg/parsers/operatingsystem/operatingsystem_linux.go index b251d6aed6..2fefcabf3d 100644 --- a/pkg/parsers/operatingsystem/operatingsystem_linux.go +++ b/pkg/parsers/operatingsystem/operatingsystem_linux.go @@ -26,6 +26,24 @@ var ( // GetOperatingSystem gets the name of the current operating system. func GetOperatingSystem() (string, error) { + if prettyName, err := getValueFromOsRelease("PRETTY_NAME"); err != nil { + return "", err + } else if prettyName != "" { + return prettyName, nil + } + + // If not set, defaults to PRETTY_NAME="Linux" + // c.f. http://www.freedesktop.org/software/systemd/man/os-release.html + return "Linux", nil +} + +// GetOperatingSystemVersion gets the version of the current operating system, as a string. +func GetOperatingSystemVersion() (string, error) { + return getValueFromOsRelease("VERSION_ID") +} + +// parses the os-release file and returns the value associated with `key` +func getValueFromOsRelease(key string) (string, error) { osReleaseFile, err := os.Open(etcOsRelease) if err != nil { if !os.IsNotExist(err) { @@ -38,28 +56,25 @@ func GetOperatingSystem() (string, error) { } defer osReleaseFile.Close() - var prettyName string + var value string + keyWithTrailingEqual := key + "=" scanner := bufio.NewScanner(osReleaseFile) for scanner.Scan() { line := scanner.Text() - if strings.HasPrefix(line, "PRETTY_NAME=") { + if strings.HasPrefix(line, keyWithTrailingEqual) { data := strings.SplitN(line, "=", 2) - prettyNames, err := shellwords.Parse(data[1]) + values, err := shellwords.Parse(data[1]) if err != nil { - return "", fmt.Errorf("PRETTY_NAME is invalid: %s", err.Error()) + return "", fmt.Errorf("%s is invalid: %s", key, err.Error()) } - if len(prettyNames) != 1 { - return "", fmt.Errorf("PRETTY_NAME needs to be enclosed by quotes if they have spaces: %s", data[1]) + if len(values) != 1 { + return "", fmt.Errorf("%s needs to be enclosed by quotes if they have spaces: %s", key, data[1]) } - prettyName = prettyNames[0] + value = values[0] } } - if prettyName != "" { - return prettyName, nil - } - // If not set, defaults to PRETTY_NAME="Linux" - // c.f. http://www.freedesktop.org/software/systemd/man/os-release.html - return "Linux", nil + + return value, nil } // IsContainerized returns true if we are running inside a container. diff --git a/pkg/parsers/operatingsystem/operatingsystem_unix_test.go b/pkg/parsers/operatingsystem/operatingsystem_linux_test.go similarity index 64% rename from pkg/parsers/operatingsystem/operatingsystem_unix_test.go rename to pkg/parsers/operatingsystem/operatingsystem_linux_test.go index d10ed4cdcd..ec9b05c08e 100644 --- a/pkg/parsers/operatingsystem/operatingsystem_unix_test.go +++ b/pkg/parsers/operatingsystem/operatingsystem_linux_test.go @@ -7,43 +7,41 @@ import ( "os" "path/filepath" "testing" + + "gotest.tools/assert" ) +type EtcReleaseParsingTest struct { + name string + content string + expected string + expectedErr string +} + func TestGetOperatingSystem(t *testing.T) { - var backup = etcOsRelease - - invalids := []struct { - content string - errorExpected string - }{ + tests := []EtcReleaseParsingTest{ { - `PRETTY_NAME=Source Mage GNU/Linux + content: `PRETTY_NAME=Source Mage GNU/Linux PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Source Mage GNU/Linux", + expectedErr: "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Source Mage GNU/Linux", }, { - `PRETTY_NAME="Ubuntu Linux + content: `PRETTY_NAME="Ubuntu Linux PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME is invalid: invalid command line string", + expectedErr: "PRETTY_NAME is invalid: invalid command line string", }, { - `PRETTY_NAME=Ubuntu' + content: `PRETTY_NAME=Ubuntu' PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME is invalid: invalid command line string", + expectedErr: "PRETTY_NAME is invalid: invalid command line string", }, { - `PRETTY_NAME' + content: `PRETTY_NAME' PRETTY_NAME=Ubuntu 14.04.LTS`, - "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS", + expectedErr: "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS", }, - } - - valids := []struct { - content string - expected string - }{ { - `NAME="Ubuntu" + content: `NAME="Ubuntu" PRETTY_NAME_AGAIN="Ubuntu 14.04.LTS" VERSION="14.04, Trusty Tahr" ID=ubuntu @@ -52,10 +50,10 @@ VERSION_ID="14.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, - "Linux", + expected: "Linux", }, { - `NAME="Ubuntu" + content: `NAME="Ubuntu" VERSION="14.04, Trusty Tahr" ID=ubuntu ID_LIKE=debian @@ -63,10 +61,10 @@ VERSION_ID="14.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, - "Linux", + expected: "Linux", }, { - `NAME=Gentoo + content: `NAME=Gentoo ID=gentoo PRETTY_NAME="Gentoo/Linux" ANSI_COLOR="1;32" @@ -74,10 +72,10 @@ HOME_URL="http://www.gentoo.org/" SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" BUG_REPORT_URL="https://bugs.gentoo.org/" `, - "Gentoo/Linux", + expected: "Gentoo/Linux", }, { - `NAME="Ubuntu" + content: `NAME="Ubuntu" VERSION="14.04, Trusty Tahr" ID=ubuntu ID_LIKE=debian @@ -86,28 +84,77 @@ VERSION_ID="14.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, - "Ubuntu 14.04 LTS", + expected: "Ubuntu 14.04 LTS", }, { - `NAME="Ubuntu" + content: `NAME="Ubuntu" VERSION="14.04, Trusty Tahr" ID=ubuntu ID_LIKE=debian PRETTY_NAME='Ubuntu 14.04 LTS'`, - "Ubuntu 14.04 LTS", + expected: "Ubuntu 14.04 LTS", }, { - `PRETTY_NAME=Source + content: `PRETTY_NAME=Source NAME="Source Mage"`, - "Source", + expected: "Source", }, { - `PRETTY_NAME=Source + content: `PRETTY_NAME=Source PRETTY_NAME="Source Mage"`, - "Source Mage", + expected: "Source Mage", }, } + runEtcReleaseParsingTests(t, tests, GetOperatingSystem) +} + +func TestGetOperatingSystemVersion(t *testing.T) { + tests := []EtcReleaseParsingTest{ + { + name: "invalid version id", + content: `VERSION_ID="18.04 +VERSION_ID=18.04`, + expectedErr: "VERSION_ID is invalid: invalid command line string", + }, + { + name: "ubuntu 14.04", + content: `NAME="Ubuntu" +PRETTY_NAME="Ubuntu 14.04.LTS" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +VERSION_ID="14.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, + expected: "14.04", + }, + { + name: "gentoo", + content: `NAME=Gentoo +ID=gentoo +PRETTY_NAME="Gentoo/Linux" +ANSI_COLOR="1;32" +HOME_URL="http://www.gentoo.org/" +SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" +BUG_REPORT_URL="https://bugs.gentoo.org/" +`, + }, + { + name: "dual version id", + content: `VERSION_ID="14.04" +VERSION_ID=18.04`, + expected: "18.04", + }, + } + + runEtcReleaseParsingTests(t, tests, GetOperatingSystemVersion) +} + +func runEtcReleaseParsingTests(t *testing.T, tests []EtcReleaseParsingTest, parsingFunc func() (string, error)) { + var backup = etcOsRelease + dir := os.TempDir() etcOsRelease = filepath.Join(dir, "etcOsRelease") @@ -116,24 +163,19 @@ PRETTY_NAME="Source Mage"`, etcOsRelease = backup }() - for _, elt := range invalids { - if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil { - t.Fatalf("failed to write to %s: %v", etcOsRelease, err) - } - s, err := GetOperatingSystem() - if err == nil || err.Error() != elt.errorExpected { - t.Fatalf("Expected an error %q, got %q (err: %v)", elt.errorExpected, s, err) - } - } - - for _, elt := range valids { - if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil { - t.Fatalf("failed to write to %s: %v", etcOsRelease, err) - } - s, err := GetOperatingSystem() - if err != nil || s != elt.expected { - t.Fatalf("Expected %q, got %q (err: %v)", elt.expected, s, err) - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := ioutil.WriteFile(etcOsRelease, []byte(test.content), 0600); err != nil { + t.Fatalf("failed to write to %s: %v", etcOsRelease, err) + } + s, err := parsingFunc() + if test.expectedErr == "" { + assert.NilError(t, err) + } else { + assert.Error(t, err, test.expectedErr) + } + assert.Equal(t, s, test.expected) + }) } } diff --git a/pkg/parsers/operatingsystem/operatingsystem_unix.go b/pkg/parsers/operatingsystem/operatingsystem_unix.go index f4792d37d5..fcc693da27 100644 --- a/pkg/parsers/operatingsystem/operatingsystem_unix.go +++ b/pkg/parsers/operatingsystem/operatingsystem_unix.go @@ -4,6 +4,7 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin import ( "errors" + "fmt" "os/exec" ) @@ -17,6 +18,12 @@ func GetOperatingSystem() (string, error) { return string(osName), nil } +// GetOperatingSystemVersion gets the version of the current operating system, as a string. +func GetOperatingSystemVersion() (string, error) { + // there's no standard unix way of getting this, sadly... + return "", fmt.Error("Unsupported on generic unix") +} + // IsContainerized returns true if we are running inside a container. // No-op on FreeBSD and Darwin, always returns false. func IsContainerized() (bool, error) { diff --git a/pkg/parsers/operatingsystem/operatingsystem_windows.go b/pkg/parsers/operatingsystem/operatingsystem_windows.go index 372de51469..a05bc76621 100644 --- a/pkg/parsers/operatingsystem/operatingsystem_windows.go +++ b/pkg/parsers/operatingsystem/operatingsystem_windows.go @@ -3,45 +3,57 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin import ( "fmt" + "github.com/docker/docker/pkg/system" "golang.org/x/sys/windows/registry" ) // GetOperatingSystem gets the name of the current operating system. func GetOperatingSystem() (string, error) { + os, err := withCurrentVersionRegistryKey(func(key registry.Key) (os string, err error) { + if os, _, err = key.GetStringValue("ProductName"); err != nil { + return "", err + } - // Default return value - ret := "Unknown Operating System" + releaseId, _, err := key.GetStringValue("ReleaseId") + if err != nil { + return + } + os = fmt.Sprintf("%s Version %s", os, releaseId) - k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) - if err != nil { - return ret, err - } - defer k.Close() + buildNumber, _, err := key.GetStringValue("CurrentBuildNumber") + if err != nil { + return + } + ubr, _, err := key.GetIntegerValue("UBR") + if err != nil { + return + } + os = fmt.Sprintf("%s (OS Build %s.%d)", os, buildNumber, ubr) - pn, _, err := k.GetStringValue("ProductName") - if err != nil { - return ret, err - } - ret = pn + return + }) - ri, _, err := k.GetStringValue("ReleaseId") - if err != nil { - return ret, err - } - ret = fmt.Sprintf("%s Version %s", ret, ri) - - cbn, _, err := k.GetStringValue("CurrentBuildNumber") - if err != nil { - return ret, err + if os == "" { + // Default return value + os = "Unknown Operating System" } - ubr, _, err := k.GetIntegerValue("UBR") - if err != nil { - return ret, err - } - ret = fmt.Sprintf("%s (OS Build %s.%d)", ret, cbn, ubr) + return os, err +} - return ret, nil +func withCurrentVersionRegistryKey(f func(registry.Key) (string, error)) (string, error) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return "", err + } + defer key.Close() + return f(key) +} + +// GetOperatingSystemVersion gets the version of the current operating system, as a string. +func GetOperatingSystemVersion() (string, error) { + version := system.GetOSVersion() + return fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build), nil } // IsContainerized returns true if we are running inside a container.