Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7252d7a6d | ||
|
|
da56a5d048 |
@@ -109,6 +109,7 @@ not.
|
||||
| `new --from <template> --name <vm>` | make a new machine from a template |
|
||||
| `new ... --host <h> --datastore <ds> --on` | where it lands, and whether it starts |
|
||||
| `new ... --spec <spec> --ip <addr>` | customise the guest: hostname and address |
|
||||
| `new ... --ip auto` | have the site's `dns` tool hand an address out |
|
||||
| `new --specs` | the customisation specifications this vCenter holds |
|
||||
| `config` | also shows what a new guest would be told about the network |
|
||||
| `host [-t]` | per-host cpu, memory, machine counts; `-t` also posts them |
|
||||
@@ -544,6 +545,68 @@ no specifications (or will not say) *and* `~/.gvmrc` has not been told the site:
|
||||
`gvm new --specs` lists what a server holds, and prints the server's own words
|
||||
when it will not say.
|
||||
|
||||
#### An address it fetches rather than one you look up
|
||||
|
||||
gvm new --from ubuntu-tpl --name web05 --ip auto
|
||||
|
||||
Where the site has a tool that hands addresses out, `--ip auto` asks it instead
|
||||
of making you look one up first. In the list the same word does it:
|
||||
|
||||
address for web05, "auto" for one from dns, empty for DHCP:
|
||||
|
||||
It shells out to `dns`, the Infoblox helper — found on the path by that name, or
|
||||
named as `dnstool` in `~/.gvmrc` where it lives somewhere else. Everything it
|
||||
knows (the appliance, the credentials, the default domain) stays its own; a
|
||||
second copy of any of that inside gvm would be a second thing to keep right.
|
||||
`gvm config` says whether one was found, because "why was I not offered that"
|
||||
is the question that follows an option quietly not being there.
|
||||
|
||||
**The name has to be free.** It is asked before anything is added, and a name
|
||||
the appliance already knows is refused rather than added to — what `dns -a`
|
||||
would do with one is either refuse in worse words, or hang a second address on
|
||||
somebody else's host record:
|
||||
|
||||
web05 already exists (web05.fhi.mpg.de at 141.14.140.182) — give the machine
|
||||
another name, or free that one with 'dns -d web05'
|
||||
|
||||
Which is the same question gvm asks the vCenter about the machine's name, one
|
||||
answer short of the same answer.
|
||||
|
||||
**And the address is said where it stays.** In the confirmation, and again in
|
||||
the line that is left on the screen afterwards — this is a number somebody
|
||||
writes down:
|
||||
|
||||
web05 is being made at 141.14.140.182 — ubuntu-tpl shows how far along it is
|
||||
|
||||
Three more things about it are deliberate:
|
||||
|
||||
* **What it says is believed before how it exits.** `dns -s` on a name that is
|
||||
not there prints `ERROR: host '...' not found` and exits 0 all the same, so a
|
||||
refusal is read out of the output. gvm's first version read the exit status,
|
||||
which took every refusal for an agreement and — the other way about — made a
|
||||
free name look like an answer nobody could parse. An appliance that cannot be
|
||||
reached is an error and not a free name: concluding "nobody has this name"
|
||||
from a server that is down is the last thing to do before asking it for an
|
||||
address.
|
||||
* **The record is read back**, not taken from the sentence the helper prints.
|
||||
`OK: host 'web05.fhi.mpg.de' added with IP '141.14.140.182'` is written for a
|
||||
person and gets reworded between versions; the record is the appliance's own
|
||||
answer to the same question, and where the two disagree the record is what the
|
||||
machine will have.
|
||||
* **It is fetched before the confirmation**, so that the question says the
|
||||
address rather than a promise of one:
|
||||
|
||||
make web05 from ubuntu-tpl on v308? y = yes, anything else = no
|
||||
in prod, powered off · named and addressed for fhi.mpg.de at 141.14.140.182 (web05.fhi.mpg.de, from dns)
|
||||
|
||||
* **And given back where it is not used.** A deployment abandoned at that
|
||||
question, or one the vCenter then refuses, releases the address rather than
|
||||
leaving a record behind for a machine that was never made.
|
||||
|
||||
The name asked for is the machine's own — the short one, since the helper puts
|
||||
the record in the site's default domain itself — which is also the hostname the
|
||||
guest is given, so the two cannot disagree.
|
||||
|
||||
Three things it refuses rather than guesses at, on either road:
|
||||
|
||||
* **An address with no netmask to go with it.** Where the specification's
|
||||
@@ -1035,6 +1098,17 @@ wrong quietly:
|
||||
specifications, and one that will not let them be read, both used to skip the
|
||||
question in silence — and that the reason and the placement both fit on the
|
||||
line they share
|
||||
* that a name already in the appliance is refused before anything is added to
|
||||
it, that an appliance which cannot be reached is not mistaken for a free
|
||||
name, and that the helper's habits are the real ones — the stand-in says what
|
||||
is wrong in its output and exits 0 while doing so, which is what the first
|
||||
version of this got wrong
|
||||
* that an address fetched from the site's tool is read back from the record
|
||||
rather than from its printed sentence, that the tool's own words come back
|
||||
when it refuses, that a fetched address is given back when the deployment
|
||||
does not happen, and that nothing is ever asked of it for an address that was
|
||||
typed — against a stand-in, because a test suite that called the real one
|
||||
would be editing the institute's network every time it ran
|
||||
* that the specification gvm writes itself carries the site's five settings and
|
||||
this machine's two, invents nothing where an address was not given, and is
|
||||
refused for a Windows template and for a configuration that is not complete —
|
||||
|
||||
+43
-13
@@ -657,7 +657,7 @@ func (b *browser) deploy(r vmRow) {
|
||||
// that runs off the edge loses whichever half was put last. `gvm new
|
||||
// --specs` is where the server's own words are.
|
||||
b.deployAsk(r, src, target, name, deployOpts{},
|
||||
"the customisation specifications could not be read (gvm new --specs)")
|
||||
deployStep{note: "the customisation specifications could not be read (gvm new --specs)"})
|
||||
return
|
||||
case len(specs) == 0 && !own:
|
||||
why := SF("%s has no customisation specifications", r.vc.Name)
|
||||
@@ -667,7 +667,7 @@ func (b *browser) deploy(r vmRow) {
|
||||
why = SF("no specifications on %s, and %s has no %s", r.vc.Name,
|
||||
configFile(), strings.Join(miss, ", "))
|
||||
}
|
||||
b.deployAsk(r, src, target, name, deployOpts{}, why)
|
||||
b.deployAsk(r, src, target, name, deployOpts{}, deployStep{note: why})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -688,12 +688,13 @@ func (b *browser) deploy(r vmRow) {
|
||||
b.choose(SF("customise %s how", name), r, lines, func(i int) {
|
||||
switch kinds[i] {
|
||||
case "":
|
||||
b.deployAsk(r, src, target, name, deployOpts{}, "")
|
||||
b.deployAsk(r, src, target, name, deployOpts{}, deployStep{})
|
||||
case "-":
|
||||
b.deployAddress(r, src, target, name, deployOpts{how: customSite, st: b.site})
|
||||
b.deployAddress(r, src, target, name,
|
||||
deployOpts{how: customSite, st: b.site, dns: b.dns})
|
||||
default:
|
||||
b.deployAddress(r, src, target, name,
|
||||
deployOpts{how: customSpec, spec: kinds[i], st: b.site})
|
||||
deployOpts{how: customSpec, spec: kinds[i], st: b.site, dns: b.dns})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -712,17 +713,35 @@ func (b *browser) deployAddress(r vmRow, src deploySource, t deployTarget, name
|
||||
// words rather than in the shorthand it used to use — "(empty = as
|
||||
// linux-static says)" reads as a sentence with a word missing even when the
|
||||
// name is there.
|
||||
ask := SF("address for %s, or empty for DHCP: ", name)
|
||||
//
|
||||
// And where the site has a tool that hands addresses out, the word for
|
||||
// that is offered too — an address one does not have to look up first is
|
||||
// the whole point of it being there.
|
||||
leave := "empty for DHCP"
|
||||
if opts.how == customSpec {
|
||||
ask = SF("address for %s, or empty to leave it to %s: ", name, opts.spec)
|
||||
leave = "empty to leave it to " + opts.spec
|
||||
}
|
||||
ask := SF("address for %s, %s: ", name, leave)
|
||||
if opts.dns != "" {
|
||||
ask = SF("address for %s, %q for one from dns, %s: ", name, autoIP, leave)
|
||||
}
|
||||
|
||||
ip, ok := b.input(ask)
|
||||
if !ok {
|
||||
b.setStatus(colDim, "nothing done")
|
||||
return
|
||||
}
|
||||
opts.ip = strings.TrimSpace(ip)
|
||||
b.deployAsk(r, src, t, name, opts, "")
|
||||
|
||||
// Fetched here rather than at the confirmation, so that the question can
|
||||
// say the address the machine will have. give hands it back where the
|
||||
// deployment does not happen after all.
|
||||
opts, rec, give, err := fetchAddress(opts, name)
|
||||
if err != nil {
|
||||
b.setStatus(colErr, err.Error())
|
||||
return
|
||||
}
|
||||
b.deployAsk(r, src, t, name, opts, deployStep{rec: rec, give: give})
|
||||
}
|
||||
|
||||
// deployAsk is the last step: what will happen, in full, and then the machine.
|
||||
@@ -730,15 +749,16 @@ func (b *browser) deployAddress(r vmRow, src deploySource, t deployTarget, name
|
||||
// note is why a step did not happen — no customisation specifications on the
|
||||
// server, or none that could be read. A step that is skipped without a word
|
||||
// looks like one that is broken.
|
||||
func (b *browser) deployAsk(r vmRow, src deploySource, t deployTarget, name string, opts deployOpts, note string) {
|
||||
func (b *browser) deployAsk(r vmRow, src deploySource, t deployTarget, name string,
|
||||
opts deployOpts, step deployStep) {
|
||||
// One line holds all of this, and a terminal cuts it from the right. So
|
||||
// what is unexpected goes first: the placement is the same every time and
|
||||
// the machine will show it afterwards, while "there was no question about
|
||||
// customisation, and here is why" is the thing somebody is looking for and
|
||||
// will not scroll to find.
|
||||
var parts []string
|
||||
if note != "" {
|
||||
parts = append(parts, note)
|
||||
if step.note != "" {
|
||||
parts = append(parts, step.note)
|
||||
}
|
||||
parts = append(parts, "in "+t.describe()+", powered off")
|
||||
|
||||
@@ -752,6 +772,9 @@ func (b *browser) deployAsk(r vmRow, src deploySource, t deployTarget, name stri
|
||||
}
|
||||
if opts.ip != "" {
|
||||
with += " at " + opts.ip
|
||||
if step.rec.Name != "" {
|
||||
with += " (" + step.rec.Name + ", from dns)"
|
||||
}
|
||||
}
|
||||
parts = append(parts, with)
|
||||
if toolsMissing(r.sess, src.ref) {
|
||||
@@ -761,17 +784,24 @@ func (b *browser) deployAsk(r vmRow, src deploySource, t deployTarget, name stri
|
||||
where := strings.Join(parts, " · ")
|
||||
|
||||
if !b.askWith(SF("make %s from %s on %s?", name, src.name, r.vc.Name), where) {
|
||||
step.giveBack() // nothing was made, so nothing keeps a fetched address
|
||||
b.setStatus(colDim, "nothing done")
|
||||
return
|
||||
}
|
||||
|
||||
b.working(SF("starting %s ...", name))
|
||||
if _, err := startDeploy(r.sess, src, t, name, opts); err != nil {
|
||||
step.giveBack()
|
||||
b.setStatus(colErr, err.Error())
|
||||
return
|
||||
}
|
||||
b.done(SF("%s is being made — %s shows how far along it is, and it appears in the list when it exists",
|
||||
name, src.name))
|
||||
// The address is in the line that survives, not only in the question that
|
||||
// was answered a moment ago: it is the one thing here somebody writes down.
|
||||
at := ""
|
||||
if opts.ip != "" && opts.customising() {
|
||||
at = " at " + opts.ip
|
||||
}
|
||||
b.done(SF("%s is being made%s — %s shows how far along it is", name, at, src.name))
|
||||
}
|
||||
|
||||
// working puts a line on the screen before an operation that will block the loop
|
||||
|
||||
@@ -761,6 +761,7 @@ type browser struct {
|
||||
|
||||
ssh string // the command `h` runs, from ~/.gvmrc; empty means plain ssh
|
||||
site site // what a machine made from a template is told about the network
|
||||
dns string // the site's address helper, or "" where this machine has none
|
||||
|
||||
// Live mode (live.go): the list re-reading itself on a timer rather than on
|
||||
// a keystroke. hist is what the trend column is drawn from, kept here
|
||||
@@ -816,7 +817,8 @@ func browseVMs(targets []VCenter, filter string, cfg Config) error {
|
||||
return errf("the interactive list needs a terminal (%v) — use 'gvm vm -l' instead", err)
|
||||
}
|
||||
|
||||
b := &browser{targets: targets, filter: filter, ssh: cfg.SSH, site: cfg.site()}
|
||||
b := &browser{targets: targets, filter: filter, ssh: cfg.SSH,
|
||||
site: cfg.site(), dns: dnsTool(cfg.DNSTool)}
|
||||
defer func() { closeSessions(b.sessions) }()
|
||||
|
||||
PF("asking %s ...\n", vcNames(targets))
|
||||
|
||||
@@ -40,6 +40,7 @@ type Config struct {
|
||||
// that are about the one machine.
|
||||
Domain string // fhi-berlin.mpg.de
|
||||
DNS string // one or more resolvers, separated by commas
|
||||
DNSTool string // the site's own address helper; unset means "dns" on the path
|
||||
Netmask string // 255.255.255.0
|
||||
Gateway string // 10.0.0.1
|
||||
Timezone string // Europe/Berlin
|
||||
@@ -489,6 +490,7 @@ func applyConfig(c *Config, m map[string]string) {
|
||||
set("ssh", &c.SSH)
|
||||
set("domain", &c.Domain)
|
||||
set("dns", &c.DNS)
|
||||
set("dnstool", &c.DNSTool)
|
||||
set("netmask", &c.Netmask)
|
||||
set("gateway", &c.Gateway)
|
||||
set("timezone", &c.Timezone)
|
||||
@@ -560,6 +562,7 @@ func applyEnv(c *Config) {
|
||||
env("GVM_SSH", &c.SSH)
|
||||
env("GVM_DOMAIN", &c.Domain)
|
||||
env("GVM_DNS", &c.DNS)
|
||||
env("GVM_DNSTOOL", &c.DNSTool)
|
||||
env("GVM_NETMASK", &c.Netmask)
|
||||
env("GVM_GATEWAY", &c.Gateway)
|
||||
env("GVM_TIMEZONE", &c.Timezone)
|
||||
@@ -679,7 +682,11 @@ func writeConfigTemplate(path string) {
|
||||
b.WriteString("# dns = 10.0.0.1, 10.0.0.2\n")
|
||||
b.WriteString("# netmask = 255.255.255.0\n")
|
||||
b.WriteString("# gateway = 10.0.0.1\n")
|
||||
b.WriteString("# timezone = Europe/Berlin\n\n")
|
||||
b.WriteString("# timezone = Europe/Berlin\n")
|
||||
b.WriteString("# An address can also be fetched rather than typed, where the site has a\n")
|
||||
b.WriteString("# tool for it: 'gvm new ... --ip auto'. Unset, gvm looks for 'dns' on the\n")
|
||||
b.WriteString("# path and offers the option only where it finds it.\n")
|
||||
b.WriteString("# dnstool = /usr/local/bin/dns\n\n")
|
||||
b.WriteString("# --- how the sheet's 'h' logs in to a guest ---\n")
|
||||
b.WriteString("# %h is where the machine's name or address goes; appended when it is\n")
|
||||
b.WriteString("# not written anywhere. Unset means '" + defaultSSH + "'.\n")
|
||||
|
||||
@@ -54,8 +54,14 @@ type deployOpts struct {
|
||||
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
|
||||
ip string // empty leaves the adapter on DHCP; autoIP fetches one
|
||||
st site // the site's own answers, from the configuration
|
||||
dns string // the site's address helper, or "" where there is none
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -309,6 +315,55 @@ func cloneSpec(t deployTarget, opts deployOpts, custom *types.CustomizationSpec)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
return opts, dnsHost{}, nothing, errf("there is no address tool on this machine — put one "+
|
||||
"on the path as 'dns', name it as dnstool in %s, or give --ip an address", configFile())
|
||||
}
|
||||
|
||||
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
|
||||
@@ -661,12 +716,26 @@ func deployCLI(vc VCenter, template, name string, opts deployOpts, yes bool) err
|
||||
{"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
|
||||
@@ -674,7 +743,11 @@ func deployCLI(vc VCenter, template, name string, opts deployOpts, yes bool) err
|
||||
how = "from " + configFile()
|
||||
}
|
||||
facts = append(facts, [2]string{"customise", how}, [2]string{"hostname", hostNameOf(custom)})
|
||||
facts = append(facts, [2]string{"address", addressOf(custom)})
|
||||
address := addressOf(custom)
|
||||
if rec.Name != "" {
|
||||
address += SF(" (%s, from %s)", rec.Name, opts.dns)
|
||||
}
|
||||
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))
|
||||
@@ -686,17 +759,23 @@ func deployCLI(vc VCenter, template, name string, opts deployOpts, yes bool) err
|
||||
}
|
||||
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
|
||||
}
|
||||
PO(SF("%s is made", name))
|
||||
made := name + " is made"
|
||||
if opts.ip != "" && opts.customising() {
|
||||
made += " at " + opts.ip
|
||||
}
|
||||
PO(made)
|
||||
return nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -365,7 +365,7 @@ func TestAMissingStepSaysWhyItIsMissing(t *testing.T) {
|
||||
kw.WriteString("n") // anything but y: the question is what is being checked
|
||||
|
||||
b.deployAsk(r, deploySource{name: "ubuntu-tpl"},
|
||||
deployTarget{where: "prod-cluster"}, "web05", deployOpts{}, note)
|
||||
deployTarget{where: "prod-cluster"}, "web05", deployOpts{}, deployStep{note: note})
|
||||
|
||||
frame := stripEscapes(<-drawn)
|
||||
pw.Close()
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// dns.go — asking the site's own tool for an address.
|
||||
//
|
||||
// A machine made from a template needs a name and an address, and at this site
|
||||
// the address does not come out of gvm's head: there is an Infoblox behind a
|
||||
// helper called `dns`, and an address that is not in it is an address nobody
|
||||
// may use. So gvm asks it rather than inventing one, and only where it is
|
||||
// there — a copy of gvm on a laptop that has no such tool simply does not offer
|
||||
// the option.
|
||||
//
|
||||
// Everything here shells out. That is deliberate: the helper already knows the
|
||||
// site's Infoblox, its credentials and its default domain, and a second
|
||||
// implementation of any of that inside gvm would be a second thing to keep
|
||||
// right.
|
||||
//
|
||||
// What it does is not read-only, and that shapes the two rules this file holds
|
||||
// to. Nothing is allocated that was not asked for, by name, at the moment it is
|
||||
// asked for. And what is allocated and then not used is given back: a
|
||||
// deployment somebody abandons at the confirmation, or one the vCenter refuses,
|
||||
// must not leave a record behind for a machine that was never made.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// dnsWait is how long the helper is given. It talks to an appliance over the
|
||||
// network, so it is not instant; it is also not a thing to wait minutes for
|
||||
// while a deployment is half set up.
|
||||
const dnsWait = 30 * time.Second
|
||||
|
||||
// autoIP is what is typed where an address would go to have one fetched. A
|
||||
// word rather than a flag of its own: --ip is already "what address does this
|
||||
// machine get", and "auto" is an answer to that question rather than a
|
||||
// different question.
|
||||
const autoIP = "auto"
|
||||
|
||||
// dnsTool is the helper to use, or "" where this machine has none.
|
||||
//
|
||||
// The configuration may name it outright, for a machine where it is not on the
|
||||
// path; otherwise it is looked up by name, which is how it is found on the
|
||||
// machines it is installed on.
|
||||
func dnsTool(configured string) string {
|
||||
if c := strings.TrimSpace(configured); c != "" {
|
||||
if path, err := exec.LookPath(c); err == nil {
|
||||
return path
|
||||
}
|
||||
return "" // named but not there: the option is not offered, and says so
|
||||
}
|
||||
path, err := exec.LookPath("dns")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// dnsHost is the part of a host record gvm reads. The helper answers in the
|
||||
// appliance's own JSON, which has thirty fields; these are the two that say
|
||||
// what the machine is called and what it may use.
|
||||
type dnsHost struct {
|
||||
Name string `json:"name"`
|
||||
Addresses []struct {
|
||||
Addr string `json:"ipv4addr"`
|
||||
} `json:"ipv4addrs"`
|
||||
}
|
||||
|
||||
// address is the first address of the record, or "" where it has none.
|
||||
func (h dnsHost) address() string {
|
||||
for _, a := range h.Addresses {
|
||||
if a.Addr != "" {
|
||||
return a.Addr
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// dnsName is the short name to ask for. The helper puts the record in the
|
||||
// site's default domain itself, so what it wants is the name and not a fully
|
||||
// qualified one — and a machine called web05.example.org would otherwise be
|
||||
// registered as web05.example.org.example.org.
|
||||
func dnsName(host string) string {
|
||||
name, _, _ := strings.Cut(strings.TrimSpace(host), ".")
|
||||
return name
|
||||
}
|
||||
|
||||
// dnsRun is one call to the helper. -y throughout: it asks before it changes
|
||||
// anything when it has a terminal, and gvm has already asked.
|
||||
//
|
||||
// The output is believed before the exit status, because that is how this tool
|
||||
// reports: `dns -s` on a name that is not there prints "ERROR: host '...' not
|
||||
// found" and exits 0 all the same. Reading only the status took every refusal
|
||||
// for an agreement — and, the other way round, made a name that was free look
|
||||
// like an answer nobody could parse.
|
||||
func dnsRun(tool string, args ...string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dnsWait)
|
||||
defer cancel()
|
||||
|
||||
out, err := exec.CommandContext(ctx, tool, append(args, "-y")...).CombinedOutput()
|
||||
text := strings.TrimSpace(string(out))
|
||||
|
||||
switch {
|
||||
case dnsSaidError(text):
|
||||
// The helper's own words, which say what is wrong far better than any
|
||||
// status: "host already exists", "no free address in the network".
|
||||
return text, errf("%s", firstLine(text))
|
||||
case err != nil:
|
||||
if text == "" {
|
||||
return "", errf("%s %s: %w", tool, strings.Join(args, " "), err)
|
||||
}
|
||||
return text, errf("%s", firstLine(text))
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// dnsSaidError and dnsNotFound read the two things the helper says about
|
||||
// itself. "not found" is an answer rather than a failure — it is what a free
|
||||
// name looks like — and everything else beginning with ERROR is a refusal.
|
||||
func dnsSaidError(out string) bool {
|
||||
return strings.HasPrefix(strings.TrimSpace(out), "ERROR")
|
||||
}
|
||||
|
||||
func dnsNotFound(out string) bool {
|
||||
return dnsSaidError(out) && strings.Contains(out, "not found")
|
||||
}
|
||||
|
||||
// dnsShow reads a host record. A name that is not there comes back as an empty
|
||||
// record and no error: "is this name free" is a question, and "no such name" is
|
||||
// its answer rather than a failure to report.
|
||||
//
|
||||
// Anything else that goes wrong is an error and stays one. Swallowing those
|
||||
// would turn an appliance nobody can reach into "the name is free", which is
|
||||
// the last thing to conclude before asking it for an address.
|
||||
func dnsShow(tool, host string) (dnsHost, error) {
|
||||
out, err := dnsRun(tool, "-s", dnsName(host))
|
||||
if err != nil {
|
||||
if dnsNotFound(out) {
|
||||
return dnsHost{}, nil
|
||||
}
|
||||
return dnsHost{}, err
|
||||
}
|
||||
|
||||
var rec dnsHost
|
||||
if err := json.Unmarshal([]byte(out), &rec); err != nil {
|
||||
return dnsHost{}, errf("%s answered something that is not a host record: %s", tool, firstLine(out))
|
||||
}
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// dnsAdd asks for an address for this name and hands back what was given.
|
||||
//
|
||||
// The record is read back rather than taken from what the helper said on the
|
||||
// way past. Its "OK: host 'web05.fhi.mpg.de' added with IP '141.14.140.182'" is
|
||||
// a sentence for a person, and a sentence is a thing that gets reworded between
|
||||
// versions; the record is the appliance's own answer to the same question. If
|
||||
// the two ever disagree, the record is what the machine will actually be given.
|
||||
func dnsAdd(tool, host string) (dnsHost, error) {
|
||||
name := dnsName(host)
|
||||
if name == "" {
|
||||
return dnsHost{}, errf("there is no name to ask for an address for")
|
||||
}
|
||||
|
||||
// Asked first, and refused rather than added to. A name that is already in
|
||||
// the appliance belongs to something — and what `dns -a` would do with it
|
||||
// is either refuse, which is this message with worse wording, or hang a
|
||||
// second address on somebody else's host record, which is worse than
|
||||
// either. It is also the same question gvm asks the vCenter about the
|
||||
// machine's name (nameTaken), one answer short of the same answer.
|
||||
if rec, err := dnsShow(tool, name); err != nil {
|
||||
return dnsHost{}, err
|
||||
} else if rec.Name != "" {
|
||||
taken := rec.Name
|
||||
if a := rec.address(); a != "" {
|
||||
taken += " at " + a
|
||||
}
|
||||
return dnsHost{}, errf("%s already exists (%s) — give the machine another name, "+
|
||||
"or free that one with 'dns -d %s'", name, taken, name)
|
||||
}
|
||||
|
||||
said, err := dnsRun(tool, "-a", name)
|
||||
if err != nil {
|
||||
return dnsHost{}, err
|
||||
}
|
||||
|
||||
rec, err := dnsShow(tool, name)
|
||||
if err != nil {
|
||||
return dnsHost{}, err
|
||||
}
|
||||
if rec.address() == "" {
|
||||
return dnsHost{}, errf("%s said %q, but the record cannot be read back", tool, firstLine(said))
|
||||
}
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// dnsRemove gives an address back. It is called where a deployment did not
|
||||
// happen after all, so its failure is worth saying and not worth stopping for:
|
||||
// the machine was not made either way, and what is left behind is a record
|
||||
// somebody can delete by hand.
|
||||
func dnsRemove(tool, host string) error {
|
||||
_, err := dnsRun(tool, "-d", dnsName(host))
|
||||
return err
|
||||
}
|
||||
|
||||
// firstLine keeps a message from a tool to one line, for a status line that has
|
||||
// one.
|
||||
func firstLine(s string) string {
|
||||
line, _, _ := strings.Cut(strings.TrimSpace(s), "\n")
|
||||
return line
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeDNS writes a stand-in for the site's helper and returns it with the file
|
||||
// it writes down what it was asked in.
|
||||
//
|
||||
// Nothing in this file may touch the real one. `dns -a` takes an address out of
|
||||
// the site's Infoblox and `dns -d` gives one back, and a test suite that did
|
||||
// either would be editing the institute's network every time somebody ran it.
|
||||
// So the thing under test is everything around the call — what gvm asks for,
|
||||
// what it makes of the answer, and what it does when the answer is no.
|
||||
//
|
||||
// It keeps the two habits of the real tool that gvm has to live with, both
|
||||
// measured rather than assumed: it says what is wrong in its output, beginning
|
||||
// with ERROR, and it exits 0 while doing so — a name that is not there is
|
||||
// "ERROR: host '...' not found" and a successful run at the same time. A
|
||||
// stand-in that answered in exit statuses would have let gvm's first version
|
||||
// pass, which believed them.
|
||||
func fakeDNS(t *testing.T, body string) (tool, log string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
tool = filepath.Join(dir, "dns")
|
||||
log = filepath.Join(dir, "asked")
|
||||
|
||||
script := "#!/bin/sh\necho \"$@\" >> " + log + "\n" + body + "\nexit 0\n"
|
||||
if err := os.WriteFile(tool, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tool, log
|
||||
}
|
||||
|
||||
// fakeInfoblox is that stand-in with a memory: a name is not there until it has
|
||||
// been added, and is gone again once it has been deleted. Which is the whole
|
||||
// shape of what gvm does with it.
|
||||
func fakeInfoblox(t *testing.T) (tool, log string) {
|
||||
t.Helper()
|
||||
state := filepath.Join(t.TempDir(), "records")
|
||||
return fakeDNS(t, `
|
||||
state=`+state+`
|
||||
case "$1" in
|
||||
-a) if grep -qx "$2" $state 2>/dev/null; then
|
||||
echo "ERROR: host '$2.fhi.mpg.de' already exists"
|
||||
else
|
||||
echo "$2" >> $state
|
||||
echo "OK: host '$2.fhi.mpg.de' added with IP '141.14.140.182'"
|
||||
fi ;;
|
||||
-s) if grep -qx "$2" $state 2>/dev/null; then
|
||||
printf '{"name":"%s.fhi.mpg.de","ipv4addrs":[{"ipv4addr":"141.14.140.182"}]}\n' "$2"
|
||||
else
|
||||
echo "ERROR: host '$2.fhi.mpg.de' not found"
|
||||
fi ;;
|
||||
-d) grep -vx "$2" $state > $state.tmp 2>/dev/null
|
||||
mv $state.tmp $state 2>/dev/null
|
||||
echo "OK: host '$2.fhi.mpg.de' deleted" ;;
|
||||
esac`)
|
||||
}
|
||||
|
||||
// asked is what the helper was called with, one call per line.
|
||||
func asked(t *testing.T, log string) []string {
|
||||
t.Helper()
|
||||
out, err := os.ReadFile(log)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
}
|
||||
|
||||
// The record is read back rather than taken from the sentence the helper prints
|
||||
// on its way past: "OK: host '...' added with IP '...'" is written for a person
|
||||
// and gets reworded between versions, while the record is the appliance's own
|
||||
// answer to the same question.
|
||||
func TestAnAddressIsReadBackFromTheRecord(t *testing.T) {
|
||||
tool, log := fakeInfoblox(t)
|
||||
|
||||
rec, err := dnsAdd(tool, "web05")
|
||||
if err != nil {
|
||||
t.Fatalf("dnsAdd: %v", err)
|
||||
}
|
||||
if got := rec.address(); got != "141.14.140.182" {
|
||||
t.Errorf("the address came back as %q", got)
|
||||
}
|
||||
if rec.Name != "web05.fhi.mpg.de" {
|
||||
t.Errorf("the name came back as %q", rec.Name)
|
||||
}
|
||||
|
||||
// Asked whether the name is free, then to add it, then what it made — and
|
||||
// never without -y, which is what keeps it from stopping to ask a question
|
||||
// nobody is there to answer.
|
||||
calls := asked(t, log)
|
||||
if len(calls) != 3 || !strings.HasPrefix(calls[0], "-s web05") ||
|
||||
!strings.HasPrefix(calls[1], "-a web05") || !strings.HasPrefix(calls[2], "-s web05") {
|
||||
t.Errorf("it was asked %v", calls)
|
||||
}
|
||||
for _, c := range calls {
|
||||
if !strings.Contains(c, "-y") {
|
||||
t.Errorf("a call was made that could stop and ask: %q", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A helper that refuses says why in its own words, which are better than any
|
||||
// gvm could put there: "host already exists", "no free address in the network".
|
||||
func TestTheHelpersOwnWordsComeBack(t *testing.T) {
|
||||
tool, _ := fakeDNS(t, `echo "ERROR: no free address in the network 141.14.128.0/20"`)
|
||||
|
||||
_, err := dnsAdd(tool, "web05")
|
||||
if err == nil {
|
||||
t.Fatal("a helper that refused was taken for one that agreed")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no free address") {
|
||||
t.Errorf("its words did not come back: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A name that is already in the appliance is refused before anything is added
|
||||
// to it — and the refusal says what is in the way and how to clear it. What
|
||||
// `dns -a` would do with it is either refuse in worse words, or hang a second
|
||||
// address on somebody else's host record.
|
||||
func TestANameThatIsTakenIsRefused(t *testing.T) {
|
||||
tool, log := fakeInfoblox(t)
|
||||
|
||||
if _, err := dnsAdd(tool, "web05"); err != nil {
|
||||
t.Fatalf("the first one went wrong: %v", err)
|
||||
}
|
||||
before := len(asked(t, log))
|
||||
|
||||
_, err := dnsAdd(tool, "web05")
|
||||
if err == nil {
|
||||
t.Fatal("a name that is already there was taken again")
|
||||
}
|
||||
for _, want := range []string{"already exists", "141.14.140.182", "dns -d web05"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the refusal does not say %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Refused before anything was asked of it beyond the question.
|
||||
calls := asked(t, log)
|
||||
if len(calls) != before+1 || !strings.HasPrefix(calls[before], "-s web05") {
|
||||
t.Errorf("more than a look was taken: %v", calls[before:])
|
||||
}
|
||||
}
|
||||
|
||||
// A helper that cannot be reached is not a free name. Swallowing that would
|
||||
// turn an appliance that is down into "nobody has this name", which is the last
|
||||
// thing to conclude before asking it for an address.
|
||||
func TestAnApplianceThatIsDownIsNotAFreeName(t *testing.T) {
|
||||
tool, _ := fakeDNS(t, `echo "ERROR: cannot reach infoblox.fhi.mpg.de"`)
|
||||
|
||||
if _, err := dnsShow(tool, "web05"); err == nil {
|
||||
t.Error("an appliance that could not be reached reported a free name")
|
||||
}
|
||||
if _, err := dnsAdd(tool, "web05"); err == nil {
|
||||
t.Error("an address was asked for over an appliance that could not be reached")
|
||||
}
|
||||
}
|
||||
|
||||
// A helper that agrees but leaves no record is not an answer either: a
|
||||
// deployment would then be given an empty address and the machine would come up
|
||||
// on nothing.
|
||||
func TestAnAddressThatCannotBeReadBackIsNoAddress(t *testing.T) {
|
||||
tool, _ := fakeDNS(t, `case "$1" in
|
||||
-a) echo "OK: host 'web05.fhi.mpg.de' added with IP '141.14.140.182'" ;;
|
||||
-s) echo '{"name": "web05.fhi.mpg.de", "ipv4addrs": []}' ;;
|
||||
esac`)
|
||||
|
||||
if _, err := dnsAdd(tool, "web05"); err == nil {
|
||||
t.Fatal("an empty record was taken for an address")
|
||||
}
|
||||
|
||||
// And something that is not a record at all.
|
||||
tool, _ = fakeDNS(t, `case "$1" in
|
||||
-a) echo "OK" ;;
|
||||
-s) echo 'not json at all' ;;
|
||||
esac`)
|
||||
if _, err := dnsAdd(tool, "web05"); err == nil {
|
||||
t.Fatal("a page of prose was taken for a host record")
|
||||
}
|
||||
}
|
||||
|
||||
// The helper puts the record in the site's own domain, so what it wants is a
|
||||
// name and not a fully qualified one — web05.example.org would otherwise be
|
||||
// registered as web05.example.org.example.org.
|
||||
func TestOnlyTheNameIsAskedFor(t *testing.T) {
|
||||
for _, c := range []struct{ in, want string }{
|
||||
{"web05", "web05"},
|
||||
{"web05.fhi-berlin.mpg.de", "web05"},
|
||||
{" web05.example.org ", "web05"},
|
||||
{"", ""},
|
||||
} {
|
||||
if got := dnsName(c.in); got != c.want {
|
||||
t.Errorf("dnsName(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An address that was fetched and then not used is given back. A deployment
|
||||
// somebody abandons at the confirmation must not leave a record behind for a
|
||||
// machine that was never made.
|
||||
func TestAFetchedAddressIsGivenBack(t *testing.T) {
|
||||
tool, log := fakeInfoblox(t)
|
||||
|
||||
opts := deployOpts{how: customSite, st: testSite(), ip: autoIP, dns: tool}
|
||||
opts, rec, give, err := fetchAddress(opts, "web05")
|
||||
if err != nil {
|
||||
t.Fatalf("fetchAddress: %v", err)
|
||||
}
|
||||
if opts.ip != "141.14.140.182" {
|
||||
t.Errorf("the deployment was given %q as its address", opts.ip)
|
||||
}
|
||||
if rec.Name == "" {
|
||||
t.Error("the record came back without a name to show")
|
||||
}
|
||||
|
||||
give()
|
||||
calls := asked(t, log)
|
||||
if n := len(calls); n == 0 || !strings.HasPrefix(calls[n-1], "-d web05") {
|
||||
t.Errorf("the address was not given back: %v", calls)
|
||||
}
|
||||
|
||||
// And having given it back, the name is free again — which is the whole
|
||||
// point of giving it back.
|
||||
if rec, err := dnsShow(tool, "web05"); err != nil || rec.Name != "" {
|
||||
t.Errorf("the name is still taken after being released: %+v (%v)", rec, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Everything above happens only where the site has such a tool. Without one,
|
||||
// "auto" is refused with what to do about it, and — the point of the whole
|
||||
// arrangement — an ordinary address is untouched either way.
|
||||
func TestWithoutAToolAutoIsRefusedAndNothingElseChanges(t *testing.T) {
|
||||
_, _, _, err := fetchAddress(deployOpts{ip: autoIP}, "web05")
|
||||
if err == nil {
|
||||
t.Fatal("an address was fetched with no tool to fetch it from")
|
||||
}
|
||||
for _, want := range []string{"no address tool", "dnstool"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the refusal does not mention %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A typed address is never handed to a tool, whether there is one or not.
|
||||
tool, log := fakeDNS(t, `echo "this should never run"; exit 1`)
|
||||
opts, rec, give, err := fetchAddress(deployOpts{ip: "10.0.0.55", dns: tool}, "web05")
|
||||
if err != nil {
|
||||
t.Fatalf("a typed address went wrong: %v", err)
|
||||
}
|
||||
if opts.ip != "10.0.0.55" || rec.Name != "" {
|
||||
t.Errorf("a typed address was changed to %q", opts.ip)
|
||||
}
|
||||
give() // must be safe, and must ask nothing
|
||||
if calls := asked(t, log); len(calls) != 0 {
|
||||
t.Errorf("the tool was called for an address that was typed: %v", calls)
|
||||
}
|
||||
|
||||
// And no address at all asks nothing either.
|
||||
if _, _, _, err := fetchAddress(deployOpts{dns: tool}, "web05"); err != nil {
|
||||
t.Errorf("a deployment with no address at all went wrong: %v", err)
|
||||
}
|
||||
if calls := asked(t, log); len(calls) != 0 {
|
||||
t.Errorf("the tool was called with no address asked for: %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// Which helper to use: the one named in the configuration, or "dns" on the
|
||||
// path, or none — and a configuration that names one that is not there offers
|
||||
// nothing rather than quietly using a different one.
|
||||
func TestWhichHelperIsUsed(t *testing.T) {
|
||||
tool, _ := fakeDNS(t, `echo hello`)
|
||||
|
||||
if got := dnsTool(tool); got != tool {
|
||||
t.Errorf("a named helper resolved to %q", got)
|
||||
}
|
||||
if got := dnsTool(filepath.Join(t.TempDir(), "not-there")); got != "" {
|
||||
t.Errorf("a named helper that is not there resolved to %q", got)
|
||||
}
|
||||
|
||||
// Found by name on the path, and not found where it is not.
|
||||
t.Setenv("PATH", filepath.Dir(tool))
|
||||
if got := dnsTool(""); got != tool {
|
||||
t.Errorf("the helper on the path resolved to %q", got)
|
||||
}
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
if got := dnsTool(""); got != "" {
|
||||
t.Errorf("a path with no helper on it resolved to %q", got)
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func run() error {
|
||||
subNew.String(&newStore, "", "datastore", "Put it on this datastore, rather than the template's own")
|
||||
subNew.Bool(&newOn, "", "on", "Power it on once it is made")
|
||||
subNew.String(&newSpec, "", "spec", "Customise the guest with this specification from the vCenter")
|
||||
subNew.String(&newIP, "", "ip", "Give it this address, with the netmask and gateway from the specification")
|
||||
subNew.String(&newIP, "", "ip", "Give it this address — or 'auto' to have the site's dns tool hand one out")
|
||||
subNew.String(&newHostname, "", "hostname", "Name the guest this, rather than after the machine")
|
||||
subNew.Bool(&newSpecs, "", "specs", "List the customisation specifications this vCenter holds")
|
||||
|
||||
@@ -333,7 +333,8 @@ func run() error {
|
||||
}
|
||||
return deployCLI(vc, newFrom, newName, deployOpts{
|
||||
host: newHost, datastore: newStore, powerOn: newOn, how: how,
|
||||
spec: newSpec, ip: newIP, hostname: newHostname, st: cfg.site(),
|
||||
spec: newSpec, ip: newIP, hostname: newHostname,
|
||||
st: cfg.site(), dns: dnsTool(cfg.DNSTool),
|
||||
}, yes)
|
||||
|
||||
case subHost.Used:
|
||||
@@ -425,6 +426,14 @@ func showConfig(cfg Config) error {
|
||||
PF("telemetry %s\n", orNone(cfg.Telemetry))
|
||||
PF("ssh %s\n", orNone(strings.Join(sshCommand(cfg.SSH, "<machine>"), " ")))
|
||||
PF("new guest %s\n", cfg.site().describe())
|
||||
// Said whether it is there or not: "why was I not offered an address" is
|
||||
// the question that follows an option quietly not being there, and this is
|
||||
// where it is answered.
|
||||
tool := dnsTool(cfg.DNSTool)
|
||||
if tool == "" {
|
||||
tool = "none found — 'gvm new --ip auto' wants one, as 'dns' on the path or dnstool here"
|
||||
}
|
||||
PF("dns tool %s\n", tool)
|
||||
PF("completion %s\n", inventoryAge())
|
||||
PF("version %s\n", version)
|
||||
return nil
|
||||
|
||||
+11
-1
@@ -85,6 +85,16 @@ telemetry = http://monitor.rz-berlin.mpg.de/telemetry.php
|
||||
# netmask = 255.255.255.0
|
||||
# gateway = 10.0.0.1
|
||||
# timezone = Europe/Berlin
|
||||
#
|
||||
# The address can be fetched instead of typed, where the site has a tool for it:
|
||||
#
|
||||
# gvm new --from ubuntu-tpl --name web05 --ip auto
|
||||
#
|
||||
# gvm looks for "dns" on the path and offers that only where it finds one; name
|
||||
# it here where it lives somewhere else. It is asked for an address under the
|
||||
# machine's own name, and the address is given back if the deployment does not
|
||||
# happen after all.
|
||||
# dnstool = /usr/local/bin/dns
|
||||
|
||||
# --- ssh, for the sheet's 'h' ---
|
||||
# The command that logs in to a machine's guest from its sheet. "%h" is where
|
||||
@@ -102,7 +112,7 @@ telemetry = http://monitor.rz-berlin.mpg.de/telemetry.php
|
||||
#
|
||||
# GVM_DEFAULT, GVM_MAILFROM, GVM_MAILTO, GVM_SMTPHOST, GVM_SMTPPORT,
|
||||
# GVM_TELEMETRY, GVM_SSH, GVM_DOMAIN, GVM_DNS, GVM_NETMASK, GVM_GATEWAY,
|
||||
# GVM_TIMEZONE
|
||||
# GVM_TIMEZONE, GVM_DNSTOOL
|
||||
# GVM_VCENTER_<NAME>_<FIELD>, e.g. GVM_VCENTER_V308_PASSWORD
|
||||
#
|
||||
# which is the way to keep a password out of a file altogether — under cron,
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.3.1
|
||||
1.3.3
|
||||
|
||||
Reference in New Issue
Block a user