Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae936420e0 | ||
|
|
1aab0cf4a5 |
@@ -265,6 +265,10 @@ It refreshes over the connections that are already open. `^r` logs in again,
|
||||
which is how a session that has died is recovered; doing that every ten seconds
|
||||
would be three logins a minute for nothing.
|
||||
|
||||
`^l` is not the only way in: making a machine from a template turns it on, since
|
||||
a clone that has just been started is exactly the screen this is for, and says
|
||||
so in the same line that reports the machine.
|
||||
|
||||
Two things come with it.
|
||||
|
||||
**The line under the table says what just changed.** A table shows what is; this
|
||||
@@ -479,10 +483,17 @@ puts it somewhere other than the template's own.
|
||||
**It does not wait.** A clone copies every disk the template has, which is
|
||||
minutes to the half hour, and a list frozen for that long is a list nobody would
|
||||
start one from. The task hangs off the template in vCenter, so the row it was
|
||||
started from shows how far along it is in its TASK column — `^l` makes that move
|
||||
on its own, and announces the new machine on the line under the table when it
|
||||
exists. `gvm new` on the command line does wait: a script that gets its prompt
|
||||
back wants the machine to be there.
|
||||
started from shows how far along it is in its TASK column. `gvm new` on the
|
||||
command line does wait: a script that gets its prompt back wants the machine to
|
||||
be there.
|
||||
|
||||
**And it goes to that column.** The machine's sheet — which is where the menu
|
||||
was opened and a page of facts about the *template* — is put away, the list
|
||||
comes back with the cursor still on the row the clone hangs off, and live mode
|
||||
turns itself on if it was off, so the percentage moves on its own and the line
|
||||
under the table announces the new machine when it exists. `^l` ends it again.
|
||||
Naming the key that would make an invisible figure move is three steps of
|
||||
housekeeping after the one decision that mattered.
|
||||
|
||||
The rest of a template's menu is greyed out with "a template" next to it, because
|
||||
vSphere will not start one, snapshot one or reconfigure one. They are greyed
|
||||
@@ -560,9 +571,16 @@ when it will not say.
|
||||
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:
|
||||
of making you look one up first. In the list the same word does it, and it is
|
||||
already standing in the line — a site with a tool hands addresses out from it,
|
||||
so that is the answer Enter gives:
|
||||
|
||||
address for web05, "auto" for one from dns, empty for DHCP:
|
||||
address for web05, "auto" for one from dns, empty for DHCP: auto
|
||||
|
||||
It is offered rather than made the meaning of the empty line, so every answer
|
||||
keeps the meaning it had: erased back to nothing it is DHCP again, or whatever
|
||||
the chosen specification says. And Enter commits nothing — an address fetched
|
||||
here is handed straight back when the deployment is not confirmed.
|
||||
|
||||
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
|
||||
@@ -586,7 +604,8 @@ answer short of the same answer.
|
||||
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
|
||||
web05 is being made at 141.14.140.182 — live on, ubuntu-tpl shows how far
|
||||
along it is
|
||||
|
||||
Three more things about it are deliberate:
|
||||
|
||||
|
||||
+43
-5
@@ -594,8 +594,14 @@ func sizePrompt(r vmRow, sz sizing, k sizeKind) string {
|
||||
// minutes to the half hour, and a list that is frozen for that long is a list
|
||||
// nobody will start one from. vCenter carries the task either way, and it hangs
|
||||
// off the template — so the row this was started from shows how far along it is
|
||||
// in its TASK column, and ^l makes that move on its own. The machine itself
|
||||
// turns up in the list when it exists, which live mode announces.
|
||||
// in its TASK column. The machine itself turns up in the list when it exists,
|
||||
// which live mode announces.
|
||||
//
|
||||
// Which is why the end of it is a screen and not only a sentence: the sheet
|
||||
// goes away, the list comes back with the cursor still on the template, and
|
||||
// live mode turns itself on. Telling somebody that a figure they cannot see
|
||||
// exists, and which key would make it move, is three steps of housekeeping
|
||||
// after the one decision that mattered.
|
||||
//
|
||||
// The question at the end is the plain one. Nothing is lost here; something is
|
||||
// made, and what has to be read before it is made is *where* — which is why
|
||||
@@ -705,7 +711,9 @@ func (b *browser) deploy(r vmRow) {
|
||||
//
|
||||
// Empty is an answer: it leaves whatever the specification says, which is
|
||||
// usually DHCP, and that is the ordinary case on a network that hands out
|
||||
// addresses.
|
||||
// addresses. Where the site has a tool of its own, the ordinary case is the
|
||||
// other one — so "auto" is in the line when the question appears, and Enter
|
||||
// takes it.
|
||||
func (b *browser) deployAddress(r vmRow, src deploySource, t deployTarget, name string, opts deployOpts) {
|
||||
// Empty is an answer, and it means two different things: with gvm's own
|
||||
// specification it leaves the adapter on DHCP, and with one of the
|
||||
@@ -722,11 +730,24 @@ func (b *browser) deployAddress(r vmRow, src deploySource, t deployTarget, name
|
||||
leave = "empty to leave it to " + opts.spec
|
||||
}
|
||||
ask := SF("address for %s, %s: ", name, leave)
|
||||
|
||||
// And where there is a tool, its word is standing in the line already, so
|
||||
// Enter is the whole answer. That is what the tool is configured for: a
|
||||
// site that has one hands out addresses from it, and typing "auto" every
|
||||
// time is asking somebody to confirm the ordinary case by hand.
|
||||
//
|
||||
// Offered rather than made the meaning of the empty line, so that every
|
||||
// answer keeps the meaning it had: erased back to nothing it is DHCP again,
|
||||
// or whatever the specification says. Nor does pressing Enter commit
|
||||
// anything — an address fetched here is handed straight back where the
|
||||
// deployment is not confirmed (deployStep.giveBack).
|
||||
preset := ""
|
||||
if opts.dns.there() {
|
||||
ask = SF("address for %s, %q for one from dns, %s: ", name, autoIP, leave)
|
||||
preset = autoIP
|
||||
}
|
||||
|
||||
ip, ok := b.input(ask)
|
||||
ip, ok := b.inputWith(ask, preset)
|
||||
if !ok {
|
||||
b.setStatus(colDim, "nothing done")
|
||||
return
|
||||
@@ -795,13 +816,30 @@ func (b *browser) deployAsk(r vmRow, src deploySource, t deployTarget, name stri
|
||||
b.setStatus(colErr, err.Error())
|
||||
return
|
||||
}
|
||||
// And then the screen it can be watched on. This was started from the
|
||||
// template's sheet — a page of facts about the machine being copied, which
|
||||
// is not what anybody is here for now. The list is: the clone's progress
|
||||
// stands in the TASK column of the row the cursor is already on, the new
|
||||
// machine turns up there when it exists, and the line under the table says
|
||||
// so. Live mode goes with it, or the percentage would sit where it was
|
||||
// until somebody pressed a key.
|
||||
//
|
||||
// Decided before done, which is what puts the task on the row, and applied
|
||||
// after it, so that the interval it schedules is the busy one.
|
||||
watching := ""
|
||||
if !b.live {
|
||||
watching = "live on, "
|
||||
}
|
||||
b.closeDetail()
|
||||
|
||||
// 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))
|
||||
b.done(SF("%s is being made%s — %s%s shows how far along it is", name, at, watching, src.name))
|
||||
b.watchLive()
|
||||
}
|
||||
|
||||
// working puts a line on the screen before an operation that will block the loop
|
||||
|
||||
@@ -1012,13 +1012,7 @@ func (b *browser) detailKey(k key) {
|
||||
|
||||
switch k.special {
|
||||
case keyEsc, keyEnter, keyBackspace, keyLeft:
|
||||
b.detail, b.dscroll = nil, 0
|
||||
// The events go with the visit, not with the machine: coming back to a
|
||||
// sheet half an hour later and finding half-hour-old events under a
|
||||
// label that says nothing about when they were read would be the one
|
||||
// stale thing on an otherwise freshly read screen. They are one
|
||||
// keystroke away again.
|
||||
b.events, b.eventsOf = nil, ""
|
||||
b.closeDetail()
|
||||
b.setStatus("", "")
|
||||
case keyCtrlA:
|
||||
b.openMenu()
|
||||
@@ -1306,8 +1300,18 @@ func (e *editor) key(k key) (finished, accepted bool) {
|
||||
// input reads one line in the status area and reports whether it was finished
|
||||
// rather than abandoned. It has its own key loop, so nothing that is typed here
|
||||
// reaches the filter.
|
||||
func (b *browser) input(label string) (string, bool) {
|
||||
b.edit = &editor{label: label}
|
||||
func (b *browser) input(label string) (string, bool) { return b.inputWith(label, "") }
|
||||
|
||||
// inputWith is the same with an answer already standing in the line, for a
|
||||
// question that has one ordinary answer worth offering: Enter takes it, and
|
||||
// Backspace is how it is refused. It is a default one can see — which is the
|
||||
// only kind worth having, since a question whose answer is decided by what was
|
||||
// not typed is one nobody knows they have agreed to.
|
||||
//
|
||||
// The cursor sits at the end of it, so typing carries on from the answer rather
|
||||
// than into the middle of it.
|
||||
func (b *browser) inputWith(label, preset string) (string, bool) {
|
||||
b.edit = &editor{label: label, runes: []rune(preset), cursor: len([]rune(preset))}
|
||||
defer func() { b.edit = nil }()
|
||||
|
||||
for {
|
||||
@@ -1717,6 +1721,19 @@ func (b *browser) openDetail() {
|
||||
b.dscroll = 0
|
||||
}
|
||||
|
||||
// closeDetail puts the sheet away and leaves the list underneath it. It is what
|
||||
// Esc does, and also what an action does when what happens next is to be
|
||||
// watched in the table rather than read on one machine's page (actions.go).
|
||||
//
|
||||
// The events go with the visit, not with the machine: coming back to a sheet
|
||||
// half an hour later and finding half-hour-old events under a label that says
|
||||
// nothing about when they were read would be the one stale thing on an
|
||||
// otherwise freshly read screen. They are one keystroke away again.
|
||||
func (b *browser) closeDetail() {
|
||||
b.detail, b.dscroll = nil, 0
|
||||
b.events, b.eventsOf = nil, ""
|
||||
}
|
||||
|
||||
// eventSheet is the history as sheet lines: the label on the first, each line in
|
||||
// the colour of its own severity. It is appended to the sheet rather than built
|
||||
// into vmDetail because vmDetail asks nothing of the network and this is the one
|
||||
|
||||
@@ -380,6 +380,29 @@ func TestTheSheetClosesWhenItsMachineGoes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Closing the sheet leaves nothing of the visit behind. The events are the part
|
||||
// that would keep: they are read once, on request, and a sheet opened again
|
||||
// half an hour later must not show them under a label that says nothing about
|
||||
// when they were read. Esc does this, and so does a deployment, which puts the
|
||||
// sheet away to leave the list watching the clone (actions.go).
|
||||
func TestClosingTheSheetForgetsTheVisit(t *testing.T) {
|
||||
b := testBrowser("ubuntu-tpl", "web01")
|
||||
b.applySort()
|
||||
b.events = []eventLine{{text: "08.09. 11:41 Cannot connect to host esx03", col: colFull}}
|
||||
b.eventsOf = b.current().id()
|
||||
b.openDetail()
|
||||
b.dscroll = 4
|
||||
|
||||
b.closeDetail()
|
||||
|
||||
if b.detail != nil || b.dscroll != 0 {
|
||||
t.Errorf("the sheet is still there: %d lines, scrolled to %d", len(b.detail), b.dscroll)
|
||||
}
|
||||
if b.events != nil || b.eventsOf != "" {
|
||||
t.Errorf("the events of %q were kept: %v", b.eventsOf, b.events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailSheetHasTheParameters(t *testing.T) {
|
||||
sheet := sheetText(vmDetail(testRow("web01", true, "10.0.0.5"), []string{"none"}, colOff))
|
||||
|
||||
|
||||
@@ -581,6 +581,8 @@ func TestTheAddressQuestionReadsAsASentence(t *testing.T) {
|
||||
t.Setenv("COLUMNS", "100")
|
||||
t.Setenv("LINES", "24")
|
||||
|
||||
tool, _ := fakeInfoblox(t) // for its there(), not for its answers
|
||||
|
||||
for _, c := range []struct {
|
||||
what string
|
||||
opts deployOpts
|
||||
@@ -589,6 +591,12 @@ func TestTheAddressQuestionReadsAsASentence(t *testing.T) {
|
||||
{"gvm's own specification", deployOpts{how: customSite, st: testSite()}, "empty for DHCP"},
|
||||
{"one the vCenter holds", deployOpts{how: customSpec, spec: "linux-static"},
|
||||
"leave it to linux-static"},
|
||||
// With an address tool, the answer it would give is standing in the
|
||||
// line already: that is what the tool is there for, and the question
|
||||
// still has to say what the other two answers mean.
|
||||
{"a site with an address tool",
|
||||
deployOpts{how: customSite, st: testSite(), dns: tool},
|
||||
`"auto" for one from dns, empty for DHCP: auto`},
|
||||
} {
|
||||
r := templateRow("ubuntu-tpl")
|
||||
b := &browser{rows: []vmRow{r}, view: []int{0}}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
// Everything else in the interactive half happens because somebody pressed a
|
||||
// key. This is the part that happens because time passed: `^l` turns it on and
|
||||
// the list re-reads itself every few seconds, which turns gvm from something one
|
||||
// looks at into something one leaves open on a second screen.
|
||||
// looks at into something one leaves open on a second screen. Starting a clone
|
||||
// turns it on as well (watchLive), because that is a screen made to be watched
|
||||
// whether or not anybody thought to ask for it first.
|
||||
//
|
||||
// Three things come with it, and they are here rather than in browse.go because
|
||||
// they only mean anything together:
|
||||
@@ -69,6 +71,27 @@ func (b *browser) toggleLive() {
|
||||
b.setStatus(colBusy, "live on — the list re-reads itself; ^l off")
|
||||
}
|
||||
|
||||
// watchLive turns live mode on because something with a figure to watch was
|
||||
// just set going, and reports whether it had to. ^l is somebody asking for a
|
||||
// screen that keeps itself up to date; this is gvm deciding that a clone it has
|
||||
// just started makes the list one of those, since the progress in the TASK
|
||||
// column is the whole reason to still be looking at it. It stays on afterwards,
|
||||
// like the mode it is — ^l ends it, and the title says it is running.
|
||||
//
|
||||
// Unlike toggleLive it does not sweep at once. The caller has just re-read the
|
||||
// row it acted on, so the first tick belongs one interval away rather than now
|
||||
// — and that interval is already the busy one, because that row is the one
|
||||
// carrying the task.
|
||||
func (b *browser) watchLive() bool {
|
||||
if b.live {
|
||||
return false
|
||||
}
|
||||
b.live = true
|
||||
b.liveGap = b.liveInterval()
|
||||
b.liveNext = time.Now().Add(b.liveGap)
|
||||
return true
|
||||
}
|
||||
|
||||
// liveReady reports whether a tick may happen now. Only the list and a
|
||||
// machine's sheet are refreshed underneath somebody: the menu decides what it
|
||||
// offers from the state it was drawn with, the picker holds a list of snapshots
|
||||
|
||||
@@ -485,3 +485,34 @@ func TestToggleLiveSaysSoAndLooksNow(t *testing.T) {
|
||||
t.Errorf("turning it off said %q", b.status)
|
||||
}
|
||||
}
|
||||
|
||||
// A clone that has just been started makes the list a screen worth watching, so
|
||||
// live mode turns itself on rather than leaving a percentage that moves only
|
||||
// when somebody remembers ^l. The first tick is an interval away rather than
|
||||
// now — the action re-read its own row a moment ago — and that interval is the
|
||||
// busy one, because the row it re-read is the one carrying the task.
|
||||
func TestStartingAClonePutsTheListOnWatch(t *testing.T) {
|
||||
b := testBrowser("ubuntu-tpl", "web01")
|
||||
b.rows[0].task = &runningTask{what: "clone", progress: 2}
|
||||
|
||||
if !b.watchLive() {
|
||||
t.Fatal("watchLive did not report that it had to turn live mode on")
|
||||
}
|
||||
if !b.live {
|
||||
t.Fatal("live mode is off after a clone was started")
|
||||
}
|
||||
if d := b.liveIn(); d > liveBusy || d < liveBusy/2 {
|
||||
t.Errorf("the first refresh is %s away, want about %s", d, liveBusy)
|
||||
}
|
||||
|
||||
// Already on is not a reason to touch it: a tick that was due in half a
|
||||
// second must not be pushed back to two because a second clone was started.
|
||||
b.liveNext = time.Now().Add(liveBusy / 4)
|
||||
due := b.liveNext
|
||||
if b.watchLive() {
|
||||
t.Error("watchLive reported turning on a mode that was already on")
|
||||
}
|
||||
if !b.liveNext.Equal(due) {
|
||||
t.Errorf("the pending tick moved from %s to %s", due, b.liveNext)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.3.7
|
||||
1.3.9
|
||||
|
||||
Reference in New Issue
Block a user