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) + } } }