Skip to content

Commit cac418b

Browse files
authored
fix: HMP echo detection and CNI dirs in the VM record (#43)
* fix: treat only the echoed command as HMP echo A password beginning with a double quote makes QEMU answer 'set_password: string expected' and 'Try "help set_password"'; both lines contain set_password, so the substring test took them for echo and the launch reported a password that was never set. The echo is now the first line carrying the command prefix, and only that line. * fix: remember the CNI dirs in the VM record so rm can release the NIC vm rm has no --cni-conf-dir/--cni-bin-dir, so a CNI VM created against a non-default CNI installation kept its NIC forever ('nic release incomplete'). run and clone now persist both dirs and every provisioning verb reads them from the record; records without them keep the defaults. * fix: match the HMP echo once by substring; rm takes the CNI dirs for older records QEMU's readline echoes the typed command with redraw sequences, so a prefix test would have taken every successful echo for a rejection. The echo is the first line containing 'set_password ' and only that line. Records written before the CNI dirs were persisted resolve flag, then record, then default, so rm --cni-conf-dir/--cni-bin-dir can release their NICs. * review: flagOr already carries the fallback; docs name the rm recovery flags
1 parent 3275932 commit cac418b

9 files changed

Lines changed: 47 additions & 10 deletions

File tree

cmd/vm/clone.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ func (h *Handler) clone(cmd *cobra.Command, srcRec *record, name string) (retErr
138138
}
139139
tapFlag, _ := cmd.Flags().GetString("tap")
140140
r.Tap = tapFlag
141+
r.CNIConfDir = flagOr(cmd, "cni-conf-dir", srcRec.CNIConfDir)
142+
r.CNIBinDir = flagOr(cmd, "cni-bin-dir", srcRec.CNIBinDir)
141143
if err = applyNet(cmd, r); err != nil {
142144
return err
143145
}

cmd/vm/commands.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ func Command(h *Handler) *cobra.Command {
7272
RunE: h.RM,
7373
}
7474
rmCmd.Flags().Bool("force", false, "force kill (immediate SIGKILL, skip the ACPI grace window)")
75+
rmCmd.Flags().String("cni-conf-dir", "", "CNI config dir for a VM created before the record remembered it")
76+
rmCmd.Flags().String("cni-bin-dir", "", "CNI plugin dir for a VM created before the record remembered it")
7577

7678
snapshotCmd := &cobra.Command{
7779
Use: "snapshot VM",

cmd/vm/handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type record struct {
5353
TapOwned bool `json:"tap_owned,omitempty"` // cocoon auto-created the TAP (tear down on rm); false for user --tap
5454
BridgeDev string `json:"bridge_dev,omitempty"` // bridge to enslave the TAP to; persisted so rm can tear down without --bridge
5555
Netns string `json:"netns,omitempty"` // netns path the qemu process runs in (CNI); "" otherwise
56+
CNIConfDir string `json:"cni_conf_dir,omitempty"`
57+
CNIBinDir string `json:"cni_bin_dir,omitempty"`
5658

5759
PID int `json:"pid"`
5860

cmd/vm/lifecycle.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,13 @@ func (h *Handler) create(cmd *cobra.Command, image, name string) (r *record, ret
207207
tap, _ := cmd.Flags().GetString("tap")
208208
huge, _ := cmd.Flags().GetBool("hugepages")
209209
exitOnReboot, _ := cmd.Flags().GetBool("exit-on-reboot")
210+
cniConfDir, _ := cmd.Flags().GetString("cni-conf-dir")
211+
cniBinDir, _ := cmd.Flags().GetString("cni-bin-dir")
210212
r = &record{
211213
Name: name, Image: image, ImageDigest: digest, Disk: overlay, OVMFCode: code, OVMFVars: ovmfVars,
212214
CPUs: cpus, Memory: mem, Storage: storage, VNCDisp: vnc, SSHPort: ssh, VNCPass: vncPass, NetMode: netMode, Tap: tap, Hugepages: huge,
213-
ExitOnReboot: exitOnReboot,
214-
VMID: utils.GenerateID(), Created: time.Now().Format(time.RFC3339),
215+
ExitOnReboot: exitOnReboot, CNIConfDir: cniConfDir, CNIBinDir: cniBinDir,
216+
VMID: utils.GenerateID(), Created: time.Now().Format(time.RFC3339),
215217
}
216218
if r.DataDisks, err = createDataDisks(ctx, dir, diskSpecs); err != nil {
217219
return nil, err

cmd/vm/net_linux.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ import (
2828
const netScope = "cm"
2929

3030
// netConf is the cocoon network config: bridge/CNI provisioning shares cocoon's forwarding plane, keyed under our own device family.
31-
func netConf(cmd *cobra.Command) *config.Config {
31+
func netConf(cmd *cobra.Command, r *record) *config.Config {
3232
return &config.Config{
3333
RootDir: home.Dir(cmd),
3434
DNS: "8.8.8.8,1.1.1.1",
35-
CNIConfDir: flagOr(cmd, "cni-conf-dir", "/etc/cni/net.d"),
36-
CNIBinDir: flagOr(cmd, "cni-bin-dir", "/opt/cni/bin"),
35+
CNIConfDir: cmp.Or(flagOr(cmd, "cni-conf-dir", ""), r.CNIConfDir, "/etc/cni/net.d"),
36+
CNIBinDir: cmp.Or(flagOr(cmd, "cni-bin-dir", ""), r.CNIBinDir, "/opt/cni/bin"),
3737
NetScope: netScope,
3838
}
3939
}
4040

4141
// newProvider builds the cocoon network provider: tap/bridge both use the bridge backend (QEMU opens the TAP in the host netns, so it must be a host-side bridge port); cni's TAP lives in a netns.
4242
func newProvider(cmd *cobra.Command, r *record) (network.Network, error) {
43-
conf := netConf(cmd)
43+
conf := netConf(cmd, r)
4444
switch r.NetMode {
4545
case netCNI:
4646
store, err := metajson.Open(cni.NewConfig(conf).JSONNamespace())
@@ -100,7 +100,7 @@ func teardownNet(ctx context.Context, cmd *cobra.Command, r *record) error {
100100
}
101101
bridgeMode := r.NetMode == netTAP || r.NetMode == netBridge
102102
if bridgeMode {
103-
defer bridge.CleanupTAPs(netConf(cmd).BridgeTAPPrefix(), []string{r.VMID})
103+
defer bridge.CleanupTAPs(netConf(cmd, r).BridgeTAPPrefix(), []string{r.VMID})
104104
}
105105
logger := log.WithFunc("cmd.vm.teardownNet")
106106
provider, err := newProvider(cmd, r)

cmd/vm/net_linux_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,26 @@ import (
1313
"github.com/cocoonstack/cocoon-macos/home"
1414
)
1515

16+
func TestNetConfResolvesFlagThenRecordThenDefault(t *testing.T) {
17+
cmd := &cobra.Command{}
18+
cmd.Flags().String("cni-conf-dir", "", "")
19+
cmd.Flags().String("cni-bin-dir", "", "")
20+
if got := netConf(cmd, &record{}).CNIConfDir; got != "/etc/cni/net.d" {
21+
t.Errorf("default CNIConfDir = %q", got)
22+
}
23+
if got := netConf(cmd, &record{CNIConfDir: "/rec/net.d", CNIBinDir: "/rec/bin"}).CNIBinDir; got != "/rec/bin" {
24+
t.Errorf("record CNIBinDir = %q", got)
25+
}
26+
if err := cmd.Flags().Set("cni-conf-dir", "/flag/net.d"); err != nil {
27+
t.Fatal(err)
28+
}
29+
if got := netConf(cmd, &record{CNIConfDir: "/rec/net.d"}).CNIConfDir; got != "/flag/net.d" {
30+
t.Errorf("flag CNIConfDir = %q, want the flag over the record", got)
31+
}
32+
}
33+
1634
func TestNetConfScope(t *testing.T) {
17-
conf := netConf(&cobra.Command{})
35+
conf := netConf(&cobra.Command{}, &record{})
1836
if got, want := conf.NetScope, "cm"; got != want {
1937
t.Errorf("NetScope = %q, want %q", got, want)
2038
}

cmd/vm/utils.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,14 @@ func setVNCPassword(ctx context.Context, monSock, pw string) error {
365365
}
366366

367367
func hmpReplied(out string) bool {
368+
echoed := false
368369
for line := range strings.SplitSeq(out, "\n") {
369370
line = strings.TrimSpace(line)
370-
if line == "" || strings.HasPrefix(line, strings.TrimSpace(hmpPrompt)) || strings.Contains(line, "set_password") {
371+
if line == "" || strings.HasPrefix(line, strings.TrimSpace(hmpPrompt)) {
372+
continue
373+
}
374+
if !echoed && strings.Contains(line, "set_password ") {
375+
echoed = true
371376
continue
372377
}
373378
return true

cmd/vm/utils_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ func TestHMPRepliedFlagsAnyMessage(t *testing.T) {
168168
{"echo and prompt only", " set_password vnc abcd\r\n(qemu) ", false},
169169
{"invalid parameter", " set_password vnc ab cd\r\nError: invalid parameter value: cd\r\n(qemu) ", true},
170170
{"display inactive", " set_password vnc abcd\r\nCould not set password\r\n(qemu) ", true},
171+
{"unterminated quote", " set_password vnc \"abc\r\nset_password: string expected\r\nTry \"help set_password\" for more information\r\n(qemu) ", true},
172+
{"readline redraw echo", "s\x1b[K\x1b[Dse\x1b[K\x1b[D\x1b[Dset_password vnc abcd\r\n(qemu) ", false},
173+
{"readline redraw then rejection", "s\x1b[K\x1b[Dset_password vnc \"abc\r\nset_password: string expected\r\n(qemu) ", true},
171174
} {
172175
if got := hmpReplied(tc.out); got != tc.want {
173176
t.Errorf("%s: hmpReplied = %v, want %v", tc.name, got, tc.want)

docs/networking.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ goes straight to that IP (no port-forward).
3131

3232
`--cni-conf-dir` (default `/etc/cni/net.d`) and `--cni-bin-dir` (default
3333
`/opt/cni/bin`) point at a non-standard CNI installation; both are ignored by
34-
the other net modes.
34+
the other net modes and are remembered in the VM record, so `rm` tears the
35+
NIC down without repeating them; a clone inherits them from its source. A VM
36+
created before the record carried them takes `rm --cni-conf-dir` and
37+
`rm --cni-bin-dir` once.
3538

3639
## Clones
3740

0 commit comments

Comments
 (0)