diff --git a/config-merger/script/merge_config.py b/config-merger/script/merge_config.py index b6dc328..d9302b4 100755 --- a/config-merger/script/merge_config.py +++ b/config-merger/script/merge_config.py @@ -250,6 +250,55 @@ def migrate_tracker_forward(user): del network['tracker_forward'] +def migrate_remove_tracker(user): + """Drop blah2's built-in tracker settings, which no longer exist. + + blah2's own tracker was removed outright: nothing consumed it (retina-gui's + Tracker page reads the retina-tracker sidecar, and blah2-api's /api/tracker + is gone with it), and it carried an unbounded leak - Track::remove() never + erased nInactive, which Tracker::process() then deep copied every CPI, worth + roughly 3 MB/h on a board with no swap. + + default.yml no longer ships these keys, but that alone reaches no node. The + merger seeds user.yml with a whole copy of default.yml on first boot and + user.yml overrides default.yml, so every node already on the estate persists + the block that shipped when it booted. Same first-boot copy problem + migrate_doppler_span() and migrate_tracker_forward() solve. + + Unconditional, unlike the migrations above, which compare against a LEGACY_* + value so a deliberate choice survives. There is no deliberate choice to + protect here: the code these keys configured does not exist, so any value is + equally dead. network.ports.track goes the same way, since the socket blah2 + published tracks on and the listener blah2-api read them with are both gone. + + Dropping the keys rather than rewriting them leaves user.yml on disk + untouched, so this stays load-bearing rather than a one-shot fixup, and a + Mender rollback regenerates the old config from the old default.yml. + + Note for anyone bisecting: once this has run, a blah2 image from before the + removal will abort on startup rather than warn, because it reads + process.tracker.enable through ryml, whose default error handler calls + abort(). Swap the config-merger image back at the same time. + """ + try: + process = user['process'] + except (KeyError, TypeError): + process = None + if isinstance(process, dict) and 'tracker' in process: + print("Dropping first-boot process.tracker from user config: blah2's " + "built-in tracker no longer exists") + del process['tracker'] + + try: + ports = user['network']['ports'] + except (KeyError, TypeError): + return + if isinstance(ports, dict) and 'track' in ports: + print("Dropping first-boot network.ports.track from user config: " + "nothing publishes or listens on it any more") + del ports['track'] + + def migrate_adsb_truth_server(user): """Drop an ADS-B truth server the user never chose, so default.yml can change it. @@ -380,6 +429,7 @@ def main(): print("Applying user overrides...") migrate_doppler_span(user) migrate_tracker_forward(user) + migrate_remove_tracker(user) migrate_adsb_truth_server(user) merge(config, user) else: diff --git a/config-merger/test/test_merge_config.py b/config-merger/test/test_merge_config.py index 2ff5be3..86b085e 100755 --- a/config-merger/test/test_merge_config.py +++ b/config-merger/test/test_merge_config.py @@ -924,6 +924,86 @@ def test_adsb_truth_absent_from_user_config(self): self.assertEqual(adsb['tar1090'], 'localhost:8078') + # --- blah2 tracker removal -------------------------------------------- + # The same first-boot-copy problem again, but unconditional: the code these + # keys configured no longer exists, so there is no deliberate value to keep. + + def write_tracker_configs(self, user_process=None, user_ports=None, + forced_config=None): + """Write a default/user/forced set where default.yml has no tracker.""" + self.write_yaml(os.path.join(self.defaults_dir, 'default.yml'), { + 'process': {'detection': {'enable': True}}, + 'network': {'ip': '0.0.0.0', 'ports': {'api': 3000, 'map': 3001}}, + }) + self.write_yaml(os.path.join(self.defaults_dir, 'forced.yml'), forced_config or {}) + user = {} + if user_process is not None: + user['process'] = user_process + if user_ports is not None: + user['network'] = {'ports': user_ports} + self.write_yaml(os.path.join(self.config_dir, 'user.yml'), user) + + def test_tracker_block_removed_from_merged_config(self): + """The first-boot copy of process.tracker does not survive the merge""" + self.write_tracker_configs(user_process={ + 'tracker': {'enable': True, 'initiate': {'M': 3, 'N': 5, 'maxAcc': 10}, + 'delete': 10, 'smooth': 'none'}}) + + process = self.read_yaml(self.run_merge())['process'] + + self.assertNotIn('tracker', process) + + def test_track_port_removed_from_merged_config(self): + """network.ports.track goes too: nothing publishes or listens on it""" + self.write_tracker_configs(user_ports={'api': 3000, 'track': 3003}) + + ports = self.read_yaml(self.run_merge())['network']['ports'] + + self.assertNotIn('track', ports) + self.assertEqual(ports['api'], 3000) + + def test_tracker_removal_is_unconditional(self): + """Tuned M/N is not a reason to keep settings for code that is gone""" + self.write_tracker_configs(user_process={ + 'tracker': {'enable': True, 'initiate': {'M': 4, 'N': 9}, 'delete': 25}}) + + process = self.read_yaml(self.run_merge())['process'] + + self.assertNotIn('tracker', process) + + def test_tracker_removal_leaves_the_rest_of_process_alone(self): + """Only the tracker block goes; the user's other process keys stay""" + self.write_tracker_configs(user_process={ + 'tracker': {'enable': True}, + 'detection': {'enable': False, 'pfa': 1e-6}, + }) + + process = self.read_yaml(self.run_merge())['process'] + + self.assertNotIn('tracker', process) + self.assertFalse(process['detection']['enable']) + self.assertEqual(process['detection']['pfa'], 1e-6) + + def test_tracker_absent_from_user_config(self): + """A user.yml with no tracker block merges without complaint""" + self.write_tracker_configs(user_process={'detection': {'enable': True}}) + + config = self.read_yaml(self.run_merge()) + + self.assertNotIn('tracker', config['process']) + self.assertNotIn('track', config['network']['ports']) + + def test_tracker_removal_does_not_rewrite_user_yml(self): + """The overlay on disk is untouched, so the removal has to stay in place""" + self.write_tracker_configs( + user_process={'tracker': {'enable': True}}, + user_ports={'api': 3000, 'track': 3003}) + + self.run_merge() + + user = self.read_yaml(os.path.join(self.config_dir, 'user.yml')) + self.assertTrue(user['process']['tracker']['enable']) + self.assertEqual(user['network']['ports']['track'], 3003) if __name__ == '__main__': unittest.main() diff --git a/config/default.yml b/config/default.yml index 5d96cbc..50cad7c 100644 --- a/config/default.yml +++ b/config/default.yml @@ -36,18 +36,6 @@ process: minDelay: 5 minDoppler: 15 nCentroid: 6 - tracker: - # blah2's own built-in tracker - superseded by the retina-tracker sidecar - # (see retina_tracker below), which is what's actually used. Left disabled - # so nothing runs its associate/promote loop or feeds its unused - # blah2-api /api/tracker endpoint. - enable: false - initiate: - M: 3 - N: 5 - maxAcc: 10 - delete: 10 - smooth: "none" network: ip: 0.0.0.0 @@ -55,7 +43,6 @@ network: api: 3000 map: 3001 detection: 3002 - track: 3003 timestamp: 4000 timing: 4001 iqdata: 4002 @@ -135,7 +122,7 @@ tar1090: adsblol_radius: 40 # retina-tracker sidecar (github.com/offworldlabs/retina-tracker) config. -# Unrelated to process.tracker above, which is blah2's own built-in tracker. +# blah2 has no built-in tracker of its own; this sidecar does all tracking. retina_tracker: # dB. Detections below this are discarded before tracking even begins - # too high and the tracker silently never confirms a track no matter how diff --git a/staging/docker-compose.staging.yml b/staging/docker-compose.staging.yml index 42f97ad..d19f36b 100644 --- a/staging/docker-compose.staging.yml +++ b/staging/docker-compose.staging.yml @@ -56,7 +56,6 @@ services: - "3000:3000" # API port - "3001:3001" # Map port - "3002:3002" # Detection port - - "3003:3003" # Track port networks: - blah2 - caddy