Browse code

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 authored on 2019/05/31 01:51:41
Showing 10 changed files
... ...
@@ -3840,6 +3840,17 @@ definitions:
3840 3840
           or "Windows Server 2016 Datacenter"
3841 3841
         type: "string"
3842 3842
         example: "Alpine Linux v3.5"
3843
+      OSVersion:
3844
+        description: |
3845
+          Version of the host's operating system
3846
+
3847
+          <p><br /></p>
3848
+
3849
+          > **Note**: The information returned in this field, including its
3850
+          > very existence, and the formatting of values, should not be considered
3851
+          > stable, and may change without notice.
3852
+        type: "string"
3853
+        example: "16.04"
3843 3854
       OSType:
3844 3855
         description: |
3845 3856
           Generic type of the operating system of the host, as returned by the
... ...
@@ -177,6 +177,7 @@ type Info struct {
177 177
 	NEventsListener    int
178 178
 	KernelVersion      string
179 179
 	OperatingSystem    string
180
+	OSVersion          string
180 181
 	OSType             string
181 182
 	Architecture       string
182 183
 	IndexServerAddress string
... ...
@@ -1061,6 +1061,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
1061 1061
 		info.KernelVersion,
1062 1062
 		info.OperatingSystem,
1063 1063
 		info.OSType,
1064
+		info.OSVersion,
1064 1065
 		info.ID,
1065 1066
 	).Set(1)
1066 1067
 	engineCpus.Set(float64(info.NCPU))
... ...
@@ -21,11 +21,14 @@ import (
21 21
 	"github.com/docker/docker/pkg/system"
22 22
 	"github.com/docker/docker/registry"
23 23
 	"github.com/docker/go-connections/sockets"
24
+	"github.com/docker/go-metrics"
24 25
 	"github.com/sirupsen/logrus"
25 26
 )
26 27
 
27 28
 // SystemInfo returns information about the host server the daemon is running on.
28 29
 func (daemon *Daemon) SystemInfo() (*types.Info, error) {
30
+	defer metrics.StartTimer(hostInfoFunctions.WithValues("system_info"))()
31
+
29 32
 	sysInfo := sysinfo.New(true)
30 33
 	cRunning, cPaused, cStopped := stateCtr.get()
31 34
 
... ...
@@ -49,6 +52,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
49 49
 		NEventsListener:    daemon.EventsService.SubscribersCount(),
50 50
 		KernelVersion:      kernelVersion(),
51 51
 		OperatingSystem:    operatingSystem(),
52
+		OSVersion:          osVersion(),
52 53
 		IndexServerAddress: registry.IndexServer,
53 54
 		OSType:             platform.OSType,
54 55
 		Architecture:       platform.Architecture,
... ...
@@ -82,6 +86,8 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
82 82
 
83 83
 // SystemVersion returns version information about the daemon.
84 84
 func (daemon *Daemon) SystemVersion() types.Version {
85
+	defer metrics.StartTimer(hostInfoFunctions.WithValues("system_version"))()
86
+
85 87
 	kernelVersion := kernelVersion()
86 88
 
87 89
 	v := types.Version{
... ...
@@ -240,8 +246,9 @@ func memInfo() *system.MemInfo {
240 240
 	return memInfo
241 241
 }
242 242
 
243
-func operatingSystem() string {
244
-	var operatingSystem string
243
+func operatingSystem() (operatingSystem string) {
244
+	defer metrics.StartTimer(hostInfoFunctions.WithValues("operating_system"))()
245
+
245 246
 	if s, err := operatingsystem.GetOperatingSystem(); err != nil {
246 247
 		logrus.Warnf("Could not get operating system name: %v", err)
247 248
 	} else {
... ...
@@ -256,9 +263,21 @@ func operatingSystem() string {
256 256
 			operatingSystem += " (containerized)"
257 257
 		}
258 258
 	}
259
+
259 260
 	return operatingSystem
260 261
 }
261 262
 
263
+func osVersion() (version string) {
264
+	defer metrics.StartTimer(hostInfoFunctions.WithValues("os_version"))()
265
+
266
+	version, err := operatingsystem.GetOperatingSystemVersion()
267
+	if err != nil {
268
+		logrus.Warnf("Could not get operating system version: %v", err)
269
+	}
270
+
271
+	return version
272
+}
273
+
262 274
 func maskCredentials(rawURL string) string {
263 275
 	parsedURL, err := url.Parse(rawURL)
264 276
 	if err != nil || parsedURL.User == nil {
... ...
@@ -17,6 +17,7 @@ const metricsPluginType = "MetricsCollector"
17 17
 var (
18 18
 	containerActions          metrics.LabeledTimer
19 19
 	networkActions            metrics.LabeledTimer
20
+	hostInfoFunctions         metrics.LabeledTimer
20 21
 	engineInfo                metrics.LabeledGauge
21 22
 	engineCpus                metrics.Gauge
22 23
 	engineMemory              metrics.Gauge
... ...
@@ -38,6 +39,7 @@ func init() {
38 38
 	} {
39 39
 		containerActions.WithValues(a).Update(0)
40 40
 	}
41
+	hostInfoFunctions = ns.NewLabeledTimer("host_info_functions", "The number of seconds it takes to call functions gathering info about the host", "function")
41 42
 
42 43
 	networkActions = ns.NewLabeledTimer("network_actions", "The number of seconds it takes to process each network action", "action")
43 44
 	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() {
45 45
 		"commit",
46 46
 		"architecture",
47 47
 		"graphdriver",
48
-		"kernel", "os",
48
+		"kernel",
49
+		"os",
49 50
 		"os_type",
51
+		"os_version",
50 52
 		"daemon_id", // ID is a randomly generated unique identifier (e.g. UUID4)
51 53
 	)
52 54
 	engineCpus = ns.NewGauge("engine_cpus", "The number of cpus that the host system of the engine has", metrics.Unit("cpus"))
... ...
@@ -26,6 +26,24 @@ var (
26 26
 
27 27
 // GetOperatingSystem gets the name of the current operating system.
28 28
 func GetOperatingSystem() (string, error) {
29
+	if prettyName, err := getValueFromOsRelease("PRETTY_NAME"); err != nil {
30
+		return "", err
31
+	} else if prettyName != "" {
32
+		return prettyName, nil
33
+	}
34
+
35
+	// If not set, defaults to PRETTY_NAME="Linux"
36
+	// c.f. http://www.freedesktop.org/software/systemd/man/os-release.html
37
+	return "Linux", nil
38
+}
39
+
40
+// GetOperatingSystemVersion gets the version of the current operating system, as a string.
41
+func GetOperatingSystemVersion() (string, error) {
42
+	return getValueFromOsRelease("VERSION_ID")
43
+}
44
+
45
+// parses the os-release file and returns the value associated with `key`
46
+func getValueFromOsRelease(key string) (string, error) {
29 47
 	osReleaseFile, err := os.Open(etcOsRelease)
30 48
 	if err != nil {
31 49
 		if !os.IsNotExist(err) {
... ...
@@ -38,28 +56,25 @@ func GetOperatingSystem() (string, error) {
38 38
 	}
39 39
 	defer osReleaseFile.Close()
40 40
 
41
-	var prettyName string
41
+	var value string
42
+	keyWithTrailingEqual := key + "="
42 43
 	scanner := bufio.NewScanner(osReleaseFile)
43 44
 	for scanner.Scan() {
44 45
 		line := scanner.Text()
45
-		if strings.HasPrefix(line, "PRETTY_NAME=") {
46
+		if strings.HasPrefix(line, keyWithTrailingEqual) {
46 47
 			data := strings.SplitN(line, "=", 2)
47
-			prettyNames, err := shellwords.Parse(data[1])
48
+			values, err := shellwords.Parse(data[1])
48 49
 			if err != nil {
49
-				return "", fmt.Errorf("PRETTY_NAME is invalid: %s", err.Error())
50
+				return "", fmt.Errorf("%s is invalid: %s", key, err.Error())
50 51
 			}
51
-			if len(prettyNames) != 1 {
52
-				return "", fmt.Errorf("PRETTY_NAME needs to be enclosed by quotes if they have spaces: %s", data[1])
52
+			if len(values) != 1 {
53
+				return "", fmt.Errorf("%s needs to be enclosed by quotes if they have spaces: %s", key, data[1])
53 54
 			}
54
-			prettyName = prettyNames[0]
55
+			value = values[0]
55 56
 		}
56 57
 	}
57
-	if prettyName != "" {
58
-		return prettyName, nil
59
-	}
60
-	// If not set, defaults to PRETTY_NAME="Linux"
61
-	// c.f. http://www.freedesktop.org/software/systemd/man/os-release.html
62
-	return "Linux", nil
58
+
59
+	return value, nil
63 60
 }
64 61
 
65 62
 // IsContainerized returns true if we are running inside a container.
66 63
new file mode 100644
... ...
@@ -0,0 +1,289 @@
0
+// +build linux freebsd
1
+
2
+package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatingsystem"
3
+
4
+import (
5
+	"io/ioutil"
6
+	"os"
7
+	"path/filepath"
8
+	"testing"
9
+
10
+	"gotest.tools/assert"
11
+)
12
+
13
+type EtcReleaseParsingTest struct {
14
+	name        string
15
+	content     string
16
+	expected    string
17
+	expectedErr string
18
+}
19
+
20
+func TestGetOperatingSystem(t *testing.T) {
21
+	tests := []EtcReleaseParsingTest{
22
+		{
23
+			content: `PRETTY_NAME=Source Mage GNU/Linux
24
+PRETTY_NAME=Ubuntu 14.04.LTS`,
25
+			expectedErr: "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Source Mage GNU/Linux",
26
+		},
27
+		{
28
+			content: `PRETTY_NAME="Ubuntu Linux
29
+PRETTY_NAME=Ubuntu 14.04.LTS`,
30
+			expectedErr: "PRETTY_NAME is invalid: invalid command line string",
31
+		},
32
+		{
33
+			content: `PRETTY_NAME=Ubuntu'
34
+PRETTY_NAME=Ubuntu 14.04.LTS`,
35
+			expectedErr: "PRETTY_NAME is invalid: invalid command line string",
36
+		},
37
+		{
38
+			content: `PRETTY_NAME'
39
+PRETTY_NAME=Ubuntu 14.04.LTS`,
40
+			expectedErr: "PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS",
41
+		},
42
+		{
43
+			content: `NAME="Ubuntu"
44
+PRETTY_NAME_AGAIN="Ubuntu 14.04.LTS"
45
+VERSION="14.04, Trusty Tahr"
46
+ID=ubuntu
47
+ID_LIKE=debian
48
+VERSION_ID="14.04"
49
+HOME_URL="http://www.ubuntu.com/"
50
+SUPPORT_URL="http://help.ubuntu.com/"
51
+BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
52
+			expected: "Linux",
53
+		},
54
+		{
55
+			content: `NAME="Ubuntu"
56
+VERSION="14.04, Trusty Tahr"
57
+ID=ubuntu
58
+ID_LIKE=debian
59
+VERSION_ID="14.04"
60
+HOME_URL="http://www.ubuntu.com/"
61
+SUPPORT_URL="http://help.ubuntu.com/"
62
+BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
63
+			expected: "Linux",
64
+		},
65
+		{
66
+			content: `NAME=Gentoo
67
+ID=gentoo
68
+PRETTY_NAME="Gentoo/Linux"
69
+ANSI_COLOR="1;32"
70
+HOME_URL="http://www.gentoo.org/"
71
+SUPPORT_URL="http://www.gentoo.org/main/en/support.xml"
72
+BUG_REPORT_URL="https://bugs.gentoo.org/"
73
+`,
74
+			expected: "Gentoo/Linux",
75
+		},
76
+		{
77
+			content: `NAME="Ubuntu"
78
+VERSION="14.04, Trusty Tahr"
79
+ID=ubuntu
80
+ID_LIKE=debian
81
+PRETTY_NAME="Ubuntu 14.04 LTS"
82
+VERSION_ID="14.04"
83
+HOME_URL="http://www.ubuntu.com/"
84
+SUPPORT_URL="http://help.ubuntu.com/"
85
+BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
86
+			expected: "Ubuntu 14.04 LTS",
87
+		},
88
+		{
89
+			content: `NAME="Ubuntu"
90
+VERSION="14.04, Trusty Tahr"
91
+ID=ubuntu
92
+ID_LIKE=debian
93
+PRETTY_NAME='Ubuntu 14.04 LTS'`,
94
+			expected: "Ubuntu 14.04 LTS",
95
+		},
96
+		{
97
+			content: `PRETTY_NAME=Source
98
+NAME="Source Mage"`,
99
+			expected: "Source",
100
+		},
101
+		{
102
+			content: `PRETTY_NAME=Source
103
+PRETTY_NAME="Source Mage"`,
104
+			expected: "Source Mage",
105
+		},
106
+	}
107
+
108
+	runEtcReleaseParsingTests(t, tests, GetOperatingSystem)
109
+}
110
+
111
+func TestGetOperatingSystemVersion(t *testing.T) {
112
+	tests := []EtcReleaseParsingTest{
113
+		{
114
+			name: "invalid version id",
115
+			content: `VERSION_ID="18.04
116
+VERSION_ID=18.04`,
117
+			expectedErr: "VERSION_ID is invalid: invalid command line string",
118
+		},
119
+		{
120
+			name: "ubuntu 14.04",
121
+			content: `NAME="Ubuntu"
122
+PRETTY_NAME="Ubuntu 14.04.LTS"
123
+VERSION="14.04, Trusty Tahr"
124
+ID=ubuntu
125
+ID_LIKE=debian
126
+VERSION_ID="14.04"
127
+HOME_URL="http://www.ubuntu.com/"
128
+SUPPORT_URL="http://help.ubuntu.com/"
129
+BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
130
+			expected: "14.04",
131
+		},
132
+		{
133
+			name: "gentoo",
134
+			content: `NAME=Gentoo
135
+ID=gentoo
136
+PRETTY_NAME="Gentoo/Linux"
137
+ANSI_COLOR="1;32"
138
+HOME_URL="http://www.gentoo.org/"
139
+SUPPORT_URL="http://www.gentoo.org/main/en/support.xml"
140
+BUG_REPORT_URL="https://bugs.gentoo.org/"
141
+`,
142
+		},
143
+		{
144
+			name: "dual version id",
145
+			content: `VERSION_ID="14.04"
146
+VERSION_ID=18.04`,
147
+			expected: "18.04",
148
+		},
149
+	}
150
+
151
+	runEtcReleaseParsingTests(t, tests, GetOperatingSystemVersion)
152
+}
153
+
154
+func runEtcReleaseParsingTests(t *testing.T, tests []EtcReleaseParsingTest, parsingFunc func() (string, error)) {
155
+	var backup = etcOsRelease
156
+
157
+	dir := os.TempDir()
158
+	etcOsRelease = filepath.Join(dir, "etcOsRelease")
159
+
160
+	defer func() {
161
+		os.Remove(etcOsRelease)
162
+		etcOsRelease = backup
163
+	}()
164
+
165
+	for _, test := range tests {
166
+		t.Run(test.name, func(t *testing.T) {
167
+			if err := ioutil.WriteFile(etcOsRelease, []byte(test.content), 0600); err != nil {
168
+				t.Fatalf("failed to write to %s: %v", etcOsRelease, err)
169
+			}
170
+			s, err := parsingFunc()
171
+			if test.expectedErr == "" {
172
+				assert.NilError(t, err)
173
+			} else {
174
+				assert.Error(t, err, test.expectedErr)
175
+			}
176
+			assert.Equal(t, s, test.expected)
177
+		})
178
+	}
179
+}
180
+
181
+func TestIsContainerized(t *testing.T) {
182
+	var (
183
+		backup                                = proc1Cgroup
184
+		nonContainerizedProc1Cgroupsystemd226 = []byte(`9:memory:/init.scope
185
+8:net_cls,net_prio:/
186
+7:cpuset:/
187
+6:freezer:/
188
+5:devices:/init.scope
189
+4:blkio:/init.scope
190
+3:cpu,cpuacct:/init.scope
191
+2:perf_event:/
192
+1:name=systemd:/init.scope
193
+`)
194
+		nonContainerizedProc1Cgroup = []byte(`14:name=systemd:/
195
+13:hugetlb:/
196
+12:net_prio:/
197
+11:perf_event:/
198
+10:bfqio:/
199
+9:blkio:/
200
+8:net_cls:/
201
+7:freezer:/
202
+6:devices:/
203
+5:memory:/
204
+4:cpuacct:/
205
+3:cpu:/
206
+2:cpuset:/
207
+`)
208
+		containerizedProc1Cgroup = []byte(`9:perf_event:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
209
+8:blkio:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
210
+7:net_cls:/
211
+6:freezer:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
212
+5:devices:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
213
+4:memory:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
214
+3:cpuacct:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
215
+2:cpu:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
216
+1:cpuset:/`)
217
+	)
218
+
219
+	dir := os.TempDir()
220
+	proc1Cgroup = filepath.Join(dir, "proc1Cgroup")
221
+
222
+	defer func() {
223
+		os.Remove(proc1Cgroup)
224
+		proc1Cgroup = backup
225
+	}()
226
+
227
+	if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroup, 0600); err != nil {
228
+		t.Fatalf("failed to write to %s: %v", proc1Cgroup, err)
229
+	}
230
+	inContainer, err := IsContainerized()
231
+	if err != nil {
232
+		t.Fatal(err)
233
+	}
234
+	if inContainer {
235
+		t.Fatal("Wrongly assuming containerized")
236
+	}
237
+
238
+	if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroupsystemd226, 0600); err != nil {
239
+		t.Fatalf("failed to write to %s: %v", proc1Cgroup, err)
240
+	}
241
+	inContainer, err = IsContainerized()
242
+	if err != nil {
243
+		t.Fatal(err)
244
+	}
245
+	if inContainer {
246
+		t.Fatal("Wrongly assuming containerized for systemd /init.scope cgroup layout")
247
+	}
248
+
249
+	if err := ioutil.WriteFile(proc1Cgroup, containerizedProc1Cgroup, 0600); err != nil {
250
+		t.Fatalf("failed to write to %s: %v", proc1Cgroup, err)
251
+	}
252
+	inContainer, err = IsContainerized()
253
+	if err != nil {
254
+		t.Fatal(err)
255
+	}
256
+	if !inContainer {
257
+		t.Fatal("Wrongly assuming non-containerized")
258
+	}
259
+}
260
+
261
+func TestOsReleaseFallback(t *testing.T) {
262
+	var backup = etcOsRelease
263
+	var altBackup = altOsRelease
264
+	dir := os.TempDir()
265
+	etcOsRelease = filepath.Join(dir, "etcOsRelease")
266
+	altOsRelease = filepath.Join(dir, "altOsRelease")
267
+
268
+	defer func() {
269
+		os.Remove(dir)
270
+		etcOsRelease = backup
271
+		altOsRelease = altBackup
272
+	}()
273
+	content := `NAME=Gentoo
274
+ID=gentoo
275
+PRETTY_NAME="Gentoo/Linux"
276
+ANSI_COLOR="1;32"
277
+HOME_URL="http://www.gentoo.org/"
278
+SUPPORT_URL="http://www.gentoo.org/main/en/support.xml"
279
+BUG_REPORT_URL="https://bugs.gentoo.org/"
280
+`
281
+	if err := ioutil.WriteFile(altOsRelease, []byte(content), 0600); err != nil {
282
+		t.Fatalf("failed to write to %s: %v", etcOsRelease, err)
283
+	}
284
+	s, err := GetOperatingSystem()
285
+	if err != nil || s != "Gentoo/Linux" {
286
+		t.Fatalf("Expected %q, got %q (err: %v)", "Gentoo/Linux", s, err)
287
+	}
288
+}
... ...
@@ -4,6 +4,7 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin
4 4
 
5 5
 import (
6 6
 	"errors"
7
+	"fmt"
7 8
 	"os/exec"
8 9
 )
9 10
 
... ...
@@ -17,6 +18,12 @@ func GetOperatingSystem() (string, error) {
17 17
 	return string(osName), nil
18 18
 }
19 19
 
20
+// GetOperatingSystemVersion gets the version of the current operating system, as a string.
21
+func GetOperatingSystemVersion() (string, error) {
22
+	// there's no standard unix way of getting this, sadly...
23
+	return "", fmt.Error("Unsupported on generic unix")
24
+}
25
+
20 26
 // IsContainerized returns true if we are running inside a container.
21 27
 // No-op on FreeBSD and Darwin, always returns false.
22 28
 func IsContainerized() (bool, error) {
23 29
deleted file mode 100644
... ...
@@ -1,247 +0,0 @@
1
-// +build linux freebsd
2
-
3
-package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatingsystem"
4
-
5
-import (
6
-	"io/ioutil"
7
-	"os"
8
-	"path/filepath"
9
-	"testing"
10
-)
11
-
12
-func TestGetOperatingSystem(t *testing.T) {
13
-	var backup = etcOsRelease
14
-
15
-	invalids := []struct {
16
-		content       string
17
-		errorExpected string
18
-	}{
19
-		{
20
-			`PRETTY_NAME=Source Mage GNU/Linux
21
-PRETTY_NAME=Ubuntu 14.04.LTS`,
22
-			"PRETTY_NAME needs to be enclosed by quotes if they have spaces: Source Mage GNU/Linux",
23
-		},
24
-		{
25
-			`PRETTY_NAME="Ubuntu Linux
26
-PRETTY_NAME=Ubuntu 14.04.LTS`,
27
-			"PRETTY_NAME is invalid: invalid command line string",
28
-		},
29
-		{
30
-			`PRETTY_NAME=Ubuntu'
31
-PRETTY_NAME=Ubuntu 14.04.LTS`,
32
-			"PRETTY_NAME is invalid: invalid command line string",
33
-		},
34
-		{
35
-			`PRETTY_NAME'
36
-PRETTY_NAME=Ubuntu 14.04.LTS`,
37
-			"PRETTY_NAME needs to be enclosed by quotes if they have spaces: Ubuntu 14.04.LTS",
38
-		},
39
-	}
40
-
41
-	valids := []struct {
42
-		content  string
43
-		expected string
44
-	}{
45
-		{
46
-			`NAME="Ubuntu"
47
-PRETTY_NAME_AGAIN="Ubuntu 14.04.LTS"
48
-VERSION="14.04, Trusty Tahr"
49
-ID=ubuntu
50
-ID_LIKE=debian
51
-VERSION_ID="14.04"
52
-HOME_URL="http://www.ubuntu.com/"
53
-SUPPORT_URL="http://help.ubuntu.com/"
54
-BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
55
-			"Linux",
56
-		},
57
-		{
58
-			`NAME="Ubuntu"
59
-VERSION="14.04, Trusty Tahr"
60
-ID=ubuntu
61
-ID_LIKE=debian
62
-VERSION_ID="14.04"
63
-HOME_URL="http://www.ubuntu.com/"
64
-SUPPORT_URL="http://help.ubuntu.com/"
65
-BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
66
-			"Linux",
67
-		},
68
-		{
69
-			`NAME=Gentoo
70
-ID=gentoo
71
-PRETTY_NAME="Gentoo/Linux"
72
-ANSI_COLOR="1;32"
73
-HOME_URL="http://www.gentoo.org/"
74
-SUPPORT_URL="http://www.gentoo.org/main/en/support.xml"
75
-BUG_REPORT_URL="https://bugs.gentoo.org/"
76
-`,
77
-			"Gentoo/Linux",
78
-		},
79
-		{
80
-			`NAME="Ubuntu"
81
-VERSION="14.04, Trusty Tahr"
82
-ID=ubuntu
83
-ID_LIKE=debian
84
-PRETTY_NAME="Ubuntu 14.04 LTS"
85
-VERSION_ID="14.04"
86
-HOME_URL="http://www.ubuntu.com/"
87
-SUPPORT_URL="http://help.ubuntu.com/"
88
-BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`,
89
-			"Ubuntu 14.04 LTS",
90
-		},
91
-		{
92
-			`NAME="Ubuntu"
93
-VERSION="14.04, Trusty Tahr"
94
-ID=ubuntu
95
-ID_LIKE=debian
96
-PRETTY_NAME='Ubuntu 14.04 LTS'`,
97
-			"Ubuntu 14.04 LTS",
98
-		},
99
-		{
100
-			`PRETTY_NAME=Source
101
-NAME="Source Mage"`,
102
-			"Source",
103
-		},
104
-		{
105
-			`PRETTY_NAME=Source
106
-PRETTY_NAME="Source Mage"`,
107
-			"Source Mage",
108
-		},
109
-	}
110
-
111
-	dir := os.TempDir()
112
-	etcOsRelease = filepath.Join(dir, "etcOsRelease")
113
-
114
-	defer func() {
115
-		os.Remove(etcOsRelease)
116
-		etcOsRelease = backup
117
-	}()
118
-
119
-	for _, elt := range invalids {
120
-		if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil {
121
-			t.Fatalf("failed to write to %s: %v", etcOsRelease, err)
122
-		}
123
-		s, err := GetOperatingSystem()
124
-		if err == nil || err.Error() != elt.errorExpected {
125
-			t.Fatalf("Expected an error %q, got %q (err: %v)", elt.errorExpected, s, err)
126
-		}
127
-	}
128
-
129
-	for _, elt := range valids {
130
-		if err := ioutil.WriteFile(etcOsRelease, []byte(elt.content), 0600); err != nil {
131
-			t.Fatalf("failed to write to %s: %v", etcOsRelease, err)
132
-		}
133
-		s, err := GetOperatingSystem()
134
-		if err != nil || s != elt.expected {
135
-			t.Fatalf("Expected %q, got %q (err: %v)", elt.expected, s, err)
136
-		}
137
-	}
138
-}
139
-
140
-func TestIsContainerized(t *testing.T) {
141
-	var (
142
-		backup                                = proc1Cgroup
143
-		nonContainerizedProc1Cgroupsystemd226 = []byte(`9:memory:/init.scope
144
-8:net_cls,net_prio:/
145
-7:cpuset:/
146
-6:freezer:/
147
-5:devices:/init.scope
148
-4:blkio:/init.scope
149
-3:cpu,cpuacct:/init.scope
150
-2:perf_event:/
151
-1:name=systemd:/init.scope
152
-`)
153
-		nonContainerizedProc1Cgroup = []byte(`14:name=systemd:/
154
-13:hugetlb:/
155
-12:net_prio:/
156
-11:perf_event:/
157
-10:bfqio:/
158
-9:blkio:/
159
-8:net_cls:/
160
-7:freezer:/
161
-6:devices:/
162
-5:memory:/
163
-4:cpuacct:/
164
-3:cpu:/
165
-2:cpuset:/
166
-`)
167
-		containerizedProc1Cgroup = []byte(`9:perf_event:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
168
-8:blkio:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
169
-7:net_cls:/
170
-6:freezer:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
171
-5:devices:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
172
-4:memory:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
173
-3:cpuacct:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
174
-2:cpu:/docker/3cef1b53c50b0fa357d994f8a1a8cd783c76bbf4f5dd08b226e38a8bd331338d
175
-1:cpuset:/`)
176
-	)
177
-
178
-	dir := os.TempDir()
179
-	proc1Cgroup = filepath.Join(dir, "proc1Cgroup")
180
-
181
-	defer func() {
182
-		os.Remove(proc1Cgroup)
183
-		proc1Cgroup = backup
184
-	}()
185
-
186
-	if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroup, 0600); err != nil {
187
-		t.Fatalf("failed to write to %s: %v", proc1Cgroup, err)
188
-	}
189
-	inContainer, err := IsContainerized()
190
-	if err != nil {
191
-		t.Fatal(err)
192
-	}
193
-	if inContainer {
194
-		t.Fatal("Wrongly assuming containerized")
195
-	}
196
-
197
-	if err := ioutil.WriteFile(proc1Cgroup, nonContainerizedProc1Cgroupsystemd226, 0600); err != nil {
198
-		t.Fatalf("failed to write to %s: %v", proc1Cgroup, err)
199
-	}
200
-	inContainer, err = IsContainerized()
201
-	if err != nil {
202
-		t.Fatal(err)
203
-	}
204
-	if inContainer {
205
-		t.Fatal("Wrongly assuming containerized for systemd /init.scope cgroup layout")
206
-	}
207
-
208
-	if err := ioutil.WriteFile(proc1Cgroup, containerizedProc1Cgroup, 0600); err != nil {
209
-		t.Fatalf("failed to write to %s: %v", proc1Cgroup, err)
210
-	}
211
-	inContainer, err = IsContainerized()
212
-	if err != nil {
213
-		t.Fatal(err)
214
-	}
215
-	if !inContainer {
216
-		t.Fatal("Wrongly assuming non-containerized")
217
-	}
218
-}
219
-
220
-func TestOsReleaseFallback(t *testing.T) {
221
-	var backup = etcOsRelease
222
-	var altBackup = altOsRelease
223
-	dir := os.TempDir()
224
-	etcOsRelease = filepath.Join(dir, "etcOsRelease")
225
-	altOsRelease = filepath.Join(dir, "altOsRelease")
226
-
227
-	defer func() {
228
-		os.Remove(dir)
229
-		etcOsRelease = backup
230
-		altOsRelease = altBackup
231
-	}()
232
-	content := `NAME=Gentoo
233
-ID=gentoo
234
-PRETTY_NAME="Gentoo/Linux"
235
-ANSI_COLOR="1;32"
236
-HOME_URL="http://www.gentoo.org/"
237
-SUPPORT_URL="http://www.gentoo.org/main/en/support.xml"
238
-BUG_REPORT_URL="https://bugs.gentoo.org/"
239
-`
240
-	if err := ioutil.WriteFile(altOsRelease, []byte(content), 0600); err != nil {
241
-		t.Fatalf("failed to write to %s: %v", etcOsRelease, err)
242
-	}
243
-	s, err := GetOperatingSystem()
244
-	if err != nil || s != "Gentoo/Linux" {
245
-		t.Fatalf("Expected %q, got %q (err: %v)", "Gentoo/Linux", s, err)
246
-	}
247
-}
... ...
@@ -3,45 +3,57 @@ package operatingsystem // import "github.com/docker/docker/pkg/parsers/operatin
3 3
 import (
4 4
 	"fmt"
5 5
 
6
+	"github.com/docker/docker/pkg/system"
6 7
 	"golang.org/x/sys/windows/registry"
7 8
 )
8 9
 
9 10
 // GetOperatingSystem gets the name of the current operating system.
10 11
 func GetOperatingSystem() (string, error) {
11
-
12
-	// Default return value
13
-	ret := "Unknown Operating System"
14
-
15
-	k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
16
-	if err != nil {
17
-		return ret, err
12
+	os, err := withCurrentVersionRegistryKey(func(key registry.Key) (os string, err error) {
13
+		if os, _, err = key.GetStringValue("ProductName"); err != nil {
14
+			return "", err
15
+		}
16
+
17
+		releaseId, _, err := key.GetStringValue("ReleaseId")
18
+		if err != nil {
19
+			return
20
+		}
21
+		os = fmt.Sprintf("%s Version %s", os, releaseId)
22
+
23
+		buildNumber, _, err := key.GetStringValue("CurrentBuildNumber")
24
+		if err != nil {
25
+			return
26
+		}
27
+		ubr, _, err := key.GetIntegerValue("UBR")
28
+		if err != nil {
29
+			return
30
+		}
31
+		os = fmt.Sprintf("%s (OS Build %s.%d)", os, buildNumber, ubr)
32
+
33
+		return
34
+	})
35
+
36
+	if os == "" {
37
+		// Default return value
38
+		os = "Unknown Operating System"
18 39
 	}
19
-	defer k.Close()
20 40
 
21
-	pn, _, err := k.GetStringValue("ProductName")
22
-	if err != nil {
23
-		return ret, err
24
-	}
25
-	ret = pn
26
-
27
-	ri, _, err := k.GetStringValue("ReleaseId")
28
-	if err != nil {
29
-		return ret, err
30
-	}
31
-	ret = fmt.Sprintf("%s Version %s", ret, ri)
32
-
33
-	cbn, _, err := k.GetStringValue("CurrentBuildNumber")
34
-	if err != nil {
35
-		return ret, err
36
-	}
41
+	return os, err
42
+}
37 43
 
38
-	ubr, _, err := k.GetIntegerValue("UBR")
44
+func withCurrentVersionRegistryKey(f func(registry.Key) (string, error)) (string, error) {
45
+	key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
39 46
 	if err != nil {
40
-		return ret, err
47
+		return "", err
41 48
 	}
42
-	ret = fmt.Sprintf("%s (OS Build %s.%d)", ret, cbn, ubr)
49
+	defer key.Close()
50
+	return f(key)
51
+}
43 52
 
44
-	return ret, nil
53
+// GetOperatingSystemVersion gets the version of the current operating system, as a string.
54
+func GetOperatingSystemVersion() (string, error) {
55
+	version := system.GetOSVersion()
56
+	return fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build), nil
45 57
 }
46 58
 
47 59
 // IsContainerized returns true if we are running inside a container.