Skip to content

Commit 005df3e

Browse files
committed
cmd/makemac: add start of command/package to create OS X builder VMs
Currently it's just a package main. It might split in two later. It might stop shelling out to govc and use the underlying API directly too, but it's hairy. Easier to work like this for now. Updates golang/go#9495 Change-Id: I0d2e19abcb5114ab7fe2e2c543d14e50897d4cbb Reviewed-on: https://go-review.googlesource.com/28584 Reviewed-by: Quentin Smith <[email protected]>
1 parent 5104600 commit 005df3e

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed

cmd/makemac/makemac.go

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
/*
6+
The makemac command starts OS X VMs for the builders.
7+
It is currently just a thin wrapper around govc.
8+
9+
See https://github.com/vmware/govmomi/tree/master/govc
10+
11+
Usage:
12+
13+
$ makemac <osx_minor_version> # e.g, 8, 9, 10, 11
14+
15+
*/
16+
package main
17+
18+
import (
19+
"context"
20+
"encoding/json"
21+
"errors"
22+
"flag"
23+
"fmt"
24+
"io/ioutil"
25+
"log"
26+
"os"
27+
"os/exec"
28+
"path"
29+
"path/filepath"
30+
"strconv"
31+
"strings"
32+
"sync"
33+
)
34+
35+
func usage() {
36+
fmt.Fprintf(os.Stderr, "Usage: makemac <osx_minor_version>\n")
37+
os.Exit(1)
38+
}
39+
40+
var flagStatus = flag.Bool("status", false, "print status only")
41+
42+
func main() {
43+
flag.Parse()
44+
if !(flag.NArg() == 1 || (*flagStatus && flag.NArg() == 0)) {
45+
usage()
46+
}
47+
minor, err := strconv.Atoi(flag.Arg(0))
48+
if err != nil && !*flagStatus {
49+
usage()
50+
}
51+
52+
ctx := context.Background()
53+
state, err := getState(ctx)
54+
if err != nil {
55+
log.Fatal(err)
56+
}
57+
58+
if *flagStatus {
59+
stj, _ := json.MarshalIndent(state, "", " ")
60+
fmt.Printf("%s\n", stj)
61+
return
62+
}
63+
64+
err = state.CreateMac(ctx, minor)
65+
if err != nil {
66+
log.Fatal(err)
67+
}
68+
}
69+
70+
// State is the state of the world.
71+
type State struct {
72+
mu sync.Mutex `json:"-"`
73+
74+
Hosts map[string]int // IP address -> running Mac VM count (including 0)
75+
VMHost map[string]string // "mac_10_8_host2b" => "10.0.0.0"
76+
HostIP map[string]string // "host-5" -> "10.0.0.0"
77+
}
78+
79+
// CreateMac creates an Mac VM running OS X 10.<minor>.
80+
func (st *State) CreateMac(ctx context.Context, minor int) (err error) {
81+
// TODO(bradfitz): return VM name, update state, etc.
82+
83+
st.mu.Lock()
84+
defer st.mu.Unlock()
85+
86+
var guestType string
87+
switch minor {
88+
case 8:
89+
guestType = "darwin12_64Guest"
90+
case 9:
91+
guestType = "darwin13_64Guest"
92+
case 10, 11:
93+
guestType = "darwin14_64Guest"
94+
default:
95+
return fmt.Errorf("unsupported makemac minor OS X version %d", minor)
96+
}
97+
98+
builderType := fmt.Sprintf("darwin-amd64-10_%d", minor)
99+
key, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), "keys", builderType))
100+
if err != nil {
101+
return err
102+
}
103+
104+
// Find the top-level datastore directory hosting the vmdk COW disk for
105+
// the linked clone. This is usually named "osx_9_frozen", but may be named
106+
// with a "_1", "_2", etc suffix. Search for it.
107+
netAppDir, err := findFrozenDir(ctx, minor)
108+
if err != nil {
109+
return fmt.Errorf("failed to find osx_%d_frozen base directory: %v", minor, err)
110+
}
111+
112+
hostNum, hostWhich, err := st.pickHost()
113+
if err != nil {
114+
return err
115+
}
116+
name := fmt.Sprintf("mac_10_%v_host%d%s", minor, hostNum, hostWhich)
117+
118+
if err := govc(ctx, "vm.create",
119+
"-m", "4096",
120+
"-c", "6",
121+
"-on=false",
122+
"-net", "dvPortGroup-Private", // 10.50.0.0/16
123+
"-g", guestType,
124+
// Put the config on the host's datastore, which
125+
// forces the VM to run on that host:
126+
"-ds", fmt.Sprintf("BOOT_%d", hostNum),
127+
name,
128+
); err != nil {
129+
return err
130+
}
131+
defer func() {
132+
if err != nil {
133+
err := govc(ctx, "vm.destroy", name)
134+
if err != nil {
135+
log.Printf("failed to destroy %v: %v", name, err)
136+
}
137+
}
138+
}()
139+
140+
if err := govc(ctx, "vm.change",
141+
"-e", "smc.present=TRUE",
142+
"-e", "ich7m.present=TRUE",
143+
"-e", "firmware=efi",
144+
"-e", fmt.Sprintf("guestinfo.key-%s=%s", builderType, strings.TrimSpace(string(key))),
145+
"-e", "guestinfo.name="+name,
146+
"-vm", name,
147+
); err != nil {
148+
return err
149+
}
150+
151+
if err := govc(ctx, "device.usb.add", "-vm", name); err != nil {
152+
return err
153+
}
154+
155+
if err := govc(ctx, "vm.disk.attach",
156+
"-vm", name,
157+
"-link=true",
158+
"-persist=false",
159+
"-ds=NetApp-1",
160+
"-disk", fmt.Sprintf("%s/osx_%d_frozen.vmdk", netAppDir, minor),
161+
); err != nil {
162+
return err
163+
}
164+
165+
if err := govc(ctx, "vm.power", "-on", name); err != nil {
166+
return err
167+
}
168+
log.Printf("Success.")
169+
return nil
170+
}
171+
172+
// govc runs "govc <args...>" and ignores its output, unless there's an error.
173+
func govc(ctx context.Context, args ...string) error {
174+
fmt.Fprintf(os.Stderr, "$ govc %v\n", strings.Join(args, " "))
175+
out, err := exec.CommandContext(ctx, "govc", args...).CombinedOutput()
176+
if err != nil {
177+
return fmt.Errorf("govc %s ...: %v, %s", args[0], err, out)
178+
}
179+
return nil
180+
}
181+
182+
const hostIPPrefix = "10.88.203." // with fourth octet starting at 10
183+
184+
// st.mu must be held.
185+
func (st *State) pickHost() (hostNum int, hostWhich string, err error) {
186+
for ip, inUse := range st.Hosts {
187+
if !strings.HasPrefix(ip, hostIPPrefix) {
188+
continue
189+
}
190+
if inUse >= 2 {
191+
// Apple policy.
192+
continue
193+
}
194+
hostNum, err = strconv.Atoi(strings.TrimPrefix(ip, hostIPPrefix))
195+
if err != nil {
196+
return 0, "", err
197+
}
198+
hostNum -= 10 // 10.88.203.11 is "BOOT_1" datastore.
199+
hostWhich = "a" // unless in use
200+
if st.whichAInUse(hostNum) {
201+
hostWhich = "b"
202+
}
203+
return
204+
}
205+
return 0, "", errors.New("no usable host found")
206+
}
207+
208+
// whichAInUse reports whether a VM is running on the provided hostNum named
209+
// with suffix "_host<n>a".
210+
//
211+
// st.mu must be held
212+
func (st *State) whichAInUse(hostNum int) bool {
213+
suffix := fmt.Sprintf("_host%da", hostNum)
214+
for name := range st.VMHost {
215+
if strings.HasSuffix(name, suffix) {
216+
return true
217+
}
218+
}
219+
return false
220+
}
221+
222+
// getStat queries govc to find the current state of the hosts and VMs.
223+
func getState(ctx context.Context) (*State, error) {
224+
st := &State{
225+
VMHost: make(map[string]string),
226+
Hosts: make(map[string]int),
227+
HostIP: make(map[string]string),
228+
}
229+
230+
var hosts elementList
231+
if err := govcJSONDecode(ctx, &hosts, "ls", "-json", "/MacStadium-ATL/host/MacMini_Cluster"); err != nil {
232+
return nil, fmt.Errorf("Reading /MacStadium-ATL/host/MacMini_Cluster: %v", err)
233+
}
234+
for _, h := range hosts.Elements {
235+
if h.Object.Self.Type == "HostSystem" {
236+
ip := path.Base(h.Path)
237+
st.Hosts[ip] = 0
238+
st.HostIP[h.Object.Self.Value] = ip
239+
}
240+
}
241+
242+
var vms elementList
243+
if err := govcJSONDecode(ctx, &vms, "ls", "-json", "/MacStadium-ATL/vm"); err != nil {
244+
return nil, fmt.Errorf("Reading /MacStadium-ATL/vm: %v", err)
245+
}
246+
for _, h := range vms.Elements {
247+
if h.Object.Self.Type == "VirtualMachine" {
248+
name := path.Base(h.Path)
249+
hostID := h.Object.Runtime.Host.Value
250+
hostIP := st.HostIP[hostID]
251+
st.VMHost[name] = hostIP
252+
if hostIP != "" && strings.HasPrefix(name, "mac_10_") {
253+
st.Hosts[hostIP]++
254+
}
255+
}
256+
}
257+
258+
return st, nil
259+
}
260+
261+
// objRef is a VMWare "Managed Object Reference".
262+
type objRef struct {
263+
Type string // e.g. "VirtualMachine"
264+
Value string // e.g. "host-12"
265+
}
266+
267+
type elementList struct {
268+
Elements []*elementJSON `json:"elements"`
269+
}
270+
271+
type elementJSON struct {
272+
Path string
273+
Object struct {
274+
Self objRef
275+
Runtime struct {
276+
Host objRef // for VMs; not present otherwise
277+
}
278+
}
279+
}
280+
281+
// govcJSONDecode runs "govc <args...>" and decodes its JSON output into dst.
282+
func govcJSONDecode(ctx context.Context, dst interface{}, args ...string) error {
283+
cmd := exec.CommandContext(ctx, "govc", args...)
284+
stdout, err := cmd.StdoutPipe()
285+
if err != nil {
286+
return err
287+
}
288+
if err := cmd.Start(); err != nil {
289+
return err
290+
}
291+
err = json.NewDecoder(stdout).Decode(dst)
292+
cmd.Process.Kill() // usually unnecessary
293+
if werr := cmd.Wait(); werr != nil && err == nil {
294+
err = werr
295+
}
296+
return err
297+
}
298+
299+
// findFrozenDir returns the name of the top-level directory on the
300+
// NetApp-1 shared datastore containing a directory starting with
301+
// "osx_<minor>_frozen". It might be that just that, or have a suffix
302+
// like "_1" or "_2".
303+
func findFrozenDir(ctx context.Context, minor int) (string, error) {
304+
out, err := exec.CommandContext(ctx, "govc", "datastore.ls", "-ds=NetApp-1").Output()
305+
if err != nil {
306+
return "", err
307+
}
308+
prefix := fmt.Sprintf("osx_%d_frozen", minor)
309+
for _, dir := range strings.Fields(string(out)) {
310+
if strings.HasPrefix(dir, prefix) {
311+
return dir, nil
312+
}
313+
}
314+
return "", os.ErrNotExist
315+
}

0 commit comments

Comments
 (0)