blob: 5a338af19ce1eb0e17ba37b48d2a1f0bc3858d6d [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
The makemac command starts OS X VMs for the builders.
It is currently just a thin wrapper around govc.
See https://github.com/vmware/govmomi/tree/master/govc
Usage:
$ makemac <osx_minor_version> # e.g, 8, 9, 10, 11, 12
*/
package main
import (
"bufio"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/build/types"
)
func usage() {
fmt.Fprintf(os.Stderr, `Usage:
makemac <osx_minor_version>
makemac -status
makemac -auto
`)
os.Exit(1)
}
var (
flagStatus = flag.Bool("status", false, "print status only")
flagAuto = flag.Bool("auto", false, "Automatically create & destroy as needed, reacting to https://farmer.golang.org/status/reverse.json status.")
flagListen = flag.String("listen", ":8713", "HTTP status port; used by auto mode only")
flagNuke = flag.Bool("destroy-all", false, "immediately destroy all running Mac VMs")
flagBaseDisk = flag.Int("base-disk", 0, "debug mode: if non-zero, print base disk of macOS 10.<value> VM and exit")
)
func main() {
flag.Parse()
numArg := flag.NArg()
ctx := context.Background()
if *flagBaseDisk != 0 {
baseDisk, err := findBaseDisk(ctx, *flagBaseDisk)
if err != nil {
log.Fatal(err)
}
fmt.Println(baseDisk)
return
}
if *flagStatus {
numArg++
}
if *flagAuto {
numArg++
}
if *flagNuke {
numArg++
}
if numArg != 1 {
usage()
}
if *flagAuto {
autoLoop()
return
}
if *flagNuke {
state, err := getState(ctx)
if err != nil {
log.Fatal(err)
}
if err := state.DestroyAllMacs(ctx); err != nil {
log.Fatal(err)
}
return
}
minor, err := strconv.Atoi(flag.Arg(0))
if err != nil && !*flagStatus {
usage()
}
state, err := getState(ctx)
if err != nil {
log.Fatal(err)
}
if *flagStatus {
stj, _ := json.MarshalIndent(state, "", " ")
fmt.Printf("%s\n", stj)
return
}
_, err = state.CreateMac(ctx, minor)
if err != nil {
log.Fatal(err)
}
}
// State is the state of the world.
type State struct {
mu sync.Mutex
Hosts map[string]int // IP address -> running Mac VM count (including 0)
VMHost map[string]string // "mac_10_8_host02b" => "10.0.0.0"
HostIP map[string]string // "host-5" -> "10.0.0.0"
VMInfo map[string]VMInfo // "mac_10_8_host02b" => ...
// VMOfSlot maps from a "slot name" to the VMWare VM name.
//
// A slot name is a tuple of (host number, "a"|"b"), where "a"
// and "b" are the two possible guests that can run per host.
// This slot name of the form "macstadium_host02b" is what's
// reported as the host name to the coordinator.
//
// The map value is the VMWare vm name, such as "mac_10_8_host02b",
// and is the map key of VMHost and VMInfo above.
VMOfSlot map[string]string // "macstadium_host02b" => "mac_10_8_host02b"
}
type VMInfo struct {
IP string
BootTime time.Time
// SlotName is the name of a place where we can run a VM.
// As of 2017-08-04 we have 20 slots total over 10 physical
// machines. (Two VMs per physical Mac Mini running ESXi)
// We use slot names of the form "macstadium_host02b"
// with a %02d digit host number and suffix 'a' and 'b'
// for which VM it is on that host.
//
// This slot name is also the name passed to the build
// coordinator as the coordinator's "host name". (which exists
// both for debugging, and for monitoring last-seen/uptime of
// dedicated builders.)
SlotName string
}
// NumCreatableVMs returns the number of VMs that can be created given
// the current capacity.
func (st *State) NumCreatableVMs() int {
st.mu.Lock()
defer st.mu.Unlock()
n := 0
for _, cur := range st.Hosts {
if cur < 2 {
n += 2 - cur
}
}
return n
}
// NumMacVMsOfVersion reports how many VMs are running Mac OS X 10.<ver>.
func (st *State) NumMacVMsOfVersion(ver int) int {
st.mu.Lock()
defer st.mu.Unlock()
prefix := fmt.Sprintf("mac_10_%v_", ver)
n := 0
for name := range st.VMInfo {
if strings.HasPrefix(name, prefix) {
n++
}
}
return n
}
// DestroyAllMacs runs "govc vm.destroy" on each running Mac VM.
func (st *State) DestroyAllMacs(ctx context.Context) error {
st.mu.Lock()
defer st.mu.Unlock()
var ret error
for name := range st.VMInfo {
log.Printf("destroying %s ...", name)
err := govc(ctx, "vm.destroy", name)
log.Printf("vm.destroy(%q) = %v", name, err)
if err != nil && ret == nil {
ret = err
}
}
return ret
}
// CreateMac creates an Mac VM running OS X 10.<minor>.
func (st *State) CreateMac(ctx context.Context, minor int) (slotName string, err error) {
// TODO(bradfitz): return VM name, update state, etc.
st.mu.Lock()
defer st.mu.Unlock()
var guestType string
switch minor {
case 8:
guestType = "darwin12_64Guest"
case 9:
guestType = "darwin13_64Guest"
case 10:
guestType = "darwin14_64Guest"
case 11:
guestType = "darwin15_64Guest"
case 12:
guestType = "darwin16_64Guest"
case 13:
// High Sierra. Requires vSphere 6.7.
// https://www.virtuallyghetto.com/2018/04/new-vsphere-6-7-apis-worth-checking-out.html
guestType = "darwin17_64Guest"
case 14:
// Mojave. Requires vSphere 6.7.
// https://www.virtuallyghetto.com/2018/04/new-vsphere-6-7-apis-worth-checking-out.html
guestType = "darwin18_64Guest"
default:
return "", fmt.Errorf("unsupported makemac minor OS X version %d", minor)
}
builderType := fmt.Sprintf("darwin-amd64-10_%d", minor)
// Up to 10.12 we used the deprecated buildlet --reverse mode, instead of --reverse-type.
// Starting with 10.14 (and 10.13 if we ever make a High Sierra image), we're switching
// to the non-deprecated mode.
if minor >= 13 {
builderType = fmt.Sprintf("host-darwin-10_%d", minor)
}
key, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), "keys", builderType))
if err != nil {
return "", err
}
baseDisk, err := findBaseDisk(ctx, minor)
if err != nil {
return "", fmt.Errorf("failed to find osx_%d_frozen_nfs base disk: %v", minor, err)
}
hostNum, hostWhich, err := st.pickHost()
if err != nil {
return "", err
}
name := fmt.Sprintf("mac_10_%v_host%02d%s", minor, hostNum, hostWhich)
slotName = fmt.Sprintf("macstadium_host%02d%s", hostNum, hostWhich)
if err := govc(ctx, "vm.create",
"-m", "4096",
"-c", "6",
"-on=false",
"-net", "dvPortGroup-Private", // 10.50.0.0/16
"-g", guestType,
// Put the config on the host's datastore, which
// forces the VM to run on that host:
"-ds", fmt.Sprintf("BOOT_%d", hostNum),
name,
); err != nil {
return "", err
}
defer func() {
if err != nil {
err := govc(ctx, "vm.destroy", name)
if err != nil {
log.Printf("failed to destroy %v: %v", name, err)
}
}
}()
if err := govc(ctx, "vm.change",
"-e", "smc.present=TRUE",
"-e", "ich7m.present=TRUE",
"-e", "firmware=efi",
"-e", fmt.Sprintf("guestinfo.key-%s=%s", builderType, strings.TrimSpace(string(key))),
"-e", "guestinfo.name="+name,
"-vm", name,
); err != nil {
return "", err
}
if err := govc(ctx, "device.usb.add", "-vm", name); err != nil {
return "", err
}
if err := govc(ctx, "vm.disk.attach",
"-vm", name,
"-link=true",
"-persist=false",
"-ds=GGLGLN-A-001-STV1",
"-disk", baseDisk,
); err != nil {
return "", err
}
if err := govc(ctx, "vm.power", "-on", name); err != nil {
return "", err
}
log.Printf("Success.")
return slotName, nil
}
// govc runs "govc <args...>" and ignores its output, unless there's an error.
func govc(ctx context.Context, args ...string) error {
fmt.Fprintf(os.Stderr, "$ govc %v\n", strings.Join(args, " "))
out, err := exec.CommandContext(ctx, "govc", args...).CombinedOutput()
if err != nil {
if isFileSystemReadOnly() {
out = append(out, "; filesystem is read-only"...)
}
return fmt.Errorf("govc %s ...: %v, %s", args[0], err, out)
}
return nil
}
const hostIPPrefix = "10.88.203." // with fourth octet starting at 10
var errNoHost = errors.New("no usable host found")
// st.mu must be held.
func (st *State) pickHost() (hostNum int, hostWhich string, err error) {
for ip, inUse := range st.Hosts {
if !strings.HasPrefix(ip, hostIPPrefix) {
continue
}
if inUse >= 2 {
// Apple policy.
continue
}
hostNum, err = strconv.Atoi(strings.TrimPrefix(ip, hostIPPrefix))
if err != nil {
return 0, "", err
}
hostNum -= 10 // 10.88.203.11 is "BOOT_1" datastore.
hostWhich = "a" // unless in use
if st.whichAInUse(hostNum) {
hostWhich = "b"
}
return
}
return 0, "", errNoHost
}
// whichAInUse reports whether a VM is running on the provided hostNum named
// with suffix "_host<%02d>a", hostnum.
//
// st.mu must be held
func (st *State) whichAInUse(hostNum int) bool {
suffix := fmt.Sprintf("_host%02da", hostNum)
for name := range st.VMHost {
if strings.HasSuffix(name, suffix) {
return true
}
}
return false
}
// getStat queries govc to find the current state of the hosts and VMs.
func getState(ctx context.Context) (*State, error) {
st := &State{
VMHost: make(map[string]string),
Hosts: make(map[string]int),
HostIP: make(map[string]string),
VMInfo: make(map[string]VMInfo),
VMOfSlot: make(map[string]string),
}
var hosts elementList
if err := govcJSONDecode(ctx, &hosts, "ls", "-json", "/MacStadium-ATL/host/MacMini_Cluster"); err != nil {
return nil, fmt.Errorf("getState: reading /MacStadium-ATL/host/MacMini_Cluster: %v", err)
}
for _, h := range hosts.Elements {
if h.Object.Self.Type == "HostSystem" {
ip := path.Base(h.Path)
st.Hosts[ip] = 0
st.HostIP[h.Object.Self.Value] = ip
}
}
var vms elementList
if err := govcJSONDecode(ctx, &vms, "ls", "-json", "/MacStadium-ATL/vm"); err != nil {
return nil, fmt.Errorf("getState: reading /MacStadium-ATL/vm: %v", err)
}
for _, h := range vms.Elements {
if h.Object.Self.Type != "VirtualMachine" {
continue
}
name := path.Base(h.Path)
hostID := h.Object.Runtime.Host.Value
hostIP := st.HostIP[hostID]
st.VMHost[name] = hostIP
if hostIP != "" && strings.HasPrefix(name, "mac_10_") {
st.Hosts[hostIP]++
var bootTime time.Time
if bt := h.Object.Summary.Runtime.BootTime; bt != "" {
bootTime, _ = time.Parse(time.RFC3339, bt)
}
var slotName string
if p := strings.Index(name, "_host"); p != -1 {
slotName = "macstadium" + name[p:] // macstadium_host02a
if exist := st.VMOfSlot[slotName]; exist != "" {
// Should never happen, but just in case.
log.Printf("ERROR: existing VM %q found in slot %q; destroying later VM %q", exist, slotName, name)
err := govc(ctx, "vm.destroy", name)
log.Printf("vm.destroy(%q) = %v", name, err)
} else {
st.VMOfSlot[slotName] = name // macstadium_host02a => mac_10_8_host02a
}
}
vi := VMInfo{
IP: hostIP,
BootTime: bootTime,
SlotName: slotName,
}
st.VMInfo[name] = vi
}
}
return st, nil
}
// objRef is a VMWare "Managed Object Reference".
type objRef struct {
Type string // e.g. "VirtualMachine"
Value string // e.g. "host-12"
}
type elementList struct {
Elements []*elementJSON `json:"elements"`
}
type elementJSON struct {
Path string
Object struct {
Self objRef
Runtime struct {
Host objRef // for VMs; not present otherwise
}
Summary struct {
Runtime struct {
BootTime string // time.RFC3339 format, or empty if not running
}
}
}
}
// govcJSONDecode runs "govc <args...>" and decodes its JSON output into dst.
func govcJSONDecode(ctx context.Context, dst interface{}, args ...string) error {
cmd := exec.CommandContext(ctx, "govc", args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
err = json.NewDecoder(stdout).Decode(dst)
if werr := cmd.Wait(); werr != nil && err == nil {
err = werr
}
return err
}
// findBaseDisk returns the path of the vmdk of the most recent
// snapshot of the osx_$(minor)_frozen_nfs VM.
func findBaseDisk(ctx context.Context, minor int) (string, error) {
vmName := fmt.Sprintf("osx_%d_frozen_nfs", minor)
out, err := exec.CommandContext(ctx, "govc", "vm.info", "-json", vmName).Output()
if err != nil {
return "", err
}
var ret struct {
VirtualMachines []struct {
Layout struct {
Snapshot []struct {
SnapshotFile []string
}
}
}
}
if err := json.Unmarshal(out, &ret); err != nil {
return "", fmt.Errorf("failed to parse vm.info JSON to find base disk: %v", err)
}
if n := len(ret.VirtualMachines); n != 1 {
if n == 0 {
return "", fmt.Errorf("VM %s not found", vmName)
}
return "", fmt.Errorf("len(ret.VirtualMachines) = %d; want 1 in JSON to find base disk: %v", n, err)
}
vm := ret.VirtualMachines[0]
if len(vm.Layout.Snapshot) < 1 {
return "", fmt.Errorf("VM %s does not have any snapshots; needs at least one", vmName)
}
ss := vm.Layout.Snapshot[len(vm.Layout.Snapshot)-1] // most recent snapshot is last in list
// Now find the first vmdk file, without its [datastore] prefix. The files are listed like:
/*
"SnapshotFile": [
"[GGLGLN-A-001-STV1] osx_14_frozen_nfs/osx_14_frozen_nfs-Snapshot2.vmsn",
"[GGLGLN-A-001-STV1] osx_14_frozen_nfs/osx_14_frozen_nfs_15.vmdk",
"[GGLGLN-A-001-STV1] osx_14_frozen_nfs/osx_14_frozen_nfs_15-000001.vmdk"
]
*/
for _, f := range ss.SnapshotFile {
if strings.HasSuffix(f, ".vmdk") {
i := strings.Index(f, "] ")
if i == -1 {
return "", fmt.Errorf("unexpected vmdk line %q in SnapshotFile", f)
}
return f[i+2:], nil
}
}
return "", fmt.Errorf("no VMDK found in snapshot for %v", vmName)
}
const autoAdjustTimeout = 5 * time.Minute
var status struct {
sync.Mutex
lastCheck time.Time
lastLog string
lastState *State
warnings []string
errors []string
}
func init() {
http.HandleFunc("/stage0/", handleStage0)
http.HandleFunc("/buildlet.darwin-amd64", handleBuildlet)
http.Handle("/", onlyAtRoot{http.HandlerFunc(handleStatus)}) // legacy status location
http.HandleFunc("/status", handleStatus)
}
func dedupLogf(format string, args ...interface{}) {
s := fmt.Sprintf(format, args...)
status.Lock()
defer status.Unlock()
if s == status.lastLog {
return
}
status.lastLog = s
log.Print(s)
}
func autoLoop() {
if addr := *flagListen; addr != "" {
go func() {
if err := http.ListenAndServe(*flagListen, nil); err != nil {
log.Fatalf("ListenAndServe: %v", err)
}
}()
}
for {
timer := time.AfterFunc(autoAdjustTimeout, watchdogFail)
autoAdjust()
timer.Stop()
time.Sleep(2 * time.Second)
}
}
func watchdogFail() {
stacks := make([]byte, 1<<20)
stacks = stacks[:runtime.Stack(stacks, true)]
log.Fatalf("timeout after %v waiting for autoAdjust(). stacks:\n%s",
autoAdjustTimeout, stacks)
}
func autoAdjust() {
status.Lock()
status.lastCheck = time.Now()
status.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), autoAdjustTimeout)
defer cancel()
st, err := getState(ctx)
if err != nil {
status.Lock()
status.errors = []string{err.Error()}
status.Unlock()
log.Print(err)
return
}
var warnings, errors []string
defer func() {
// Set status.lastState once we're now longer using it.
if st != nil {
status.Lock()
status.lastState = st
status.warnings = warnings
status.errors = errors
status.Unlock()
}
}()
req, _ := http.NewRequest("GET", "https://farmer.golang.org/status/reverse.json", nil)
req = req.WithContext(ctx)
res, err := http.DefaultClient.Do(req)
if err != nil {
errors = append(errors, fmt.Sprintf("getting /status/reverse.json from coordinator: %v", err))
log.Printf("getting reverse status: %v", err)
return
}
defer res.Body.Close()
var rstat types.ReverseBuilderStatus
if err := json.NewDecoder(res.Body).Decode(&rstat); err != nil {
errors = append(errors, fmt.Sprintf("decoding /status/reverse.json from coordinator: %v", err))
log.Printf("decoding reverse.json: %v", err)
return
}
revHost := make(map[string]*types.ReverseBuilder)
for hostType, hostStatus := range rstat.HostTypes {
if !strings.HasPrefix(hostType, "host-darwin-10") {
continue
}
for name, revBuild := range hostStatus.Machines {
revHost[name] = revBuild
}
}
// Destroy running VMs that appear to be dead and not connected to the coordinator.
// TODO: do these all concurrently.
dirty := false
for name, vi := range st.VMInfo {
if vi.BootTime.After(time.Now().Add(-3 * time.Minute)) {
// Recently created. It takes about a minute
// to boot and connect to the coordinator, so
// give it 3 minutes of grace before killing
// it.
continue
}
rh := revHost[name]
if rh == nil {
// Look it up by its slot name instead.
rh = revHost[vi.SlotName]
}
if rh == nil {
log.Printf("Destroying VM %q unknown to coordinator...", name)
err := govc(ctx, "vm.destroy", name)
log.Printf("vm.destroy(%q) = %v", name, err)
dirty = true
if err != nil {
warnings = append(warnings, fmt.Sprintf("vm.destroy(%q) = %v", name, err))
}
}
}
for {
if dirty {
st, err = getState(ctx)
if err != nil {
errors = append(errors, err.Error())
log.Print(err)
return
}
}
canCreate := st.NumCreatableVMs()
if canCreate <= 0 {
dedupLogf("All Mac VMs running.")
return
}
ver := wantedMacVersionNext(st, &rstat)
if ver == 0 {
dedupLogf("Have capacity for %d more Mac VMs, but none requested by coordinator.", canCreate)
return
}
dedupLogf("Have capacity for %d more Mac VMs; creating requested 10.%d ...", canCreate, ver)
slotName, err := st.CreateMac(ctx, ver)
if err != nil {
errStr := fmt.Sprintf("Error creating 10.%d: %v", ver, err)
errors = append(errors, errStr)
log.Print(errStr)
return
}
log.Printf("Created 10.%d VM on %q", ver, slotName)
dirty = true
}
}
// wantedMacVersionNext returns the macOS 10.x version to create next,
// or 0 to not make anything. It gets the latest reverse buildlet
// status from the coordinator.
func wantedMacVersionNext(st *State, rstat *types.ReverseBuilderStatus) int {
// TODO: improve this logic at some point, probably when the
// coordinator has a proper scheduler (Issue 19178) and when
// the coordinator keeps 1 of each builder type ready to go.
// For now just use the static configuration in
// dashboard/builders.go of how many are expected, which ends
// up in ReverseBuilderStatus.
for hostType, hostStatus := range rstat.HostTypes {
if !strings.HasPrefix(hostType, "host-darwin-10_") {
continue
}
ver, err := strconv.Atoi(strings.TrimPrefix(hostType, "host-darwin-10_"))
if err != nil {
log.Printf("ERROR: unexpected host type %q", hostType)
continue
}
want := hostStatus.Expect - st.NumMacVMsOfVersion(ver)
if want > 0 {
return ver
}
}
return 0
}
func handleStatus(w http.ResponseWriter, r *http.Request) {
status.Lock()
defer status.Unlock()
w.Header().Set("Content-Type", "application/json")
// Locking the lastState shouldn't matter since we
// currently only set status.lastState once the
// *Status is no longer in use, but lock it anyway, in
// case usage changes in the future.
if st := status.lastState; st != nil {
st.mu.Lock()
defer st.mu.Unlock()
}
// TODO: probably more status, as needed.
res := &struct {
LastCheck string
LastLog string
LastState *State
Warnings []string
Errors []string
}{
LastCheck: status.lastCheck.UTC().Format(time.RFC3339),
LastLog: status.lastLog,
LastState: status.lastState,
Warnings: status.warnings,
Errors: status.errors,
}
j, _ := json.MarshalIndent(res, "", "\t")
w.Write(j)
}
// handleStage0 serves the shell script for buildlets to run on boot, based
// on their macOS version.
//
// Starting with the macOS 10.14 (Mojave) image, their baked-in stage0.sh
// script does:
//
// while true; do (curl http://10.50.0.2:8713/stage0/$(sw_vers -productVersion)| sh); sleep 5; done
func handleStage0(w http.ResponseWriter, r *http.Request) {
// ver will be like "10.14.4"
// Nothing currently uses this, but it might be useful in the future.
ver := strings.TrimPrefix(r.RequestURI, "/stage0/")
_ = ver
fmt.Fprintf(w, "set -e\nset -x\n")
fmt.Fprintf(w, "export GO_BUILDER_ENV=macstadium_vm\n")
fmt.Fprintf(w, "curl -o buildlet http://10.50.0.2:8713/buildlet.darwin-amd64\n")
fmt.Fprintf(w, "chmod +x buildlet; ./buildlet")
}
func handleBuildlet(w http.ResponseWriter, r *http.Request) {
bin, err := getLatestMacBuildlet(r.Context())
if err != nil {
log.Printf("error getting buildlet from GCS: %v", err)
http.Error(w, "error getting buildlet from GCS", 500)
}
w.Header().Set("Content-Length", fmt.Sprint(len(bin)))
w.Write(bin)
}
// buildlet binary caching by its last seen ETag from HEAD responses
var (
buildletMu sync.Mutex
lastEtag string
lastBuildlet []byte // last buildlet binary for lastEtag
)
func getLatestMacBuildlet(ctx context.Context) (bin []byte, err error) {
req, _ := http.NewRequest("HEAD", "https://storage.googleapis.com/go-builder-data/buildlet.darwin-amd64", nil)
req = req.WithContext(ctx)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("%s from HEAD to %s", res.Status, req.URL)
}
etag := res.Header.Get("Etag")
if etag == "" {
return nil, fmt.Errorf("HEAD of %s lacked ETag", req.URL)
}
buildletMu.Lock()
if etag == lastEtag {
bin = lastBuildlet
log.Printf("served cached buildlet of %s", etag)
buildletMu.Unlock()
return bin, nil
}
buildletMu.Unlock()
log.Printf("fetching buildlet from GCS...")
req, _ = http.NewRequest("GET", "https://storage.googleapis.com/go-builder-data/buildlet.darwin-amd64", nil)
req = req.WithContext(ctx)
res, err = http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("%s from GET to %s", res.Status, req.URL)
}
etag = res.Header.Get("Etag")
log.Printf("fetched buildlet from GCS with etag %s", etag)
if etag == "" {
return nil, fmt.Errorf("GET of %s lacked ETag", req.URL)
}
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
buildletMu.Lock()
defer buildletMu.Unlock()
lastEtag = etag
lastBuildlet = slurp
return lastBuildlet, nil
}
// onlyAtRoot is an http.Handler wrapper that enforces that it's
// called at /, else it serves a 404.
type onlyAtRoot struct{ h http.Handler }
func (h onlyAtRoot) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
h.h.ServeHTTP(w, r)
}
func isFileSystemReadOnly() bool {
f, err := os.Open("/proc/mounts")
if err != nil {
return false
}
defer f.Close()
// Look for line:
// /dev/sda1 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
bs := bufio.NewScanner(f)
for bs.Scan() {
f := strings.Fields(bs.Text())
if len(f) < 4 {
continue
}
mountPoint, state := f[1], f[3]
if mountPoint == "/" {
return strings.HasPrefix(state, "ro,")
}
}
return false
}