From 69210638e5ff81cfdd92e0a6b2d59fe3969eb92b Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Mon, 16 Feb 2026 20:05:31 +0100 Subject: [PATCH] Add verification steps to speaker migration process This update adds explicit verification checks after applying changes via XML, Hosts, and ResolvConf migration methods. The service now verifies that configuration files are correctly updated on the device before considering the migration successful, preventing unreliable states. --- docs/guides/SOUNDTOUCH-SERVICE.md | 2 +- pkg/service/setup/setup.go | 42 ++++++++++++++++++++++++++++--- pkg/service/setup/setup_test.go | 30 ++++++++++++++++++++-- 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/docs/guides/SOUNDTOUCH-SERVICE.md b/docs/guides/SOUNDTOUCH-SERVICE.md index a518e9b..cdfba57 100644 --- a/docs/guides/SOUNDTOUCH-SERVICE.md +++ b/docs/guides/SOUNDTOUCH-SERVICE.md @@ -283,7 +283,7 @@ The most robust and flexible DNS-based migration method. It utilizes the device' # Patch udhcpc.script if it exists (e.g. SoundTouch 10) TARGET_SCRIPT="/opt/Bose/udhcpc.script" if [ -f "$TARGET_SCRIPT" ] && ! grep -q "$HOOK_MARKER" "$TARGET_SCRIPT"; then - sed -i '/echo "search \$search_list # \$interface" >> \$RESOLV_CONF/a \ [ -f '"$HOOK_MARKER"' ] && cat '"$HOOK_MARKER"' >> '"$RESOLV_CONF"' && dns=""' "$TARGET_SCRIPT" + sed -i '/echo "search \$search_list # \$interface" >> \$RESOLV_CONF/a \ [ -f '"$HOOK_MARKER"' ] && cat '"$HOOK_MARKER"' >> '"\$RESOLV_CONF"' && dns=""' "$TARGET_SCRIPT" fi fi ``` diff --git a/pkg/service/setup/setup.go b/pkg/service/setup/setup.go index 727ecfa..056f9a4 100644 --- a/pkg/service/setup/setup.go +++ b/pkg/service/setup/setup.go @@ -713,6 +713,16 @@ func (m *Manager) migrateViaXML(deviceIP, targetURL, proxyURL string, options ma logs += "Uploaded new configuration to " + remotePath + "\n" + // 2. Verify the configuration on device + if verification, err := client.Run(fmt.Sprintf("cat %s", remotePath)); err == nil { + if !strings.Contains(verification, cfg.MargeServerUrl) { + return logs, fmt.Errorf("verification failed: uploaded config on %s does not contain expected margeServerUrl", deviceIP) + } + logs += "Verified configuration on device\n" + } else { + logs += fmt.Sprintf("Warning: could not verify configuration on device: %v\n", err) + } + return logs, nil } @@ -1036,9 +1046,21 @@ func (m *Manager) migrateViaHosts(deviceIP, targetURL string) (string, error) { logs += "Uploaded updated /etc/hosts\n" + // 4. Verify /etc/hosts on device + if verification, err := client.Run("cat /etc/hosts"); err == nil { + for _, domain := range domains { + if !strings.Contains(verification, domain) || !strings.Contains(verification, hostIP) { + return logs, fmt.Errorf("verification failed: /etc/hosts on %s does not contain expected redirection for %s", deviceIP, domain) + } + } + logs += "Verified /etc/hosts on device\n" + } else { + logs += fmt.Sprintf("Warning: could not verify /etc/hosts on device: %v\n", err) + } + fmt.Printf("Updated /etc/hosts on %s:\n%s\n", deviceIP, hostsContent) - // 4. Inject CA Certificate + // 5. Inject CA Certificate summary := &MigrationSummary{} m.checkCACertTrusted(summary, deviceIP) @@ -1112,7 +1134,7 @@ if [ -f "%s" ]; then targetScript="/opt/Bose/udhcpc.script" if [ -f "$targetScript" ] && ! grep -q "%s" "$targetScript"; then logger -t "aftertouch" "Patching $targetScript with Aftertouch DNS hook" - sed -i '/echo "search \$search_list # \$interface" >> \$RESOLV_CONF/a \ [ -f '"%s"' ] && cat '"%s"' >> '"$RESOLV_CONF"' && dns=""' "$targetScript" + sed -i '/echo "search \$search_list # \$interface" >> \$RESOLV_CONF/a \ [ -f '"%s"' ] && cat '"%s"' >> '"\$RESOLV_CONF"' && dns=""' "$targetScript" fi fi `, hookMarker, targetDHCPFile, hookMarker, targetDHCPFile, targetDHCPFile, hookMarker, hookMarker, targetDHCPFile, hookMarker, hookMarker, hookMarker) @@ -1165,6 +1187,13 @@ fi logs += fmt.Sprintf("Failed to apply patch immediately to %s: %v\n", targetDHCPFile, err) } else { logs += fmt.Sprintf("Applied patch to %s\n", targetDHCPFile) + + // Verify patch on 50default + if verification, err := client.Run(fmt.Sprintf("grep -q \"%s\" %s && echo \"OK\"", hookMarker, targetDHCPFile)); err == nil && strings.TrimSpace(verification) == "OK" { + logs += fmt.Sprintf("Verified patch on %s\n", targetDHCPFile) + } else { + logs += fmt.Sprintf("Warning: could not verify patch on %s: %v\n", targetDHCPFile, err) + } } // Apply patch immediately to /opt/Bose/udhcpc.script if it exists @@ -1179,11 +1208,18 @@ fi _, _ = client.Run(fmt.Sprintf("cp %s.original %s", targetScript, targetScript)) } - patchCmdScript := fmt.Sprintf("sed -i '/echo \"search \\$search_list # \\$interface\" >> \\$RESOLV_CONF/a \\ [ -f '\"%s\"' ] && cat '\"%s\"' >> '\"$RESOLV_CONF\"' && dns=\"\"' %s", hookMarker, hookMarker, targetScript) + patchCmdScript := fmt.Sprintf("sed -i '/echo \"search \\$search_list # \\$interface\" >> \\$RESOLV_CONF/a \\ [ -f '\"%s\"' ] && cat '\"%s\"' >> '\"\\$RESOLV_CONF\"' && dns=\"\"' %s", hookMarker, hookMarker, targetScript) if _, err := client.Run(patchCmdScript); err != nil { logs += fmt.Sprintf("Failed to apply patch immediately to %s: %v\n", targetScript, err) } else { logs += fmt.Sprintf("Applied patch to %s\n", targetScript) + + // Verify patch on udhcpc.script + if verification, err := client.Run(fmt.Sprintf("grep -q \"%s\" %s && echo \"OK\"", hookMarker, targetScript)); err == nil && strings.TrimSpace(verification) == "OK" { + logs += fmt.Sprintf("Verified patch on %s\n", targetScript) + } else { + logs += fmt.Sprintf("Warning: could not verify patch on %s: %v\n", targetScript, err) + } } } diff --git a/pkg/service/setup/setup_test.go b/pkg/service/setup/setup_test.go index 9dd6274..6b8743f 100644 --- a/pkg/service/setup/setup_test.go +++ b/pkg/service/setup/setup_test.go @@ -52,6 +52,10 @@ func TestMigrateViaHosts(t *testing.T) { runFunc: func(command string) (string, error) { runCalls = append(runCalls, command) if command == "cat /etc/hosts" { + // Handle both initial read and verification read + if len(runCalls) > 2 { // Rough heuristic: verification happens after upload + return "192.168.1.100\tstreaming.bose.com\n192.168.1.100\tupdates.bose.com\n192.168.1.100\tstats.bose.com\n192.168.1.100\tbmx.bose.com\n192.168.1.100\tcontent.api.bose.io\n192.168.1.100\tevents.api.bosecm.com\n192.168.1.100\tbose-prod.apigee.net\n192.168.1.100\tworldwide.bose.com", nil + } return "127.0.0.1 localhost", nil } if strings.HasPrefix(command, "[ -f") { @@ -122,9 +126,14 @@ func TestMigrateViaHosts_UpdateExisting(t *testing.T) { m := NewManager("http://192.168.1.100:8000", nil, cm) m.NewSSH = func(host string) SSHClient { + runCount := 0 return &mockSSH{ runFunc: func(command string) (string, error) { + runCount++ if command == "cat /etc/hosts" { + if runCount > 1 { + return "127.0.0.1 localhost\n192.168.1.100\tstreaming.bose.com\n192.168.1.100\tupdates.bose.com\n192.168.1.100\tstats.bose.com\n192.168.1.100\tbmx.bose.com\n192.168.1.100\tcontent.api.bose.io\n192.168.1.100\tevents.api.bosecm.com\n192.168.1.100\tbose-prod.apigee.net\n192.168.1.100\tworldwide.bose.com", nil + } return "127.0.0.1 localhost\n1.2.3.4\tstreaming.bose.com\n1.2.3.4\tupdates.bose.com", nil } if strings.HasPrefix(command, "[ -f") { @@ -598,6 +607,10 @@ func TestMigrateViaHosts_SkipCAIfTrusted(t *testing.T) { runFunc: func(command string) (string, error) { runCalls = append(runCalls, command) if command == "cat /etc/hosts" { + // Handle both initial read and verification read + if len(runCalls) > 2 { // Rough heuristic: verification happens after upload + return "192.168.1.100\tstreaming.bose.com\n192.168.1.100\tupdates.bose.com\n192.168.1.100\tstats.bose.com\n192.168.1.100\tbmx.bose.com\n192.168.1.100\tcontent.api.bose.io\n192.168.1.100\tevents.api.bosecm.com\n192.168.1.100\tbose-prod.apigee.net\n192.168.1.100\tworldwide.bose.com", nil + } return "127.0.0.1 localhost", nil } if strings.HasPrefix(command, "grep -F") { @@ -1115,6 +1128,9 @@ func TestMigrateViaResolvConf(t *testing.T) { if command == "cat /mnt/nv/rc.local" { return "#!/bin/sh\n", nil } + if strings.HasPrefix(command, "grep -q \"/mnt/nv/aftertouch.resolv.conf\"") { + return "OK", nil + } if strings.HasPrefix(command, "[ -f") { return "", fmt.Errorf("file not found") } @@ -1177,6 +1193,9 @@ func TestMigrateViaResolvConf_CorruptedRcLocal(t *testing.T) { // Simulate corrupted file containing error message return "cat: can't open '/mnt/nv/rc.local': No such file or directory", nil } + if strings.HasPrefix(command, "grep -q \"/mnt/nv/aftertouch.resolv.conf\"") { + return "OK", nil + } if strings.HasPrefix(command, "[ -f") { return "", fmt.Errorf("file not found") } @@ -1233,6 +1252,9 @@ func TestMigrateViaResolvConf_UdhcpcScript(t *testing.T) { if command == "cat /mnt/nv/rc.local" { return "#!/bin/sh\n", nil } + if strings.HasPrefix(command, "grep -q \"/mnt/nv/aftertouch.resolv.conf\"") { + return "OK", nil + } if command == "[ -f "+targetScript+" ]" { return "", nil // file exists } @@ -1270,8 +1292,12 @@ func TestMigrateViaResolvConf_UdhcpcScript(t *testing.T) { if !strings.Contains(rcLocal, "targetScript=\"/opt/Bose/udhcpc.script\"") { t.Errorf("rc.local missing targetScript definition: %s", rcLocal) } - if !strings.Contains(rcLocal, "sed -i '/echo \"search \\$search_list # \\$interface\" >> \\$RESOLV_CONF/a") { - t.Errorf("rc.local missing sed patch for udhcpc.script: %s", rcLocal) + if !strings.Contains(rcLocal, "sed -i '/echo \"search \\$search_list # \\$interface\" >> \\$RESOLV_CONF/a \\ [ -f '\"$HOOK_MARKER\"' ] && cat '\"$HOOK_MARKER\"' >> '\"\\$RESOLV_CONF\"' && dns=\"\"' \"$targetScript\"") { + // Note: The actual string in rcLocal might have variables expanded or escaped depending on how it was constructed. + // Let's check for the critical part: the escaped $RESOLV_CONF + if !strings.Contains(rcLocal, ">> '\"\\$RESOLV_CONF\"'") { + t.Errorf("rc.local missing correctly escaped RESOLV_CONF in sed patch for udhcpc.script: %s", rcLocal) + } } }