diff --git a/hosting/mixins.py b/hosting/mixins.py index 5fb83383..4d597b74 100644 --- a/hosting/mixins.py +++ b/hosting/mixins.py @@ -1,7 +1,10 @@ +from django.contrib import messages from django.shortcuts import redirect from django.conf import settings 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.models import OpenNebulaManager @@ -14,7 +17,15 @@ class ProcessVMSelectionMixin(object): template_id = int(request.POST.get('vm_template_id')) configuration_id = int(request.POST.get('configuration')) - template = OpenNebulaManager().get_template(template_id) + try: + 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 configuration = HostingPlan.objects.get(id=configuration_id) diff --git a/hosting/views.py b/hosting/views.py index c2470707..c145acbe 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1557,6 +1557,11 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView): serializer = VirtualMachineSerializer(queryset, many=True) return serializer.data except ConnectionRefusedError: + logger.exception( + "Could not load VMs of user %s (%s) from OpenNebula" % ( + owner.username, owner.email + ) + ) messages.error(self.request, 'We could not load your VMs due to a backend connection \ error. Please try again in a few minutes' diff --git a/opennebula_api/exceptions.py b/opennebula_api/exceptions.py index e9d3c0f7..0af23c85 100644 --- a/opennebula_api/exceptions.py +++ b/opennebula_api/exceptions.py @@ -8,3 +8,13 @@ class UserExistsError(Exception): class UserCredentialError(Exception): 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 diff --git a/opennebula_api/models.py b/opennebula_api/models.py index 2f76f423..a9dbc92e 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -9,7 +9,10 @@ from oca.pool import WrongNameError, WrongIdError from hosting.models import HostingOrder from utils.models import CustomUser from utils.tasks import save_ssh_key, save_ssh_key_error_handler -from .exceptions import KeyExistsError, UserExistsError, UserCredentialError +from .exceptions import ( + KeyExistsError, UserExistsError, UserCredentialError, + TemplateNotFoundError +) logger = logging.getLogger(__name__) @@ -425,16 +428,60 @@ class OpenNebulaManager(): except: return [] - def get_template(self, template_id): + @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): + """ + 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: 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) - except Exception as ex: - logger.debug("Template Id we are looking for : %s" % template_id) - logger.error(str(ex)) + except WrongIdError: + logger.error( + "OpenNebula template %s does not exist" % template_id + ) + raise TemplateNotFoundError(template_id) + except ConnectionRefusedError: + raise + except Exception: + logger.exception( + "Could not fetch OpenNebula template %s" % template_id + ) raise ConnectionRefusedError def create_template(self, name, cores, memory, disk_size, core_price, diff --git a/opennebula_api/serializers.py b/opennebula_api/serializers.py index 34cdde7c..20befbc7 100644 --- a/opennebula_api/serializers.py +++ b/opennebula_api/serializers.py @@ -1,12 +1,39 @@ import ipaddress +import logging from builtins import hasattr from rest_framework import serializers from oca import OpenNebulaException +from .exceptions import TemplateNotFoundError 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): + """ + VMs are named '