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.