all: split builder config into builder & host configs

Our builders are named of the form "GOOS-GOARCH" or
"GOOS-GOARCH-suffix".

Over time we've grown many builders. This CL doesn't change
that. Builders continue to be named and operate as before.

Previously the build configuration file (dashboard/builders.go) made
each builder type ("linux-amd64-race", etc) define how to create a
host running a buildlet of that type, even though many builders had
identical host configs. For example, these builders all share the same
host type (a Kubernetes container):

   linux-amd64
   linux-amd64-race
   linux-386
   linux-386-387

And these are the same host type (a GCE VM):

   windows-amd64-gce
   windows-amd64-race
   windows-386-gce

This CL creates a new concept of a "hostType" which defines how
the buildlet is created (Kube, GCE, Reverse, and how), and then each
builder itself references a host type.

Users never see the hostType. (except perhaps in gomote list output)
But they at least never need to care about them.

Reverse buildlets now can only be one hostType at a time, which
simplifies things. We were no longer using multiple roles per machine
once moving to VMs for OS X.

gomote continues to operate as it did previously but its underlying
protocol changed and clients will need to be updated. As a new
feature, gomote now has a new flag to let you reuse a buildlet host
connection for different builder rules if they share the same
underlying host type. But users can ignore that.

This CL is a long-standing TODO (previously attempted and aborted) and
will make many things easier and faster, including the linux-arm
cross-compilation effort, and keeping pre-warmed buildlets of VM types
ready to go.

Updates golang/go#17104

Change-Id: Iad8387f48680424a8441e878a2f4762bf79ea4d2
Reviewed-on: https://go-review.googlesource.com/29551
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
diff --git a/buildlet/gce.go b/buildlet/gce.go
index be99551..129739a 100644
--- a/buildlet/gce.go
+++ b/buildlet/gce.go
@@ -74,12 +74,15 @@
 
 // StartNewVM boots a new VM on GCE and returns a buildlet client
 // configured to speak to it.
-func StartNewVM(ts oauth2.TokenSource, instName, builderType string, opts VMOpts) (*Client, error) {
+func StartNewVM(ts oauth2.TokenSource, instName, hostType string, opts VMOpts) (*Client, error) {
 	computeService, _ := compute.New(oauth2.NewClient(oauth2.NoContext, ts))
 
-	conf, ok := dashboard.Builders[builderType]
+	hconf, ok := dashboard.Hosts[hostType]
 	if !ok {
-		return nil, fmt.Errorf("invalid builder type %q", builderType)
+		return nil, fmt.Errorf("invalid host type %q", hostType)
+	}
+	if !hconf.IsGCE() {
+		return nil, fmt.Errorf("host type %q is not a GCE host type", hostType)
 	}
 
 	zone := opts.Zone
@@ -96,9 +99,9 @@
 	usePreempt := false
 Try:
 	prefix := "https://www.googleapis.com/compute/v1/projects/" + projectID
-	machType := prefix + "/zones/" + zone + "/machineTypes/" + conf.MachineType()
+	machType := prefix + "/zones/" + zone + "/machineTypes/" + hconf.MachineType()
 	diskType := "https://www.googleapis.com/compute/v1/projects/" + projectID + "/zones/" + zone + "/diskTypes/pd-ssd"
-	if conf.RegularDisk {
+	if hconf.RegularDisk {
 		diskType = "" // a spinning disk
 	}
 
@@ -126,7 +129,7 @@
 				Type:       "PERSISTENT",
 				InitializeParams: &compute.AttachedDiskInitializeParams{
 					DiskName:    instName,
-					SourceImage: "https://www.googleapis.com/compute/v1/projects/" + projectID + "/global/images/" + conf.VMImage,
+					SourceImage: "https://www.googleapis.com/compute/v1/projects/" + projectID + "/global/images/" + hconf.VMImage,
 					DiskType:    diskType,
 				},
 			},
@@ -159,8 +162,8 @@
 	// which the VMs are configured to download at boot and run.
 	// This lets us/ update the buildlet more easily than
 	// rebuilding the whole VM image.
-	addMeta("buildlet-binary-url", conf.BuildletBinaryURL(buildenv.ByProjectID(opts.ProjectID)))
-	addMeta("builder-type", builderType)
+	addMeta("buildlet-binary-url", hconf.BuildletBinaryURL(buildenv.ByProjectID(opts.ProjectID)))
+	addMeta("buildlet-host-type", hostType)
 	if !opts.TLS.IsZero() {
 		addMeta("tls-cert", opts.TLS.CertPEM)
 		addMeta("tls-key", opts.TLS.KeyPEM)
@@ -304,7 +307,7 @@
 	Name   string
 	IPPort string
 	TLS    KeyPair
-	Type   string
+	Type   string // buildlet type
 }
 
 // ListVMs lists all VMs.
@@ -329,13 +332,13 @@
 				meta[it.Key] = *it.Value
 			}
 		}
-		builderType := meta["builder-type"]
-		if builderType == "" {
+		hostType := meta["buildlet-host-type"]
+		if hostType == "" {
 			continue
 		}
 		vm := VM{
 			Name: inst.Name,
-			Type: builderType,
+			Type: hostType,
 			TLS: KeyPair{
 				CertPEM: meta["tls-cert"],
 				KeyPEM:  meta["tls-key"],
diff --git a/buildlet/kube.go b/buildlet/kube.go
index c539a65..4cdd2c9 100644
--- a/buildlet/kube.go
+++ b/buildlet/kube.go
@@ -67,10 +67,10 @@
 
 // StartPod creates a new pod on a Kubernetes cluster and returns a buildlet client
 // configured to speak to it.
-func StartPod(ctx context.Context, kubeClient *kubernetes.Client, podName, builderType string, opts PodOpts) (*Client, error) {
-	conf, ok := dashboard.Builders[builderType]
+func StartPod(ctx context.Context, kubeClient *kubernetes.Client, podName, hostType string, opts PodOpts) (*Client, error) {
+	conf, ok := dashboard.Hosts[hostType]
 	if !ok || conf.KubeImage == "" {
-		return nil, fmt.Errorf("invalid builder type %q", builderType)
+		return nil, fmt.Errorf("invalid builder type %q", hostType)
 	}
 	pod := &api.Pod{
 		TypeMeta: api.TypeMeta{
@@ -81,7 +81,7 @@
 			Name: podName,
 			Labels: map[string]string{
 				"name": podName,
-				"type": builderType,
+				"type": hostType,
 				"role": "buildlet",
 			},
 			Annotations: map[string]string{},
@@ -132,7 +132,7 @@
 	// This lets us/ update the buildlet more easily than
 	// rebuilding the whole pod image.
 	addEnv("META_BUILDLET_BINARY_URL", conf.BuildletBinaryURL(buildenv.ByProjectID(opts.ProjectID)))
-	addEnv("META_BUILDER_TYPE", builderType)
+	addEnv("META_BUILDLET_HOST_TYPE", hostType)
 	if !opts.TLS.IsZero() {
 		addEnv("META_TLS_CERT", opts.TLS.CertPEM)
 		addEnv("META_TLS_KEY", opts.TLS.KeyPEM)
diff --git a/buildlet/remote.go b/buildlet/remote.go
index 8de2165..c949a57 100644
--- a/buildlet/remote.go
+++ b/buildlet/remote.go
@@ -58,17 +58,25 @@
 	return cc.hc, nil
 }
 
-// CreateBuildlet creates a new buildlet of the given type on cc.
+// CreateBuildlet creates a new buildlet of the given builder type on
+// cc.
+//
+// This takes a builderType (instead of a hostType), but the
+// returned buildlet can be used as any builder that has the same
+// underlying buildlet type. For instance, a linux-amd64 buildlet can
+// act as either linux-amd64 or linux-386-387.
+//
 // It may expire at any time.
 // To release it, call Client.Destroy.
-func (cc *CoordinatorClient) CreateBuildlet(buildletType string) (*Client, error) {
+func (cc *CoordinatorClient) CreateBuildlet(builderType string) (*Client, error) {
 	hc, err := cc.client()
 	if err != nil {
 		return nil, err
 	}
 	ipPort, _ := cc.instance().TLSHostPort() // must succeed if client did
 	form := url.Values{
-		"type": {buildletType},
+		"version":     {"20160922"}, // checked by cmd/coordinator/remote.go
+		"builderType": {builderType},
 	}
 	req, _ := http.NewRequest("POST",
 		"https://"+ipPort+"/buildlet/create",
@@ -100,10 +108,11 @@
 }
 
 type RemoteBuildlet struct {
-	Type    string // "openbsd-386"
-	Name    string // "buildlet-adg-openbsd-386-2"
-	Created time.Time
-	Expires time.Time
+	HostType    string // "host-linux-kubestd"
+	BuilderType string // "linux-386-387"
+	Name        string // "buildlet-adg-openbsd-386-2"
+	Created     time.Time
+	Expires     time.Time
 }
 
 func (cc *CoordinatorClient) RemoteBuildlets() ([]RemoteBuildlet, error) {
@@ -113,7 +122,6 @@
 	}
 	ipPort, _ := cc.instance().TLSHostPort() // must succeed if client did
 	req, _ := http.NewRequest("GET", "https://"+ipPort+"/buildlet/list", nil)
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.SetBasicAuth(cc.Auth.Username, cc.Auth.Password)
 	res, err := hc.Do(req)
 	if err != nil {
diff --git a/cmd/buildlet/Makefile b/cmd/buildlet/Makefile
index 8139853..4bd5f2d 100644
--- a/cmd/buildlet/Makefile
+++ b/cmd/buildlet/Makefile
@@ -58,16 +58,16 @@
 
 dev-buildlet.linux-arm: FORCE
 	go install golang.org/x/build/cmd/upload
-	upload --verbose --osarch=$@ --file=go:golang.org/x/build/cmd/buildlet --public dev-go-builder-data/$@
+	upload --verbose --osarch=$@ --file=go:golang.org/x/build/cmd/buildlet --public dev-go-builder-data/buildlet.linux-arm
 
 dev-buildlet.linux-amd64: FORCE
 	go install golang.org/x/build/cmd/upload
-	upload --verbose --osarch=$@ --file=go:golang.org/x/build/cmd/buildlet --public dev-go-builder-data/$@
+	upload --verbose --osarch=$@ --file=go:golang.org/x/build/cmd/buildlet --public dev-go-builder-data/buildlet.linux-amd64
 
 dev-buildlet.windows-amd64: FORCE
 	go install golang.org/x/build/cmd/upload
-	upload --verbose --osarch=$@ --file=go:golang.org/x/build/cmd/buildlet --public dev-go-builder-data/$@
+	upload --verbose --osarch=$@ --file=go:golang.org/x/build/cmd/buildlet --public dev-go-builder-data/buildlet.windows-amd64
 
 dev-buildlet.plan9-386: FORCE
 	go install golang.org/x/build/cmd/upload
-	upload --verbose --osarch=$@ --file=go:golang.org/x/build/cmd/buildlet --public dev-go-builder-data/$@
+	upload --verbose --osarch=$@ --file=go:golang.org/x/build/cmd/buildlet --public dev-go-builder-data/buildlet.plan9-386
diff --git a/cmd/buildlet/buildlet.go b/cmd/buildlet/buildlet.go
index 1a65859..5b5e730 100644
--- a/cmd/buildlet/buildlet.go
+++ b/cmd/buildlet/buildlet.go
@@ -509,7 +509,7 @@
 		defer res.Body.Close()
 		if res.StatusCode != http.StatusOK {
 			log.Printf("writetgz: failed to fetch tgz URL %s: status=%v", urlStr, res.Status)
-			http.Error(w, fmt.Sprintf("fetching provided url: %s", res.Status), http.StatusInternalServerError)
+			http.Error(w, fmt.Sprintf("writetgz: fetching provided URL %q: %s", urlStr, res.Status), http.StatusInternalServerError)
 			return
 		}
 		tgz = res.Body
@@ -736,7 +736,14 @@
 		f.Flush()
 	}
 
-	env := append(baseEnv(), r.PostForm["env"]...)
+	goarch := "amd64" // unless we find otherwise
+	for _, pair := range r.PostForm["env"] {
+		if hasPrefixFold(pair, "GOARCH=") {
+			goarch = pair[len("GOARCH="):]
+		}
+	}
+
+	env := append(baseEnv(goarch), r.PostForm["env"]...)
 	env = envutil.Dedup(runtime.GOOS == "windows", env)
 	env = setPathEnv(env, r.PostForm["path"], *workDir)
 
@@ -869,21 +876,17 @@
 	}
 }
 
-func baseEnv() []string {
+func baseEnv(goarch string) []string {
 	if runtime.GOOS == "windows" {
-		return windowsBaseEnv()
+		return windowsBaseEnv(goarch)
 	}
 	return os.Environ()
 }
 
-func windowsBaseEnv() (e []string) {
+func windowsBaseEnv(goarch string) (e []string) {
 	e = append(e, "GOBUILDEXIT=1") // exit all.bat with completion status
-	btype, err := metadata.InstanceAttributeValue("builder-type")
-	if err != nil {
-		log.Fatalf("Failed to get builder-type: %v", err)
-		return nil
-	}
-	is64 := strings.HasPrefix(btype, "windows-amd64")
+
+	is64 := goarch != "386"
 	for _, pair := range os.Environ() {
 		const pathEq = "PATH="
 		if hasPrefixFold(pair, pathEq) {
@@ -1239,9 +1242,9 @@
 		log.Printf("Not on GCE; not remounting root filesystem.")
 		return
 	}
-	btype, err := metadata.InstanceAttributeValue("builder-type")
+	btype, err := metadata.InstanceAttributeValue("buildlet-type")
 	if _, ok := err.(metadata.NotDefinedError); ok && len(btype) == 0 {
-		log.Printf("Not remounting root filesystem due to missing builder-type metadata.")
+		log.Printf("Not remounting root filesystem due to missing buildlet-type metadata.")
 		return
 	}
 	if err != nil {
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index bb5fe08..ab74c01 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -51,6 +51,7 @@
 	"golang.org/x/build/livelog"
 	"golang.org/x/build/types"
 	"golang.org/x/net/context"
+	"golang.org/x/time/rate"
 )
 
 const subrepoPrefix = "golang.org/x/"
@@ -337,7 +338,9 @@
 		case work := <-workc:
 			if !mayBuildRev(work) {
 				if inStaging {
-					log.Printf("may not build %v; skipping", work)
+					if _, ok := dashboard.Builders[work.name]; ok && logCantBuildStaging.Allow() {
+						log.Printf("may not build %v; skipping", work)
+					}
 				}
 				continue
 			}
@@ -358,19 +361,23 @@
 func stagingClusterBuilders() map[string]dashboard.BuildConfig {
 	m := map[string]dashboard.BuildConfig{}
 	for _, name := range []string{
-		//		"linux-arm",
-		//		"linux-amd64",
-		"linux-amd64-kube",
-		//		"linux-amd64-race",
-		//		"windows-amd64-gce",
-		//		"plan9-386",
+		"linux-arm",
+		"linux-arm-arm5",
+		"linux-amd64",
+		"linux-386-387",
+		"windows-amd64-gce",
+		"windows-386-gce",
 	} {
-		m[name] = dashboard.Builders[name]
+		if c, ok := dashboard.Builders[name]; ok {
+			m[name] = c
+		} else {
+			panic(fmt.Sprintf("unknown builder %q", name))
+		}
 	}
 
 	// Also permit all the reverse buildlets:
 	for name, bc := range dashboard.Builders {
-		if bc.IsReverse {
+		if bc.IsReverse() {
 			m[name] = bc
 		}
 	}
@@ -390,6 +397,11 @@
 	return building
 }
 
+var (
+	logUnknownBuilder   = rate.NewLimiter(rate.Every(5*time.Second), 2)
+	logCantBuildStaging = rate.NewLimiter(rate.Every(1*time.Second), 2)
+)
+
 // mayBuildRev reports whether the build type & revision should be started.
 // It returns true if it's not already building, and if a reverse buildlet is
 // required, if an appropriate machine is registered.
@@ -403,11 +415,17 @@
 	if strings.Contains(rev.name, "netbsd") {
 		return false
 	}
-	buildConf := dashboard.Builders[rev.name]
-	if buildConf.IsReverse {
-		return reversePool.CanBuild(rev.name)
+	buildConf, ok := dashboard.Builders[rev.name]
+	if !ok {
+		if logUnknownBuilder.Allow() {
+			log.Printf("unknown builder %q", rev.name)
+		}
+		return false
 	}
-	if buildConf.KubeImage != "" && kubeErr != nil {
+	if buildConf.IsReverse() {
+		return reversePool.CanBuild(buildConf.HostType)
+	}
+	if buildConf.IsKube() && kubeErr != nil {
 		return false
 	}
 	return true
@@ -611,14 +629,19 @@
 // TODO(bradfitz): it also currently does not support subrepos.
 func findWorkLoop(work chan<- builderRev) {
 	// Useful for debugging a single run:
-	if inStaging && false {
+	if inStaging {
 		//work <- builderRev{name: "linux-arm", rev: "c9778ec302b2e0e0d6027e1e0fca892e428d9657", subName: "tools", subRev: "ac303766f5f240c1796eeea3dc9bf34f1261aa35"}
-		work <- builderRev{name: "linux-amd64", rev: "cdc0ebbebe64d8fa601914945112db306c85c426"}
-		log.Printf("Test work awaiting arm")
-		if false {
-			for !reversePool.CanBuild("linux-arm") {
+		const debugArm = false
+		if debugArm {
+			for !reversePool.CanBuild("buildlet-linux-arm") {
+				log.Printf("waiting for ARM to register.")
 				time.Sleep(time.Second)
 			}
+			log.Printf("ARM machine(s) registered.")
+			work <- builderRev{name: "linux-arm", rev: "3129c67db76bc8ee13a1edc38a6c25f9eddcbc6c"}
+		} else {
+			work <- builderRev{name: "windows-amd64-gce", rev: "3129c67db76bc8ee13a1edc38a6c25f9eddcbc6c"}
+			work <- builderRev{name: "windows-386-gce", rev: "3129c67db76bc8ee13a1edc38a6c25f9eddcbc6c"}
 		}
 
 		// Still run findWork but ignore what it does.
@@ -1183,21 +1206,23 @@
 type BuildletPool interface {
 	// GetBuildlet returns a new buildlet client.
 	//
-	// The machineType is the machine type (e.g. "linux-amd64-race").
+	// The hostType is the key into the dashboard.Hosts
+	// map (such as "host-linux-kubestd"), NOT the buidler type
+	// ("linux-386").
 	//
 	// Users of GetBuildlet must both call Client.Close when done
 	// with the client as well as cancel the provided Context.
 	//
 	// The ctx may have context values of type buildletTimeoutOpt
 	// and highPriorityOpt.
-	GetBuildlet(ctx context.Context, machineType string, lg logger) (*buildlet.Client, error)
+	GetBuildlet(ctx context.Context, hostType string, lg logger) (*buildlet.Client, error)
 
 	String() string // TODO(bradfitz): more status stuff
 }
 
 // GetBuildlets creates up to n buildlets and sends them on the returned channel
 // before closing the channel.
-func GetBuildlets(ctx context.Context, pool BuildletPool, n int, machineType string, lg logger) <-chan *buildlet.Client {
+func GetBuildlets(ctx context.Context, pool BuildletPool, n int, hostType string, lg logger) <-chan *buildlet.Client {
 	ch := make(chan *buildlet.Client) // NOT buffered
 	var wg sync.WaitGroup
 	wg.Add(n)
@@ -1205,11 +1230,11 @@
 		go func(i int) {
 			defer wg.Done()
 			sp := lg.createSpan("get_helper", fmt.Sprintf("helper %d/%d", i+1, n))
-			bc, err := pool.GetBuildlet(ctx, machineType, lg)
+			bc, err := pool.GetBuildlet(ctx, hostType, lg)
 			sp.done(err)
 			if err != nil {
 				if err != context.Canceled {
-					log.Printf("failed to get a %s buildlet: %v", machineType, err)
+					log.Printf("failed to get a %s buildlet: %v", hostType, err)
 				}
 				return
 			}
@@ -1231,13 +1256,16 @@
 }
 
 func poolForConf(conf dashboard.BuildConfig) BuildletPool {
-	if conf.VMImage != "" {
+	switch {
+	case conf.IsGCE():
 		return gcePool
-	}
-	if conf.KubeImage != "" {
+	case conf.IsKube():
 		return kubePool // Kubernetes
+	case conf.IsReverse():
+		return reversePool
+	default:
+		panic(fmt.Sprintf("no buildlet pool for builder type %q", conf.Name))
 	}
-	return reversePool
 }
 
 func newBuild(rev builderRev) (*buildStatus, error) {
@@ -1276,20 +1304,8 @@
 	}()
 }
 
-func (st *buildStatus) buildletType() string {
-	if v := st.conf.BuildletType; v != "" {
-		return v
-	}
-	return st.conf.Name
-}
-
-func (st *buildStatus) buildletPool() (BuildletPool, error) {
-	buildletType := st.buildletType()
-	bconf, ok := dashboard.Builders[buildletType]
-	if !ok {
-		return nil, fmt.Errorf("invalid BuildletType %q for %q", buildletType, st.conf.Name)
-	}
-	return poolForConf(bconf), nil
+func (st *buildStatus) buildletPool() BuildletPool {
+	return poolForConf(st.conf)
 }
 
 func (st *buildStatus) expectedMakeBashDuration() time.Duration {
@@ -1312,7 +1328,7 @@
 	// TODO: move this to dashboard/builders.go? But once we based on on historical
 	// measurements, it'll need GCE services (bigtable/bigquery?), so it's probably
 	// better in this file.
-	pool, _ := st.buildletPool()
+	pool := st.buildletPool()
 	switch pool.(type) {
 	case *gceBuildletPool:
 		return time.Minute
@@ -1341,7 +1357,7 @@
 // ready, such that they're ready when make.bash is done. But we don't
 // want to start too early, lest we waste idle resources during make.bash.
 func (st *buildStatus) getHelpersReadySoon() {
-	if st.isSubrepo() || st.conf.NumTestHelpers(st.isTry()) == 0 || st.conf.IsReverse {
+	if st.isSubrepo() || st.conf.NumTestHelpers(st.isTry()) == 0 || st.conf.IsReverse() {
 		return
 	}
 	time.AfterFunc(st.expectedMakeBashDuration()-st.expectedBuildletStartDuration(),
@@ -1359,8 +1375,8 @@
 }
 
 func (st *buildStatus) onceInitHelpersFunc() {
-	pool, _ := st.buildletPool() // won't return an error since we called it already
-	st.helpers = GetBuildlets(st.ctx, pool, st.conf.NumTestHelpers(st.isTry()), st.buildletType(), st)
+	pool := st.buildletPool()
+	st.helpers = GetBuildlets(st.ctx, pool, st.conf.NumTestHelpers(st.isTry()), st.conf.HostType, st)
 }
 
 // We should try to build from a snapshot if this is a subrepo build, we can
@@ -1378,12 +1394,9 @@
 
 func (st *buildStatus) build() error {
 	st.buildRecord().put()
-	pool, err := st.buildletPool()
-	if err != nil {
-		return err
-	}
+	pool := st.buildletPool()
 	sp := st.createSpan("get_buildlet")
-	bc, err := pool.GetBuildlet(st.ctx, st.buildletType(), st)
+	bc, err := pool.GetBuildlet(st.ctx, st.conf.HostType, st)
 	sp.done(err)
 	if err != nil {
 		return fmt.Errorf("failed to get a buildlet: %v", err)
diff --git a/cmd/coordinator/debug.go b/cmd/coordinator/debug.go
index 4e84fcb..1ace760 100644
--- a/cmd/coordinator/debug.go
+++ b/cmd/coordinator/debug.go
@@ -26,7 +26,7 @@
 		if r.Method == "GET" {
 			w.Header().Set("Content-Type", "text/html; charset=utf-8")
 			buf := new(bytes.Buffer)
-			if err := tmplDoSomeWork.Execute(buf, reversePool.Modes()); err != nil {
+			if err := tmplDoSomeWork.Execute(buf, reversePool.HostTypes()); err != nil {
 				http.Error(w, fmt.Sprintf("dosomework: %v", err), http.StatusInternalServerError)
 			}
 			buf.WriteTo(w)
diff --git a/cmd/coordinator/gce.go b/cmd/coordinator/gce.go
index 44e7231..ddfc937 100644
--- a/cmd/coordinator/gce.go
+++ b/cmd/coordinator/gce.go
@@ -234,13 +234,13 @@
 	p.disabled = !enabled
 }
 
-func (p *gceBuildletPool) GetBuildlet(ctx context.Context, typ string, lg logger) (bc *buildlet.Client, err error) {
-	conf, ok := dashboard.Builders[typ]
+func (p *gceBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg logger) (bc *buildlet.Client, err error) {
+	hconf, ok := dashboard.Hosts[hostType]
 	if !ok {
-		return nil, fmt.Errorf("gcepool: unknown buildlet type %q", typ)
+		return nil, fmt.Errorf("gcepool: unknown host type %q", hostType)
 	}
 	qsp := lg.createSpan("awaiting_gce_quota")
-	err = p.awaitVMCountQuota(ctx, conf.GCENumCPU())
+	err = p.awaitVMCountQuota(ctx, hconf.GCENumCPU())
 	qsp.done(err)
 	if err != nil {
 		return nil, err
@@ -251,7 +251,7 @@
 		deleteIn = vmDeleteTimeout
 	}
 
-	instName := "buildlet-" + typ + "-rn" + randHex(7)
+	instName := "buildlet-" + strings.TrimPrefix(hostType, "host-") + "-rn" + randHex(7)
 	p.setInstanceUsed(instName, true)
 
 	gceBuildletSpan := lg.createSpan("create_gce_buildlet", instName)
@@ -264,11 +264,11 @@
 		curSpan      = createSpan // either instSpan or waitBuildlet
 	)
 
-	log.Printf("Creating GCE VM %q for %s", instName, typ)
-	bc, err = buildlet.StartNewVM(tokenSource, instName, typ, buildlet.VMOpts{
+	log.Printf("Creating GCE VM %q for %s", instName, hostType)
+	bc, err = buildlet.StartNewVM(tokenSource, instName, hostType, buildlet.VMOpts{
 		ProjectID:   buildEnv.ProjectName,
 		Zone:        buildEnv.Zone,
-		Description: fmt.Sprintf("Go Builder for %s", typ),
+		Description: fmt.Sprintf("Go Builder for %s", hostType),
 		DeleteIn:    deleteIn,
 		OnInstanceRequested: func() {
 			log.Printf("GCE VM %q now booting", instName)
@@ -295,10 +295,10 @@
 	})
 	if err != nil {
 		curSpan.done(err)
-		log.Printf("Failed to create VM for %s: %v", typ, err)
+		log.Printf("Failed to create VM for %s: %v", hostType, err)
 		if needDelete {
 			deleteVM(buildEnv.Zone, instName)
-			p.putVMCountQuota(conf.GCENumCPU())
+			p.putVMCountQuota(hconf.GCENumCPU())
 		}
 		p.setInstanceUsed(instName, false)
 		return nil, err
@@ -306,12 +306,12 @@
 	waitBuildlet.done(nil)
 	bc.SetDescription("GCE VM: " + instName)
 	bc.SetOnHeartbeatFailure(func() {
-		p.putBuildlet(bc, typ, instName)
+		p.putBuildlet(bc, hostType, instName)
 	})
 	return bc, nil
 }
 
-func (p *gceBuildletPool) putBuildlet(bc *buildlet.Client, typ, instName string) error {
+func (p *gceBuildletPool) putBuildlet(bc *buildlet.Client, hostType, instName string) error {
 	// TODO(bradfitz): add the buildlet to a freelist (of max N
 	// items) for up to 10 minutes since when it got started if
 	// it's never seen a command execution failure, and we can
@@ -324,11 +324,11 @@
 	deleteVM(buildEnv.Zone, instName)
 	p.setInstanceUsed(instName, false)
 
-	conf, ok := dashboard.Builders[typ]
+	hconf, ok := dashboard.Hosts[hostType]
 	if !ok {
 		panic("failed to lookup conf") // should've worked if we did it before
 	}
-	p.putVMCountQuota(conf.GCENumCPU())
+	p.putVMCountQuota(hconf.GCENumCPU())
 	return nil
 }
 
diff --git a/cmd/coordinator/kube.go b/cmd/coordinator/kube.go
index ebc78fb..e2d98a3 100644
--- a/cmd/coordinator/kube.go
+++ b/cmd/coordinator/kube.go
@@ -350,10 +350,10 @@
 	}
 }
 
-func (p *kubeBuildletPool) GetBuildlet(ctx context.Context, typ string, lg logger) (*buildlet.Client, error) {
-	conf, ok := dashboard.Builders[typ]
-	if !ok || conf.KubeImage == "" {
-		return nil, fmt.Errorf("kubepool: invalid builder type %q", typ)
+func (p *kubeBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg logger) (*buildlet.Client, error) {
+	hconf, ok := dashboard.Hosts[hostType]
+	if !ok || !hconf.IsKube() {
+		return nil, fmt.Errorf("kubepool: invalid host type %q", hostType)
 	}
 	if kubeErr != nil {
 		return nil, kubeErr
@@ -367,19 +367,19 @@
 		deleteIn = podDeleteTimeout
 	}
 
-	podName := "buildlet-" + typ + "-rn" + randHex(7)
+	podName := "buildlet-" + strings.TrimPrefix(hostType, "host-") + "-rn" + randHex(7)
 
 	// Get an estimate for when the pod will be started/running and set
 	// the context timeout based on that
 	var needDelete bool
 
 	lg.logEventTime("creating_kube_pod", podName)
-	log.Printf("Creating Kubernetes pod %q for %s", podName, typ)
+	log.Printf("Creating Kubernetes pod %q for %s", podName, hostType)
 
-	bc, err := buildlet.StartPod(ctx, kubeClient, podName, typ, buildlet.PodOpts{
+	bc, err := buildlet.StartPod(ctx, kubeClient, podName, hostType, buildlet.PodOpts{
 		ProjectID:     buildEnv.ProjectName,
 		ImageRegistry: registryPrefix,
-		Description:   fmt.Sprintf("Go Builder for %s at %s", typ),
+		Description:   fmt.Sprintf("Go Builder for %s at %s", hostType),
 		DeleteIn:      deleteIn,
 		OnPodCreating: func() {
 			lg.logEventTime("pod_creating")
@@ -593,7 +593,9 @@
 				}
 			}
 		}
-		log.Printf("Kubernetes pod cleanup loop stats: %+v", stats)
+		if stats.Pods > 0 {
+			log.Printf("Kubernetes pod cleanup loop stats: %+v", stats)
+		}
 		time.Sleep(time.Minute)
 	}
 }
diff --git a/cmd/coordinator/remote.go b/cmd/coordinator/remote.go
index 901fa79..a597615 100644
--- a/cmd/coordinator/remote.go
+++ b/cmd/coordinator/remote.go
@@ -43,11 +43,12 @@
 }
 
 type remoteBuildlet struct {
-	User    string // "user-foo" build key
-	Name    string // dup of key
-	Type    string
-	Created time.Time
-	Expires time.Time
+	User        string // "user-foo" build key
+	Name        string // dup of key
+	HostType    string
+	BuilderType string // default builder config to use if not overwritten
+	Created     time.Time
+	Expires     time.Time
 
 	buildlet *buildlet.Client
 }
@@ -57,7 +58,7 @@
 	defer remoteBuildlets.Unlock()
 	n := 0
 	for {
-		name = fmt.Sprintf("%s-%s-%d", rb.User, rb.Type, n)
+		name = fmt.Sprintf("%s-%s-%d", rb.User, rb.BuilderType, n)
 		if _, ok := remoteBuildlets.m[name]; ok {
 			n++
 		} else {
@@ -86,14 +87,19 @@
 		http.Error(w, "POST required", 400)
 		return
 	}
-	typ := r.FormValue("type")
-	if typ == "" {
-		http.Error(w, "missing 'type' parameter", 400)
+	const serverVersion = "20160922" // sent by cmd/gomote via buildlet/remote.go
+	if version := r.FormValue("version"); version < serverVersion {
+		http.Error(w, fmt.Sprintf("gomote client version %q is too old; predates server version %q", version, serverVersion), 400)
 		return
 	}
-	bconf, ok := dashboard.Builders[typ]
+	builderType := r.FormValue("builderType")
+	if builderType == "" {
+		http.Error(w, "missing 'builderType' parameter", 400)
+		return
+	}
+	bconf, ok := dashboard.Builders[builderType]
 	if !ok {
-		http.Error(w, "unknown builder type in 'type' parameter", 400)
+		http.Error(w, "unknown builder type in 'builderType' parameter", 400)
 		return
 	}
 	user, _, _ := r.BasicAuth()
@@ -115,12 +121,12 @@
 	resc := make(chan *buildlet.Client)
 	errc := make(chan error)
 	go func() {
-		bc, err := pool.GetBuildlet(ctx, typ, loggerFunc(func(event string, optText ...string) {
+		bc, err := pool.GetBuildlet(ctx, bconf.HostType, loggerFunc(func(event string, optText ...string) {
 			var extra string
 			if len(optText) > 0 {
 				extra = " " + optText[0]
 			}
-			log.Printf("creating buildlet %s for %s: %s%s", typ, user, event, extra)
+			log.Printf("creating buildlet %s for %s: %s%s", bconf.HostType, user, event, extra)
 		}))
 		if bc != nil {
 			resc <- bc
@@ -132,11 +138,12 @@
 		select {
 		case bc := <-resc:
 			rb := &remoteBuildlet{
-				User:     user,
-				Type:     typ,
-				buildlet: bc,
-				Created:  time.Now(),
-				Expires:  time.Now().Add(remoteBuildletIdleTimeout),
+				User:        user,
+				BuilderType: builderType,
+				HostType:    bconf.HostType,
+				buildlet:    bc,
+				Created:     time.Now(),
+				Expires:     time.Now().Add(remoteBuildletIdleTimeout),
 			}
 			rb.Name = addRemoteBuildlet(rb)
 			jenc, err := json.MarshalIndent(rb, "", "  ")
@@ -165,7 +172,7 @@
 // always wrapped in requireBuildletProxyAuth.
 func handleBuildletList(w http.ResponseWriter, r *http.Request) {
 	if r.Method != "GET" {
-		http.Error(w, "POST required", 400)
+		http.Error(w, "GET required", 400)
 		return
 	}
 	res := make([]*remoteBuildlet, 0) // so it's never JSON "null"
diff --git a/cmd/coordinator/reverse.go b/cmd/coordinator/reverse.go
index 4ec79eb..325325e 100644
--- a/cmd/coordinator/reverse.go
+++ b/cmd/coordinator/reverse.go
@@ -42,6 +42,7 @@
 	"time"
 
 	"golang.org/x/build/buildlet"
+	"golang.org/x/build/dashboard"
 	"golang.org/x/build/revdial"
 	"golang.org/x/net/context"
 )
@@ -63,28 +64,24 @@
 
 var errInUse = errors.New("all buildlets are in use")
 
-func (p *reverseBuildletPool) tryToGrab(machineType string) (*buildlet.Client, error) {
+func (p *reverseBuildletPool) tryToGrab(hostType string) (*buildlet.Client, error) {
 	p.mu.Lock()
 	defer p.mu.Unlock()
-	usableCount := 0
+	candidates := 0
 	for _, b := range p.buildlets {
-		usable := false
-		for _, m := range b.modes {
-			if m == machineType {
-				usable = true
-				usableCount++
-				break
-			}
+		isCandidate := b.hostType == hostType
+		if isCandidate {
+			candidates++
 		}
-		if usable && b.inUseAs == "" {
+		if isCandidate && !b.inUse {
 			// Found an unused match.
-			b.inUseAs = machineType
+			b.inUse = true
 			b.inUseTime = time.Now()
 			return b.client, nil
 		}
 	}
-	if usableCount == 0 {
-		return nil, fmt.Errorf("no buildlets registered for machine type %q", machineType)
+	if candidates == 0 {
+		return nil, fmt.Errorf("no buildlets registered for host type %q", hostType)
 	}
 	return nil, errInUse
 }
@@ -127,14 +124,15 @@
 		return false
 	}
 	p.mu.Lock()
-	if b.inUseAs == "health" { // sanity check
+	if b.inHealthCheck { // sanity check
 		panic("previous health check still running")
 	}
-	if b.inUseAs != "" {
+	if b.inUse {
 		p.mu.Unlock()
 		return true // skip busy buildlets
 	}
-	b.inUseAs = "health"
+	b.inUse = true
+	b.inHealthCheck = true
 	b.inUseTime = time.Now()
 	res := make(chan error, 1)
 	go func() {
@@ -154,7 +152,7 @@
 
 	if err != nil {
 		// remove bad buildlet
-		log.Printf("Health check fail; removing reverse buildlet %s %v: %v", b.client, b.modes, err)
+		log.Printf("Health check fail; removing reverse buildlet %v (type %v): %v", b.hostname, b.hostType, err)
 		go b.client.Close()
 		go p.nukeBuildlet(b.client)
 		return false
@@ -163,11 +161,12 @@
 	p.mu.Lock()
 	defer p.mu.Unlock()
 
-	if b.inUseAs != "health" {
+	if !b.inHealthCheck {
 		// buildlet was grabbed while lock was released; harmless.
 		return true
 	}
-	b.inUseAs = ""
+	b.inUse = false
+	b.inHealthCheck = false
 	b.inUseTime = time.Now()
 	p.noteBuildletAvailable()
 	return true
@@ -178,23 +177,23 @@
 	highPriorityBuildlet   = make(map[string]chan *buildlet.Client)
 )
 
-func highPriChan(typ string) chan *buildlet.Client {
+func highPriChan(hostType string) chan *buildlet.Client {
 	highPriorityBuildletMu.Lock()
 	defer highPriorityBuildletMu.Unlock()
-	if c, ok := highPriorityBuildlet[typ]; ok {
+	if c, ok := highPriorityBuildlet[hostType]; ok {
 		return c
 	}
 	c := make(chan *buildlet.Client)
-	highPriorityBuildlet[typ] = c
+	highPriorityBuildlet[hostType] = c
 	return c
 }
 
-func (p *reverseBuildletPool) GetBuildlet(ctx context.Context, machineType string, lg logger) (*buildlet.Client, error) {
+func (p *reverseBuildletPool) GetBuildlet(ctx context.Context, hostType string, lg logger) (*buildlet.Client, error) {
 	seenErrInUse := false
 	isHighPriority, _ := ctx.Value(highPriorityOpt{}).(bool)
-	sp := lg.createSpan("wait_static_builder", machineType)
+	sp := lg.createSpan("wait_static_builder", hostType)
 	for {
-		b, err := p.tryToGrab(machineType)
+		b, err := p.tryToGrab(hostType)
 		if err == errInUse {
 			if !seenErrInUse {
 				lg.logEventTime("waiting_machine_in_use")
@@ -202,7 +201,7 @@
 			}
 			var highPri chan *buildlet.Client
 			if isHighPriority {
-				highPri = highPriChan(machineType)
+				highPri = highPriChan(hostType)
 			}
 			select {
 			case <-ctx.Done():
@@ -222,7 +221,7 @@
 			return nil, err
 		} else {
 			select {
-			case highPriChan(machineType) <- b:
+			case highPriChan(hostType) <- b:
 				// Somebody else was more important.
 			default:
 				sp.done(nil)
@@ -245,113 +244,84 @@
 }
 
 func (p *reverseBuildletPool) WriteHTMLStatus(w io.Writer) {
-	// total maps from a builder type to the number of machines which are
+	// total maps from a host type to the number of machines which are
 	// capable of that role.
 	total := make(map[string]int)
-	// inUse and inUseOther track the number of machines using machines.
-	// inUse is how many machines are building that type, and inUseOther counts
-	// how many machines are occupied doing a similar role on that hardware.
-	// e.g. "darwin-amd64-10_10" occupied as a "darwin-arm-a5ios",
-	// or "linux-arm" as a "linux-arm-arm5" count as inUseOther.
+	// inUse track the number of non-idle host types.
 	inUse := make(map[string]int)
-	inUseOther := make(map[string]int)
 
-	var machineBuf bytes.Buffer
+	var buf bytes.Buffer
 	p.mu.Lock()
 	buildlets := append([]*reverseBuildlet(nil), p.buildlets...)
-	sort.Sort(byModeThenHostname(buildlets))
+	sort.Sort(byTypeThenHostname(buildlets))
 	for _, b := range buildlets {
 		machStatus := "<i>idle</i>"
-		if b.inUseAs != "" {
-			machStatus = "working as <b>" + b.inUseAs + "</b>"
+		if b.inUse {
+			machStatus = "working"
 		}
-		fmt.Fprintf(&machineBuf, "<li>%s (%s) version %s, %s: connected %v, %s for %v</li>\n",
+		fmt.Fprintf(&buf, "<li>%s (%s) version %s, %s: connected %v, %s for %v</li>\n",
 			b.hostname,
 			b.conn.RemoteAddr(),
 			b.version,
-			strings.Join(b.modes, ", "),
+			b.hostType,
 			time.Since(b.regTime),
 			machStatus,
 			time.Since(b.inUseTime))
-		for _, mode := range b.modes {
-			if b.inUseAs != "" && b.inUseAs != "health" {
-				if mode == b.inUseAs {
-					inUse[mode]++
-				} else {
-					inUseOther[mode]++
-				}
-			}
-			total[mode]++
+		total[b.hostType]++
+		if b.inUse && !b.inHealthCheck {
+			inUse[b.hostType]++
 		}
 	}
 	p.mu.Unlock()
 
-	var modes []string
-	for mode := range total {
-		modes = append(modes, mode)
+	var typs []string
+	for typ := range total {
+		typs = append(typs, typ)
 	}
-	sort.Strings(modes)
+	sort.Strings(typs)
 
 	io.WriteString(w, "<b>Reverse pool summary</b><ul>")
-	if len(modes) == 0 {
+	if len(typs) == 0 {
 		io.WriteString(w, "<li>no connections</li>")
 	}
-	for _, mode := range modes {
-		use, other := inUse[mode], inUseOther[mode]
-		if use+other == 0 {
-			fmt.Fprintf(w, "<li>%s: 0/%d</li>", mode, total[mode])
-		} else {
-			fmt.Fprintf(w, "<li>%s: %d/%d (%d + %d other)</li>", mode, use+other, total[mode], use, other)
-		}
+	for _, typ := range typs {
+		fmt.Fprintf(w, "<li>%s: %d/%d</li>", typ, inUse[typ], total[typ])
 	}
 	io.WriteString(w, "</ul>")
 
-	fmt.Fprintf(w, "<b>Reverse pool machine detail</b><ul>%s</ul>", machineBuf.Bytes())
+	fmt.Fprintf(w, "<b>Reverse pool machine detail</b><ul>%s</ul>", buf.Bytes())
 }
 
 func (p *reverseBuildletPool) String() string {
+	// This doesn't currently show up anywhere, so ignore it for now.
+	return "TODO: some reverse buildlet summary"
+}
+
+// HostTypes returns the a deduplicated list of buildlet types curently supported
+// by the pool.
+func (p *reverseBuildletPool) HostTypes() (types []string) {
+	s := make(map[string]bool)
 	p.mu.Lock()
-	inUse := 0
-	total := len(p.buildlets)
 	for _, b := range p.buildlets {
-		if b.inUseAs != "" && b.inUseAs != "health" {
-			inUse++
-		}
+		s[b.hostType] = true
 	}
 	p.mu.Unlock()
 
-	return fmt.Sprintf("Reverse pool capacity: %d/%d %s", inUse, total, p.Modes())
+	for t := range s {
+		types = append(types, t)
+	}
+	sort.Strings(types)
+	return types
 }
 
-// Modes returns the a deduplicated list of buildlet modes curently supported
-// by the pool. Buildlet modes are described on reverseBuildlet comments.
-func (p *reverseBuildletPool) Modes() (modes []string) {
-	mm := make(map[string]bool)
-	p.mu.Lock()
-	for _, b := range p.buildlets {
-		for _, mode := range b.modes {
-			mm[mode] = true
-		}
-	}
-	p.mu.Unlock()
-
-	for mode := range mm {
-		modes = append(modes, mode)
-	}
-	sort.Strings(modes)
-	return modes
-}
-
-// CanBuild reports whether the pool has a machine capable of building mode.
-// The machine may be in use, so you may have to wait.
-func (p *reverseBuildletPool) CanBuild(mode string) bool {
+// CanBuild reports whether the pool has a machine capable of building mode,
+// even if said machine isn't currently idle.
+func (p *reverseBuildletPool) CanBuild(hostType string) bool {
 	p.mu.Lock()
 	defer p.mu.Unlock()
 	for _, b := range p.buildlets {
-		for _, m := range b.modes {
-			if m == mode {
-				return true
-			}
+		if b.hostType == hostType {
+			return true
 		}
 	}
 	return false
@@ -381,28 +351,17 @@
 	conn    net.Conn
 	regTime time.Time // when it was first connected
 
-	// modes is the set of valid modes for this buildlet.
-	//
-	// A mode is the equivalent of a builder name, for example
-	// "darwin-amd64", "android-arm", or "linux-amd64-race".
-	//
-	// Each buildlet may potentially have several modes. For example a
-	// Mac OS X machine with an attached iOS device may be registered
-	// as both "darwin-amd64", "darwin-arm64".
-	modes []string
+	// hostType is the configuration of this machine.
+	// It is the key into the dashboard.Hosts map.
+	hostType string
 
-	// inUseAs signifies that the buildlet is in use as the named mode.
+	// inUseAs signifies that the buildlet is in use.
 	// inUseTime is when it entered that state.
-	// Both are guarded by the mutex on reverseBuildletPool.
-	inUseAs   string
-	inUseTime time.Time
-}
-
-func (b *reverseBuildlet) firstMode() string {
-	if len(b.modes) == 0 {
-		return ""
-	}
-	return b.modes[0]
+	// inHealthCheck is whether it's inUse due to a health check.
+	// All three are guarded by the mutex on reverseBuildletPool.
+	inUse         bool
+	inUseTime     time.Time
+	inHealthCheck bool
 }
 
 func handleReverse(w http.ResponseWriter, r *http.Request) {
@@ -411,6 +370,8 @@
 		return
 	}
 	// Check build keys.
+
+	// modes can be either 1 buildlet type (new way) or builder mode(s) (the old way)
 	modes := r.Header["X-Go-Builder-Type"]
 	gobuildkeys := r.Header["X-Go-Builder-Key"]
 	if len(modes) == 0 || len(modes) != len(gobuildkeys) {
@@ -440,6 +401,9 @@
 		modes = filtered
 	}
 
+	// Collapse their modes down into a singluar hostType
+	hostType := mapBuilderToHostType(modes)
+
 	conn, bufrw, err := w.(http.Hijacker).Hijack()
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -447,8 +411,7 @@
 	}
 
 	revDialer := revdial.NewDialer(bufrw, conn)
-
-	log.Printf("Registering reverse buildlet %q (%s) for modes %v", hostname, r.RemoteAddr, modes)
+	log.Printf("Registering reverse buildlet %q (%s) for host type %v", hostname, r.RemoteAddr, hostType)
 
 	(&http.Response{StatusCode: http.StatusSwitchingProtocols, Proto: "HTTP/1.1"}).Write(conn)
 
@@ -460,7 +423,7 @@
 			},
 		},
 	})
-	client.SetDescription(fmt.Sprintf("reverse peer %s/%s for modes %v", hostname, r.RemoteAddr, modes))
+	client.SetDescription(fmt.Sprintf("reverse peer %s/%s for host type %v", hostname, r.RemoteAddr, hostType))
 
 	var isDead struct {
 		sync.Mutex
@@ -504,7 +467,7 @@
 	b := &reverseBuildlet{
 		hostname:  hostname,
 		version:   r.Header.Get("X-Go-Builder-Version"),
-		modes:     modes,
+		hostType:  hostType,
 		client:    client,
 		conn:      conn,
 		inUseTime: now,
@@ -516,15 +479,41 @@
 
 var registerBuildlet = func(modes []string) {} // test hook
 
-type byModeThenHostname []*reverseBuildlet
+type byTypeThenHostname []*reverseBuildlet
 
-func (s byModeThenHostname) Len() int      { return len(s) }
-func (s byModeThenHostname) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-func (s byModeThenHostname) Less(i, j int) bool {
+func (s byTypeThenHostname) Len() int      { return len(s) }
+func (s byTypeThenHostname) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s byTypeThenHostname) Less(i, j int) bool {
 	bi, bj := s[i], s[j]
-	mi, mj := bi.firstMode(), bj.firstMode()
-	if mi == mj {
+	ti, tj := bi.hostType, bj.hostType
+	if ti == tj {
 		return bi.hostname < bj.hostname
 	}
-	return mi < mj
+	return ti < tj
+}
+
+// mapBuilderToHostType maps from the user's Request.Header["X-Go-Builder-Type"]
+// mode list down into a single host type, or the empty string if unknown.
+func mapBuilderToHostType(modes []string) string {
+	// First, see if any of the provided modes are a host type.
+	// If so, this is an updated client.
+	for _, v := range modes {
+		if _, ok := dashboard.Hosts[v]; ok {
+			return v
+		}
+	}
+
+	// Else, it's an old client, still speaking in terms of
+	// builder names.  See if any are registered aliases. First
+	// one wins. (There are no ambiguities in the wild.)
+	for hostType, hconf := range dashboard.Hosts {
+		for _, alias := range hconf.ReverseAliases {
+			for _, v := range modes {
+				if v == alias {
+					return hostType
+				}
+			}
+		}
+	}
+	return ""
 }
diff --git a/cmd/gomote/list.go b/cmd/gomote/list.go
index b3af3ac..d36b598 100644
--- a/cmd/gomote/list.go
+++ b/cmd/gomote/list.go
@@ -34,7 +34,7 @@
 		log.Fatal(err)
 	}
 	for _, rb := range rbs {
-		fmt.Printf("%s\t%s\texpires in %v\n", rb.Name, rb.Type, rb.Expires.Sub(time.Now()))
+		fmt.Printf("%s\t%s\t%s\texpires in %v\n", rb.Name, rb.BuilderType, rb.HostType, rb.Expires.Sub(time.Now()))
 	}
 
 	return nil
@@ -50,9 +50,9 @@
 	var ok bool
 	for _, rb := range rbs {
 		if rb.Name == name {
-			conf, ok = namedConfig(rb.Type)
+			conf, ok = dashboard.Builders[rb.BuilderType]
 			if !ok {
-				err = fmt.Errorf("builder %q exists, but unknown type %q", name, rb.Type)
+				err = fmt.Errorf("builder %q exists, but unknown builder type %q", name, rb.BuilderType)
 				return
 			}
 			break
@@ -74,15 +74,3 @@
 	cc := coordinatorClient()
 	return cc.NamedBuildlet(name)
 }
-
-// namedConfig returns the builder configuration that matches the given mote
-// name. It matches prefixes to accommodate motes than have "-n" suffixes.
-func namedConfig(name string) (dashboard.BuildConfig, bool) {
-	match := ""
-	for cname := range dashboard.Builders {
-		if strings.HasPrefix(name, cname) && len(cname) > len(match) {
-			match = cname
-		}
-	}
-	return dashboard.Builders[match], match != ""
-}
diff --git a/cmd/gomote/run.go b/cmd/gomote/run.go
index a14f0bc..197d877 100644
--- a/cmd/gomote/run.go
+++ b/cmd/gomote/run.go
@@ -32,6 +32,8 @@
 	fs.StringVar(&path, "path", "", "Comma-separated list of ExecOpts.Path elements. The special string 'EMPTY' means to run without any $PATH. The empty string (default) does not modify the $PATH.")
 	var dir string
 	fs.StringVar(&dir, "dir", "", "Directory to run from. Defaults to the directory of the command, or the work directory if -system is true.")
+	var builderEnv string
+	fs.StringVar(&builderEnv, "builderenv", "", "Optional alternate builder to act like. Must share the same underlying buildlet host type, or it's an error. For instance, linux-amd64-race or linux-386-387 are compatible with linux-amd64, but openbsd-amd64 and openbsd-386 are different hosts.")
 
 	fs.Parse(args)
 	if fs.NArg() < 2 {
@@ -46,6 +48,18 @@
 		return err
 	}
 
+	if builderEnv != "" {
+		altConf, ok := dashboard.Builders[builderEnv]
+		if !ok {
+			return fmt.Errorf("unknown --builderenv=%q builder value", builderEnv)
+		}
+		if altConf.HostType != conf.HostType {
+			return fmt.Errorf("--builderEnv=%q has host type %q, which is not compatible with the named buildlet's host type %q",
+				builderEnv, altConf.HostType, conf.HostType)
+		}
+		conf = altConf
+	}
+
 	var pathOpt []string
 	if path == "EMPTY" {
 		pathOpt = []string{} // non-nil
diff --git a/cmd/release/release.go b/cmd/release/release.go
index 58708b0..b664e77 100644
--- a/cmd/release/release.go
+++ b/cmd/release/release.go
@@ -256,7 +256,7 @@
 	}
 
 	var hostArch string // non-empty if we're cross-compiling (s390x)
-	if b.MakeOnly && bc.KubeImage != "" && (bc.GOARCH() != "amd64" && bc.GOARCH() != "386") {
+	if b.MakeOnly && bc.IsKube() && (bc.GOARCH() != "amd64" && bc.GOARCH() != "386") {
 		hostArch = "amd64"
 	}
 
diff --git a/dashboard/builders.go b/dashboard/builders.go
index a36e966..15387c6 100644
--- a/dashboard/builders.go
+++ b/dashboard/builders.go
@@ -7,6 +7,7 @@
 package dashboard
 
 import (
+	"fmt"
 	"strconv"
 	"strings"
 
@@ -18,25 +19,258 @@
 // This map should not be modified by other packages.
 var Builders = map[string]BuildConfig{}
 
-// A BuildConfig describes how to run a builder.
-type BuildConfig struct {
-	// Name is the unique name of the builder, in the form of
-	// "darwin-386" or "linux-amd64-race".
-	Name string
+// Hosts contains the names and configs of all the types of
+// buildlets. They can be VMs, containers, or dedicated machines.
+var Hosts = map[string]*HostConfig{
+	"host-linux-kubestd": &HostConfig{
+		Notes:           "Kubernetes container on GKE.",
+		KubeImage:       "linux-x86-std:latest",
+		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
+		env:             []string{"GOROOT_BOOTSTRAP=/go1.4"},
+	},
+	"host-nacl-kube": &HostConfig{
+		Notes:           "Kubernetes container on GKE.",
+		KubeImage:       "linux-x86-nacl:latest",
+		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
+		env:             []string{"GOROOT_BOOTSTRAP=/go1.4"},
+	},
+	"host-s390x-cross-kube": &HostConfig{
+		Notes:           "Kubernetes container on GKE.",
+		KubeImage:       "linux-s390x-stretch:latest",
+		buildletURLTmpl: "https://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
+		env:             []string{"GOROOT_BOOTSTRAP=/go1.4"},
+	},
+	"host-linux-clang": &HostConfig{
+		Notes:           "GCE VM with clang.",
+		VMImage:         "linux-buildlet-clang",
+		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
+		env:             []string{"GOROOT_BOOTSTRAP=/go1.4"},
+	},
+	"host-linux-sid": &HostConfig{
+		Notes:           "GCE VM with Debian sid.",
+		VMImage:         "linux-buildlet-sid",
+		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
+		env:             []string{"GOROOT_BOOTSTRAP=/go1.4"},
+	},
+	"host-linux-arm": &HostConfig{
+		IsReverse:      true,
+		env:            []string{"GOROOT_BOOTSTRAP=/usr/local/go"},
+		ReverseAliases: []string{"linux-arm", "linux-arm-arm5"},
+	},
+	"host-openbsd-amd64-58-gce": &HostConfig{
+		VMImage:            "openbsd-amd64-58",
+		machineType:        "n1-highcpu-4",
+		buildletURLTmpl:    "https://storage.googleapis.com/$BUCKET/buildlet.openbsd-amd64",
+		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-openbsd-amd64-gce58.tar.gz",
+		Notes:              "OpenBSD 5.8; GCE VM is built from script in build/env/openbsd-amd64",
+	},
+	"host-openbsd-386-58-gce": &HostConfig{
+		VMImage:            "openbsd-386-58",
+		machineType:        "n1-highcpu-4",
+		buildletURLTmpl:    "https://storage.googleapis.com/$BUCKET/buildlet.openbsd-386",
+		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-openbsd-386-gce58.tar.gz",
+		Notes:              "OpenBSD 5.8; GCE VM is built from script in build/env/openbsd-386",
+	},
+	"host-freebsd-93-gce": &HostConfig{
+		VMImage:            "freebsd-amd64-gce93",
+		machineType:        "n1-highcpu-4",
+		buildletURLTmpl:    "https://storage.googleapis.com/$BUCKET/buildlet.freebsd-amd64",
+		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
+	},
+	"host-freebsd-101-gce": &HostConfig{
+		VMImage:            "freebsd-amd64-gce101",
+		Notes:              "FreeBSD 10.1; GCE VM is built from script in build/env/freebsd-amd64",
+		machineType:        "n1-highcpu-4",
+		buildletURLTmpl:    "http://storage.googleapis.com/$BUCKET/buildlet.freebsd-amd64", // TODO(bradfitz): why was this http instead of https?
+		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
+		env:                []string{"CC=clang"},
+	},
+	"host-netbsd-gce": &HostConfig{
+		VMImage:            "netbsd-amd64-gce",
+		Notes:              "NetBSD tip; GCE VM is built from script in build/env/netbsd-amd64",
+		machineType:        "n1-highcpu-2",
+		buildletURLTmpl:    "http://storage.googleapis.com/$BUCKET/buildlet.netbsd-amd64",
+		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/gobootstrap-netbsd-amd64.tar.gz",
+	},
+	"host-plan9-386-gce": &HostConfig{
+		VMImage:            "plan9-386-v3",
+		Notes:              "Plan 9 from 0intro; GCE VM is built from script in build/env/plan9-386",
+		buildletURLTmpl:    "http://storage.googleapis.com/$BUCKET/buildlet.plan9-386",
+		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/gobootstrap-plan9-386.tar.gz",
 
-	Notes       string // notes for humans
-	Owner       string // e.g. "bradfitz@golang.org", empty means golang-dev
-	VMImage     string // e.g. "openbsd-amd64-58"
-	KubeImage   string // e.g. "linux-buildlet-std:latest" (suffix after "gcr.io/<PROJ>/")
+		// We *were* using n1-standard-1 because Plan 9 can only
+		// reliably use a single CPU. Using 2 or 4 and we see
+		// test failures. See:
+		//    https://golang.org/issue/8393
+		//    https://golang.org/issue/9491
+		// n1-standard-1 has 3.6 GB of memory which WAS (see below)
+		// overkill (userspace probably only sees 2GB anyway),
+		// but it's the cheapest option. And plenty to keep
+		// our ~250 MB of inputs+outputs in its ramfs.
+		//
+		// But the docs says "For the n1 series of machine
+		// types, a virtual CPU is implemented as a single
+		// hyperthread on a 2.6GHz Intel Sandy Bridge Xeon or
+		// Intel Ivy Bridge Xeon (or newer) processor. This
+		// means that the n1-standard-2 machine type will see
+		// a whole physical core."
+		//
+		// ... so we used n1-highcpu-2 (1.80 RAM, still
+		// plenty), just so we can get 1 whole core for the
+		// single-core Plan 9. It will see 2 virtual cores and
+		// only use 1, but we hope that 1 will be more powerful
+		// and we'll stop timing out on tests.
+		machineType: "n1-highcpu-4",
+	},
+	"host-windows-gce": &HostConfig{
+		VMImage:            "windows-buildlet-v2",
+		machineType:        "n1-highcpu-4",
+		buildletURLTmpl:    "http://storage.googleapis.com/$BUCKET/buildlet.windows-amd64",
+		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-windows-amd64.tar.gz",
+		RegularDisk:        true,
+	},
+	"host-darwin-10_8": &HostConfig{
+		IsReverse: true,
+		Notes:     "MacStadium OS X 10.8 VM under VMWare ESXi",
+		env: []string{
+			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
+		},
+		ReverseAliases: []string{"darwin-amd64-10_8"},
+	},
+	"host-darwin-10_10": &HostConfig{
+		IsReverse: true,
+		Notes:     "MacStadium OS X 10.10 VM under VMWare ESXi",
+		env: []string{
+			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
+		},
+		ReverseAliases: []string{"darwin-amd64-10_10"},
+	},
+	"host-darwin-10_11": &HostConfig{
+		IsReverse: true,
+		Notes:     "MacStadium OS X 10.11 VM under VMWare ESXi",
+		env: []string{
+			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
+		},
+		ReverseAliases: []string{"darwin-amd64-10_11"},
+	},
+	"host-linux-s390x": &HostConfig{
+		Notes:          "run by IBM",
+		IsReverse:      true,
+		env:            []string{"GOROOT_BOOTSTRAP=/var/buildlet/go-linux-s390x-bootstrap"},
+		ReverseAliases: []string{"linux-s390x-ibm"},
+	},
+	"host-linux-ppc64le-osu": &HostConfig{
+		Notes:          "Debian jessie; run by Go team on osuosl.org",
+		IsReverse:      true,
+		env:            []string{"GOROOT_BOOTSTRAP=/usr/local/go-bootstrap"},
+		ReverseAliases: []string{"linux-ppc64le-buildlet"},
+	},
+	"host-linux-arm64-linaro": &HostConfig{
+		Notes:          "Ubuntu wily; run by Go team, from linaro",
+		IsReverse:      true,
+		env:            []string{"GOROOT_BOOTSTRAP=/usr/local/go-bootstrap"},
+		ReverseAliases: []string{"linux-arm64-buildlet"},
+	},
+	"host-solaris-amd64": &HostConfig{
+		Notes:          "run by Go team on Joyent, on a SmartOS 'infrastructure container'",
+		IsReverse:      true,
+		env:            []string{"GOROOT_BOOTSTRAP=/root/go-solaris-amd64-bootstrap"},
+		ReverseAliases: []string{"solaris-amd64-smartosbuildlet"},
+	},
+}
+
+func init() {
+	for key, c := range Hosts {
+		if key == "" {
+			panic("empty string key in Hosts")
+		}
+		if c.HostType == "" {
+			c.HostType = key
+		}
+		if c.HostType != key {
+			panic(fmt.Sprintf("HostType %q != key %q", c.HostType, key))
+		}
+		nSet := 0
+		if c.VMImage != "" {
+			nSet++
+		}
+		if c.KubeImage != "" {
+			nSet++
+		}
+		if c.IsReverse {
+			nSet++
+		}
+		if nSet != 1 {
+			panic(fmt.Sprintf("exactly one of VMImage, KubeImage, IsReverse must be set for host %q; got %v", key, nSet))
+		}
+		if c.buildletURLTmpl == "" && (c.VMImage != "" || c.KubeImage != "") {
+			panic(fmt.Sprintf("missing buildletURLTmpl for host type %q", key))
+		}
+	}
+}
+
+// A HostConfig describes the available ways to obtain buildlets of
+// different types. Some host configs can server multiple
+// builders. For example, a host config of "host-linux-kube-std" can
+// serve linux-amd64, linux-amd64-race, linux-386, linux-386-387, etc.
+type HostConfig struct {
+	// HostType is the unique name of this host config. It is also
+	// the key in the Hosts map.
+	HostType string
+
+	// buildletURLTmpl is the URL "template" ($BUCKET is auto-expanded)
+	// for the URL to the buildlet binary.
+	// This field is required for GCE and Kubernetes builders. It's not
+	// needed for reverse buildlets because in that case, the buildlets
+	// are already running and their stage0 should know how to update it
+	// it automatically.
+	buildletURLTmpl string
+
+	// Exactly 1 of these must be set:
+	VMImage   string // e.g. "openbsd-amd64-58"
+	KubeImage string // e.g. "linux-buildlet-std:latest" (suffix after "gcr.io/<PROJ>/")
+	IsReverse bool   // if true, only use the reverse buildlet pool
+
+	// GCE options, if VMImage != ""
 	machineType string // optional GCE instance type
+	RegularDisk bool   // if true, use spinning disk instead of SSD
+
+	// Optional base env. GOROOT_BOOTSTRAP should go here if the buildlet
+	// has Go 1.4+ baked in somewhere.
+	env []string
 
 	// These template URLs may contain $BUCKET which is expanded to the
 	// relevant Cloud Storage bucket as specified by the build environment.
 	goBootstrapURLTmpl string // optional URL to a built Go 1.4+ tar.gz
-	buildletURLTmpl    string // optional override buildlet URL
 
-	IsReverse   bool // if true, only use the reverse buildlet pool
-	RegularDisk bool // if true, use spinning disk instead of SSD
+	Owner string // optional email of owner; "bradfitz@golang.org", empty means golang-dev
+	Notes string // notes for humans
+
+	// ReverseAliases lists alternate names for this buildlet
+	// config, for older clients doing a reverse dial into the
+	// coordinator from outside. This prevents us from updating
+	// 75+ dedicated machines/VMs atomically, switching them to
+	// the new "host-*" names.
+	// This is only applicable if IsReverse.
+	ReverseAliases []string
+}
+
+// A BuildConfig describes how to run a builder.
+type BuildConfig struct {
+	// Name is the unique name of the builder, in the form of
+	// "GOOS-GOARCH" or "GOOS-GOARCH-suffix". For example,
+	// "darwin-386", "linux-386-387", "linux-amd64-race". Some
+	// suffixes are well-known and carry special meaning, such as
+	// "-race".
+	Name string
+
+	// HostType is the required key into the Hosts map, describing
+	// the type of host this build will run on.
+	// For example, "host-linux-kube-std".
+	HostType string
+
+	Notes string // notes for humans
+
 	TryOnly     bool // only used for trybots, and not regular builds
 	CompileOnly bool // if true, compile tests, but don't run them
 	FlakyNet    bool // network tests are flaky (try anyway, but ignore some failures)
@@ -48,20 +282,6 @@
 	numTestHelpers    int
 	numTryTestHelpers int // for trybots. if 0, numTesthelpers is used
 
-	// BuildletType optionally specifies the type of buildlet to
-	// request from the buildlet pool. If empty, it defaults to
-	// the value of Name.
-	//
-	// These should be used to minimize builder types, so the buildlet pool
-	// implementations can reuse buildlets from similar-enough builds.
-	// (e.g. a shared linux-386 trybot can be reused for some linux-amd64
-	// or linux-amd64-race tests, etc)
-	//
-	// TODO(bradfitz): break BuildConfig up into BuildConfig and
-	// BuildletConfig and have a BuildConfig refer to a
-	// BuildletConfig. There's no much confusion now.
-	BuildletType string
-
 	env           []string // extra environment ("key=value") pairs
 	allScriptArgs []string
 }
@@ -71,9 +291,18 @@
 	if c.FlakyNet {
 		env = append(env, "GO_BUILDER_FLAKY_NET=1")
 	}
+	env = append(env, c.hostConf().env...)
 	return append(env, c.env...)
 }
 
+func (c *BuildConfig) IsReverse() bool { return c.hostConf().IsReverse }
+
+func (c *BuildConfig) IsKube() bool { return c.hostConf().IsKube() }
+func (c *HostConfig) IsKube() bool  { return c.KubeImage != "" }
+
+func (c *BuildConfig) IsGCE() bool { return c.hostConf().IsGCE() }
+func (c *HostConfig) IsGCE() bool  { return c.VMImage != "" }
+
 func (c *BuildConfig) GOOS() string { return c.Name[:strings.Index(c.Name, "-")] }
 
 func (c *BuildConfig) GOARCH() string {
@@ -94,17 +323,21 @@
 	return strings.Join(x, "/")
 }
 
-// BuildletBinaryURL returns the public URL of this builder's buildlet.
-func (c *BuildConfig) GoBootstrapURL(e *buildenv.Environment) string {
-	return strings.Replace(c.goBootstrapURLTmpl, "$BUCKET", e.BuildletBucket, 1)
+func (c *BuildConfig) hostConf() *HostConfig {
+	if c, ok := Hosts[c.HostType]; ok {
+		return c
+	}
+	panic(fmt.Sprintf("missing buildlet config for buildlet %q", c.Name))
 }
 
 // BuildletBinaryURL returns the public URL of this builder's buildlet.
-func (c *BuildConfig) BuildletBinaryURL(e *buildenv.Environment) string {
+func (c *BuildConfig) GoBootstrapURL(e *buildenv.Environment) string {
+	return strings.Replace(c.hostConf().goBootstrapURLTmpl, "$BUCKET", e.BuildletBucket, 1)
+}
+
+// BuildletBinaryURL returns the public URL of this builder's buildlet.
+func (c *HostConfig) BuildletBinaryURL(e *buildenv.Environment) string {
 	tmpl := c.buildletURLTmpl
-	if tmpl == "" {
-		return "http://storage.googleapis.com/" + e.BuildletBucket + "/buildlet." + c.GOOS() + "-" + c.GOARCH()
-	}
 	return strings.Replace(tmpl, "$BUCKET", e.BuildletBucket, 1)
 }
 
@@ -246,7 +479,7 @@
 }
 
 // MachineType returns the GCE machine type to use for this builder.
-func (c *BuildConfig) MachineType() string {
+func (c *HostConfig) MachineType() string {
 	if v := c.machineType; v != "" {
 		return v
 	}
@@ -255,14 +488,15 @@
 
 // ShortOwner returns a short human-readable owner.
 func (c BuildConfig) ShortOwner() string {
-	if c.Owner == "" {
+	owner := c.hostConf().Owner
+	if owner == "" {
 		return "go-dev"
 	}
-	return strings.TrimSuffix(c.Owner, "@golang.org")
+	return strings.TrimSuffix(owner, "@golang.org")
 }
 
 // GCENumCPU reports the number of GCE CPUs this buildlet requires.
-func (c *BuildConfig) GCENumCPU() int {
+func (c *HostConfig) GCENumCPU() int {
 	t := c.MachineType()
 	n, _ := strconv.Atoi(t[strings.LastIndex(t, "-")+1:])
 	return n
@@ -277,70 +511,51 @@
 
 func init() {
 	addBuilder(BuildConfig{
-		Name:               "freebsd-amd64-gce93",
-		VMImage:            "freebsd-amd64-gce93",
-		machineType:        "n1-highcpu-2",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
-		numTestHelpers:     1,
+		Name:           "freebsd-amd64-gce93",
+		HostType:       "host-freebsd-93-gce",
+		numTestHelpers: 1,
 	})
 	addBuilder(BuildConfig{
-		Name:               "freebsd-amd64-gce101",
-		Notes:              "FreeBSD 10.1; GCE VM is built from script in build/env/freebsd-amd64",
-		VMImage:            "freebsd-amd64-gce101",
-		machineType:        "n1-highcpu-2",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
-		env:                []string{"CC=clang"},
-		numTestHelpers:     2,
-		numTryTestHelpers:  4,
+		Name:              "freebsd-amd64-gce101",
+		HostType:          "host-freebsd-101-gce",
+		numTestHelpers:    2,
+		numTryTestHelpers: 4,
 	})
 	addBuilder(BuildConfig{
-		Name:               "freebsd-amd64-race",
-		VMImage:            "freebsd-amd64-gce101",
-		machineType:        "n1-highcpu-4",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
-		env:                []string{"CC=clang"},
+		Name:     "freebsd-amd64-race",
+		HostType: "host-freebsd-101-gce",
 	})
 	addBuilder(BuildConfig{
-		Name:    "freebsd-386-gce101",
-		VMImage: "freebsd-amd64-gce101",
-		//BuildletType: "freebsd-amd64-gce101",
-		machineType:        "n1-highcpu-2",
-		buildletURLTmpl:    "http://storage.googleapis.com/$BUCKET/buildlet.freebsd-amd64",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
-		env:                []string{"GOARCH=386", "GOHOSTARCH=386", "CC=clang"},
-		numTestHelpers:     3,
+		Name:     "freebsd-386-gce101",
+		HostType: "host-freebsd-101-gce",
+		env:      []string{"GOARCH=386", "GOHOSTARCH=386"},
 	})
 	addBuilder(BuildConfig{
-		Name:            "linux-386",
-		KubeImage:       "linux-x86-std:latest",
-		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
-		env:             []string{"GOROOT_BOOTSTRAP=/go1.4", "GOARCH=386", "GOHOSTARCH=386"},
-		numTestHelpers:  3,
+		Name:              "linux-386",
+		HostType:          "host-linux-kubestd",
+		env:               []string{"GOARCH=386", "GOHOSTARCH=386"},
+		numTestHelpers:    1,
+		numTryTestHelpers: 3,
 	})
 	addBuilder(BuildConfig{
-		Name:            "linux-386-387",
-		Notes:           "GO386=387",
-		KubeImage:       "linux-x86-std:latest",
-		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
-		env:             []string{"GOROOT_BOOTSTRAP=/go1.4", "GOARCH=386", "GOHOSTARCH=386", "GO386=387"},
+		Name:     "linux-386-387",
+		Notes:    "GO386=387",
+		HostType: "host-linux-kubestd",
+		env:      []string{"GOARCH=386", "GOHOSTARCH=386", "GO386=387"},
 	})
 	addBuilder(BuildConfig{
-		Name:            "linux-amd64",
-		KubeImage:       "linux-x86-std:latest",
-		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
-		env:             []string{"GOROOT_BOOTSTRAP=/go1.4"},
-		numTestHelpers:  3,
+		Name:           "linux-amd64",
+		HostType:       "host-linux-kubestd",
+		numTestHelpers: 3,
 	})
 
 	addMiscCompile := func(suffix, rx string) {
 		addBuilder(BuildConfig{
-			Name:            "misc-compile" + suffix,
-			TryOnly:         true,
-			CompileOnly:     true,
-			KubeImage:       "linux-x86-std:latest",
-			Notes:           "Runs buildall.sh to cross-compile std packages for " + rx + ", but doesn't run any tests.",
-			buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
-			env:             []string{"GOROOT_BOOTSTRAP=/go1.4"},
+			Name:        "misc-compile" + suffix,
+			HostType:    "host-linux-kubestd",
+			TryOnly:     true,
+			CompileOnly: true,
+			Notes:       "Runs buildall.sh to cross-compile std packages for " + rx + ", but doesn't run any tests.",
 			allScriptArgs: []string{
 				// Filtering pattern to buildall.bash:
 				rx,
@@ -353,11 +568,10 @@
 	addMiscCompile("-plan9", "^plan9-")
 
 	addBuilder(BuildConfig{
-		Name:      "linux-amd64-nocgo",
-		Notes:     "cgo disabled",
-		KubeImage: "linux-x86-std:latest",
+		Name:     "linux-amd64-nocgo",
+		HostType: "host-linux-kubestd",
+		Notes:    "cgo disabled",
 		env: []string{
-			"GOROOT_BOOTSTRAP=/go1.4",
 			"CGO_ENABLED=0",
 			// This USER=root was required for Docker-based builds but probably isn't required
 			// in the VM anymore, since the buildlet probably already has this in its environment.
@@ -366,310 +580,226 @@
 		},
 	})
 	addBuilder(BuildConfig{
-		Name:      "linux-amd64-noopt",
-		Notes:     "optimizations and inlining disabled",
-		KubeImage: "linux-x86-std:latest",
-		env:       []string{"GOROOT_BOOTSTRAP=/go1.4", "GO_GCFLAGS=-N -l"},
+		Name:     "linux-amd64-noopt",
+		Notes:    "optimizations and inlining disabled",
+		HostType: "host-linux-kubestd",
+		env:      []string{"GO_GCFLAGS=-N -l"},
 	})
 	addBuilder(BuildConfig{
 		Name:        "linux-amd64-ssacheck",
+		HostType:    "host-linux-kubestd",
 		CompileOnly: true,
 		Notes:       "SSA internal checks enabled",
-		KubeImage:   "linux-x86-std:latest",
-		env:         []string{"GOROOT_BOOTSTRAP=/go1.4", "GO_GCFLAGS=-d=ssa/check/on"},
+		env:         []string{"GO_GCFLAGS=-d=ssa/check/on"},
 	})
 	addBuilder(BuildConfig{
 		Name:              "linux-amd64-race",
-		KubeImage:         "linux-x86-std:latest",
-		env:               []string{"GOROOT_BOOTSTRAP=/go1.4"},
+		HostType:          "host-linux-kubestd",
 		numTestHelpers:    2,
 		numTryTestHelpers: 5,
 	})
 	addBuilder(BuildConfig{
-		Name:    "linux-386-clang",
-		VMImage: "linux-buildlet-clang",
-		//BuildletType: "linux-amd64-clang",
-		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
-		env:             []string{"GOROOT_BOOTSTRAP=/go1.4", "CC=/usr/bin/clang", "GOHOSTARCH=386"},
+		Name:     "linux-386-clang",
+		HostType: "host-linux-clang",
+		Notes:    "Debian wheezy + clang 3.5 instead of gcc",
+		env:      []string{"CC=/usr/bin/clang", "GOHOSTARCH=386"},
 	})
 	addBuilder(BuildConfig{
-		Name:    "linux-amd64-clang",
-		Notes:   "Debian wheezy + clang 3.5 instead of gcc",
-		VMImage: "linux-buildlet-clang",
-		env:     []string{"GOROOT_BOOTSTRAP=/go1.4", "CC=/usr/bin/clang"},
+		Name:     "linux-amd64-clang",
+		HostType: "host-linux-clang",
+		Notes:    "Debian wheezy + clang 3.5 instead of gcc",
+		env:      []string{"CC=/usr/bin/clang"},
 	})
 	addBuilder(BuildConfig{
-		Name:            "linux-386-sid",
-		VMImage:         "linux-buildlet-sid",
-		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
-		env:             []string{"GOROOT_BOOTSTRAP=/go1.4", "GOHOSTARCH=386"},
+		Name:     "linux-386-sid",
+		HostType: "host-linux-sid",
+		Notes:    "Debian sid (unstable)",
+		env:      []string{"GOHOSTARCH=386"},
 	})
 	addBuilder(BuildConfig{
-		Name:    "linux-amd64-sid",
-		Notes:   "Debian sid (unstable)",
-		VMImage: "linux-buildlet-sid",
-		env:     []string{"GOROOT_BOOTSTRAP=/go1.4"},
+		Name:     "linux-amd64-sid",
+		HostType: "host-linux-sid",
+		Notes:    "Debian sid (unstable)",
 	})
 	addBuilder(BuildConfig{
 		Name:              "linux-arm",
-		IsReverse:         true,
+		HostType:          "host-linux-arm",
 		FlakyNet:          true,
 		numTestHelpers:    2,
 		numTryTestHelpers: 7,
-		env:               []string{"GOROOT_BOOTSTRAP=/usr/local/go"},
 	})
 	addBuilder(BuildConfig{
-		Name:      "linux-arm-arm5",
-		IsReverse: true,
-		FlakyNet:  true,
+		Name:     "linux-arm-arm5",
+		HostType: "host-linux-arm",
+		Notes:    "GOARM=5, but running on newer-than GOARM=5 hardware",
+		FlakyNet: true,
 		env: []string{
-			"GOROOT_BOOTSTRAP=/usr/local/go",
 			"GOARM=5",
 			"GO_TEST_TIMEOUT_SCALE=5", // slow.
 		},
 	})
 	addBuilder(BuildConfig{
-		Name:            "nacl-386",
-		KubeImage:       "linux-x86-nacl:latest",
-		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
-		numTestHelpers:  3,
-		env:             []string{"GOROOT_BOOTSTRAP=/go1.4", "GOOS=nacl", "GOARCH=386", "GOHOSTOS=linux", "GOHOSTARCH=amd64"},
+		Name:           "nacl-386",
+		HostType:       "host-nacl-kube",
+		numTestHelpers: 3,
+		env:            []string{"GOOS=nacl", "GOARCH=386", "GOHOSTOS=linux", "GOHOSTARCH=amd64"},
 	})
 	addBuilder(BuildConfig{
-		Name:            "nacl-amd64p32",
-		KubeImage:       "linux-x86-nacl:latest",
-		buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
-		numTestHelpers:  3,
-		env:             []string{"GOROOT_BOOTSTRAP=/go1.4", "GOOS=nacl", "GOARCH=amd64p32", "GOHOSTOS=linux", "GOHOSTARCH=amd64"},
+		Name:           "nacl-amd64p32",
+		HostType:       "host-nacl-kube",
+		numTestHelpers: 3,
+		env:            []string{"GOOS=nacl", "GOARCH=amd64p32", "GOHOSTOS=linux", "GOHOSTARCH=amd64"},
 	})
 	addBuilder(BuildConfig{
-		Name:               "openbsd-amd64-gce58",
-		Notes:              "OpenBSD 5.8; GCE VM is built from script in build/env/openbsd-amd64",
-		VMImage:            "openbsd-amd64-58",
-		machineType:        "n1-highcpu-2",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-openbsd-amd64-gce58.tar.gz",
-		numTestHelpers:     2,
-		numTryTestHelpers:  5,
+		Name:              "openbsd-amd64-gce58",
+		HostType:          "host-openbsd-amd64-58-gce",
+		numTestHelpers:    2,
+		numTryTestHelpers: 5,
 	})
 	addBuilder(BuildConfig{
-		Name:               "openbsd-386-gce58",
-		Notes:              "OpenBSD 5.8; GCE VM is built from script in build/env/openbsd-386",
-		VMImage:            "openbsd-386-58",
-		machineType:        "n1-highcpu-2",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-openbsd-386-gce58.tar.gz",
-		numTestHelpers:     2,
+		Name:           "openbsd-386-gce58",
+		HostType:       "host-openbsd-386-58-gce",
+		numTestHelpers: 2,
 	})
 	addBuilder(BuildConfig{
-		Name:               "netbsd-amd64-gce",
-		Notes:              "NetBSD tip; GCE VM is built from script in build/env/netbsd-amd64",
-		VMImage:            "netbsd-amd64-gce",
-		machineType:        "n1-highcpu-2",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/gobootstrap-netbsd-amd64.tar.gz",
-		numTestHelpers:     1,
+		Name:           "netbsd-amd64-gce",
+		HostType:       "host-netbsd-gce",
+		numTestHelpers: 1,
 	})
 
 	addBuilder(BuildConfig{
-		Name:               "plan9-386",
-		Notes:              "Plan 9 from 0intro; GCE VM is built from script in build/env/plan9-386",
-		VMImage:            "plan9-386-v3",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/gobootstrap-plan9-386.tar.gz",
-
-		// We *were* using n1-standard-1 because Plan 9 can only
-		// reliably use a single CPU. Using 2 or 4 and we see
-		// test failures. See:
-		//    https://golang.org/issue/8393
-		//    https://golang.org/issue/9491
-		// n1-standard-1 has 3.6 GB of memory which WAS (see below)
-		// overkill (userspace probably only sees 2GB anyway),
-		// but it's the cheapest option. And plenty to keep
-		// our ~250 MB of inputs+outputs in its ramfs.
-		//
-		// But the docs says "For the n1 series of machine
-		// types, a virtual CPU is implemented as a single
-		// hyperthread on a 2.6GHz Intel Sandy Bridge Xeon or
-		// Intel Ivy Bridge Xeon (or newer) processor. This
-		// means that the n1-standard-2 machine type will see
-		// a whole physical core."
-		//
-		// ... so we used n1-highcpu-2 (1.80 RAM, still
-		// plenty), just so we can get 1 whole core for the
-		// single-core Plan 9. It will see 2 virtual cores and
-		// only use 1, but we hope that 1 will be more powerful
-		// and we'll stop timing out on tests.
-		machineType: "n1-highcpu-4",
-
+		Name:           "plan9-386",
+		HostType:       "host-plan9-386-gce",
 		numTestHelpers: 1,
 	})
 	addBuilder(BuildConfig{
-		Name:               "windows-amd64-gce",
-		VMImage:            "windows-buildlet-v2",
-		machineType:        "n1-highcpu-2",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-windows-amd64.tar.gz",
-		RegularDisk:        true,
-		env:                []string{"GOARCH=amd64", "GOHOSTARCH=amd64"},
-		numTestHelpers:     1,
-		numTryTestHelpers:  5,
+		Name:              "windows-amd64-gce",
+		HostType:          "host-windows-gce",
+		env:               []string{"GOARCH=amd64", "GOHOSTARCH=amd64"},
+		numTestHelpers:    1,
+		numTryTestHelpers: 5,
 	})
 	addBuilder(BuildConfig{
-		Name:               "windows-amd64-race",
-		Notes:              "Only runs -race tests (./race.bat)",
-		VMImage:            "windows-buildlet-v2",
-		machineType:        "n1-highcpu-4",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-windows-amd64.tar.gz",
-		RegularDisk:        true,
-		env:                []string{"GOARCH=amd64", "GOHOSTARCH=amd64"},
+		Name:     "windows-amd64-race",
+		HostType: "host-windows-gce",
+		Notes:    "Only runs -race tests (./race.bat)",
+		env:      []string{"GOARCH=amd64", "GOHOSTARCH=amd64"},
 	})
 	addBuilder(BuildConfig{
-		Name:        "windows-386-gce",
-		VMImage:     "windows-buildlet-v2",
-		machineType: "n1-highcpu-2",
-		// TODO(bradfitz): once buildlet type vs. config type is split: BuildletType:   "windows-amd64-gce",
-		buildletURLTmpl:    "http://storage.googleapis.com/$BUCKET/buildlet.windows-amd64",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-windows-386.tar.gz",
-		RegularDisk:        true,
-		env:                []string{"GOARCH=386", "GOHOSTARCH=386"},
-		numTestHelpers:     1,
-		numTryTestHelpers:  5,
+		Name:              "windows-386-gce",
+		HostType:          "host-windows-gce",
+		env:               []string{"GOARCH=386", "GOHOSTARCH=386"},
+		numTestHelpers:    1,
+		numTryTestHelpers: 5,
 	})
 	addBuilder(BuildConfig{
-		Name:      "darwin-amd64-10_8",
-		Notes:     "MacStadium OS X 10.8 VM under VMWare ESXi",
-		IsReverse: true,
-		env: []string{
-			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
-		},
+		Name:     "darwin-amd64-10_8",
+		HostType: "host-darwin-10_8",
 	})
 	addBuilder(BuildConfig{
-		Name:      "darwin-amd64-10_10",
-		Notes:     "MacStadium OS X 10.10 VM under VMWare ESXi",
-		IsReverse: true,
-		env: []string{
-			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
-		},
+		Name:     "darwin-amd64-10_10",
+		HostType: "host-darwin-10_10",
 	})
 	addBuilder(BuildConfig{
 		Name:              "darwin-amd64-10_11",
-		Notes:             "MacStadium OS X 10.11 VM under VMWare ESXi",
+		HostType:          "host-darwin-10_11",
 		numTestHelpers:    2,
 		numTryTestHelpers: 3,
-		IsReverse:         true,
-		env: []string{
-			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
-		},
 	})
 
 	addBuilder(BuildConfig{
-		Name:               "android-arm-sdk19",
-		Notes:              "Android ARM device running android-19 (KitKat 4.4), attatched to Mac Mini",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
-		IsReverse:          true,
-		env:                []string{"GOOS=android", "GOARCH=arm"},
-		numTestHelpers:     1, // limited resources
+		Name:  "android-arm-sdk19",
+		Notes: "Android ARM device running android-19 (KitKat 4.4), attatched to Mac Mini",
+		env:   []string{"GOOS=android", "GOARCH=arm"},
 	})
 	addBuilder(BuildConfig{
-		Name:               "android-arm64-sdk21",
-		Notes:              "Android arm64 device using the android-21 toolchain, attatched to Mac Mini",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
-		IsReverse:          true,
-		env:                []string{"GOOS=android", "GOARCH=arm64"},
-		numTestHelpers:     1, // limited resources
+		Name:  "android-arm64-sdk21",
+		Notes: "Android arm64 device using the android-21 toolchain, attatched to Mac Mini",
+		env:   []string{"GOOS=android", "GOARCH=arm64"},
 	})
 	addBuilder(BuildConfig{
-		Name:               "android-386-sdk21",
-		Notes:              "Android 386 device using the android-21 toolchain, attatched to Mac Mini",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
-		IsReverse:          true,
-		env:                []string{"GOOS=android", "GOARCH=386"},
-		numTestHelpers:     1, // limited resources
+		Name:  "android-386-sdk21",
+		Notes: "Android 386 device using the android-21 toolchain, attatched to Mac Mini",
+		env:   []string{"GOOS=android", "GOARCH=386"},
 	})
 	addBuilder(BuildConfig{
-		Name:               "android-amd64-sdk21",
-		Notes:              "Android amd64 device using the android-21 toolchain, attatched to Mac Mini",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
-		IsReverse:          true,
-		env:                []string{"GOOS=android", "GOARCH=amd64"},
-		numTestHelpers:     1, // limited resources
+		Name:  "android-amd64-sdk21",
+		Notes: "Android amd64 device using the android-21 toolchain, attatched to Mac Mini",
+		env:   []string{"GOOS=android", "GOARCH=amd64"},
 	})
 	addBuilder(BuildConfig{
-		Name:               "darwin-arm-a5ios",
-		Notes:              "iPhone 4S (A5 processor), via a Mac Mini",
-		Owner:              "crawshaw@golang.org",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
-		IsReverse:          true,
-		env:                []string{"GOARCH=arm", "GOHOSTARCH=amd64"},
+		Name:  "darwin-arm-a5ios",
+		Notes: "iPhone 4S (A5 processor), via a Mac Mini; owned by crawshaw",
+		env:   []string{"GOARCH=arm", "GOHOSTARCH=amd64"},
 	})
 	addBuilder(BuildConfig{
-		Name:               "darwin-arm64-a7ios",
-		Notes:              "iPad Mini 3 (A7 processor), via a Mac Mini",
-		Owner:              "crawshaw@golang.org",
-		goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
-		IsReverse:          true,
-		env:                []string{"GOARCH=arm64", "GOHOSTARCH=amd64"},
+		Name:  "darwin-arm64-a7ios",
+		Notes: "iPad Mini 3 (A7 processor), via a Mac Mini; owned by crawshaw",
+		env:   []string{"GOARCH=arm64", "GOHOSTARCH=amd64"},
+	})
+
+	addBuilder(BuildConfig{
+		Name:     "solaris-amd64-smartosbuildlet",
+		HostType: "host-solaris-amd64",
 	})
 	addBuilder(BuildConfig{
-		Name:      "solaris-amd64-smartosbuildlet",
-		Notes:     "run by Go team on Joyent, on a SmartOS 'infrastructure container'",
-		IsReverse: true,
-		env:       []string{"GOROOT_BOOTSTRAP=/root/go-solaris-amd64-bootstrap"},
+		Name:     "linux-ppc64le-buildlet",
+		HostType: "host-linux-ppc64le-osu",
+		FlakyNet: true,
 	})
 	addBuilder(BuildConfig{
-		Name:      "linux-ppc64le-buildlet",
-		Notes:     "Debian jessie; run by Go team on osuosl.org",
-		IsReverse: true,
-		FlakyNet:  true,
-		env:       []string{"GOROOT_BOOTSTRAP=/usr/local/go-bootstrap"},
-	})
-	addBuilder(BuildConfig{
-		Name:      "linux-arm64-buildlet",
-		Notes:     "Ubuntu wily; run by Go team, from linaro",
-		IsReverse: true,
-		FlakyNet:  true,
-		env:       []string{"GOROOT_BOOTSTRAP=/usr/local/go-bootstrap"},
+		Name:     "linux-arm64-buildlet",
+		HostType: "host-linux-arm64-linaro",
+		FlakyNet: true,
 	})
 	addBuilder(BuildConfig{
 		Name:           "linux-s390x-ibm",
-		Notes:          "run by IBM",
-		IsReverse:      true,
+		HostType:       "host-linux-s390x",
 		numTestHelpers: 0,
-		env:            []string{"GOROOT_BOOTSTRAP=/var/buildlet/go-linux-s390x-bootstrap"},
 	})
 	addBuilder(BuildConfig{
-		Name:            "linux-s390x-crosscompile",
-		Notes:           "s390x cross-compile builder for releases; doesn't run tests",
-		KubeImage:       "linux-s390x-stretch:latest",
-		CompileOnly:     true,
-		TryOnly:         true, // but not in trybot set for now
-		buildletURLTmpl: "https://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
+		Name:        "linux-s390x-crosscompile",
+		HostType:    "host-s390x-cross-kube",
+		Notes:       "s390x cross-compile builder for releases; doesn't run tests",
+		CompileOnly: true,
+		TryOnly:     true, // but not in trybot set for now
 		env: []string{
-			"GOROOT_BOOTSTRAP=/go1.4",
 			"CGO_ENABLED=1",
 			"GOARCH=s390x",
 			"GOHOSTARCH=amd64",
 			"CC_FOR_TARGET=s390x-linux-gnu-gcc",
 		},
 	})
+}
 
-	addBuilder(BuildConfig{
-		Name:           "solaris-amd64-oraclejtsylve",
-		Notes:          "temporary test builder run by jtsylve",
-		IsReverse:      true,
-		numTestHelpers: 0,
-		env:            []string{"GOROOT_BOOTSTRAP=/usr/local/go-bootstrap"},
-	})
+func (c BuildConfig) isMobile() bool {
+	return strings.HasPrefix(c.Name, "android-") || strings.HasPrefix(c.Name, "darwin-arm")
 }
 
 func addBuilder(c BuildConfig) {
 	if c.Name == "" {
 		panic("empty name")
 	}
+	if c.isMobile() && c.HostType == "" {
+		htyp := "host-" + c.Name
+		if _, ok := Hosts[htyp]; !ok {
+			Hosts[htyp] = &HostConfig{
+				HostType:           htyp,
+				IsReverse:          true,
+				goBootstrapURLTmpl: "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
+				ReverseAliases:     []string{c.Name},
+			}
+			c.HostType = htyp
+		}
+	}
+	if c.HostType == "" {
+		panic(fmt.Sprintf("missing HostType for builder %q", c.Name))
+	}
 	if _, dup := Builders[c.Name]; dup {
 		panic("dup name")
 	}
-	if (c.VMImage == "" && c.KubeImage == "") && !c.IsReverse {
-		panic("empty VMImage and KubeImage on non-reverse builder")
-	}
-	if c.VMImage != "" && c.KubeImage != "" {
-		panic("there can be only one of VMImage/KubeImage")
+	if _, ok := Hosts[c.HostType]; !ok {
+		panic(fmt.Sprintf("undefined HostType %q for builder %q", c.HostType, c.Name))
 	}
 	Builders[c.Name] = c
 }