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.
257 lines
8.4 KiB
Python
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
|