Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

6 changed files with 23 additions and 282 deletions

View file

@ -1,10 +1,7 @@
from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from opennebula_api.exceptions import TemplateNotFoundError
from opennebula_api.serializers import VirtualMachineTemplateSerializer from opennebula_api.serializers import VirtualMachineTemplateSerializer
from opennebula_api.models import OpenNebulaManager from opennebula_api.models import OpenNebulaManager
@ -17,15 +14,7 @@ class ProcessVMSelectionMixin(object):
template_id = int(request.POST.get('vm_template_id')) template_id = int(request.POST.get('vm_template_id'))
configuration_id = int(request.POST.get('configuration')) configuration_id = int(request.POST.get('configuration'))
try: template = OpenNebulaManager().get_template(template_id)
template = OpenNebulaManager().get_template(template_id)
except TemplateNotFoundError:
messages.error(
request,
_('The selected configuration is no longer available. '
'Please choose another one.')
)
return redirect(reverse('hosting:index'))
data = VirtualMachineTemplateSerializer(template).data data = VirtualMachineTemplateSerializer(template).data
configuration = HostingPlan.objects.get(id=configuration_id) configuration = HostingPlan.objects.get(id=configuration_id)

View file

@ -1557,11 +1557,6 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView):
serializer = VirtualMachineSerializer(queryset, many=True) serializer = VirtualMachineSerializer(queryset, many=True)
return serializer.data return serializer.data
except ConnectionRefusedError: except ConnectionRefusedError:
logger.exception(
"Could not load VMs of user %s (%s) from OpenNebula" % (
owner.username, owner.email
)
)
messages.error(self.request, messages.error(self.request,
'We could not load your VMs due to a backend connection \ 'We could not load your VMs due to a backend connection \
error. Please try again in a few minutes' error. Please try again in a few minutes'

View file

@ -8,13 +8,3 @@ class UserExistsError(Exception):
class UserCredentialError(Exception): class UserCredentialError(Exception):
pass pass
class TemplateNotFoundError(Exception):
"""Raised when an OpenNebula VM template id does not exist (any more)."""
def __init__(self, template_id):
super(TemplateNotFoundError, self).__init__(
"OpenNebula template %s does not exist" % template_id
)
self.template_id = template_id

View file

@ -9,10 +9,7 @@ from oca.pool import WrongNameError, WrongIdError
from hosting.models import HostingOrder from hosting.models import HostingOrder
from utils.models import CustomUser from utils.models import CustomUser
from utils.tasks import save_ssh_key, save_ssh_key_error_handler from utils.tasks import save_ssh_key, save_ssh_key_error_handler
from .exceptions import ( from .exceptions import KeyExistsError, UserExistsError, UserCredentialError
KeyExistsError, UserExistsError, UserCredentialError,
TemplateNotFoundError
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -428,60 +425,16 @@ class OpenNebulaManager():
except: except:
return [] return []
@staticmethod
def resolve_template_id(template_id):
"""
Map a (possibly retired) template id to its replacement, as
configured in settings.UPDATED_TEMPLATES_DICT. Ids without a
mapping are returned unchanged.
"""
template_id = int(template_id)
return settings.UPDATED_TEMPLATES_DICT.get(template_id, template_id)
def get_template_names(self):
"""
Return a dict {template_id: template_name} of every template
visible to oneadmin, in a single RPC call.
:raises ConnectionRefusedError: if OpenNebula is unreachable
"""
try:
template_pool = self._get_template_pool()
return dict(
(template.id, template.name) for template in template_pool
)
except ConnectionRefusedError:
raise
except Exception:
# Any other failure (socket timeout, XML-RPC fault, ...) is a
# backend problem as far as the callers are concerned
logger.exception("Could not fetch the OpenNebula template pool")
raise ConnectionRefusedError
def get_template(self, template_id): def get_template(self, template_id):
""" template_id = int(template_id)
Return the oca template object for the given id.
:raises TemplateNotFoundError: if the template (after applying
settings.UPDATED_TEMPLATES_DICT) does not exist in OpenNebula
:raises ConnectionRefusedError: if OpenNebula is unreachable or
fails in any other way
"""
template_id = self.resolve_template_id(template_id)
try: try:
template_pool = self._get_template_pool() template_pool = self._get_template_pool()
if template_id in settings.UPDATED_TEMPLATES_DICT.keys():
template_id = settings.UPDATED_TEMPLATES_DICT[template_id]
return template_pool.get_by_id(template_id) return template_pool.get_by_id(template_id)
except WrongIdError: except Exception as ex:
logger.error( logger.debug("Template Id we are looking for : %s" % template_id)
"OpenNebula template %s does not exist" % template_id logger.error(str(ex))
)
raise TemplateNotFoundError(template_id)
except ConnectionRefusedError:
raise
except Exception:
logger.exception(
"Could not fetch OpenNebula template %s" % template_id
)
raise ConnectionRefusedError raise ConnectionRefusedError
def create_template(self, name, cores, memory, disk_size, core_price, def create_template(self, name, cores, memory, disk_size, core_price,

View file

@ -1,44 +1,12 @@
import ipaddress import ipaddress
import logging
from builtins import hasattr from builtins import hasattr
from rest_framework import serializers from rest_framework import serializers
from oca import OpenNebulaException from oca import OpenNebulaException
from .exceptions import TemplateNotFoundError
from .models import OpenNebulaManager from .models import OpenNebulaManager
logger = logging.getLogger(__name__)
TEMPLATE_NAME_PREFIXES = ('public-', 'ipv6only-')
def strip_template_prefix(name):
"""
Remove the DCL naming prefix ('public-', 'ipv6only-') from a template
or VM name. str.lstrip() must not be used here: it strips a character
set, so 'public-ubuntu'.lstrip('public-') gives 'ntu'.
"""
for prefix in TEMPLATE_NAME_PREFIXES:
if name.startswith(prefix):
return name[len(prefix):]
return name
def template_name_from_vm_name(vm_name, vm_id):
"""
VMs created by dynamicweb are named '<template name>-<vm id>' (see
OpenNebulaManager.create_vm), so the template name can be recovered
from the VM name when the template itself has been deleted. VMs that
were created or renamed outside dynamicweb do not follow that pattern
and are returned unchanged.
"""
suffix = '-%s' % vm_id
if vm_name.endswith(suffix):
return vm_name[:-len(suffix)]
return vm_name
class VirtualMachineTemplateSerializer(serializers.Serializer): class VirtualMachineTemplateSerializer(serializers.Serializer):
"""Serializer to map the virtual machine template instance into JSON format.""" """Serializer to map the virtual machine template instance into JSON format."""
@ -68,7 +36,10 @@ class VirtualMachineTemplateSerializer(serializers.Serializer):
return int(obj.template.memory) / 1024 return int(obj.template.memory) / 1024
def get_name(self, obj): def get_name(self, obj):
return strip_template_prefix(obj.name) if obj.name.startswith('public-'):
return obj.name.lstrip('public-')
else:
return obj.name
class VirtualMachineSerializer(serializers.Serializer): class VirtualMachineSerializer(serializers.Serializer):
@ -162,45 +133,13 @@ class VirtualMachineSerializer(serializers.Serializer):
price += int(disk.size) / 1024 * 0.6 price += int(disk.size) / 1024 * 0.6
return price return price
def _template_name(self, template_id):
"""
Resolve a template id to its name using one template pool fetch per
serializer instance. With many=True the child serializer is shared
across all VMs, so a whole VM list costs a single RPC call instead
of one OpenNebulaManager (and template pool fetch) per VM.
:raises TemplateNotFoundError: if the template no longer exists
:raises ConnectionRefusedError: if OpenNebula is unreachable
"""
if not hasattr(self, '_template_names'):
self._template_names = OpenNebulaManager().get_template_names()
template_id = OpenNebulaManager.resolve_template_id(template_id)
if template_id not in self._template_names:
raise TemplateNotFoundError(template_id)
return self._template_names[template_id]
def get_configuration(self, obj): def get_configuration(self, obj):
"""
The name of the template the VM was created from, without the DCL
prefix. A VM whose template has since been deleted from OpenNebula
must not break serialization of the whole VM list, so fall back to
the template name embedded in the VM name.
"""
template_id = obj.template.template_id template_id = obj.template.template_id
try: template = OpenNebulaManager().get_template(template_id)
name = self._template_name(template_id) if template.name.startswith('public-'):
except TemplateNotFoundError as err: return template.name.lstrip('public-')
logger.warning( else:
"VM %s: %s, falling back to VM name" % (obj.id, err) return template.name
)
name = template_name_from_vm_name(obj.name, obj.id)
except ConnectionRefusedError:
logger.error(
"VM %s: could not fetch template pool, falling back to "
"VM name" % obj.id
)
name = template_name_from_vm_name(obj.name, obj.id)
return strip_template_prefix(name)
def get_ipv4(self, obj): def get_ipv4(self, obj):
""" """
@ -229,7 +168,10 @@ class VirtualMachineSerializer(serializers.Serializer):
return '-' return '-'
def get_name(self, obj): def get_name(self, obj):
return strip_template_prefix(obj.name) if obj.name.startswith('public-'):
return obj.name.lstrip('public-')
else:
return obj.name
class VMTemplateSerializer(serializers.Serializer): class VMTemplateSerializer(serializers.Serializer):

View file

@ -2,18 +2,11 @@ import random
import string import string
from django.conf import settings from django.conf import settings
from django.test import TestCase, override_settings from django.test import TestCase
from unittest import skipIf from unittest import skipIf
from unittest.mock import MagicMock, patch
from oca.pool import WrongIdError
from .exceptions import TemplateNotFoundError
from .models import OpenNebulaManager from .models import OpenNebulaManager
from .serializers import ( from .serializers import VirtualMachineSerializer
VirtualMachineSerializer, strip_template_prefix,
template_name_from_vm_name
)
from utils.models import CustomUser from utils.models import CustomUser
@ -156,124 +149,3 @@ class VirtualMachineSerializerTestCase(TestCase):
serialized.data.get('name'), vm.name.lstrip('public-') serialized.data.get('name'), vm.name.lstrip('public-')
) )
break 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', 26623),
'public-Ubuntu 20.10'
)
def test_template_name_from_vm_name_keeps_custom_names(self):
# VMs created or renamed outside dynamicweb
self.assertEqual(template_name_from_vm_name('gitea', 27001), 'gitea')
self.assertEqual(
template_name_from_vm_name('ipv6only-Alpine 3.15-27397-Jonas',
27397),
'ipv6only-Alpine 3.15-27397-Jonas'
)
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)