From a1754a4500197ccbb90b4376d4092a8782670936 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Fri, 22 May 2026 19:05:17 +0200 Subject: [PATCH] feat(setup): remote_services CLI integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setup remote-services subcommand: enables (default) or removes (--remove) the remote_services SSH-enablement marker via SSH, targeting persistent locations (/etc or /mnt/nv) before the volatile /tmp fallback - setup plan now includes a "persist remote_services" step when the marker is only in /tmp (would be lost on next reboot, breaking SSH mid-migration) - setup plan state header shows a [⚠] line when remote_services is enabled but not persistent Co-Authored-By: Claude Sonnet 4.6 --- cmd/soundtouch-cli/cmd_setup.go | 64 ++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/cmd/soundtouch-cli/cmd_setup.go b/cmd/soundtouch-cli/cmd_setup.go index 5555c40..75689c3 100644 --- a/cmd/soundtouch-cli/cmd_setup.go +++ b/cmd/soundtouch-cli/cmd_setup.go @@ -48,6 +48,7 @@ func setupCommand() *cli.Command { setupWaitAPCmd(), setupWaitOnlineCmd(), setupSSHCheckCmd(), + setupRemoteServicesCmd(), setupInstallCACmd(), setupMigrateCmd(), setupRebootCmd(), @@ -536,6 +537,51 @@ func setupSSHCheckCmd() *cli.Command { } } +func setupRemoteServicesCmd() *cli.Command { + return &cli.Command{ + Name: "remote-services", + Usage: "Enable (default) or disable the remote_services SSH-enablement marker on the speaker", + Before: RequireHost, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "remove", + Usage: "Remove all remote_services marker files (disables SSH after next reboot)", + }, + }, + Action: func(c *cli.Context) error { + cfg := GetClientConfig(c) + m := setup.NewManager("", nil, nil) + + var ( + logs string + err error + ) + if c.Bool("remove") { + logs, err = m.RemoveRemoteServices(cfg.Host) + } else { + logs, err = m.EnsureRemoteServices(cfg.Host) + } + + if logs != "" { + fmt.Print(logs) + } + + if err != nil { + PrintError(err.Error()) + return err + } + + if c.Bool("remove") { + PrintSuccess("remote_services removed — SSH will no longer be enabled after next reboot") + } else { + PrintSuccess("remote_services enabled at a persistent location") + } + + return nil + }, + } +} + func setupInstallCACmd() *cli.Command { return &cli.Command{ Name: "install-ca", @@ -1211,6 +1257,10 @@ func renderPlanState(deviceIP string, inspect *setup.InspectReport, summary *set check(summary.IsPaired), check(summary.IsMigrated), yesNo(summary.TelnetMigrated), yesNo(summary.XMLMigrated), yesNo(summary.HostsMigrated), yesNo(summary.ResolvMigrated)) + + if summary.RemoteServicesEnabled && !summary.RemoteServicesPersistent { + fmt.Println(" [⚠] remote_services enabled but not persistent (will be lost on reboot)") + } } func firmwareOf(info *setup.DeviceInfoXML) string { @@ -1253,7 +1303,19 @@ func buildPlanSteps( host = "" // subsequent commands target the discovered IP } - if !reset && summary != nil && summary.IsMigrated && (!includePair || summary.IsPaired) { + // Persist remote_services before anything else when it's only in /tmp. + // SSH is reachable now, but the marker would be lost on the next reboot — + // which could happen mid-migration if power is cut or the reboot step runs + // before persistence is confirmed. + if !reset && summary != nil && summary.RemoteServicesEnabled && !summary.RemoteServicesPersistent { + steps = append(steps, planStep{ + title: "Persist remote_services so SSH survives a reboot", + cmd: fmt.Sprintf("soundtouch-cli --host=%s setup remote-services", host), + reason: "Marker is currently in /tmp only — lost on next reboot, which would break SSH mid-migration.", + }) + } + + if !reset && summary != nil && summary.IsMigrated && (!includePair || summary.IsPaired) && len(steps) == 0 { return steps }