Files
gvm/deploy.go
Michael WesemannandClaude Opus 5 94ebe679b7 [mike@mwxm4]
One dialect: -j, and nothing older than v2.5.0 is talked to.

The text road is gone. It said the same things in a sentence and exited 0 while
refusing, and keeping it meant keeping a second way to be wrong for the sake of
versions nobody is running.

An older helper is not silently ignored. "There is none" and "the one you have
is too old" send somebody to two different places, so the helper carries why it
cannot be used and both the refusal and `gvm config` say it:

    /usr/local/bin/dns is 2.4.4, and gvm wants 2.5.0 or newer — 'dns --update' fetches it

A version that cannot be read at all is a third reason, said as itself: gvm
would rather say so than talk to something whose answers it cannot predict.

And the version check found a bug in itself while being tested. The line -v
prints carries two versions —

    dns - infoblox helper (v2.5.0 (1282), toolbox v0.5.0, mwx'2026)

— and taking whichever came first read the toolbox's 0.5.0 the moment the
tool's own could not be read. Which is exactly the quiet wrong answer the check
exists to prevent. It is anchored on the bracket now, and the trap is a test
case of its own.

Checked read-only against the real v2.5.0: read as 2.5.0, usable, and a host
lookup coming back with its address.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 10:30:33 +02:00

783 lines
29 KiB
Go

// deploy.go — making a new machine from a template.
//
// Everything else gvm does happens to a machine that already exists. This is the
// one thing that brings one into being, and that makes it the only operation
// here whose hardest question is not "may I" but "where".
//
// A template is not a machine that happens to be switched off. vSphere takes its
// resource pool away when it is marked as one, so there is nowhere for a clone
// of it to run until somebody says where — and that somewhere is the one thing
// that cannot be inherited from the source. Everything else can: the folder it
// sits in, the datastore it lives on, the hardware it was built with.
//
// So the placement is worked out before anything is asked, and the confirmation
// says it in full. "A new machine appeared somewhere on the estate" is not an
// outcome anybody should get from a keystroke.
//
// What this deliberately does not do is customise the guest — no hostname, no
// address, no domain join. That is a second machine's worth of vSphere
// (CustomizationSpec), it is site policy rather than a tool's business, and a
// half-done version of it that sets a hostname and leaves the address to DHCP
// would be worse than leaving it alone. The new machine is its template, under
// a new name, in a place somebody chose.
package main
import (
"net"
"sort"
"strings"
"time"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
)
// cloneWait is how long the command line watches a deployment before it stops
// watching. A clone copies every disk the template has, which on a fat template
// over a busy datastore is genuinely half an hour — so this is long, and like
// every other wait in gvm it is a limit on a frozen terminal rather than on the
// operation. The interactive half does not wait at all (see deploy).
const cloneWait = 60 * time.Minute
// maxVMName is what vSphere takes for a machine's name. Typing stops there
// rather than sending something the server will refuse.
const maxVMName = 80
// deployOpts is what a deployment may be told, beyond the name.
type deployOpts struct {
host string // land it here, by name; empty leaves the choice to the cluster
datastore string // empty means the template's own
powerOn bool
// What the guest is told about itself, and where that comes from.
how custom
spec string // which one, when how is customSpec
hostname string // empty means the machine's own name
ip string // empty leaves the adapter on DHCP; autoIP fetches one
st site // the site's own answers, from the configuration
dns dnsHelper // the site's address helper, where there is one
}
// autoAddress reports whether the address is to be fetched rather than typed.
func (o deployOpts) autoAddress() bool {
return strings.EqualFold(strings.TrimSpace(o.ip), autoIP)
}
// custom is where a customisation comes from. Two roads, and they are the same
// two everywhere in vSphere: a specification the vCenter already holds, where
// the site's answers live in the vCenter and gvm overrides only this machine's
// two facts; or one gvm writes itself out of ~/.gvmrc, which is the same site
// answers kept somewhere else. The second is the shorter road for a site that
// has no specifications; the first is the only road for Windows.
//
// It is a decision carried rather than one worked out from the other fields.
// Inferring it was a small mistake with a visible end: the interactive half
// settles which road before it asks for an address, so at that moment the
// fields are all still empty — which read as "no customisation at all" and
// produced the question "address for web05 (empty = as says)", with a hole in
// it where the name of a specification that was never chosen would have gone.
type custom int
const (
customNone custom = iota // leave the guest as the template made it
customSite // gvm writes the specification, from the configuration
customSpec // one the vCenter holds, named in spec
)
// customising reports whether anything is to be done to the guest at all.
func (o deployOpts) customising() bool { return o.how != customNone }
// building reports whether gvm is to write the specification itself rather than
// take one from the vCenter.
func (o deployOpts) building() bool { return o.how == customSite }
// deployTarget is where the new machine will go, resolved from the template and
// the options before anything is sent — so the question that is asked is the
// same thing that then happens.
type deployTarget struct {
folder types.ManagedObjectReference
pool types.ManagedObjectReference
where string // the cluster or host that pool belongs to, in words
host *types.ManagedObjectReference
hostName string
datastore *types.ManagedObjectReference
dsName string
}
// describe is the placement as the confirmation says it: where it will run, on
// which host if one was named, and on which datastore if one was.
func (t deployTarget) describe() string {
out := []string{t.where}
if t.hostName != "" {
out = append(out, "on "+t.hostName)
}
if t.dsName != "" {
out = append(out, "datastore "+t.dsName)
}
return strings.Join(out, " · ")
}
// templateProps are what has to be read of the source. The sweep does not carry
// the folder a machine sits in — nothing else needs it — and "is this a
// template" is in the summary the list already holds, but it is asked again
// here: this is the one place that acts on the answer.
var templateProps = []string{"name", "parent", "summary.config.template",
"summary.config.guestId", "summary.runtime.host"}
// deploySource is the template, read fresh.
type deploySource struct {
ref types.ManagedObjectReference
name string
folder types.ManagedObjectReference
host *types.ManagedObjectReference
template bool
guestID string // vSphere's own word for what is installed, e.g. ubuntu64Guest
}
// windows reports whether the template holds Windows, which decides whether gvm
// can write a customisation for it at all: Linux takes a LinuxPrep, which is
// five lines of network, and Windows takes a Sysprep, which is a licence key,
// an administrator password and a domain to join.
//
// From the guest id vSphere itself keeps, which is what the machine was created
// as. A template whose id says nothing is treated as Linux and left to the
// server to refuse — guessing "Windows" from silence would turn every template
// with an unset id into one gvm will not deploy.
func (src deploySource) windows() bool {
return strings.Contains(strings.ToLower(src.guestID), "windows")
}
func sourceOf(s *session, ref types.ManagedObjectReference) (deploySource, error) {
var mvm mo.VirtualMachine
vm := object.NewVirtualMachine(s.client.Client, ref)
if err := vm.Properties(s.ctx, ref, templateProps, &mvm); err != nil {
return deploySource{}, errf("%s: cannot read %s: %w", s.vc.Name, ref.Value, err)
}
src := deploySource{ref: ref, name: mvm.Name, host: mvm.Summary.Runtime.Host,
guestID: mvm.Summary.Config.GuestId}
if c := mvm.Summary.Config; c.Template {
src.template = true
}
if mvm.Parent == nil {
return src, errf("%s is in no folder, so there is nowhere to put a copy of it", src.name)
}
src.folder = *mvm.Parent
return src, nil
}
// targetFor works out where the new machine goes.
//
// The resource pool is the part that cannot be left out. A template has none of
// its own — vSphere takes it away when a machine is marked as one — so the pool
// of whatever the template is registered on is used instead: the cluster's,
// where it is in one, which leaves the choice of host to DRS the way every other
// deployment on that cluster does. Naming a host overrides both, and pins it.
func targetFor(s *session, src deploySource, opts deployOpts) (deployTarget, error) {
t := deployTarget{folder: src.folder}
hostRef := src.host
if opts.host != "" {
ref, name, err := hostByName(s, opts.host)
if err != nil {
return t, err
}
hostRef, t.host, t.hostName = &ref, &ref, name
}
if hostRef == nil {
return t, errf("%s is not registered on any host, so there is nothing to work out where a copy of it would run", src.name)
}
pool, where, err := poolOfHost(s, *hostRef)
if err != nil {
return t, err
}
t.pool, t.where = pool, where
if opts.datastore != "" {
ref, name, err := datastoreByName(s, opts.datastore)
if err != nil {
return t, err
}
t.datastore, t.dsName = &ref, name
}
return t, nil
}
// poolOfHost is the root resource pool of whatever a host belongs to, and the
// name of that thing. For a host in a cluster this is the cluster's pool, which
// is what makes DRS place the machine; for a standalone host it is the host's
// own.
func poolOfHost(s *session, host types.ManagedObjectReference) (types.ManagedObjectReference, string, error) {
var hosts []mo.HostSystem
if err := s.objects([]types.ManagedObjectReference{host}, []string{"name", "parent"}, &hosts); err != nil {
return types.ManagedObjectReference{}, "", err
}
if len(hosts) == 0 || hosts[0].Parent == nil {
return types.ManagedObjectReference{}, "", errf("%s: cannot tell what %s belongs to", s.vc.Name, host.Value)
}
var crs []mo.ComputeResource
if err := s.objects([]types.ManagedObjectReference{*hosts[0].Parent}, []string{"name", "resourcePool"}, &crs); err != nil {
return types.ManagedObjectReference{}, "", err
}
if len(crs) == 0 || crs[0].ResourcePool == nil {
return types.ManagedObjectReference{}, "", errf("%s: %s has no resource pool to run a machine in",
s.vc.Name, shortHost(hosts[0].Name))
}
where := crs[0].Name
if where == hosts[0].Name {
where = shortHost(where) // a standalone host's compute resource is named after it
}
return *crs[0].ResourcePool, where, nil
}
// hostByName and datastoreByName resolve what was asked for by name, and say
// what there was when it is not found: a typo answered with "no such host" and
// nothing else is a puzzle, and the list is short.
func hostByName(s *session, name string) (types.ManagedObjectReference, string, error) {
hosts, err := s.hosts("name")
if err != nil {
return types.ManagedObjectReference{}, "", err
}
var had []string
for _, h := range hosts {
short := shortHost(h.Name)
if strings.EqualFold(short, name) || strings.EqualFold(h.Name, name) {
return h.Reference(), short, nil
}
had = append(had, short)
}
return types.ManagedObjectReference{}, "", errf("%s has no host called %q — it has %s",
s.vc.Name, name, strings.Join(had, ", "))
}
func datastoreByName(s *session, name string) (types.ManagedObjectReference, string, error) {
stores, err := s.datastores("name")
if err != nil {
return types.ManagedObjectReference{}, "", err
}
var had []string
for _, d := range stores {
if strings.EqualFold(d.Name, name) {
return d.Reference(), d.Name, nil
}
had = append(had, d.Name)
}
return types.ManagedObjectReference{}, "", errf("%s has no datastore called %q — it has %s",
s.vc.Name, name, strings.Join(had, ", "))
}
// checkName is what vSphere will take, asked before it is typed into a
// confirmation rather than after.
func checkName(name string) error {
n := strings.TrimSpace(name)
switch {
case n == "":
return errf("the new machine needs a name")
case len(n) > maxVMName:
return errf("%d characters is longer than the %d vSphere takes for a name", len(n), maxVMName)
case strings.ContainsAny(n, "/\\"):
return errf("a machine's name cannot hold a slash")
}
return nil
}
// nameTaken reports whether the server already has a machine of that name.
// vSphere refuses a duplicate itself, several seconds into the clone; asking
// first turns that into an answer before anything starts.
func nameTaken(s *session, name string) bool {
vm, err := s.vm(name)
return err == nil && vm != nil
}
// cloneSpec is what is sent: where it goes, what the guest is told about
// itself, and that what comes out is a machine. Its own function so all of that
// can be checked without a server — a spec built twice, once in the code and
// once in a test, proves nothing.
//
// Template is false and stated rather than left out: cloning a template
// produces another template unless something says otherwise, and a second
// template nobody asked for is the kind of thing that is only noticed weeks
// later, when somebody wonders why the machine will not start.
func cloneSpec(t deployTarget, opts deployOpts, custom *types.CustomizationSpec) types.VirtualMachineCloneSpec {
return types.VirtualMachineCloneSpec{
Location: types.VirtualMachineRelocateSpec{
Pool: &t.pool,
Host: t.host,
Datastore: t.datastore,
},
Customization: custom,
PowerOn: opts.powerOn,
Template: false,
}
}
// deployStep is what the steps before the confirmation left behind: why one of
// them did not happen, what the address tool handed out, and how to give that
// back. Three things that travel together, because they are all answers to
// "what has happened so far" — and a confirmation with eight arguments is one
// nobody can call correctly.
type deployStep struct {
note string // why a step was skipped, said on the confirmation
rec dnsHost // the record the address came from, where one was fetched
give func() // hands that address back
}
// giveBack releases a fetched address, and does nothing where none was.
func (s deployStep) giveBack() {
if s.give != nil {
s.give()
}
}
// fetchAddress turns an address of "auto" into one the site's tool handed out,
// and hands back the name it was registered under and a way to give it back.
//
// The record is made before the confirmation rather than after it, so that the
// question says the address the machine will actually have rather than a
// promise of one. That is worth a record being made for a deployment somebody
// then abandons — as long as it is given back, which is what the returned
// function is for.
func fetchAddress(opts deployOpts, name string) (deployOpts, dnsHost, func(), error) {
nothing := func() {}
if !opts.autoAddress() {
return opts, dnsHost{}, nothing, nil
}
if !opts.dns.there() {
// Why, rather than that: a tool that is too old and no tool at all are
// two different things to go and do something about.
return opts, dnsHost{}, nothing, errf("%s — or give --ip an address", opts.dns.why())
}
host := strings.TrimSpace(opts.hostname)
if host == "" {
host = name
}
rec, err := dnsAdd(opts.dns, host)
if err != nil {
return opts, dnsHost{}, nothing, err
}
opts.ip = rec.address()
return opts, rec, func() { dnsRemove(opts.dns, host) }, nil
}
// ------------------------------------------------------- telling the guest
// specNames are the customisation specifications this vCenter holds, in the
// order it lists them.
func specNames(s *session) ([]string, error) {
m := object.NewCustomizationSpecManager(s.client.Client)
info, err := m.Info(s.ctx)
if err != nil {
return nil, errf("%s: cannot read the customisation specifications: %w", s.vc.Name, err)
}
out := make([]string, 0, len(info))
for _, i := range info {
out = append(out, i.Name)
}
sort.Strings(out)
return out, nil
}
// customizationFor is the specification the vCenter holds, with this machine's
// own two facts written into it.
//
// Only those two. Everything else the specification says — the domain, the DNS
// servers, the netmask, the gateway, the timezone, whether the guest is Linux
// or Windows — is the site's answer, kept where the site keeps it. gvm knowing
// better than the vCenter about any of that is how a tool ends up with a
// network policy of its own that nobody remembers agreeing to.
func customizationFor(s *session, opts deployOpts, src deploySource, name string) (*types.CustomizationSpec, error) {
if opts.building() {
return builtSpec(opts, src, name)
}
m := object.NewCustomizationSpecManager(s.client.Client)
item, err := m.GetCustomizationSpec(s.ctx, strings.TrimSpace(opts.spec))
if err != nil {
had, _ := specNames(s)
if len(had) == 0 {
return nil, errf("%s has no customisation specification called %q, and none at all — "+
"they are made in the vSphere client, under Policies and Profiles",
s.vc.Name, opts.spec)
}
return nil, errf("%s has no customisation specification called %q — it has %s",
s.vc.Name, opts.spec, strings.Join(had, ", "))
}
spec := item.Spec
host := strings.TrimSpace(opts.hostname)
if host == "" {
host = name // the machine's own name, which is what one means by a hostname
}
if err := setHostName(&spec, host); err != nil {
return nil, err
}
if opts.ip != "" {
if err := setAddress(&spec, opts.ip); err != nil {
return nil, err
}
}
return &spec, nil
}
// builtSpec is the customisation gvm writes itself: the site's answers from
// ~/.gvmrc, and this machine's name and address.
//
// It is Linux only, and says so rather than producing something that half
// works. Windows is a Sysprep — a licence key, an administrator password, a
// domain to join and a workgroup if it does not — and none of that is a thing
// to keep in a configuration file next to the SMTP relay. A site with Windows
// templates wants a specification in the vCenter, which is what --spec is for.
func builtSpec(opts deployOpts, src deploySource, name string) (*types.CustomizationSpec, error) {
if src.windows() {
return nil, errf("%s is a Windows template, and gvm does not write a Sysprep — "+
"make a specification in the vSphere client and name it with --spec", src.name)
}
st := opts.st
if !st.ready() {
return nil, errf("a machine cannot be told about the network until %s says %s — "+
"or name a specification the vCenter holds with --spec",
configFile(), strings.Join(st.missing(), ", "))
}
host := strings.TrimSpace(opts.hostname)
if host == "" {
host = name
}
adapter := types.CustomizationIPSettings{
Ip: &types.CustomizationDhcpIpGenerator{},
SubnetMask: st.netmask,
Gateway: []string{st.gateway},
DnsServerList: st.dns,
}
if ip := strings.TrimSpace(opts.ip); ip != "" {
if err := checkAddress(ip, st); err != nil {
return nil, err
}
adapter.Ip = &types.CustomizationFixedIp{IpAddress: ip}
}
spec := &types.CustomizationSpec{
Identity: &types.CustomizationLinuxPrep{
HostName: &types.CustomizationFixedName{Name: host},
Domain: st.domain,
TimeZone: st.timezone,
HwClockUTC: types.NewBool(true),
},
GlobalIPSettings: types.CustomizationGlobalIPSettings{
DnsServerList: st.dns,
DnsSuffixList: []string{st.domain},
},
NicSettingMap: []types.CustomizationAdapterMapping{{Adapter: adapter}},
}
return spec, nil
}
// checkAddress is what gvm can tell about an address before the guest has it:
// that it is one, that the mask and the gateway are, and that the gateway is
// somewhere the machine could reach. The last is a warning's worth of wrong
// rather than an error's — a routed setup can put a gateway anywhere — but
// almost every time it is a typo, and a machine with an unreachable gateway is
// one somebody drives to the console for.
func checkAddress(ip string, st site) error {
addr := net.ParseIP(strings.TrimSpace(ip))
if addr == nil || addr.To4() == nil {
return errf("%q is not an IPv4 address", ip)
}
mask := net.ParseIP(st.netmask)
if mask == nil || mask.To4() == nil {
return errf("the netmask in %s is %q, which is not one", configFile(), st.netmask)
}
gw := net.ParseIP(st.gateway)
if gw == nil || gw.To4() == nil {
return errf("the gateway in %s is %q, which is not an address", configFile(), st.gateway)
}
return nil
}
// gatewayOffSubnet reports whether the gateway is outside the network the
// address and mask describe — said on the confirmation rather than refused.
func gatewayOffSubnet(ip string, st site) bool {
addr, gw := net.ParseIP(strings.TrimSpace(ip)).To4(), net.ParseIP(st.gateway).To4()
mask := net.ParseIP(st.netmask).To4()
if addr == nil || gw == nil || mask == nil {
return false // not knowing is not the same as knowing it is wrong
}
m := net.IPMask(mask)
return !addr.Mask(m).Equal(gw.Mask(m))
}
// setHostName writes the name into whichever kind of identity the
// specification carries. The two that matter are Linux and Windows; the third
// is a Windows answer file somebody wrote by hand, and a tool that reached into
// that to change one line would be guessing at a format it does not own.
func setHostName(spec *types.CustomizationSpec, name string) error {
fixed := &types.CustomizationFixedName{Name: name}
switch id := spec.Identity.(type) {
case *types.CustomizationLinuxPrep:
id.HostName = fixed
case *types.CustomizationSysprep:
id.UserData.ComputerName = fixed
case *types.CustomizationSysprepText:
return errf("that specification is a Windows answer file written by hand — " +
"gvm will not edit one; put the machine's name in the file, or use a specification with a name field")
case *types.CustomizationCloudinitPrep:
return errf("that specification is a cloud-init one, which carries the hostname inside its " +
"metadata — gvm does not rewrite that; leave --hostname off and let cloud-init set it")
default:
return errf("that specification has no kind of identity gvm knows how to name (%T)", spec.Identity)
}
return nil
}
// setAddress puts a fixed address on the specification's first adapter, and
// keeps everything else about it.
//
// The netmask and the gateway are deliberately not asked for: they belong to
// the network, the specification already carries them, and a machine given an
// address with a mask invented by the tool that deployed it is a machine that
// half works. Where the specification has no mask — because its adapter is set
// to DHCP — it is refused, because that is a question for the vCenter and not
// for a command line.
func setAddress(spec *types.CustomizationSpec, ip string) error {
if net.ParseIP(strings.TrimSpace(ip)) == nil {
return errf("%q is not an address", ip)
}
if len(spec.NicSettingMap) == 0 {
return errf("that specification has no network adapter in it, so there is nowhere to put an address")
}
nic := &spec.NicSettingMap[0]
if nic.Adapter.SubnetMask == "" {
return errf("that specification's adapter has no subnet mask — it is set to take one from DHCP, " +
"so an address given here would arrive without one; put a mask in the specification, or leave --ip off")
}
nic.Adapter.Ip = &types.CustomizationFixedIp{IpAddress: strings.TrimSpace(ip)}
return nil
}
// toolsMissing reports whether the template looks as though it has no VMware
// Tools, which is what carries out a customisation inside the guest.
//
// A guess, and treated as one: the version is what the machine last reported,
// so a template made from a machine that never ran says nothing here. It is
// worth saying anyway — finding out that the hostname was never set is
// otherwise something that happens twenty minutes later, on a guest that came
// up under the template's own name.
func toolsMissing(s *session, ref types.ManagedObjectReference) bool {
var mvm mo.VirtualMachine
vm := object.NewVirtualMachine(s.client.Client, ref)
if err := vm.Properties(s.ctx, ref, []string{"config.tools"}, &mvm); err != nil {
return false // not known is not the same as not there
}
return mvm.Config != nil && mvm.Config.Tools != nil && mvm.Config.Tools.ToolsVersion == 0
}
// startDeploy sends the clone and hands back the task without waiting for it.
// Who waits is the caller's business: the command line does, the interactive
// list does not (see deploy).
func startDeploy(s *session, src deploySource, t deployTarget, name string, opts deployOpts) (*object.Task, error) {
if !src.template {
return nil, errf("%s is a machine, not a template — gvm only makes copies of templates", src.name)
}
if err := checkName(name); err != nil {
return nil, err
}
if nameTaken(s, name) {
return nil, errf("%s already has a machine called %s", s.vc.Name, name)
}
var custom *types.CustomizationSpec
if opts.customising() {
var err error
if custom, err = customizationFor(s, opts, src, strings.TrimSpace(name)); err != nil {
return nil, err
}
}
vm := object.NewVirtualMachine(s.client.Client, src.ref)
folder := object.NewFolder(s.client.Client, t.folder)
task, err := vm.Clone(s.ctx, folder, strings.TrimSpace(name), cloneSpec(t, opts, custom))
if err != nil {
return nil, errf("%s: cannot start making %s from %s: %w", s.vc.Name, name, src.name, err)
}
return task, nil
}
// hostNameOf and addressOf read back what the specification will actually do,
// for the confirmation. Read back rather than repeated from the options: what
// is shown is then the thing that was built, not the thing that was asked for.
func hostNameOf(spec *types.CustomizationSpec) string {
var name types.BaseCustomizationName
switch id := spec.Identity.(type) {
case *types.CustomizationLinuxPrep:
name = id.HostName
case *types.CustomizationSysprep:
name = id.UserData.ComputerName
}
if fixed, ok := name.(*types.CustomizationFixedName); ok {
return fixed.Name
}
return "as the specification says"
}
func addressOf(spec *types.CustomizationSpec) string {
if len(spec.NicSettingMap) == 0 {
return "as the specification says"
}
nic := spec.NicSettingMap[0].Adapter
fixed, ok := nic.Ip.(*types.CustomizationFixedIp)
if !ok {
return "as the specification says (DHCP, most likely)"
}
out := fixed.IpAddress
if nic.SubnetMask != "" {
out += " netmask " + nic.SubnetMask
}
if len(nic.Gateway) > 0 {
out += " via " + strings.Join(nic.Gateway, ", ")
}
return out
}
// ---------------------------------------------------------- the command line
// specsCLI is `gvm new --specs`: what the vCenter has to customise a guest
// with, and nothing else. It exists because the question it answers is the one
// that follows "why was I not asked which specification to use" — and because
// a name typed at --spec is worth being able to look up.
func specsCLI(vc VCenter) error {
s, err := connect(vc)
if err != nil {
return err
}
defer s.close()
names, err := specNames(s)
if err != nil {
return err
}
if len(names) == 0 {
PE(vc.Name+" holds no customisation specifications",
"they are made in the vSphere client, under Policies and Profiles")
return nil
}
for _, n := range names {
PO(n)
}
return nil
}
// deployCLI is `gvm new`: work out where it goes, say so, ask, and then wait —
// a script that gets its prompt back wants the machine to exist.
func deployCLI(vc VCenter, template, name string, opts deployOpts, yes bool) error {
s, err := connect(vc)
if err != nil {
return err
}
defer s.close()
if err := checkName(name); err != nil {
return err
}
vm, err := s.vm(template)
if err != nil {
return err
}
src, err := sourceOf(s, vm.Reference())
if err != nil {
return err
}
if !src.template {
return errf("%s is a machine, not a template — gvm only makes copies of templates", src.name)
}
t, err := targetFor(s, src, opts)
if err != nil {
return err
}
if nameTaken(s, name) {
return errf("%s already has a machine called %s", vc.Name, name)
}
started := "no — o in the list, or --on next time"
if opts.powerOn {
started = "yes, as soon as it is made"
}
facts := [][2]string{
{"from", src.name},
{"new machine", name},
{"where", t.describe()},
{"powered on", started},
}
// An address to be fetched is fetched here, before the question, so that the
// question says the address rather than the promise of one. give hands it
// back where the deployment does not happen after all.
opts, rec, give, err := fetchAddress(opts, name)
if err != nil {
return err
}
if rec.Name != "" {
// Said here, where it stays on the screen, and not only inside the
// confirmation: this is a number somebody writes down.
PO(SF("%s: %s", rec.Name, rec.address()))
}
if opts.customising() {
// Resolved before the question, not after it: a specification that does
// not exist, or one whose adapter has no netmask, is something to hear
// about now rather than at the end of a clone.
custom, err := customizationFor(s, opts, src, name)
if err != nil {
give()
return err
}
how := opts.spec
if opts.building() {
how = "from " + configFile()
}
facts = append(facts, [2]string{"customise", how}, [2]string{"hostname", hostNameOf(custom)})
address := addressOf(custom)
if rec.Name != "" {
address += SF(" (%s, from %s)", rec.Name, opts.dns.path)
}
facts = append(facts, [2]string{"address", address})
if opts.building() && opts.ip != "" && gatewayOffSubnet(opts.ip, opts.st) {
PE(SF("the gateway %s is not on the same network as %s — the machine will not reach it",
opts.st.gateway, opts.ip))
}
if toolsMissing(s, src.ref) {
PE(src.name + " reports no VMware Tools, which is what carries a customisation out inside " +
"the guest — the machine will be made either way, but it may come up as the template did")
}
}
ok, err := confirmFacts(vc, "make "+name+" from the template "+src.name, facts, yes)
if err != nil || !ok {
give() // nothing was made, so nothing keeps the address
return err
}
task, err := startDeploy(s, src, t, name, opts)
if err != nil {
give()
return err
}
PO(SF("making %s from %s ...", name, src.name))
if err := waitTask(s.ctx, task, cloneWait, SF("making %s", name)); err != nil {
return err
}
made := name + " is made"
if opts.ip != "" && opts.customising() {
made += " at " + opts.ip
}
PO(made)
return nil
}