forked from ungleich-public/dynamicweb
Do not break the VM list when a VM's OpenNebula template was deleted
The "my virtual machines" page showed "We could not load your VMs due to a
backend connection error" for any user owning a VM whose template had been
removed from OpenNebula. OpenNebulaManager.get_template() converted every
failure, including WrongIdError for a missing template, into
ConnectionRefusedError, and VirtualMachineSerializer.get_configuration()
let that propagate, so one VM with a stale template id took down the whole
list. The only log output was an empty ERROR line, because WrongIdError
has no message.
* Add opennebula_api.exceptions.TemplateNotFoundError and raise it from
get_template() for a missing template; keep ConnectionRefusedError for
genuine backend failures and log them with a traceback.
* Add OpenNebulaManager.get_template_names() and resolve_template_id() so
the serializer fetches the template pool once per list instead of
constructing an OpenNebulaManager and fetching the pool for every VM.
* get_configuration() now falls back to the template name embedded in the
VM name ("<template>-<vm id>") when the template is missing or the pool
cannot be fetched, and logs why.
* Replace str.lstrip('public-') with a real prefix strip (lstrip removes a
character set, so "public-ubuntu" became "ntu") and also strip
"ipv6only-".
* ProcessVMSelectionMixin shows "configuration is no longer available"
instead of an unhandled error when ordering a retired template.
* VirtualMachinesPlanListView logs the exception behind the banner.
* Add mock-based unit tests for the fallback, the UPDATED_TEMPLATES
mapping, the single pool fetch and the new exception.
The UPDATED_TEMPLATES setting keeps working as an operational override for
mapping retired template ids to their replacements.
This commit is contained in:
parent
9cdf4593cf
commit
cb16114aba
6 changed files with 268 additions and 23 deletions
|
|
@ -2,11 +2,18 @@ import random
|
|||
import string
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from unittest import skipIf
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from oca.pool import WrongIdError
|
||||
|
||||
from .exceptions import TemplateNotFoundError
|
||||
from .models import OpenNebulaManager
|
||||
from .serializers import VirtualMachineSerializer
|
||||
from .serializers import (
|
||||
VirtualMachineSerializer, strip_template_prefix,
|
||||
template_name_from_vm_name
|
||||
)
|
||||
from utils.models import CustomUser
|
||||
|
||||
|
||||
|
|
@ -149,3 +156,115 @@ class VirtualMachineSerializerTestCase(TestCase):
|
|||
serialized.data.get('name'), vm.name.lstrip('public-')
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
class TemplateNameHelpersTestCase(TestCase):
|
||||
def test_strip_template_prefix(self):
|
||||
self.assertEqual(strip_template_prefix('public-Ubuntu 20.10'),
|
||||
'Ubuntu 20.10')
|
||||
self.assertEqual(strip_template_prefix('ipv6only-Alpine 3.21'),
|
||||
'Alpine 3.21')
|
||||
self.assertEqual(strip_template_prefix('Devuan Beowulf'),
|
||||
'Devuan Beowulf')
|
||||
|
||||
def test_strip_template_prefix_is_not_lstrip(self):
|
||||
# str.lstrip('public-') strips a character set and would give 'ntu'
|
||||
self.assertEqual(strip_template_prefix('public-ubuntu'), 'ubuntu')
|
||||
|
||||
def test_template_name_from_vm_name(self):
|
||||
self.assertEqual(
|
||||
template_name_from_vm_name('public-Ubuntu 20.10-26623'),
|
||||
'public-Ubuntu 20.10'
|
||||
)
|
||||
|
||||
|
||||
def _fake_vm(vm_id, name, template_id):
|
||||
vm = MagicMock()
|
||||
vm.id = vm_id
|
||||
vm.name = name
|
||||
vm.template.template_id = template_id
|
||||
return vm
|
||||
|
||||
|
||||
class VirtualMachineSerializerMissingTemplateTestCase(TestCase):
|
||||
"""
|
||||
A VM whose OpenNebula template has been deleted must not break the
|
||||
serialization of the whole VM list (the symptom was the generic
|
||||
"backend connection error" banner on /hosting/my-virtual-machines).
|
||||
"""
|
||||
|
||||
def _configuration(self, vms, template_names):
|
||||
serializer = VirtualMachineSerializer(vms, many=True)
|
||||
with patch.object(OpenNebulaManager, '__init__', return_value=None), \
|
||||
patch.object(OpenNebulaManager, 'get_template_names',
|
||||
return_value=template_names) as get_names:
|
||||
configurations = [
|
||||
serializer.child.get_configuration(vm) for vm in vms
|
||||
]
|
||||
return configurations, get_names
|
||||
|
||||
@override_settings(UPDATED_TEMPLATES_DICT={})
|
||||
def test_missing_template_falls_back_to_vm_name(self):
|
||||
vms = [
|
||||
_fake_vm(28344, 'public-Alpine 3.21-28344', 310),
|
||||
_fake_vm(26623, 'public-Ubuntu 20.10-26623', 164),
|
||||
]
|
||||
configurations, _ = self._configuration(
|
||||
vms, {310: 'public-Alpine 3.21'}
|
||||
)
|
||||
self.assertEqual(configurations, ['Alpine 3.21', 'Ubuntu 20.10'])
|
||||
|
||||
@override_settings(UPDATED_TEMPLATES_DICT={164: 192})
|
||||
def test_updated_templates_mapping_is_applied(self):
|
||||
vms = [_fake_vm(26623, 'public-Ubuntu 20.10-26623', 164)]
|
||||
configurations, _ = self._configuration(
|
||||
vms, {192: 'public-Ubuntu 20.10 (new)'}
|
||||
)
|
||||
self.assertEqual(configurations, ['Ubuntu 20.10 (new)'])
|
||||
|
||||
@override_settings(UPDATED_TEMPLATES_DICT={})
|
||||
def test_template_pool_is_fetched_once_per_list(self):
|
||||
vms = [
|
||||
_fake_vm(1, 'public-Alpine 3.21-1', 310),
|
||||
_fake_vm(2, 'public-Alpine 3.21-2', 310),
|
||||
_fake_vm(3, 'public-Ubuntu 20.10-3', 164),
|
||||
]
|
||||
_, get_names = self._configuration(vms, {310: 'public-Alpine 3.21'})
|
||||
self.assertEqual(get_names.call_count, 1)
|
||||
|
||||
@override_settings(UPDATED_TEMPLATES_DICT={})
|
||||
def test_unreachable_template_pool_falls_back_to_vm_name(self):
|
||||
vms = [_fake_vm(26623, 'public-Ubuntu 20.10-26623', 164)]
|
||||
serializer = VirtualMachineSerializer(vms, many=True)
|
||||
with patch.object(OpenNebulaManager, '__init__', return_value=None), \
|
||||
patch.object(OpenNebulaManager, 'get_template_names',
|
||||
side_effect=ConnectionRefusedError):
|
||||
configuration = serializer.child.get_configuration(vms[0])
|
||||
self.assertEqual(configuration, 'Ubuntu 20.10')
|
||||
|
||||
|
||||
class OpenNebulaManagerGetTemplateTestCase(TestCase):
|
||||
@override_settings(UPDATED_TEMPLATES_DICT={164: 192})
|
||||
def test_resolve_template_id(self):
|
||||
self.assertEqual(OpenNebulaManager.resolve_template_id(164), 192)
|
||||
self.assertEqual(OpenNebulaManager.resolve_template_id('164'), 192)
|
||||
self.assertEqual(OpenNebulaManager.resolve_template_id(310), 310)
|
||||
|
||||
@override_settings(UPDATED_TEMPLATES_DICT={})
|
||||
def test_missing_template_raises_template_not_found(self):
|
||||
pool = MagicMock()
|
||||
pool.get_by_id.side_effect = WrongIdError
|
||||
with patch.object(OpenNebulaManager, '__init__', return_value=None), \
|
||||
patch.object(OpenNebulaManager, '_get_template_pool',
|
||||
return_value=pool):
|
||||
with self.assertRaises(TemplateNotFoundError) as ctx:
|
||||
OpenNebulaManager().get_template(164)
|
||||
self.assertEqual(ctx.exception.template_id, 164)
|
||||
|
||||
@override_settings(UPDATED_TEMPLATES_DICT={})
|
||||
def test_unreachable_pool_still_raises_connection_error(self):
|
||||
with patch.object(OpenNebulaManager, '__init__', return_value=None), \
|
||||
patch.object(OpenNebulaManager, '_get_template_pool',
|
||||
side_effect=ConnectionRefusedError):
|
||||
with self.assertRaises(ConnectionRefusedError):
|
||||
OpenNebulaManager().get_template(164)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue