Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/spin-machine/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func TestFlagsAreTheSpec(t *testing.T) {
every.Cmdline.Console = ""

deferred := defaults
deferred.IncomingDefer = true
deferred.Incoming = "defer"

emulated := defaults
emulated.Accel, emulated.QEMU = "tcg", filepath.Join(dir, "bin/qemu-system-x86_64-tcg")
Expand Down
9 changes: 2 additions & 7 deletions cmd/spin-machine/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,7 @@ func (o *machineFlags) spec() (machine.Spec, error) {
if o.qmp != "" {
s.Monitors = []machine.Monitor{{Socket: o.qmp}}
}
// defer is QEMU's own word for "the source comes over QMP": see restore.
if o.incoming == "defer" {
s.IncomingDefer = true
} else {
s.Incoming = o.incoming
}
s.Incoming = o.incoming
s.Serial = o.console

disk := o.disk
Expand Down Expand Up @@ -664,7 +659,7 @@ func (o *restoreFlags) register(fs *flag.FlagSet) {
//
// `boot --incoming file:PATH` loads the same file in one step. This is the other
// form, and the one a caller that resumes checkpoints uses: the machine is started
// with -incoming defer (Spec.IncomingDefer), and the state is named over QMP once
// with -incoming defer (Spec.Incoming "defer"), and the state is named over QMP once
// the caller is ready for it. Driving that by hand is what this command is for.
func restore(o restoreFlags) error {
if o.qmp == "" || o.from == "" {
Expand Down
6 changes: 3 additions & 3 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ A checkpoint is one state file — memory, device state and CPU state, written b

Stopping a VM here and resuming it there is a lifecycle, and this repository does not
implement one — it builds the machine that lifecycle runs on, and offers the arguments it
needs: `Incoming` (a state to load at exec time, `-incoming file:…`) and `IncomingDefer`
(start with no state and wait to be told where it is, over QMP, for a caller that starts the
machine before it has the state in hand).
needs: `Incoming`, either a state to load at exec time (`-incoming file:…`) or `defer` (start
with no state and wait to be told where it is, over QMP, for a caller that starts the machine
before it has the state in hand).

`spin-machine save --qmp … --to state` is the first half by hand, and the second is either
`spin-machine boot --incoming file:state` or `boot --incoming defer` followed by
Expand Down
47 changes: 11 additions & 36 deletions machine/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const virtioModern = "disable-legacy=on"
// qemuOpt escapes a value for a QEMU option string, which splits on a single comma
// and reads a doubled one as a literal. Every free-form value a caller supplies goes
// through it: a disk path "a,readonly=off" is otherwise a second option, not part of
// the path. A value picked from a fixed set — a disk's format and cache mode, the
// the path. A value picked from a fixed set — a disk's format, the
// accelerator, a CPU model name — is not escaped but refused by validate when it is not
// one of the set, because an escaped "qcow2,,x" is still not a format.
func qemuOpt(v string) string { return strings.ReplaceAll(v, ",", ",,") }
Expand Down Expand Up @@ -588,16 +588,12 @@ type Spec struct {
// what the line is made of.
Cmdline Cmdline

// IncomingDefer starts QEMU with no machine state, waiting to be told over
// QMP where to load it from (migrate-incoming): how a checkpoint is resumed by
// a caller that starts the machine before it has the state in hand, or wants a
// word with QEMU before the first byte is read. Whoever drives that owns the
// lifecycle; this is the machine argument they need.
IncomingDefer bool

// Incoming names that source at exec time instead — a migration URI, most
// usefully "file:/path/to/state". It is how a VM saved on one machine is
// resumed, rather than booted, on another.
// Incoming is -incoming: where the machine's state comes from when it is resumed
// rather than booted. A migration URI names it at exec time, most usefully
// "file:/path/to/state". "defer", QEMU's own word, starts QEMU with no state,
// waiting to be told over QMP where to load it from (migrate-incoming): how a
// checkpoint is resumed by a caller that starts the machine before it has the
// state in hand, or wants a word with QEMU before the first byte is read.
//
// The guest does not know this happened: it continues from the instruction it
// was stopped at, with the memory, the devices and the clock it had. Which is
Expand Down Expand Up @@ -828,8 +824,6 @@ func (s Spec) validate() error {
// The two forms of -incoming are one flag, and QEMU takes it once. A spec carrying
// both is a caller that has not decided whether the source is named at exec time or
// over QMP afterwards, and guessing for them is how a VM restores from the wrong one.
case s.IncomingDefer && s.Incoming != "":
return fmt.Errorf("both IncomingDefer and Incoming %q: -incoming takes one form", s.Incoming)
case s.Memory.SizeMB < 1:
return fmt.Errorf("memory is %d MB", s.Memory.SizeMB)
case len(s.Disks) > maxDisks:
Expand Down Expand Up @@ -1196,11 +1190,7 @@ func (s Spec) appendFDSets(args []string) []string {

// appendIncoming is -incoming, when the machine is to be restored rather than booted.
func (s Spec) appendIncoming(args []string) []string {
// -incoming, in whichever of its two forms this machine was given. validate has
// already refused a spec carrying both.
if s.IncomingDefer {
args = append(args, "-incoming", "defer")
} else if s.Incoming != "" {
if s.Incoming != "" {
args = append(args, "-incoming", s.Incoming)
}
return args
Expand Down Expand Up @@ -1275,25 +1265,10 @@ func (s Spec) fingerprint(hostCPU func() (string, error)) (string, error) {
}
write(f.name, sum)
}
ident, err := s.identity(hostCPU)
if err != nil {
return "", err
}
write("identity", ident)

return hex.EncodeToString(h.Sum(nil)), nil
}

// identity is everything the fingerprint hashes except the contents of those five
// files: the machine's shape, its device topology, and the host's own CPU when the
// guest is being shown it.
func (s Spec) identity(hostCPU func() (string, error)) (string, error) {
// The machine's shape, its device topology, and the host's own CPU when the guest is
// being shown it.
shape := s.Shape()

var b strings.Builder
write := func(key, value string) {
_, _ = fmt.Fprintf(&b, "%s=%d:%s\n", key, len(value), value)
}
write("machine", shape.Machine)
write("accel", shape.Accel)
write("cpu", shape.CPU)
Expand Down Expand Up @@ -1323,7 +1298,7 @@ func (s Spec) identity(hostCPU func() (string, error)) (string, error) {
write("host-cpu", cpu)
}

return b.String(), nil
return hex.EncodeToString(h.Sum(nil)), nil
}

// topology is the machine's device list — which models, at which slots — with
Expand Down
3 changes: 1 addition & 2 deletions machine/machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,6 @@ func TestValidateRefuses(t *testing.T) {
{"a NIC with a 64-bit MAC", func(s *Spec) { s.NICs = []NIC{{TapFD: 3, MAC: "02:00:5e:10:00:00:00:01"}} }},
// Both forms of -incoming: QEMU takes the flag once, and which source a VM
// restores from is not something to guess at on the caller's behalf.
{"both forms of -incoming", func(s *Spec) { s.IncomingDefer, s.Incoming = true, "file:/state" }},
} {
t.Run(tc.what, func(t *testing.T) {
s := spec(t)
Expand Down Expand Up @@ -830,7 +829,7 @@ func TestIncomingNamesTheSourceOnTheCommandLine(t *testing.T) {
want string
}{
{"a URI at exec time", func(s *Spec) { s.Incoming = "file:/state" }, "file:/state"},
{"deferred to QMP", func(s *Spec) { s.IncomingDefer = true }, "defer"},
{"deferred to QMP", func(s *Spec) { s.Incoming = "defer" }, "defer"},
{"a boot", func(*Spec) {}, ""},
} {
t.Run(tc.what, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion machine/qemu_accepts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func TestQEMUAcceptsEveryArgument(t *testing.T) {
// where to load it from.
name: "restore target",
spec: func(s Spec) Spec {
s.IncomingDefer = true
s.Incoming = "defer"
return s
},
}, {
Expand Down