1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

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 <rougej+github@gmail.com>
This commit is contained in:
Jean Rouge 2019-05-30 09:51:41 -07:00 committed by root
parent 8d760280a2
commit d363a1881e
9 changed files with 207 additions and 95 deletions

View file

@ -3840,6 +3840,17 @@ definitions:
or "Windows Server 2016 Datacenter" or "Windows Server 2016 Datacenter"
type: "string" type: "string"
example: "Alpine Linux v3.5" example: "Alpine Linux v3.5"
OSVersion:
description: |
Version of the host's operating system
<p><br /></p>
> **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: OSType:
description: | description: |
Generic type of the operating system of the host, as returned by the Generic type of the operating system of the host, as returned by the

View file

@ -177,6 +177,7 @@ type Info struct {
NEventsListener int NEventsListener int
KernelVersion string KernelVersion string
OperatingSystem string OperatingSystem string
OSVersion string
OSType string OSType string
Architecture string Architecture string
IndexServerAddress string IndexServerAddress string

View file

@ -1061,6 +1061,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
info.KernelVersion, info.KernelVersion,
info.OperatingSystem, info.OperatingSystem,
info.OSType, info.OSType,
info.OSVersion,
info.ID, info.ID,
).Set(1) ).Set(1)
engineCpus.Set(float64(info.NCPU)) engineCpus.Set(float64(info.NCPU))

View file

@ -21,11 +21,14 @@ import (
"github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/system"
"github.com/docker/docker/registry" "github.com/docker/docker/registry"
"github.com/docker/go-connections/sockets" "github.com/docker/go-connections/sockets"
"github.com/docker/go-metrics"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// SystemInfo returns information about the host server the daemon is running on. // SystemInfo returns information about the host server the daemon is running on.
func (daemon *Daemon) SystemInfo() (*types.Info, error) { func (daemon *Daemon) SystemInfo() (*types.Info, error) {
defer metrics.StartTimer(hostInfoFunctions.WithValues("system_info"))()
sysInfo := sysinfo.New(true) sysInfo := sysinfo.New(true)
cRunning, cPaused, cStopped := stateCtr.get() cRunning, cPaused, cStopped := stateCtr.get()
@ -49,6 +52,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
NEventsListener: daemon.EventsService.SubscribersCount(), NEventsListener: daemon.EventsService.SubscribersCount(),
KernelVersion: kernelVersion(), KernelVersion: kernelVersion(),
OperatingSystem: operatingSystem(), OperatingSystem: operatingSystem(),
OSVersion: osVersion(),
IndexServerAddress: registry.IndexServer, IndexServerAddress: registry.IndexServer,
OSType: platform.OSType, OSType: platform.OSType,
Architecture: platform.Architecture, Architecture: platform.Architecture,
@ -82,6 +86,8 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
// SystemVersion returns version information about the daemon. // SystemVersion returns version information about the daemon.
func (daemon *Daemon) SystemVersion() types.Version { func (daemon *Daemon) SystemVersion() types.Version {
defer metrics.StartTimer(hostInfoFunctions.WithValues("system_version"))()
kernelVersion := kernelVersion() kernelVersion := kernelVersion()
v := types.Version{ v := types.Version{
@ -240,8 +246,9 @@ func memInfo() *system.MemInfo {
return memInfo return memInfo
} }
func operatingSystem() string { func operatingSystem() (operatingSystem string) {
var operatingSystem string defer metrics.StartTimer(hostInfoFunctions.WithValues("operating_system"))()
if s, err := operatingsystem.GetOperatingSystem(); err != nil { if s, err := operatingsystem.GetOperatingSystem(); err != nil {
logrus.Warnf("Could not get operating system name: %v", err) logrus.Warnf("Could not get operating system name: %v", err)
} else { } else {
@ -256,9 +263,21 @@ func operatingSystem() string {
operatingSystem += " (containerized)" operatingSystem += " (containerized)"
} }
} }
return operatingSystem 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 { func maskCredentials(rawURL string) string {
parsedURL, err := url.Parse(rawURL) parsedURL, err := url.Parse(rawURL)
if err != nil || parsedURL.User == nil { if err != nil || parsedURL.User == nil {

View file

@ -17,6 +17,7 @@ const metricsPluginType = "MetricsCollector"
var ( var (
containerActions metrics.LabeledTimer containerActions metrics.LabeledTimer
networkActions metrics.LabeledTimer networkActions metrics.LabeledTimer
hostInfoFunctions metrics.LabeledTimer
engineInfo metrics.LabeledGauge engineInfo metrics.LabeledGauge
engineCpus metrics.Gauge engineCpus metrics.Gauge
engineMemory metrics.Gauge engineMemory metrics.Gauge
@ -38,6 +39,7 @@ func init() {
} { } {
containerActions.WithValues(a).Update(0) 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") 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"), 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", "commit",
"architecture", "architecture",
"graphdriver", "graphdriver",
"kernel", "os", "kernel",
"os",
"os_type", "os_type",
"os_version",
"daemon_id", // ID is a randomly generated unique identifier (e.g. UUID4) "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")) engineCpus = ns.NewGauge("engine_cpus", "The number of cpus that the host system of the engine has", metrics.Unit("cpus"))

View file

@ -26,6 +26,24 @@ var (
// GetOperatingSystem gets the name of the current operating system. // GetOperatingSystem gets the name of the current operating system.
func GetOperatingSystem() (string, error) { 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) osReleaseFile, err := os.Open(etcOsRelease)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
@ -38,28 +56,25 @@ func GetOperatingSystem() (string, error) {
} }
defer osReleaseFile.Close() defer osReleaseFile.Close()
var prettyName string var value string
keyWithTrailingEqual := key + "="
scanner := bufio.NewScanner(osReleaseFile) scanner := bufio.NewScanner(osReleaseFile)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if strings.HasPrefix(line, "PRETTY_NAME=") { if strings.HasPrefix(line, keyWithTrailingEqual) {
data := strings.SplitN(line, "=", 2) data := strings.SplitN(line, "=", 2)
prettyNames, err := shellwords.Parse(data[1]) values, err := shellwords.Parse(data[1])
if err != nil { 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 { if len(values) != 1 {
return "", fmt.Errorf("PRETTY_NAME needs to be enclosed by quotes if they have spaces: %s", data[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 return value, nil
}
// If not set, defaults to PRETTY_NAME="Linux"
// c.f. http://www.freedesktop.org/software/systemd/man/os-release.html
return "Linux", nil
} }
// IsContainerized returns true if we are running inside a container. // IsContainerized returns true if we are running inside a container.

View file

@ -7,43 +7,41 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"gotest.tools/assert"
) )
type EtcReleaseParsingTest struct {
name string
content string
expected string
expectedErr string
}
func TestGetOperatingSystem(t *testing.T) { func TestGetOperatingSystem(t *testing.T) {
var backup = etcOsRelease tests := []EtcReleaseParsingTest{
invalids := []struct {
content string
errorExpected string
}{
{ {
`PRETTY_NAME=Source Mage GNU/Linux content: `PRETTY_NAME=Source Mage GNU/Linux
PRETTY_NAME=Ubuntu 14.04.LTS`, 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=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=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=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" PRETTY_NAME_AGAIN="Ubuntu 14.04.LTS"
VERSION="14.04, Trusty Tahr" VERSION="14.04, Trusty Tahr"
ID=ubuntu ID=ubuntu
@ -52,10 +50,10 @@ VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/" HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
"Linux", expected: "Linux",
}, },
{ {
`NAME="Ubuntu" content: `NAME="Ubuntu"
VERSION="14.04, Trusty Tahr" VERSION="14.04, Trusty Tahr"
ID=ubuntu ID=ubuntu
ID_LIKE=debian ID_LIKE=debian
@ -63,10 +61,10 @@ VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/" HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
"Linux", expected: "Linux",
}, },
{ {
`NAME=Gentoo content: `NAME=Gentoo
ID=gentoo ID=gentoo
PRETTY_NAME="Gentoo/Linux" PRETTY_NAME="Gentoo/Linux"
ANSI_COLOR="1;32" ANSI_COLOR="1;32"
@ -74,10 +72,10 @@ HOME_URL="http://www.gentoo.org/"
SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" SUPPORT_URL="http://www.gentoo.org/main/en/support.xml"
BUG_REPORT_URL="https://bugs.gentoo.org/" BUG_REPORT_URL="https://bugs.gentoo.org/"
`, `,
"Gentoo/Linux", expected: "Gentoo/Linux",
}, },
{ {
`NAME="Ubuntu" content: `NAME="Ubuntu"
VERSION="14.04, Trusty Tahr" VERSION="14.04, Trusty Tahr"
ID=ubuntu ID=ubuntu
ID_LIKE=debian ID_LIKE=debian
@ -86,28 +84,77 @@ VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/" HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`, 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" VERSION="14.04, Trusty Tahr"
ID=ubuntu ID=ubuntu
ID_LIKE=debian ID_LIKE=debian
PRETTY_NAME='Ubuntu 14.04 LTS'`, 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"`, NAME="Source Mage"`,
"Source", expected: "Source",
}, },
{ {
`PRETTY_NAME=Source content: `PRETTY_NAME=Source
PRETTY_NAME="Source Mage"`, 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() dir := os.TempDir()
etcOsRelease = filepath.Join(dir, "etcOsRelease") etcOsRelease = filepath.Join(dir, "etcOsRelease")
@ -116,24 +163,19 @@ PRETTY_NAME="Source Mage"`,
etcOsRelease = backup etcOsRelease = backup
}() }()
for _, elt := range invalids { for _, test := range tests {
if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil { t.Run(test.name, func(t *testing.T) {
t.Fatalf("failed to write to %s: %v", etcOsRelease, err) if err := ioutil.WriteFile(etcOsRelease, []byte(test.content), 0600); err != nil {
} t.Fatalf("failed to write to %s: %v", etcOsRelease, err)
s, err := GetOperatingSystem() }
if err == nil || err.Error() != elt.errorExpected { s, err := parsingFunc()
t.Fatalf("Expected an error %q, got %q (err: %v)", elt.errorExpected, s, err) if test.expectedErr == "" {
} assert.NilError(t, err)
} } else {
assert.Error(t, err, test.expectedErr)
for _, elt := range valids { }
if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil { assert.Equal(t, s, test.expected)
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)
}
} }
} }

View file

@ -4,6 +4,7 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin
import ( import (
"errors" "errors"
"fmt"
"os/exec" "os/exec"
) )
@ -17,6 +18,12 @@ func GetOperatingSystem() (string, error) {
return string(osName), nil 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. // IsContainerized returns true if we are running inside a container.
// No-op on FreeBSD and Darwin, always returns false. // No-op on FreeBSD and Darwin, always returns false.
func IsContainerized() (bool, error) { func IsContainerized() (bool, error) {

View file

@ -3,45 +3,57 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin
import ( import (
"fmt" "fmt"
"github.com/docker/docker/pkg/system"
"golang.org/x/sys/windows/registry" "golang.org/x/sys/windows/registry"
) )
// GetOperatingSystem gets the name of the current operating system. // GetOperatingSystem gets the name of the current operating system.
func GetOperatingSystem() (string, error) { 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 releaseId, _, err := key.GetStringValue("ReleaseId")
ret := "Unknown Operating System" 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) buildNumber, _, err := key.GetStringValue("CurrentBuildNumber")
if err != nil { if err != nil {
return ret, err return
} }
defer k.Close() 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") return
if err != nil { })
return ret, err
}
ret = pn
ri, _, err := k.GetStringValue("ReleaseId") if os == "" {
if err != nil { // Default return value
return ret, err os = "Unknown Operating System"
}
ret = fmt.Sprintf("%s Version %s", ret, ri)
cbn, _, err := k.GetStringValue("CurrentBuildNumber")
if err != nil {
return ret, err
} }
ubr, _, err := k.GetIntegerValue("UBR") return os, err
if err != nil { }
return ret, err
}
ret = fmt.Sprintf("%s (OS Build %s.%d)", ret, cbn, ubr)
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. // IsContainerized returns true if we are running inside a container.