forked from sandia-minimega/minimega
/
external.go
293 lines (243 loc) · 7.44 KB
/
external.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// Copyright (2012) Sandia Corporation.
// Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
// the U.S. Government retains certain rights in this software.
package main
import (
"bridge"
"errors"
"fmt"
"minicli"
log "minilog"
"nbd"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
)
var (
MIN_QEMU = []int{1, 6}
MIN_DNSMASQ = []int{2, 73}
MIN_OVS = []int{1, 11}
// MIN_KERNEL for Overlayfs
MIN_KERNEL = []int{3, 18}
)
// externalDependencies contains all the external programs that minimega
// invokes. We check for the existence of these on startup and on `check`.
var externalDependencies = map[string]bool{
"dnsmasq": true, // used in mulitple
"kvm": true, // used in multiple
"mount": true, // used in multiple
"dhclient": true, // used in bridge_cli.go
"ip": true, // used in bridge_cli.go
"scp": true, // used in deploy.go
"ssh": true, // used in deploy.go
"cp": true, // used in disk.go
"qemu-img": true, // used in disk.go
"ntfs-3g": true, // used in disk.go
"blockdev": true, // used in disk.go
"ovs-vsctl": true, // used in external.go
"taskset": true, // used in optimize.go
"tar": true, // used in cli.go
}
func init() {
// Add in dependencies from imported packages
for _, v := range bridge.ExternalDependencies {
externalDependencies[v] = true
}
for _, v := range nbd.ExternalDependencies {
externalDependencies[v] = true
}
}
var externalCLIHandlers = []minicli.Handler{
{ // check
HelpShort: "check that all external executables dependencies exist",
HelpLong: `
minimega maintains a list of external packages that it depends on, such as
qemu. Calling check will attempt to find each of these executables in the
avaiable path and check to make sure they meet the minimum version
requirements. Returns errors for all missing executables and all minimum
versions not met.`,
Patterns: []string{
"check",
},
Call: wrapSimpleCLI(func(_ *Namespace, _ *minicli.Command, _ *minicli.Response) error {
return checkExternal()
}),
},
}
// checkExternal checks for the presence of each of the external processes we
// may call, and error if any aren't in our path.
func checkExternal() error {
// make sure we're using a new enough kernel
if err := checkVersion("kernel", MIN_KERNEL, kernelVersion); err != nil {
return err
}
// make sure we have all binaries
if err := checkDependencies(); err != nil {
return err
}
// everything we want exists, but we have a few minimum versions to check
if err := checkVersion("dnsmasq", MIN_DNSMASQ, dnsmasqVersion); err != nil {
return err
}
if err := checkVersion("ovs-vsctl", MIN_OVS, ovsVersion); err != nil {
return err
}
if err := checkVersion("qemu", MIN_QEMU, qemuVersion); err != nil {
return err
}
// now check that ovs is actually running...
if err := bridge.CheckOVS(); err != nil {
return errors.New("openvswitch does not appear to be running")
}
// check kvm module is loaded
if !lsModule("kvm") {
// warn since not a hard requirement
log.Warn("no kvm module detected, is virtualization enabled?")
}
return nil
}
// checkDependencies checks whether each of the processes in
// externalDependencies exists or not
func checkDependencies() error {
var errs []error
for name := range externalDependencies {
path, err := exec.LookPath(name)
if err == nil {
log.Info("%v found at: %v", name, path)
}
errs = append(errs, err)
}
return makeErrSlice(errs)
}
func kernelVersion() ([]int, error) {
var utsname syscall.Utsname
if err := syscall.Uname(&utsname); err != nil {
return nil, fmt.Errorf("check kernel version failed: %v", err)
}
// convert []int8 to string so that we can call parseVersion on it
buf := make([]byte, len(utsname.Release))
for i, v := range utsname.Release {
buf[i] = byte(v)
}
return parseVersion("kernel", string(buf))
}
func dnsmasqVersion() ([]int, error) {
out, err := processWrapper("dnsmasq", "-v")
if err != nil {
return nil, fmt.Errorf("check dnsmasq version failed: %v", err)
}
f := strings.Fields(out)
if len(f) < 3 {
return nil, fmt.Errorf("cannot parse dnsmasq version: %v", out)
}
return parseVersion("dnsmasq", f[2])
}
func ovsVersion() ([]int, error) {
out, err := processWrapper("ovs-vsctl", "-V")
if err != nil {
return nil, fmt.Errorf("check ovs version failed: %v", err)
}
f := strings.Fields(out)
if len(f) < 4 {
return nil, fmt.Errorf("cannot parse ovs version: %v", out)
}
return parseVersion("ovs", f[3])
}
func qemuVersion() ([]int, error) {
out, err := processWrapper("kvm", "-version")
if err != nil {
return nil, fmt.Errorf("check qemu version failed: %v", err)
}
f := strings.Fields(out)
if len(f) < 4 {
return nil, fmt.Errorf("cannot parse qemu version: %v", out)
}
return parseVersion("qemu", f[3])
}
// parseVersion parses a version string like 1.2.3, returning a slice of ints
func parseVersion(name, version string) ([]int, error) {
var res []int
for _, v := range strings.Split(version, ".") {
i, err := strconv.Atoi(v)
if err != nil {
// if the string contains non-numeric characters, trim those and
// then immediately return the result, if we have a valid number
for i, r := range v {
if r < '0' || r > '9' {
v = v[:i]
break
}
}
if i, err := strconv.Atoi(v); err == nil {
res = append(res, i)
return res, nil
}
return nil, fmt.Errorf("cannot parse %v version: %v", name, version)
}
res = append(res, i)
}
return res, nil
}
// printVersion joins a slice of ints with dots to produce a version string
func printVersion(version []int) string {
var res []string
for _, v := range version {
res = append(res, strconv.Itoa(v))
}
return strings.Join(res, ".")
}
// checkVersion compares the return value of versionFn against min, returning
// an error if the version is less than min or versionFn failed.
func checkVersion(name string, min []int, versionFn func() ([]int, error)) error {
version, err := versionFn()
if err != nil {
return err
}
got := printVersion(version)
want := printVersion(min)
log.Info("%v version: %v, minimum: %v", name, got, want)
for i := range min {
if i >= len(version) || version[i] < min[i] {
// minimum version was more specific (e.g. 1.1.1 against 1.1) or
// minimum version is greater in the current index => fail
return fmt.Errorf("%v version does not meet minimum: %v < %v", name, got, want)
} else if version[i] > min[i] {
// version exceeds minimum
break
}
}
// must match or exceed
return nil
}
// lsModule returns true if the specified module is in the `lsmod` output
func lsModule(s string) bool {
log.Info("checking for kernel module: %v", s)
out, err := processWrapper("lsmod")
if err != nil {
log.Warn("unable to check lsmod for %v: %v", s, err)
return false
}
return strings.Contains(out, s)
}
// processWrapper executes the given arg list and returns a combined
// stdout/stderr and any errors. processWrapper blocks until the process exits.
func processWrapper(args ...string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("empty argument list")
}
start := time.Now()
out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
stop := time.Now()
log.Debug("cmd %v completed in %v", args[0], stop.Sub(start))
return string(out), err
}
func process(s string) (string, error) {
p, err := exec.LookPath(s)
if err == exec.ErrNotFound {
// add executable name to error
return "", fmt.Errorf("executable not found in $PATH: %v", s)
}
return p, err
}