Carry over OCI variables changed in Proxmox and keep the LXC update badge on old checks

This commit is contained in:
MacRimi
2026-10-18 11:40:17 +02:00
parent f2bb006728
commit e3c9162982
9 changed files with 171 additions and 26 deletions
+61
View File
@@ -155,5 +155,66 @@ class ProposalTests(unittest.TestCase):
self.assertEqual(json.loads(history[0].read_text())['deployment']['mounts'], [])
class EnvironmentAdoptionTests(unittest.TestCase):
marker = '11111111-1111-1111-1111-111111111111'
def record(self, env):
config = f'description: proxmenux-instance={self.marker}\nenv: {env}\n'.encode()
return {
'vmid': 200, 'status': 'installed', 'installation_id': self.marker,
'deployment': {'mounts': [], 'devices': [], 'environment': [
{'name': 'APP_PASSWORD', 'value': 'first-secret', 'sensitive': True}]},
'observed': {'config': config.decode(), 'config_sha256': sha(config),
'archive_path': '/unused.tar', 'resolved_registry_digest': 'sha256:test',
'image': {'manifest_digest': 'sha256:test', 'defaults': {'Env': ['PATH=/bin']}}},
}
def propose(self, record, env):
config = f'description: proxmenux-instance={self.marker}\nenv: {env}\n'.encode()
with (patch.object(reconcile.instances, 'ROOT', Path('/inert')),
patch.object(reconcile.instances, 'identity', return_value=self.marker),
patch.object(reconcile.instances, 'location', return_value=Path(__file__)),
patch.object(reconcile.gpu_devices, 'capture', return_value={}),
patch.object(reconcile.host_mounts, 'capture_sources', return_value={}),
patch.object(reconcile.transaction, 'preflight')):
return reconcile.propose(record, config)
def test_a_password_changed_in_proxmox_is_adopted_without_showing_it(self):
record = self.record('PATH=/bin\0APP_PASSWORD=first-secret')
proposal = self.propose(record, 'PATH=/bin\0APP_PASSWORD=my-own-secret\0TZ=Europe/Madrid')
environment = proposal['candidate']['deployment']['environment']
self.assertEqual(environment, [
{'name': 'APP_PASSWORD', 'value': 'my-own-secret', 'sensitive': True},
{'name': 'TZ', 'value': 'Europe/Madrid', 'sensitive': True}])
self.assertEqual(proposal['details'], ['env APP_PASSWORD: value changed in Proxmox',
'env TZ: added in Proxmox'])
self.assertNotIn('my-own-secret', str(proposal['details']))
self.assertEqual(record['deployment']['environment'][0]['value'], 'first-secret')
def test_reordered_variables_need_no_decision(self):
record = self.record('PATH=/bin\0APP_PASSWORD=first-secret')
proposal = self.propose(record, 'APP_PASSWORD=first-secret\0PATH=/bin')
self.assertEqual(proposal['details'], [])
def test_removing_an_image_default_is_refused(self):
record = self.record('PATH=/bin\0APP_PASSWORD=first-secret')
with self.assertRaisesRegex(ValueError, 'env'):
self.propose(record, 'APP_PASSWORD=first-secret')
def test_removing_a_saved_variable_removes_it_from_the_contract(self):
record = self.record('PATH=/bin\0APP_PASSWORD=first-secret')
proposal = self.propose(record, 'PATH=/bin')
self.assertEqual(proposal['candidate']['deployment']['environment'], [])
self.assertEqual(proposal['details'], ['env APP_PASSWORD: removed in Proxmox'])
def test_other_changes_next_to_the_variables_are_still_refused(self):
record = self.record('PATH=/bin')
config = (f'description: proxmenux-instance={self.marker}\nenv: PATH=/bin\0A=1\n'
'net0: name=eth0,bridge=vmbr1\n').encode()
with (patch.object(reconcile.instances, 'identity', return_value=self.marker),
self.assertRaisesRegex(ValueError, 'net0')):
reconcile.propose(record, config)
if __name__ == '__main__':
unittest.main()
+18 -3
View File
@@ -169,12 +169,13 @@ class ModifyMenuTests(unittest.TestCase):
class ModifyDispatchTests(unittest.TestCase):
def call_manager(self, proposal, lifecycle):
def call_manager(self, proposal, lifecycle, ui=None, offer=None):
import oci_instances
import oci_instance_reconcile
saved = record()
row = {'vmid': 115, 'reason': 'matched', 'stack': False, 'pending': False, 'status': 'installed'}
with patch.object(management, 'carry_records', return_value={}), \
with patch.object(management.images, 'offer_removal', offer or Mock()), \
patch.object(management, 'carry_records', return_value={}), \
patch.object(management, 'check_selected', return_value=row), \
patch.object(management, '_work_backup_ready', return_value=True), \
patch.object(oci_instances, 'read', return_value=saved), \
@@ -182,7 +183,7 @@ class ModifyDispatchTests(unittest.TestCase):
patch.object(oci_instance_reconcile, 'propose', return_value=None), \
patch.object(recreation, 'edit_recreation', return_value=proposal), \
patch.object(management, '_run_lifecycle', side_effect=lifecycle):
return management.manage_instance(ROOT, Mock(), row, action='modify')
return management.manage_instance(ROOT, ui or Mock(), row, action='modify')
def test_cancel_does_not_start_a_lifecycle_transaction(self):
lifecycle = Mock()
@@ -203,6 +204,20 @@ class ModifyDispatchTests(unittest.TestCase):
self.assertTrue(self.call_manager(proposal, lifecycle))
self.assertEqual(seen, [proposal])
def test_the_image_question_only_follows_a_recreated_container(self):
proposal = {'operation': 'recreate', 'candidate': record(), 'base_config_sha256': 'fingerprint'}
ui = Mock(unattended=False)
for recreated in (False, True):
with self.subTest(recreated=recreated):
def lifecycle(command, title):
if recreated:
Path(management.os.environ['OCI_RECREATED_MARKER']).touch()
return True
offer = Mock()
self.assertTrue(self.call_manager(proposal, lifecycle, ui, offer))
self.assertEqual(offer.called, recreated)
self.assertNotIn('OCI_RECREATED_MARKER', management.os.environ)
if __name__ == '__main__':
unittest.main()