Compare commits
No commits in common. "master" and "master" have entirely different histories.
6 changed files with 23 additions and 282 deletions
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue