dynamicweb/opennebula_api/serializers.py
PCoder cb16114aba 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.
2026-10-19 20:53:32 +05:30

257 lines
8.4 KiB
Python

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 '<template name>-<vm id>' at creation time (see
OpenNebulaManager.create_vm), so the template name can be recovered
from the VM name when the template itself has been deleted.
"""
return vm_name.rsplit('-', 1)[0]
class VirtualMachineTemplateSerializer(serializers.Serializer):
"""Serializer to map the virtual machine template instance into JSON format."""
id = serializers.IntegerField(read_only=True)
name = serializers.SerializerMethodField()
cores = serializers.SerializerMethodField()
disk_size = serializers.SerializerMethodField()
memory = serializers.SerializerMethodField()
def get_cores(self, obj):
if hasattr(obj.template, 'vcpu'):
return obj.template.vcpu
return ''
def get_disk_size(self, obj):
template = obj.template
disk_size = 0
try:
for disk in template.disks:
disk_size += int(disk.size)
return disk_size / 1024
except:
return 0
def get_memory(self, obj):
return int(obj.template.memory) / 1024
def get_name(self, obj):
return strip_template_prefix(obj.name)
class VirtualMachineSerializer(serializers.Serializer):
"""Serializer to map the virtual machine instance into JSON format."""
name = serializers.SerializerMethodField()
cores = serializers.IntegerField(source='template.vcpu')
disk = serializers.IntegerField(write_only=True)
set_memory = serializers.IntegerField(write_only=True, label='Memory')
memory = serializers.SerializerMethodField()
disk_size = serializers.SerializerMethodField()
hdd_size = serializers.SerializerMethodField()
ssd_size = serializers.SerializerMethodField()
ipv4 = serializers.SerializerMethodField()
ipv6 = serializers.SerializerMethodField()
vm_id = serializers.IntegerField(read_only=True, source='id')
state = serializers.CharField(read_only=True, source='str_state')
price = serializers.SerializerMethodField()
ssh_key = serializers.CharField(write_only=True)
configuration = serializers.SerializerMethodField()
template_id = serializers.ChoiceField(
choices=[(key.id, key.name) for key in
OpenNebulaManager().try_get_templates()
],
source='template.template_id',
write_only=True,
default=[]
)
def create(self, validated_data):
owner = validated_data['owner']
ssh_key = validated_data['ssh_key']
cores = validated_data['template']['vcpu']
memory = validated_data['set_memory']
disk = validated_data['disk']
template_id = validated_data['template']['template_id']
specs = {
'cpu': cores,
'disk_size': disk,
'memory': memory,
}
try:
manager = OpenNebulaManager(email=owner.username,
password=owner.password,
)
opennebula_id = manager.create_vm(template_id=template_id,
ssh_key=ssh_key,
specs=specs)
except OpenNebulaException as err:
raise serializers.ValidationError(
"OpenNebulaException occured. {0}".format(err)
)
return manager.get_vm(opennebula_id)
def get_memory(self, obj):
return int(obj.template.memory) / 1024
def get_disk_size(self, obj):
template = obj.template
disk_size = 0
for disk in template.disks:
disk_size += int(disk.size)
return disk_size / 1024
def get_ssd_size(self, obj):
template = obj.template
disk_size = 0
for disk in template.disks:
if disk.datastore == 'cephds':
disk_size += int(disk.size)
return disk_size / 1024
def get_hdd_size(self, obj):
template = obj.template
disk_size = 0
for disk in template.disks:
if disk.datastore == 'ceph_hdd_ds':
disk_size += int(disk.size)
return disk_size / 1024
def get_price(self, obj):
template = obj.template
price = float(template.vcpu) * 5.0
price += (int(template.memory) / 1024 * 2.0)
for disk in template.disks:
price += int(disk.size) / 1024 * 0.6
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):
"""
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
try:
name = self._template_name(template_id)
except TemplateNotFoundError as err:
logger.warning(
"VM %s: %s, falling back to VM name" % (obj.id, err)
)
name = template_name_from_vm_name(obj.name)
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)
return strip_template_prefix(name)
def get_ipv4(self, obj):
"""
Get the IPv4s from the given VM
:param obj: The VM in contention
:return: Returns csv string of all IPv4s added to this VM otherwise returns "-" if no IPv4 is available
"""
ipv4 = []
for nic in obj.template.nics:
if hasattr(nic, 'ip'):
ipv4.append(nic.ip)
if len(ipv4) > 0:
return ', '.join(ipv4)
else:
return '-'
def get_ipv6(self, obj):
ipv6 = []
for nic in obj.template.nics:
if hasattr(nic, 'ip6_global'):
ipv6.append(nic.ip6_global)
if len(ipv6) > 0:
return ', '.join(ipv6)
else:
return '-'
def get_name(self, obj):
return strip_template_prefix(obj.name)
class VMTemplateSerializer(serializers.Serializer):
"""Serializer to map the VMTemplate instance into JSON format."""
id = serializers.IntegerField(
read_only=True, source='opennebula_vm_template_id'
)
name = serializers.CharField(read_only=True)
def hexstr2int(string):
return int(string.replace(':', ''), 16)
FIRST_MAC = hexstr2int('02:00:b3:39:79:4d')
FIRST_V4 = ipaddress.ip_address('185.203.112.2')
COUNT = 1000
def v4_from_mac(mac):
"""Calculates the IPv4 address from a MAC address.
mac: string (the colon-separated representation)
returns: ipaddress.ip_address object with the v4 address
"""
return FIRST_V4 + (hexstr2int(mac) - FIRST_MAC)
def is_in_v4_range(mac):
return FIRST_MAC <= hexstr2int(mac) < FIRST_MAC + 1000