fix: localSystemID + discoverChassis use Output() to skip sudo PAM noise

Follow-up to d509df7. The sudo PAM audit bug exists in two more vpcd call
sites beyond portToBr: localSystemID() and discoverChassis().

When vpcd.service's AmbientCapabilities trip sudo's PAM, stderr carries
"sudo: unable to send audit message: Operation not permitted". CombinedOutput
merges that into stdout and corrupts the parsed value:

  localSystemID → localID="sudo: ...\n\"add0104e-..."
  discoverChassis → tries to parse the stderr banner as chassis name/hostname

Downstream effect: discoverChassis skips the real local chassis as "stale",
vpcd falls back to cfg.ChassisNames (e.g. "chassis-josh-banksia") which no
chassis in SBDB actually uses, so every gateway_chassis row points at a
phantom chassis. cr-gw* chassisredirect ports never bind → no proxy-ARP
for EIPs → external ping fails with ARP timeout.

Switch both to Output() + exec.ExitError.Stderr, matching d509df7's pattern.

Found during mulga-998.b reboot drill while verifying external reachability:
chassis discovery log showed `localID="sudo: unable to send audit message...`,
`cr-gw-vpc-* chassis=[]`, and host ping to EIP returned Destination Host
Unreachable. With this fix applied, discoverChassis returns the real system-id
and (after stale gateway_chassis rows are cleared) cr-gw binds, proxy-ARP
responds, and EIP ping succeeds end-to-end.

Note: existing gateway_chassis rows written pre-fix still carry the phantom
chassis_name. Cleanup is one-shot — delete those rows and trigger an IGW
detach+attach, or run reset-dev-env.sh. vpcd does not reconcile existing
rows on startup (potential future retrofit, similar to Fix 3's
RetrofitAllExternalLocalnetOptions).
This commit is contained in:
joshsiv-mulga
2026-04-24 16:31:19 +10:00
parent d509df731a
commit 1059412e28
+22 -4
View File
@@ -183,10 +183,22 @@ var checkOVNController = func() error {
// localSystemID returns the OVS external-ids:system-id, which is the chassis
// name that the local ovn-controller registers in the Southbound DB.
//
// Uses Output() (stdout only) for the same reason as portToBr: vpcd.service's
// AmbientCapabilities trips sudo's PAM into emitting audit warnings on stderr,
// which CombinedOutput would merge into stdout and poison the system-id.
// A corrupted localID causes discoverChassis to skip the live chassis as
// "stale", leaving gateway_chassis pointing at a fallback name that no real
// chassis owns → cr-gw* chassisredirect ports stay unbound → no proxy-ARP
// for EIPs.
var localSystemID = func() (string, error) {
out, err := sudoCommand("ovs-vsctl", "get", "open_vswitch", ".", "external-ids:system-id").CombinedOutput()
out, err := sudoCommand("ovs-vsctl", "get", "open_vswitch", ".", "external-ids:system-id").Output()
if err != nil {
return "", fmt.Errorf("ovs-vsctl get system-id: %s: %w", strings.TrimSpace(string(out)), err)
var stderr string
if exitErr, ok := err.(*exec.ExitError); ok {
stderr = strings.TrimSpace(string(exitErr.Stderr))
}
return "", fmt.Errorf("ovs-vsctl get system-id: %s: %w", stderr, err)
}
// ovs-vsctl wraps the value in quotes
return strings.Trim(strings.TrimSpace(string(out)), "\""), nil
@@ -211,9 +223,15 @@ var discoverChassis = func(sbAddr string) ([]string, error) {
// OVN 25.03+ removed the "list-chassis" convenience command.
// Use "--columns=name,hostname list Chassis" which works on all versions.
args = append(args, "--bare", "--columns=name,hostname", "list", "Chassis")
out, err := sudoCommand("ovn-sbctl", args...).CombinedOutput()
// Output() not CombinedOutput(): sudo PAM audit noise on stderr would
// otherwise be parsed as chassis name/hostname pairs.
out, err := sudoCommand("ovn-sbctl", args...).Output()
if err != nil {
return nil, fmt.Errorf("ovn-sbctl list Chassis: %s: %w", strings.TrimSpace(string(out)), err)
var stderr string
if exitErr, ok := err.(*exec.ExitError); ok {
stderr = strings.TrimSpace(string(exitErr.Stderr))
}
return nil, fmt.Errorf("ovn-sbctl list Chassis: %s: %w", stderr, err)
}
return parseChassisList(string(out), localID, localHostname), nil