Skip to content

Commit

Permalink
Adding OS version info to the nodes' Info struct
Browse files Browse the repository at this point in the history
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
moby/swarmkit#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>
  • Loading branch information
wk8 authored and root committed Jun 6, 2019
1 parent 89f7cda commit 6763d7c
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 93 deletions.
41 changes: 28 additions & 13 deletions pkg/parsers/operatingsystem/operatingsystem_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,41 @@ import (
"os"
"path/filepath"
"testing"

"gotest.tools/assert"
)

func TestGetOperatingSystem(t *testing.T) {
var backup = etcOsRelease
type EtcReleaseParsingTest struct {
name string
content string
expected string
expectedErr string
}

invalids := []struct {
content string
errorExpected string
}{
func TestGetOperatingSystem(t *testing.T) {
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
Expand All @@ -52,32 +50,32 @@ 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
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"
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
Expand All @@ -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")

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

Expand Down
7 changes: 7 additions & 0 deletions pkg/parsers/operatingsystem/operatingsystem_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin

import (
"errors"
"fmt"
"os/exec"
)

Expand All @@ -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) {
Expand Down
68 changes: 40 additions & 28 deletions pkg/parsers/operatingsystem/operatingsystem_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

// Default return value
ret := "Unknown Operating System"

k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
if err != nil {
return ret, err
os, err := withCurrentVersionRegistryKey(func(key registry.Key) (os string, err error) {
if os, _, err = key.GetStringValue("ProductName"); err != nil {
return "", err
}

releaseId, _, err := key.GetStringValue("ReleaseId")
if err != nil {
return
}
os = fmt.Sprintf("%s Version %s", os, releaseId)

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)

return
})

if os == "" {
// Default return value
os = "Unknown Operating System"
}
defer k.Close()

pn, _, err := k.GetStringValue("ProductName")
if err != nil {
return ret, err
}
ret = pn

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
}
return os, err
}

ubr, _, err := k.GetIntegerValue("UBR")
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 ret, err
return "", err
}
ret = fmt.Sprintf("%s (OS Build %s.%d)", ret, cbn, ubr)
defer key.Close()
return f(key)
}

return ret, nil
// 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.
Expand Down

0 comments on commit 6763d7c

Please sign in to comment.