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.
This commit is contained in:
Tobias Gesellchen
2026-02-16 20:17:25 +01:00
parent 6aef2b807d
commit 69210638e5
3 changed files with 68 additions and 6 deletions
+1 -1
View File
@@ -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
```
+39 -3
View File
@@ -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)
}
}
}
+28 -2
View File
@@ -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)
}
}
}