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:
PCoder 2026-10-19 20:53:32 +05:30
commit cb16114aba
6 changed files with 268 additions and 23 deletions

View file

@ -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,