mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 18:32:13 +00:00
110 lines
6.0 KiB
Python
110 lines
6.0 KiB
Python
"""Menu speedups avoid unused probes, retaining confirmation and capacity checks."""
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import Mock,patch
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
sys.path[:0]=[str(ROOT/'src'),str(ROOT/'remote')]
|
|
from proxmenux_oci import management as menu
|
|
import oci_instances as instances
|
|
import oci_instance_reconcile as reconcile
|
|
import oci_remove as remover
|
|
import oci_work_backup as backup
|
|
|
|
|
|
class UpdateEntryTests(unittest.TestCase):
|
|
def run_update(self,approved,ready):
|
|
events=[]
|
|
record={'status':'installed','vmid':110,'deployment':{},'template':{}}
|
|
row={'vmid':110,'reason':'matched','stack':False,'pending':False,'status':'installed'}
|
|
ui=Mock();ui.unattended=True
|
|
ui.review.side_effect=lambda *a,**kw:events.append('review') or approved
|
|
with patch.object(menu,'carry_records',return_value={}) as carry, \
|
|
patch.object(menu,'check_selected',return_value=row), \
|
|
patch.object(menu,'_selected_configuration',return_value=b'current config'), \
|
|
patch.object(instances,'read',return_value=record),patch.object(reconcile,'propose',return_value=None), \
|
|
patch.object(menu,'_work_backup_ready',side_effect=lambda *a:events.append('backup') or ready), \
|
|
patch.object(menu,'_run_lifecycle',side_effect=lambda *a:events.append('lifecycle') or True):
|
|
result=menu.manage_instance(ROOT,ui,row,action='update')
|
|
carry.assert_called_once_with(ROOT,vmids=[110],verify=True)
|
|
return result,events
|
|
|
|
def test_declining_update_does_not_probe_disks_or_storages(self):
|
|
result,events=self.run_update(False,False)
|
|
self.assertFalse(result);self.assertEqual(events,['review'])
|
|
|
|
def test_accepted_update_checks_backup_space_before_starting(self):
|
|
result,events=self.run_update(True,True)
|
|
self.assertTrue(result);self.assertEqual(events,['review','backup','lifecycle'])
|
|
|
|
def test_insufficient_space_still_prevents_the_update(self):
|
|
result,events=self.run_update(True,False)
|
|
self.assertFalse(result);self.assertEqual(events,['review','backup'])
|
|
|
|
|
|
class BackupDestinationTests(unittest.TestCase):
|
|
def status(self,local_free,saved_free=None,only_needed=True):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root=Path(directory);saved= root/'nas';saved.mkdir()
|
|
record={'deployment':{backup.KEY:'nas'}}
|
|
with patch.object(backup,'application',return_value=(110,[110,111])), \
|
|
patch.object(instances,'read',return_value=record), \
|
|
patch.object(backup,'required',return_value=100) as required, \
|
|
patch.object(backup,'available',side_effect=lambda p:saved_free if Path(p)==saved else local_free), \
|
|
patch.object(backup,'storage_path',return_value=saved if saved_free is not None else None) as path, \
|
|
patch.object(backup,'file_storages',return_value=[{'storage':'enough','free':200},{'storage':'small','free':50}]) as alternatives:
|
|
result=backup.status(root,110,only_needed=only_needed)
|
|
required.assert_called_once_with([110,111])
|
|
return result,path.call_count,alternatives.call_count
|
|
|
|
def test_local_room_avoids_saved_storage_and_alternative_queries(self):
|
|
result,saved,alternatives=self.status(200,saved_free=1000)
|
|
self.assertTrue(result['fits']);self.assertEqual((saved,alternatives),(0,0))
|
|
|
|
def test_saved_destination_that_fits_avoids_alternative_queries(self):
|
|
result,saved,alternatives=self.status(50,saved_free=200)
|
|
self.assertTrue(result['saved_fits']);self.assertEqual((saved,alternatives),(1,0))
|
|
|
|
def test_when_neither_fits_all_available_destinations_are_checked(self):
|
|
result,saved,alternatives=self.status(50,saved_free=40)
|
|
self.assertFalse(result['fits']);self.assertFalse(result['saved_fits'])
|
|
self.assertEqual(result['candidates'],[{'storage':'enough','free':200}])
|
|
self.assertEqual(alternatives,1)
|
|
|
|
def test_missing_saved_storage_still_offers_alternatives(self):
|
|
result,saved,alternatives=self.status(50)
|
|
self.assertEqual(result['candidates'],[{'storage':'enough','free':200}])
|
|
self.assertEqual(alternatives,1)
|
|
|
|
def test_complete_status_keeps_legacy_fields_and_all_candidates(self):
|
|
result,saved,alternatives=self.status(200,saved_free=300,only_needed=False)
|
|
self.assertTrue(result['fits']);self.assertTrue(result['saved_fits'])
|
|
self.assertEqual((saved,alternatives),(1,1))
|
|
self.assertEqual(result['candidates'],[{'storage':'enough','free':200}])
|
|
|
|
|
|
class RemovalPreviewTests(unittest.TestCase):
|
|
def preview(self,reader):
|
|
record={'vmid':110,'deployment':{},'template':{'catalog_ui':{'title':'Example'}}}
|
|
with patch.object(remover,'members_of',return_value=(110,record,[110])), \
|
|
patch.object(instances,'read',return_value=record), \
|
|
patch.object(menu,'_selected_configuration',side_effect=reader), \
|
|
patch.object(remover,'guest_config',side_effect=AssertionError('Duplicate pct process')), \
|
|
patch.object(remover,'host_directories',return_value=[]), \
|
|
patch.object(remover,'private_bridge',return_value=None):
|
|
return menu._removal_summary(ROOT,110)
|
|
|
|
def test_preview_reuses_verified_config_and_keeps_volume_and_host_path_details(self):
|
|
config=b'hostname: example\nrootfs: local:subvol-110-disk-0,size=8G\nmp0: /mnt/media,mp=/media,backup=0\n'
|
|
text=self.preview(lambda v:config)
|
|
self.assertIn('8G',text);self.assertIn('/mnt/media',text)
|
|
self.assertIn('not targeted for removal',text)
|
|
|
|
def test_missing_guest_can_still_be_reviewed_without_a_second_probe(self):
|
|
text=self.preview(lambda v:(_ for _ in ()).throw(RuntimeError('guest missing')))
|
|
self.assertIn('CT 110',text);self.assertIn('rootfs -',text)
|
|
|
|
|
|
if __name__=='__main__':unittest.main()
|