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"
type: "string"
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:
description: |
Generic type of the operating system of the host, as returned by the

View file

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

View file

@ -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))

View file

@ -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 {

View file

@ -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"))

View file

@ -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.

View file

@ -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)
})
}
}

View file

@ -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) {

View file

@ -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.