dynamicweb/opennebula_api/serializers.py
PCoder f30e70c20c Only derive the template name from VM names dynamicweb itself produced
On production 143 of 233 active VMs reference a deleted template, and
many of those were created or renamed outside dynamicweb ("gitea",
"ipv6only-Alpine 3.15-27397-Jonas"). Blindly cutting at the last "-"
mangled such names, so only strip a trailing "-<vm id>" suffix, which is
exactly what OpenNebulaManager.create_vm appends.
2026-10-20 17:01:40 +05:30

262 lines
8.6 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, 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):
"""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, 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):
"""
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