Sfoglia il codice sorgente

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>
Jean Rouge 6 anni fa
parent
commit
d363a1881e

+ 11 - 0
api/swagger.yaml

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

+ 1 - 0
api/types/types.go

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

+ 1 - 0
daemon/daemon.go

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

+ 21 - 2
daemon/info.go

@@ -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 {
-	var operatingSystem string
+func operatingSystem() (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 {

+ 5 - 1
daemon/metrics.go

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

+ 28 - 13
pkg/parsers/operatingsystem/operatingsystem_linux.go

@@ -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 {
-				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.
 // IsContainerized returns true if we are running inside a container.

+ 94 - 52
pkg/parsers/operatingsystem/operatingsystem_unix_test.go → pkg/parsers/operatingsystem/operatingsystem_linux_test.go

@@ -7,43 +7,41 @@ import (
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"testing"
 	"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=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 {
-		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)
+		})
 	}
 	}
 }
 }
 
 

+ 7 - 0
pkg/parsers/operatingsystem/operatingsystem_unix.go

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

+ 40 - 28
pkg/parsers/operatingsystem/operatingsystem_windows.go

@@ -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) {
-
-	// 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 {
 	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.
 // IsContainerized returns true if we are running inside a container.