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
38 changes: 38 additions & 0 deletions config-merger/script/merge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
93 changes: 93 additions & 0 deletions config-merger/test/test_merge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
7 changes: 6 additions & 1 deletion config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading