diff --git a/config-merger/script/merge_config.py b/config-merger/script/merge_config.py index 69ae48d..b6dc328 100755 --- a/config-merger/script/merge_config.py +++ b/config-merger/script/merge_config.py @@ -47,6 +47,11 @@ 'port': 3012, } +# The ADS-B truth server default.yml shipped before the truth feed was pointed at +# the node's own tar1090. A user.yml holding exactly this is the merger's own +# first-boot copy rather than anyone's choice. See migrate_adsb_truth_server(). +LEGACY_ADSB_TAR1090 = 'sfo1.retnode.com' + def get_node_id_from_mender(): """Read node_id from Mender device identity file (generated by mender-device-identity)""" @@ -245,6 +250,38 @@ def migrate_tracker_forward(user): del network['tracker_forward'] +def migrate_adsb_truth_server(user): + """Drop an ADS-B truth server the user never chose, so default.yml can change it. + + The same first-boot copy problem as migrate_tracker_forward(): every node on + the estate persists whatever truth server shipped when it booted, so changing + default.yml alone reaches none of them. + + Narrow for the same reason that one is. 'sfo1.retnode.com' names a single + machine that no second node has any reason to read its truth from, and it + stopped answering: it returns HTTP 530 from every node and from off-estate, + so a node still pointed there is getting no ADS-B at all and has nothing to + lose. Any other value is a real choice and is left alone, including a + receiver's own address, which is wrong in a different way this cannot judge. + + Dropping the key rather than rewriting it means whatever default.yml ships + applies, including a later change of host or port. user.yml on disk is + untouched, so this stays load-bearing rather than a one-shot fixup. + """ + try: + adsb = user['truth']['adsb'] + except (KeyError, TypeError): + return + + if not isinstance(adsb, dict): + return + + if adsb.get('tar1090') == LEGACY_ADSB_TAR1090: + print(f"Dropping first-boot ADS-B truth server ({LEGACY_ADSB_TAR1090}) " + "from user config so the shipped default applies") + del adsb['tar1090'] + + def ensure_node_id(user_config_path): """Add/update node_id in user config from Mender device identity""" try: @@ -343,6 +380,7 @@ def main(): print("Applying user overrides...") migrate_doppler_span(user) migrate_tracker_forward(user) + migrate_adsb_truth_server(user) merge(config, user) else: print("User config is empty, using defaults") diff --git a/config-merger/test/test_merge_config.py b/config-merger/test/test_merge_config.py index 1ed65aa..2ff5be3 100755 --- a/config-merger/test/test_merge_config.py +++ b/config-merger/test/test_merge_config.py @@ -831,6 +831,99 @@ def test_tracker_forward_absent_from_user_config(self): self.assertTrue(forward['enabled']) self.assertEqual(forward['port'], 30100) + # --- ADS-B truth server migration ------------------------------------- + # Same first-boot-copy problem again: the shipped truth server persists in + # every overlay, so default.yml alone cannot move a node off a dead host. + + def default_with_truth(self, tar1090='localhost:8078'): + """A default.yml carrying the shipped ADS-B truth block.""" + return { + 'truth': { + 'adsb': { + 'enabled': True, + 'tar1090': tar1090, + 'adsb2dd': 'localhost:49155', + } + } + } + + def write_truth_configs(self, user_adsb, forced_config=None): + """Write a default/user/forced set differing only in the truth block.""" + self.write_yaml(os.path.join(self.defaults_dir, 'default.yml'), self.default_with_truth()) + self.write_yaml(os.path.join(self.defaults_dir, 'forced.yml'), forced_config or {}) + self.write_yaml(os.path.join(self.config_dir, 'user.yml'), {'truth': {'adsb': user_adsb}}) + + def test_adsb_truth_first_boot_copy_migrated(self): + """The remote host that shipped gives way to the node's own tar1090""" + self.write_truth_configs({'enabled': True, 'tar1090': 'sfo1.retnode.com'}) + + adsb = self.read_yaml(self.run_merge())['truth']['adsb'] + + self.assertEqual(adsb['tar1090'], 'localhost:8078') + + def test_adsb_truth_migration_leaves_the_rest_of_adsb_alone(self): + """Only the truth server moves; every other setting is still the user's""" + self.write_truth_configs( + {'enabled': True, 'tar1090': 'sfo1.retnode.com', + 'adsb2dd': 'localhost:49155', 'delay_tolerance': 4.5}) + + adsb = self.read_yaml(self.run_merge())['truth']['adsb'] + + self.assertEqual(adsb['tar1090'], 'localhost:8078') + self.assertEqual(adsb['delay_tolerance'], 4.5) + + def test_adsb_truth_deliberate_server_kept(self): + """A node aimed at some other host is a choice, not a first-boot copy""" + self.write_truth_configs({'tar1090': 'adsb.example.internal:8080'}) + + adsb = self.read_yaml(self.run_merge())['truth']['adsb'] + + self.assertEqual(adsb['tar1090'], 'adsb.example.internal:8080') + + def test_adsb_truth_receiver_address_kept(self): + """A receiver's own address is wrong differently, and not ours to rewrite""" + self.write_truth_configs({'tar1090': '192.168.1.143:30005'}) + + adsb = self.read_yaml(self.run_merge())['truth']['adsb'] + + self.assertEqual(adsb['tar1090'], '192.168.1.143:30005') + + def test_adsb_truth_legacy_host_with_a_port_kept(self): + """The legacy host named with a port is not the string we shipped""" + self.write_truth_configs({'tar1090': 'sfo1.retnode.com:8078'}) + + adsb = self.read_yaml(self.run_merge())['truth']['adsb'] + + self.assertEqual(adsb['tar1090'], 'sfo1.retnode.com:8078') + + def test_adsb_truth_forced_still_wins(self): + """forced.yml keeps the last word over a migrated truth server""" + self.write_truth_configs( + {'tar1090': 'sfo1.retnode.com'}, + forced_config={'truth': {'adsb': {'tar1090': 'forced.example:8078'}}}, + ) + + adsb = self.read_yaml(self.run_merge())['truth']['adsb'] + + self.assertEqual(adsb['tar1090'], 'forced.example:8078') + + def test_adsb_truth_migration_does_not_rewrite_user_yml(self): + """The overlay on disk is untouched, so the migration has to stay in place""" + self.write_truth_configs({'tar1090': 'sfo1.retnode.com'}) + + self.run_merge() + + user = self.read_yaml(os.path.join(self.config_dir, 'user.yml')) + self.assertEqual(user['truth']['adsb']['tar1090'], 'sfo1.retnode.com') + + def test_adsb_truth_absent_from_user_config(self): + """A user.yml with no truth server just takes the default""" + self.write_truth_configs({'enabled': True}) + + adsb = self.read_yaml(self.run_merge())['truth']['adsb'] + + self.assertEqual(adsb['tar1090'], 'localhost:8078') + if __name__ == '__main__': unittest.main() diff --git a/config/default.yml b/config/default.yml index 2ff9448..5d96cbc 100644 --- a/config/default.yml +++ b/config/default.yml @@ -83,7 +83,12 @@ network: truth: adsb: enabled: true - tar1090: 'sfo1.retnode.com' + # The node's own tar1090 container, which every node runs on the same port. + # Node-invariant on purpose: a local receiver is described by tar1090.adsb_source + # below and merged in there, so adding one never means editing this. Pointing + # this at a receiver directly is the mistake it is shaped to prevent, because + # a receiver's raw feed port speaks BEAST and this is fetched over HTTP. + tar1090: 'localhost:8078' adsb2dd: 'localhost:49155' delay_tolerance: 2.0 doppler_tolerance: 5.0