diff --git a/config-merger/script/merge_config.py b/config-merger/script/merge_config.py index 1372d36..322bad0 100755 --- a/config-merger/script/merge_config.py +++ b/config-merger/script/merge_config.py @@ -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)""" @@ -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: @@ -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") diff --git a/config-merger/test/test_merge_config.py b/config-merger/test/test_merge_config.py index e507ae1..f0d0aba 100755 --- a/config-merger/test/test_merge_config.py +++ b/config-merger/test/test_merge_config.py @@ -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() diff --git a/config/default.yml b/config/default.yml index 50c10b9..798d8e0 100644 --- a/config/default.yml +++ b/config/default.yml @@ -22,8 +22,8 @@ process: ambiguity: delayMin: -10 delayMax: 400 - dopplerMin: -200 - dopplerMax: 200 + dopplerMin: -300 + dopplerMax: 300 clutter: enable: true delayMin: -10