Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions config-merger/script/merge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
80 changes: 80 additions & 0 deletions config-merger/test/test_merge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
15 changes: 1 addition & 14 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,13 @@ 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
ports:
api: 3000
map: 3001
detection: 3002
track: 3003
timestamp: 4000
timing: 4001
iqdata: 4002
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion staging/docker-compose.staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ services:
- "3000:3000" # API port
- "3001:3001" # Map port
- "3002:3002" # Detection port
- "3003:3003" # Track port
networks:
- blah2
- caddy
Expand Down
Loading