1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2025-07-31 12:41:08 +00:00

Rest framework (#24)

* Add rest framework for API: First Commit

* modify some shell scripts to make variable references safer; modify some python scripts to reduce the code complexity and cyclomatic complexity of functions.

* Add REST API for some webvirtcloud functions. Instance list/delete/create, compute list/delete/create, storages-network list/retrieve. Add swagger and redoc for API interface

* update requirements

Co-authored-by: herengui <herengui@uniontech.com>
This commit is contained in:
catborise 2022-08-22 15:12:33 +03:00 committed by GitHub
parent 92254401dc
commit cfce71ec2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1170 additions and 348 deletions

View file

@ -0,0 +1,100 @@
from rest_framework import serializers
from instances.models import Flavor, Instance, MigrateInstance, CreateInstance
class InstanceSerializer(serializers.ModelSerializer):
class Meta:
model = Instance
fields = ['id', 'compute', 'name', 'uuid', 'is_template', 'created', 'drbd']
class InstanceDetailsSerializer(serializers.ModelSerializer):
class Meta:
model = Instance
fields = [
'id',
'compute',
'status',
'uuid',
'name',
'title',
'description',
'is_template',
'created',
'drbd',
'arch',
'machine',
'vcpu',
'memory',
'firmware',
'nvram',
'bootmenu',
'boot_order',
'disks',
'media',
'media_iso',
'snapshots',
'networks',
'console_type',
'console_port',
'console_keymap',
'console_listener_address',
'video_model',
'guest_agent_ready',
'autostart']
class FlavorSerializer(serializers.ModelSerializer):
class Meta:
model = Flavor
fields = ['label', 'memory', 'vcpu', 'disk']
class CreateInstanceSerializer(serializers.ModelSerializer):
firmware_choices = (
('', 'BIOS'),
#('UEFI', 'UEFI'),
)
firmware = serializers.ChoiceField(choices = firmware_choices)
graphics = serializers.CharField(initial='vnc')
video = serializers.CharField(initial='vga')
storage = serializers.CharField(initial='default')
cache_mode = serializers.CharField(initial='none')
virtio = serializers.BooleanField(initial=True)
qemu_ga = serializers.BooleanField(initial=True)
class Meta:
model = CreateInstance
fields = [
'name',
'firmware',
'vcpu',
'vcpu_mode',
'memory',
'networks',
'mac',
'nwfilter',
'storage',
'hdd_size',
'cache_mode',
'meta_prealloc',
'virtio',
'qemu_ga',
'console_pass',
'graphics',
'video',
'listener_addr'
]
class MigrateSerializer(serializers.ModelSerializer):
instance = Instance.objects.all().prefetch_related("userinstance_set")
live = serializers.BooleanField(initial=True)
xml_del = serializers.BooleanField(initial=True)
class Meta:
model = MigrateInstance
fields = ['instance', 'target_compute', 'live', 'xml_del', 'offline', 'autoconverge', 'compress', 'postcopy', 'unsafe']

224
instances/api/viewsets.py Normal file
View file

@ -0,0 +1,224 @@
from django.shortcuts import get_object_or_404
from appsettings.settings import app_settings
from computes.models import Compute
from computes import utils
from instances.models import Flavor, Instance
from instances.views import get_instance
from instances.utils import migrate_instance
from instances.views import poweron, powercycle, poweroff, force_off, suspend, resume, destroy as instance_destroy
from rest_framework import status, viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from vrtManager import util
from vrtManager.create import wvmCreate
from .serializers import FlavorSerializer, InstanceSerializer, InstanceDetailsSerializer, MigrateSerializer, CreateInstanceSerializer
class InstancesViewSet(viewsets.ViewSet):
"""
A simple ViewSet for listing or retrieving ALL/Compute Instances.
"""
permission_classes = [permissions.IsAuthenticated]
def list(self, request):
if request.user.is_superuser or request.user.has_perm("instances.view_instances"):
queryset = Instance.objects.all().prefetch_related("userinstance_set")
else:
queryset = Instance.objects.filter(userinstance__user=request.user).prefetch_related("userinstance_set")
serializer = InstanceSerializer(queryset, many=True, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, pk=None, compute_pk=None):
queryset = get_instance(request.user, pk)
serializer = InstanceSerializer(queryset, context={'request': request})
return Response(serializer.data)
class InstanceViewSet(viewsets.ViewSet):
"""
A simple ViewSet for listing or retrieving Compute Instances.
"""
#serializer_class = CreateInstanceSerializer
permission_classes = [permissions.IsAuthenticated]
def list(self, request, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
utils.refresh_instance_database(compute)
queryset = Instance.objects.filter(compute=compute).prefetch_related("userinstance_set")
serializer = InstanceSerializer(queryset, many=True, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, pk=None, compute_pk=None):
queryset = get_instance(request.user, pk)
serializer = InstanceDetailsSerializer(queryset, context={'request': request})
return Response(serializer.data)
def destroy(self, request, pk=None, compute_pk=None):
instance_destroy(request, pk)
return Response({'status': 'Instance is destroyed'})
@action(detail=True, methods=['post'])
def poweron(self, request, pk=None):
poweron(request, pk)
return Response({'status': 'poweron command send'})
@action(detail=True, methods=['post'])
def poweroff(self, request, pk=None):
poweroff(request, pk)
return Response({'status': 'poweroff command send'})
@action(detail=True, methods=['post'])
def powercycle(self, request, pk=None):
powercycle(request, pk)
return Response({'status': 'powercycle command send'})
@action(detail=True, methods=['post'])
def forceoff(self, request, pk=None):
force_off(request, pk)
return Response({'status': 'force off command send'})
@action(detail=True, methods=['post'])
def suspend(self, request, pk=None):
suspend(request, pk)
return Response({'status': 'suspend command send'})
@action(detail=True, methods=['post'])
def resume(self, request, pk=None):
resume(request, pk)
return Response({'status': 'resume command send'})
class MigrateViewSet(viewsets.ViewSet):
"""
A viewset for migrating instances.
"""
serializer_class = MigrateSerializer
queryset = ""
def create(self, request):
serializer = MigrateSerializer(data=request.data)
if serializer.is_valid():
instance = serializer.validated_data['instance']
target_host = serializer.validated_data['target_compute']
live = serializer.validated_data['live']
unsafe = serializer.validated_data['unsafe']
xml_del = serializer.validated_data['xml_del']
offline = serializer.validated_data['offline']
autoconverge = serializer.validated_data['autoconverge']
postcopy = serializer.validated_data['postcopy']
compress = serializer.validated_data['compress']
migrate_instance(target_host, instance, request.user, live, unsafe, xml_del, offline, autoconverge, compress, postcopy)
return Response({'status': 'instance migrate is started'})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class FlavorViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows flavor to be viewed.
"""
queryset = Flavor.objects.all().order_by('id')
serializer_class = FlavorSerializer
permission_classes = [permissions.IsAuthenticated]
class CreateInstanceViewSet(viewsets.ViewSet):
"""
A viewset for creating instances.
"""
serializer_class = CreateInstanceSerializer
queryset = ""
def create(self, request, compute_pk=None, arch=None, machine=None):
serializer = CreateInstanceSerializer(data=request.data,
context = {'compute_pk': compute_pk,
'arch': arch,
'machine': machine
})
if serializer.is_valid():
volume_list = []
default_bus = app_settings.INSTANCE_VOLUME_DEFAULT_BUS
default_io = app_settings.INSTANCE_VOLUME_DEFAULT_IO
default_discard = app_settings.INSTANCE_VOLUME_DEFAULT_DISCARD
default_zeroes = app_settings.INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES
default_scsi_disk_model = app_settings.INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER
default_disk_format = app_settings.INSTANCE_VOLUME_DEFAULT_FORMAT
default_disk_owner_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
default_disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
compute = Compute.objects.get(pk=compute_pk)
conn = wvmCreate(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
path = conn.create_volume(
serializer.validated_data['storage'],
serializer.validated_data['name'],
serializer.validated_data['hdd_size'],
default_disk_format,
serializer.validated_data['meta_prealloc'],
default_disk_owner_uid,
default_disk_owner_gid,
)
volume = {}
firmware = {}
volume["device"] = "disk"
volume["path"] = path
volume["type"] = conn.get_volume_format_type(path)
volume["cache_mode"] = serializer.validated_data["cache_mode"]
volume["bus"] = default_bus
if volume["bus"] == "scsi":
volume["scsi_model"] = default_scsi_disk_model
volume["discard_mode"] = default_discard
volume["detect_zeroes_mode"] = default_zeroes
volume["io_mode"] = default_io
volume_list.append(volume)
if "UEFI" in serializer.validated_data['firmware']:
firmware["loader"] = serializer.validated_data['firmware'].split(":")[1].strip()
firmware["secure"] = "no"
firmware["readonly"] = "yes"
firmware["type"] = "pflash"
if "secboot" in firmware["loader"] and machine != "q35":
machine = "q35"
firmware["secure"] = "yes"
ret = conn.create_instance(
serializer.validated_data['name'],
serializer.validated_data['memory'],
serializer.validated_data['vcpu'],
serializer.validated_data['vcpu_mode'],
util.randomUUID(),
arch,
machine,
firmware,
volume_list,
serializer.validated_data['networks'],
serializer.validated_data['nwfilter'],
serializer.validated_data['graphics'],
serializer.validated_data['virtio'],
serializer.validated_data['listener_addr'],
serializer.validated_data['video'],
serializer.validated_data['console_pass'],
serializer.validated_data['mac'],
serializer.validated_data['qemu_ga'],
)
msg = f"Instance {serializer.validated_data['name']} is created"
return Response({'status': msg })
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View file

@ -4,9 +4,9 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from appsettings.models import AppSettings
from webvirtcloud.settings import QEMU_CONSOLE_LISTEN_ADDRESSES, QEMU_KEYMAPS
from webvirtcloud.settings import QEMU_CONSOLE_LISTENER_ADDRESSES, QEMU_KEYMAPS
from .models import Flavor
from .models import CreateInstance, Flavor
class FlavorForm(forms.ModelForm):
@ -29,32 +29,36 @@ class ConsoleForm(forms.Form):
type_choices = ((c, c) for c in AppSettings.objects.get(key="QEMU_CONSOLE_DEFAULT_TYPE").choices_as_list())
keymap_choices = [('auto', 'Auto')] + list((c, c) for c in QEMU_KEYMAPS)
self.fields['type'] = forms.ChoiceField(choices=type_choices)
self.fields['listen_on'] = forms.ChoiceField(choices=QEMU_CONSOLE_LISTEN_ADDRESSES)
self.fields['listen_on'] = forms.ChoiceField(choices=QEMU_CONSOLE_LISTENER_ADDRESSES)
self.fields['keymap'] = forms.ChoiceField(choices=keymap_choices)
class NewVMForm(forms.Form):
name = forms.CharField(error_messages={'required': _('No Virtual Machine name has been entered')}, max_length=64)
firmware = forms.CharField(max_length=50, required=False)
vcpu = forms.IntegerField(error_messages={'required': _('No VCPU has been entered')})
vcpu_mode = forms.CharField(max_length=20, required=False)
disk = forms.IntegerField(required=False)
memory = forms.IntegerField(error_messages={'required': _('No RAM size has been entered')})
networks = forms.CharField(error_messages={'required': _('No Network pool has been choosen')})
nwfilter = forms.CharField(required=False)
storage = forms.CharField(max_length=20, required=False)
template = forms.CharField(required=False)
images = forms.CharField(required=False)
cache_mode = forms.CharField(error_messages={'required': _('Please select HDD cache mode')})
hdd_size = forms.IntegerField(required=False)
meta_prealloc = forms.BooleanField(required=False)
virtio = forms.BooleanField(required=False)
qemu_ga = forms.BooleanField(required=False)
mac = forms.CharField(required=False)
console_pass = forms.CharField(required=False, empty_value="", widget=forms.PasswordInput())
graphics = forms.CharField(error_messages={'required': _('Please select a graphics type')})
video = forms.CharField(error_messages={'required': _('Please select a video driver')})
listener_addr = forms.ChoiceField(required=True, widget=forms.RadioSelect, choices=QEMU_CONSOLE_LISTEN_ADDRESSES)
class NewVMForm(forms.ModelForm):
# name = forms.CharField(error_messages={'required': _('No Virtual Machine name has been entered')}, max_length=64)
# firmware = forms.CharField(max_length=50, required=False)
# vcpu = forms.IntegerField(error_messages={'required': _('No VCPU has been entered')})
# vcpu_mode = forms.CharField(max_length=20, required=False)
# disk = forms.IntegerField(required=False)
# memory = forms.IntegerField(error_messages={'required': _('No RAM size has been entered')})
# networks = forms.CharField(error_messages={'required': _('No Network pool has been choosen')})
# nwfilter = forms.CharField(required=False)
# storage = forms.CharField(max_length=20, required=False)
# template = forms.CharField(required=False)
# images = forms.CharField(required=False)
# cache_mode = forms.CharField(error_messages={'required': _('Please select HDD cache mode')})
# hdd_size = forms.IntegerField(required=False)
# meta_prealloc = forms.BooleanField(required=False)
# virtio = forms.BooleanField(required=False)
# qemu_ga = forms.BooleanField(required=False)
# mac = forms.CharField(required=False)
# console_pass = forms.CharField(required=False, empty_value="", widget=forms.PasswordInput())
# graphics = forms.CharField(error_messages={'required': _('Please select a graphics type')})
# video = forms.CharField(error_messages={'required': _('Please select a video driver')})
# listener_addr = forms.ChoiceField(required=True, widget=forms.RadioSelect, choices=QEMU_CONSOLE_LISTENER_ADDRESSES)
class Meta:
model = CreateInstance
fields = '__all__'
exclude = ['compute']
def clean_name(self):
name = self.cleaned_data['name']

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.14 on 2022-07-22 08:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('instances', '0009_auto_20200717_0524'),
]
operations = [
migrations.AlterModelOptions(
name='permissionset',
options={'default_permissions': (), 'managed': False, 'permissions': [('clone_instances', 'Can clone instances'), ('passwordless_console', 'Can access console without password'), ('view_instances', 'Can view instances'), ('snapshot_instances', 'Can snapshot instances')]},
),
migrations.AddField(
model_name='instance',
name='drbd',
field=models.CharField(default='None', max_length=24, verbose_name='drbd'),
),
]

View file

@ -1,7 +1,10 @@
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from libvirt import VIR_DOMAIN_XML_SECURE
from vrtManager.create import wvmCreate
from webvirtcloud.settings import QEMU_CONSOLE_LISTENER_ADDRESSES
from computes.models import Compute
from vrtManager.instance import wvmInstance
@ -150,8 +153,8 @@ class Instance(models.Model):
return self.proxy.get_console_keymap()
@cached_property
def console_listen_address(self):
return self.proxy.get_console_listen_addr()
def console_listener_address(self):
return self.proxy.get_console_listener_addr()
@cached_property
def guest_agent(self):
@ -206,6 +209,50 @@ class Instance(models.Model):
return self.proxy.get_image_formats()
class MigrateInstance(models.Model):
instance = models.ForeignKey(Instance, on_delete=models.DO_NOTHING)
target_compute = models.ForeignKey(Compute, related_name='target', on_delete=models.DO_NOTHING)
live = models.BooleanField(_('Live'), blank=False)
xml_del = models.BooleanField(_('Undefine XML'), blank=False, default=True)
offline = models.BooleanField(_('Offline'), blank=False)
autoconverge = models.BooleanField(_('Auto Converge'), blank=False, default=True)
compress = models.BooleanField(_('Compress'), blank=False, default=False)
postcopy = models.BooleanField(_('Post Copy'), blank=False, default=False)
unsafe = models.BooleanField(_('Unsafe'), blank=False, default=False)
class Meta:
managed = False
class CreateInstance(models.Model):
compute = models.ForeignKey(Compute, related_name='host', on_delete=models.DO_NOTHING)
name = models.CharField(max_length=64, error_messages={'required': _('No Virtual Machine name has been entered')})
firmware = models.CharField(max_length=50)
vcpu = models.IntegerField(error_messages={'required': _('No VCPU has been entered')})
vcpu_mode = models.CharField(max_length=20, blank=True)
disk = models.IntegerField(blank=True)
memory = models.IntegerField(error_messages={'required': _('No RAM size has been entered')})
networks = models.CharField(max_length=256, error_messages={'required': _('No Network pool has been choosen')})
nwfilter = models.CharField(max_length=256, blank=True)
storage = models.CharField(max_length=256, blank=True)
template = models.CharField(max_length=256, blank=True)
images = models.CharField(max_length=256, blank=True)
cache_mode = models.CharField(max_length=12, error_messages={'required': _('Please select HDD cache mode')})
hdd_size = models.IntegerField(blank=True)
meta_prealloc = models.BooleanField(default=False, blank=True)
virtio = models.BooleanField(default=True)
qemu_ga = models.BooleanField(default=False)
mac = models.CharField(max_length=17, blank=True)
console_pass = models.CharField(max_length=64, blank=True)
graphics = models.CharField(max_length=12, error_messages={'required': _('Please select a graphics type')})
video = models.CharField(max_length=12, error_messages={'required': _('Please select a video driver')})
listener_addr = models.CharField(max_length=20, choices=QEMU_CONSOLE_LISTENER_ADDRESSES)
class Meta:
managed = False
class PermissionSet(models.Model):
"""
Dummy model for holding set of permissions we need to be automatically added by Django

View file

@ -30,9 +30,9 @@
<td>
<span class="text-success">{% trans "Connected" %}</span>
</td>
{% if app_settings.VM_DRBD_STATUS == 'True' %}
<td class="d-none d-sm-table-cell"></td>
{% endif %}
{% if app_settings.VM_DRBD_STATUS == 'True' %}
<td class="d-none d-sm-table-cell"></td>
{% endif %}
<td class="d-none d-sm-table-cell text-center">{{ compute.cpu_count }}</td>
<td class="d-none d-sm-table-cell text-right">{{ compute.ram_size|filesizeformat }}</td>
<td>
@ -68,11 +68,11 @@
<span class="text-warning">{% trans "Suspended" %}</span>
{% endif %}
</td>
{% if app_settings.VM_DRBD_STATUS == 'True' %}
<td>
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="text-success">{% else %}<span class="text-danger">{% endif %}{{ instance.drbd }}</span>
</td>
{% endif %}
{% if app_settings.VM_DRBD_STATUS == 'True' %}
<td>
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="text-success">{% else %}<span class="text-danger">{% endif %}{{ instance.drbd }}</span>
</td>
{% endif %}
<td>{{ instance.proxy.instance.info.3 }}</td>
<td>{{ instance.cur_memory }} MB</td>
<td class="text-nowrap">

View file

@ -15,7 +15,7 @@
{% for field in form %}
{% for error in field.errors %}
<div class="alert alert-danger">
<strong>{{ error|escape }}</strong>
<strong>{{ field.label }}:</strong> <span>{{ error|escape }}</span>
</div>
{% endfor %}
{% endfor %}

View file

@ -19,7 +19,7 @@
{% for field in form %}
{% for error in field.errors %}
<div class="alert alert-danger">
<strong>{{ error|escape }}</strong>
<strong>{{ field.label }}:</strong> <span>{{ error|escape }}</span>
</div>
{% endfor %}
{% endfor %}

View file

@ -269,9 +269,9 @@
});
$(document).ready(function () {
// set current console listen address or fall back to default
var console_listen_address = "{{ console_listen_address }}";
if (console_listen_address != '') {
$("#console_select_listen_address option[value='" + console_listen_address + "']").prop('selected', true);
var console_listener_address = "{{ console_listener_address }}";
if (console_listener_address != '') {
$("#console_select_listener_address option[value='" + console_listener_address + "']").prop('selected', true);
}
});
$(document).ready(function () {

View file

@ -67,12 +67,12 @@ def instance(request, pk):
console_form = ConsoleForm(
initial={
"type": instance.console_type,
"listen_on": instance.console_listen_address,
"listen_on": instance.console_listener_address,
"password": instance.console_passwd,
"keymap": instance.console_keymap,
}
)
console_listen_addresses = settings.QEMU_CONSOLE_LISTEN_ADDRESSES
console_listener_addresses = settings.QEMU_CONSOLE_LISTENER_ADDRESSES
bottom_bar = app_settings.VIEW_INSTANCE_DETAIL_BOTTOM_BAR
allow_admin_or_not_template = request.user.is_superuser or request.user.is_staff or not instance.is_template
try:
@ -344,7 +344,7 @@ def destroy(request, pk):
except Exception:
userinstance = UserInstance(is_delete=request.user.is_superuser)
if request.method == "POST" and userinstance.is_delete:
if request.method in ["POST", "DELETE"] and userinstance.is_delete:
if instance.proxy.get_status() == 1:
instance.proxy.force_shutdown()
@ -390,7 +390,7 @@ def migrate(request, pk):
target_host = Compute.objects.get(id=compute_id)
try:
utils.migrate_instance(target_host, instance, request.user, live, unsafe, xml_del, offline)
utils.migrate_instance(target_host, instance, request.user, live, unsafe, xml_del, offline, autoconverge, compress, postcopy)
except libvirtError as err:
messages.error(request, err)
@ -1239,7 +1239,7 @@ def update_console(request, pk):
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
if "listen_on" in form.changed_data:
instance.proxy.set_console_listen_addr(form.cleaned_data["listen_on"])
instance.proxy.set_console_listener_addr(form.cleaned_data["listen_on"])
msg = _("Set VNC listen address")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
@ -1385,7 +1385,7 @@ def create_instance(request, compute_id, arch, machine):
default_disk_owner_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
default_disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
default_scsi_disk_model = app_settings.INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER
listener_addr = settings.QEMU_CONSOLE_LISTEN_ADDRESSES
listener_addr = settings.QEMU_CONSOLE_LISTENER_ADDRESSES
mac_auto = util.randomMAC()
disk_devices = conn.get_disk_device_types(arch, machine)
disk_buses = conn.get_disk_bus_types(arch, machine)
@ -1545,7 +1545,7 @@ def create_instance(request, compute_id, arch, machine):
volumes=volume_list,
networks=data["networks"],
virtio=data["virtio"],
listen_addr=data["listener_addr"],
listener_addr=data["listener_addr"],
nwfilter=data["nwfilter"],
graphics=data["graphics"],
video=data["video"],
@ -1568,6 +1568,7 @@ def create_instance(request, compute_id, arch, machine):
conn.close()
except libvirtError as lib_err:
messages.error(request, lib_err)
return render(request, "create_instance_w2.html", locals())