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
42 changes: 42 additions & 0 deletions config-merger/script/merge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
import yaml
from mergedeep import merge

# The Doppler span default.yml shipped up to and including v0.4.5.0. A user.yml
# holding exactly this pair is assumed to be the merger's own first-boot copy
# rather than anyone's choice. See migrate_doppler_span().
LEGACY_DOPPLER_MIN = -200
LEGACY_DOPPLER_MAX = 200


def get_node_id_from_mender():
"""Read node_id from Mender device identity file (generated by mender-device-identity)"""
Expand Down Expand Up @@ -157,6 +163,41 @@ def migrate_gain_reduction(config):
config['capture']['device']['gainReduction'] = [gain, gain]


def migrate_doppler_span(user):
"""Drop a Doppler span the user never chose, so default.yml can change it.

On first boot the merger seeds user.yml with a whole copy of default.yml, so
every deployed node persists the shipped Doppler span whether or not anyone
selected it. Without this, a change to default.yml could never reach a node
that has already booted once.

Only the exact legacy pair is dropped, and only when both bounds match: any
other value is a deliberate setting and is left alone. owl runs +/-1000 Hz,
which must survive. A node that genuinely wants +/-200 Hz is indistinguishable
from a first-boot copy, so it moves to the new default and has to be set
again if that is not wanted.

Dropping the keys rather than rewriting them means whatever default.yml ships
takes effect, including a later change to some other value. user.yml on disk
is untouched, so this stays load-bearing: remove it and nodes fall back to
the +/-200 Hz their overlay still holds.
"""
try:
ambiguity = user['process']['ambiguity']
except (KeyError, TypeError):
return

if not isinstance(ambiguity, dict):
return

if (ambiguity.get('dopplerMin') == LEGACY_DOPPLER_MIN
and ambiguity.get('dopplerMax') == LEGACY_DOPPLER_MAX):
print(f"Dropping first-boot Doppler span ({LEGACY_DOPPLER_MIN}/{LEGACY_DOPPLER_MAX} Hz) "
"from user config so the shipped default applies")
del ambiguity['dopplerMin']
del ambiguity['dopplerMax']


def ensure_node_id(user_config_path):
"""Add/update node_id in user config from Mender device identity"""
try:
Expand Down Expand Up @@ -253,6 +294,7 @@ def main():

if user: # Only merge if user.yml has actual content
print("Applying user overrides...")
migrate_doppler_span(user)
merge(config, user)
else:
print("User config is empty, using defaults")
Expand Down
101 changes: 101 additions & 0 deletions config-merger/test/test_merge_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,5 +617,106 @@ def test_retina_tracker_yaml_user_override(self):
self.assertEqual(output['tracker']['min_snr'], 4.5)


# --- Doppler span migration -------------------------------------------
# The merger seeds user.yml from default.yml on first boot, so every node
# persists the shipped span. These cover which of those persisted values
# the merger is allowed to move.

def default_with_span(self, doppler_min=-300, doppler_max=300):
"""A default.yml carrying the shipped ambiguity block."""
return {
'process': {
'ambiguity': {
'delayMin': -10,
'delayMax': 400,
'dopplerMin': doppler_min,
'dopplerMax': doppler_max,
}
}
}

def write_span_configs(self, user_ambiguity, forced_config=None):
"""Write a default/user/forced set differing only in the ambiguity block."""
self.write_yaml(os.path.join(self.defaults_dir, 'default.yml'), self.default_with_span())
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'),
{'process': {'ambiguity': user_ambiguity}})

def test_doppler_span_first_boot_copy_migrated(self):
"""A user.yml holding the old shipped span gives way to the new default"""
self.write_span_configs({'delayMin': -10, 'delayMax': 400,
'dopplerMin': -200, 'dopplerMax': 200})

output = self.read_yaml(self.run_merge())
ambiguity = output['process']['ambiguity']

self.assertEqual(ambiguity['dopplerMin'], -300)
self.assertEqual(ambiguity['dopplerMax'], 300)
# Only the Doppler bounds move; the rest of the block is the user's.
self.assertEqual(ambiguity['delayMin'], -10)
self.assertEqual(ambiguity['delayMax'], 400)

def test_doppler_span_deliberate_override_kept(self):
"""A span nobody could have got from a first-boot copy survives"""
self.write_span_configs({'dopplerMin': -1000, 'dopplerMax': 1000})

output = self.read_yaml(self.run_merge())

self.assertEqual(output['process']['ambiguity']['dopplerMin'], -1000)
self.assertEqual(output['process']['ambiguity']['dopplerMax'], 1000)

def test_doppler_span_partial_legacy_match_kept(self):
"""One legacy bound is not the legacy pair, so neither bound moves"""
self.write_span_configs({'dopplerMin': -200, 'dopplerMax': 1000})

output = self.read_yaml(self.run_merge())

self.assertEqual(output['process']['ambiguity']['dopplerMin'], -200)
self.assertEqual(output['process']['ambiguity']['dopplerMax'], 1000)

def test_doppler_span_asymmetric_legacy_value_kept(self):
"""An asymmetric span that happens to touch 200 is still deliberate"""
self.write_span_configs({'dopplerMin': -200, 'dopplerMax': 400})

output = self.read_yaml(self.run_merge())

self.assertEqual(output['process']['ambiguity']['dopplerMin'], -200)
self.assertEqual(output['process']['ambiguity']['dopplerMax'], 400)

def test_doppler_span_forced_still_wins(self):
"""forced.yml keeps the last word over a migrated span"""
self.write_span_configs(
{'dopplerMin': -200, 'dopplerMax': 200},
forced_config={'process': {'ambiguity': {'dopplerMin': -250, 'dopplerMax': 250}}},
)

output = self.read_yaml(self.run_merge())

self.assertEqual(output['process']['ambiguity']['dopplerMin'], -250)
self.assertEqual(output['process']['ambiguity']['dopplerMax'], 250)

def test_doppler_span_migration_does_not_rewrite_user_yml(self):
"""The overlay on disk is untouched, so the migration has to stay in place"""
self.write_span_configs({'dopplerMin': -200, 'dopplerMax': 200})

self.run_merge()

user = self.read_yaml(os.path.join(self.config_dir, 'user.yml'))
self.assertEqual(user['process']['ambiguity']['dopplerMin'], -200)
self.assertEqual(user['process']['ambiguity']['dopplerMax'], 200)

def test_doppler_span_absent_from_user_config(self):
"""A user.yml with no ambiguity block just takes the default"""
self.write_yaml(os.path.join(self.defaults_dir, 'default.yml'), self.default_with_span())
self.write_yaml(os.path.join(self.defaults_dir, 'forced.yml'), {})
self.write_yaml(os.path.join(self.config_dir, 'user.yml'),
{'network': {'node_id': 'test-node'}})

output = self.read_yaml(self.run_merge())

self.assertEqual(output['process']['ambiguity']['dopplerMin'], -300)
self.assertEqual(output['process']['ambiguity']['dopplerMax'], 300)


if __name__ == '__main__':
unittest.main()
4 changes: 2 additions & 2 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ process:
ambiguity:
delayMin: -10
delayMax: 400
dopplerMin: -200
dopplerMax: 200
dopplerMin: -300
dopplerMax: 300
clutter:
enable: true
delayMin: -10
Expand Down
Loading