mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-04 20:02:07 +00:00
fix(oci): check high availability in the preflight so tests without Proxmox keep working
This commit is contained in:
@@ -624,7 +624,11 @@ def without_public_routes(config, vmid):
|
|||||||
def refuse_high_availability(vmid):
|
def refuse_high_availability(vmid):
|
||||||
"""Recreating a container that HA manages would race the HA manager, which
|
"""Recreating a container that HA manages would race the HA manager, which
|
||||||
restarts or relocates it in the middle of the replacement."""
|
restarts or relocates it in the middle of the replacement."""
|
||||||
resources = json.loads(run('pvesh', 'get', '/cluster/ha/resources', '--output-format', 'json'))
|
try:
|
||||||
|
answer = run('pvesh', 'get', '/cluster/ha/resources', '--output-format', 'json')
|
||||||
|
except FileNotFoundError:
|
||||||
|
return # Not a Proxmox VE node: there is no HA manager to race.
|
||||||
|
resources = json.loads(answer)
|
||||||
if any(r.get('sid') == f'ct:{vmid}' for r in resources):
|
if any(r.get('sid') == f'ct:{vmid}' for r in resources):
|
||||||
raise ValueError(translate(
|
raise ValueError(translate(
|
||||||
'This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, '
|
'This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, '
|
||||||
@@ -641,6 +645,9 @@ def preflight(record, candidate, config, coordinated=None, in_place=False, basel
|
|||||||
raise ValueError(translate('Stack members are updated together with their stack'))
|
raise ValueError(translate('Stack members are updated together with their stack'))
|
||||||
if instances.identity(config) != record['installation_id']:
|
if instances.identity(config) != record['installation_id']:
|
||||||
raise ValueError(translate('The container identity changed; the container is not replaced'))
|
raise ValueError(translate('The container identity changed; the container is not replaced'))
|
||||||
|
if not in_place and not coordinated:
|
||||||
|
# A stack checks its members itself; an in-place edit keeps the container.
|
||||||
|
refuse_high_availability(record['vmid'])
|
||||||
for key, value in (external_changes(record, config, adopt=not coordinated) if baseline else {}).items():
|
for key, value in (external_changes(record, config, adopt=not coordinated) if baseline else {}).items():
|
||||||
section, name = ADOPTABLE[key]
|
section, name = ADOPTABLE[key]
|
||||||
recorded = deployment.get(section, {}) if section else deployment
|
recorded = deployment.get(section, {}) if section else deployment
|
||||||
@@ -1213,7 +1220,8 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i
|
|||||||
runtime_settings.file_binds(before))
|
runtime_settings.file_binds(before))
|
||||||
original_gpu = gpu_devices.planned(record['deployment'])
|
original_gpu = gpu_devices.planned(record['deployment'])
|
||||||
desired_gpu = gpu_devices.planned(candidate['deployment'])
|
desired_gpu = gpu_devices.planned(candidate['deployment'])
|
||||||
refuse_high_availability(vmid)
|
if coordinated:
|
||||||
|
refuse_high_availability(vmid)
|
||||||
archive = archive.resolve(strict=True)
|
archive = archive.resolve(strict=True)
|
||||||
check_archive(archive)
|
check_archive(archive)
|
||||||
image = image_from_archive(str(archive))
|
image = image_from_archive(str(archive))
|
||||||
|
|||||||
@@ -232,9 +232,8 @@ def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=Non
|
|||||||
# An unsupported native edit still uses the existing installer. Decide
|
# An unsupported native edit still uses the existing installer. Decide
|
||||||
# the route before changing anything; actual validation/API failures
|
# the route before changing anything; actual validation/API failures
|
||||||
# must not silently switch to a destructive fallback.
|
# must not silently switch to a destructive fallback.
|
||||||
|
# Also refuses a container that HA manages, before the image is downloaded.
|
||||||
transaction.preflight(record, desired, config)
|
transaction.preflight(record, desired, config)
|
||||||
# Refused before the image is downloaded, not after.
|
|
||||||
transaction.refuse_high_availability(vmid)
|
|
||||||
msg_ok(translate('Container checked'))
|
msg_ok(translate('Container checked'))
|
||||||
current = record['observed']['image'] if operation == 'update' else None
|
current = record['observed']['image'] if operation == 'update' else None
|
||||||
check = lambda: work_backup.locate(
|
check = lambda: work_backup.locate(
|
||||||
|
|||||||
@@ -26,6 +26,28 @@ class HighAvailabilityTests(unittest.TestCase):
|
|||||||
with ha_resources(200):
|
with ha_resources(200):
|
||||||
transaction.refuse_high_availability(101)
|
transaction.refuse_high_availability(101)
|
||||||
|
|
||||||
|
def test_a_host_without_pvesh_has_no_ha_to_refuse(self):
|
||||||
|
with patch.object(transaction, 'run', side_effect=FileNotFoundError('pvesh')):
|
||||||
|
transaction.refuse_high_availability(101)
|
||||||
|
|
||||||
|
def test_the_preflight_of_a_replacement_refuses_it(self):
|
||||||
|
record = {'vmid': 101, 'installation_id': 'install-1', 'deployment': {}}
|
||||||
|
with patch.object(transaction.instances, 'identity', return_value='install-1'), ha_resources(101), \
|
||||||
|
self.assertRaises(ValueError) as refused:
|
||||||
|
transaction.preflight(record, {'deployment': {}}, b'')
|
||||||
|
self.assertIn('ct:101', str(refused.exception))
|
||||||
|
|
||||||
|
def test_an_in_place_edit_and_a_stack_member_are_not_refused_here(self):
|
||||||
|
record = {'vmid': 101, 'installation_id': 'install-1', 'deployment': {}}
|
||||||
|
refuse = Mock()
|
||||||
|
with patch.object(transaction.instances, 'identity', return_value='install-1'), \
|
||||||
|
patch.object(transaction, 'refuse_high_availability', refuse), \
|
||||||
|
patch.object(transaction, 'external_changes', side_effect=RuntimeError('past the check')):
|
||||||
|
for options in ({'in_place': True}, {'coordinated': {'id': 'stack'}}):
|
||||||
|
with self.subTest(options=options), self.assertRaises(RuntimeError):
|
||||||
|
transaction.preflight(record, {'deployment': {}}, b'', **options)
|
||||||
|
refuse.assert_not_called()
|
||||||
|
|
||||||
def test_an_update_stops_before_downloading_the_image(self):
|
def test_an_update_stops_before_downloading_the_image(self):
|
||||||
record = {'status': 'installed', 'observed': {'image': {}}}
|
record = {'status': 'installed', 'observed': {'image': {}}}
|
||||||
resolve = Mock()
|
resolve = Mock()
|
||||||
@@ -35,11 +57,11 @@ class HighAvailabilityTests(unittest.TestCase):
|
|||||||
stack.enter_context(patch.object(update.instances, 'command', return_value=b''))
|
stack.enter_context(patch.object(update.instances, 'command', return_value=b''))
|
||||||
stack.enter_context(patch.object(update.transaction, 'candidate_contract', return_value={}))
|
stack.enter_context(patch.object(update.transaction, 'candidate_contract', return_value={}))
|
||||||
stack.enter_context(patch.object(update.transaction, 'external_changes', return_value={}))
|
stack.enter_context(patch.object(update.transaction, 'external_changes', return_value={}))
|
||||||
stack.enter_context(patch.object(update.transaction, 'preflight'))
|
stack.enter_context(patch.object(update.transaction, 'preflight',
|
||||||
|
side_effect=ValueError('managed by high availability')))
|
||||||
stack.enter_context(patch.object(update, 'resolve_archive', resolve))
|
stack.enter_context(patch.object(update, 'resolve_archive', resolve))
|
||||||
stack.enter_context(patch.object(update, 'msg_info'))
|
stack.enter_context(patch.object(update, 'msg_info'))
|
||||||
stack.enter_context(patch.object(update, 'msg_ok'))
|
stack.enter_context(patch.object(update, 'msg_ok'))
|
||||||
stack.enter_context(ha_resources(101))
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
update.update(101)
|
update.update(101)
|
||||||
resolve.assert_not_called()
|
resolve.assert_not_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user