1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-25 07:35:22 +00:00

Merge pull request #527 from catborise/master

Rest framework (#24)
This commit is contained in:
catborise 2022-08-22 15:35:19 +03:00 committed by GitHub
commit a67a51eaed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1014 additions and 88 deletions

View file

@ -432,6 +432,17 @@ Now when you login with an LDAP user it will be assigned the rights defined. The
If you'd like to move a user from ldap to WebVirtCloud, just change its password from the UI and (eventually) remove from the group in ldap
## REST API / BETA
Webvirtcloud provides a REST API for programmatic access.
To access API methods open your browser and check them with Swagger interface
```bash
http://<webvirtloud-address:port>/swagger
```
```bash
http://<webvirtloud-address:port>/redoc
```
## Screenshots
Instance Detail:

View file

@ -0,0 +1,27 @@
from rest_framework import serializers
from computes.models import Compute
from vrtManager.connection import (
CONN_SOCKET,
CONN_SSH,
CONN_TCP,
CONN_TLS,
)
class ComputeSerializer(serializers.ModelSerializer):
# Use <input type="password"> for the input.
password = serializers.CharField(style={'input_type': 'password'})
# Use a radio input instead of a select input.
conn_types = (
(CONN_SSH, 'SSH'),
(CONN_TCP, 'TCP'),
(CONN_TLS, 'TLS'),
(CONN_SOCKET, 'SOCK'),
)
type = serializers.ChoiceField(choices=conn_types)
class Meta:
model = Compute
fields = ['id', 'name', 'hostname', 'login', 'password', 'type', 'details']

58
computes/api/viewsets.py Normal file
View file

@ -0,0 +1,58 @@
from computes.models import Compute
from rest_framework import viewsets
from rest_framework import permissions
from vrtManager.create import wvmCreate
from .serializers import ComputeSerializer
from rest_framework.response import Response
class ComputeViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows computes to be viewed or edited.
"""
queryset = Compute.objects.all().order_by('name')
serializer_class = ComputeSerializer
permission_classes = [permissions.IsAuthenticated]
class ComputeArchitecturesView(viewsets.ViewSet):
def list(self, request, compute_pk=None):
"""
Return a list of supported host architectures.
"""
compute = Compute.objects.get(pk=compute_pk)
conn = wvmCreate(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
return Response(conn.get_hypervisors_machines())
def retrieve(self, request, compute_pk=None, pk=None):
compute = Compute.objects.get(pk=compute_pk)
conn = wvmCreate(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
return Response(conn.get_machine_types(pk))
class ComputeMachinesView(viewsets.ViewSet):
def list(self, request, compute_pk=None, archs_pk=None):
"""
Return a list of supported host architectures.
"""
compute = Compute.objects.get(pk=compute_pk)
conn = wvmCreate(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
return Response(conn.get_machine_types(archs_pk))

View file

@ -1,7 +1,6 @@
from virtsecrets.views import secrets
from django.urls import include, path
# from instances.views import create_instance, create_instance_select_type
from interfaces.views import interface, interfaces
from networks.views import network, networks
from nwfilters.views import nwfilter, nwfilters
@ -34,8 +33,6 @@ urlpatterns = [
path('nwfilters/', nwfilters, name='nwfilters'),
path('nwfilter/<str:nwfltr>/', nwfilter, name='nwfilter'),
path('virtsecrets/', secrets, name='virtsecrets'),
# path('create/', create_instance_select_type, name='create_instance_select_type'),
# path('create/archs/<str:arch>/machines/<str:machine>/', create_instance, name='create_instance'),
path('archs/<str:arch>/machines/', views.get_compute_machine_types, name='machines'),
path(
'archs/<str:arch>/machines/<str:machine>/disks/<str:disk>/buses/',

View file

@ -1,17 +1,22 @@
Django==3.2.14
django_bootstrap5==21.2
django-icons==21.1
Django==3.2.15
django_bootstrap5==22.1
django-icons==22.1
django-login-required-middleware==0.8
django-otp==1.1.3
django-qr-code==2.3.0
gunicorn==20.1.0
libsass==0.21.0
libvirt-python==8.5.0
libvirt-python==8.6.0
lxml==4.9.1
qrcode==7.3.1
rwlock==0.0.7
websockify==0.10.0
zipp==3.6.0
ldap3==2.9.1
python-socketio==5.7.0
eventlet==0.33.1
python-engineio==4.3.4
python-socketio==5.7.1
eventlet==0.33.1
djangorestframework==3.13.1
drf-nested-routers==0.93.4
drf-yasg==1.21.3
markdown==3.4.1

View file

@ -112,7 +112,7 @@ def get_connection_infos(token):
connport = 22
connuser = instance.compute.login
conntype = instance.compute.type
console_host = conn.get_console_listen_addr()
console_host = conn.get_console_listener_addr()
console_port = conn.get_console_port()
console_socket = conn.get_console_socket()
except Exception as e:

View file

@ -1,7 +1,7 @@
-r ../conf/requirements.txt
coverage==6.2
django-debug-toolbar==3.2.4
pycodestyle==2.8.0
pyflakes==2.4.0
pylint==2.13.9
coverage==6.4.4
django-debug-toolbar==3.6.0
pycodestyle==2.9.1
pyflakes==2.5.0
pylint==2.14.5
yapf==0.32.0

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())

View file

@ -0,0 +1,11 @@
from rest_framework import serializers
from interfaces.models import Interfaces
class InterfacesSerializer(serializers.ModelSerializer):
class Meta:
model = Interfaces
fields = ['name', 'type', 'state', 'mac']

View file

@ -0,0 +1,32 @@
from django.shortcuts import get_object_or_404
from computes.models import Compute
from rest_framework import status, viewsets
from vrtManager.interface import wvmInterfaces, wvmInterface
from .serializers import InterfacesSerializer
from rest_framework.response import Response
class InterfaceViewSet(viewsets.ViewSet):
"""
A viewset for listing retrieving interfaces.
"""
def list(self, request, compute_pk=None):
queryset = []
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmInterfaces(compute.hostname, compute.login, compute.password, compute.type)
ifaces = conn.get_ifaces()
for iface in ifaces:
interf = wvmInterface(compute.hostname, compute.login, compute.password, compute.type, iface)
queryset.append(interf.get_details())
serializer = InterfacesSerializer(queryset, many=True, context={'request': request})
return Response(serializer.data)

View file

@ -1,3 +1,13 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
# Create your models here.
class Interfaces(models.Model):
name = models.CharField(_('name'), max_length=20, error_messages={'required': _('No interface name has been entered')})
type = models.CharField(_('status'), max_length=12)
state = models.CharField(_('device'), max_length=100)
mac = models.CharField(_('forward'), max_length=24)
class Meta:
managed = False

View file

@ -0,0 +1,19 @@
from rest_framework import serializers
from networks.models import Networks
class NetworksSerializer(serializers.ModelSerializer):
class Meta:
model = Networks
fields = ['name', 'status', 'device', 'forward']
# class VolumeSerializer(serializers.ModelSerializer):
# allocation = serializers.ReadOnlyField()
# meta_prealloc = serializers.BooleanField(write_only=True)
# class Meta:
# model = Volume
# fields = ['name', 'type', 'allocation', 'size', 'meta_prealloc']

26
networks/api/viewsets.py Normal file
View file

@ -0,0 +1,26 @@
from django.shortcuts import get_object_or_404
from computes.models import Compute
from rest_framework import status, viewsets
from vrtManager.network import wvmNetworks
from .serializers import NetworksSerializer
from rest_framework.response import Response
class NetworkViewSet(viewsets.ViewSet):
"""
A viewset for listing retrieving networks.
"""
def list(self, request, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmNetworks(compute.hostname, compute.login, compute.password, compute.type)
queryset = conn.get_networks_info()
serializer = NetworksSerializer(queryset, many=True, context={'request': request})
return Response(serializer.data)

View file

@ -1,3 +1,13 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
# Create your models here.
class Networks(models.Model):
name = models.CharField(_('name'), max_length=20, error_messages={'required': _('No network name has been entered')})
status = models.CharField(_('status'), max_length=12)
device = models.CharField(_('device'), max_length=100)
forward = models.CharField(_('forward'), max_length=24)
class Meta:
managed = False

View file

@ -0,0 +1,25 @@
from rest_framework import serializers
from storages.models import Storages, Storage, Volume
class StoragesSerializer(serializers.ModelSerializer):
class Meta:
model = Storages
fields = ['name', 'status', 'type', 'size', 'volumes']
class StorageSerializer(serializers.ModelSerializer):
volumes = serializers.ReadOnlyField()
class Meta:
model = Storage
fields = ['state', 'size', 'free', 'status', 'path', 'type', 'autostart', 'volumes']
class VolumeSerializer(serializers.ModelSerializer):
allocation = serializers.ReadOnlyField()
meta_prealloc = serializers.BooleanField(write_only=True)
class Meta:
model = Volume
fields = ['name', 'type', 'allocation', 'size', 'meta_prealloc']

162
storages/api/viewsets.py Normal file
View file

@ -0,0 +1,162 @@
from django.shortcuts import get_object_or_404
from computes.models import Compute
from rest_framework import status, viewsets
from rest_framework.decorators import action
from appsettings.settings import app_settings
from vrtManager.storage import wvmStorages, wvmStorage
from .serializers import StoragesSerializer, StorageSerializer, VolumeSerializer
from rest_framework.response import Response
class StorageViewSet(viewsets.ViewSet):
"""
A viewset for listing retrieving storages.
"""
def list(self, request, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorages(compute.hostname, compute.login, compute.password, compute.type)
queryset = conn.get_storages_info()
serializer = StoragesSerializer(queryset, many=True, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, pk=None, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pk)
infoset = {
"state": conn.is_active(),
"size": conn.get_size()[0],
"free": conn.get_size()[1],
"status": conn.get_status(),
"path": conn.get_target_path(),
"type": conn.get_type(),
"autostart": conn.get_autostart(),
"volumes": conn.update_volumes()
}
serializer = StorageSerializer(infoset, many=False, context={'request': request})
return Response(serializer.data)
@action(detail=True, methods=['post'])
def start(self, request, pk=None, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pk)
ret = conn.start()
conn.close()
return Response({'status': 'Pool start command send: ' + str(ret)})
@action(detail=True, methods=['post'])
def stop(self, request, pk=None, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pk)
ret = conn.stop()
conn.close()
return Response({'status': 'Pool stop command send: ' + str(ret)})
@action(detail=True, methods=['post'])
def refresh(self, request, pk=None, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pk)
ret = conn.refresh()
conn.close()
return Response({'status': 'Pool refresh command send: ' + str(ret)})
@action(detail=True, methods=['post'])
def XML_description(self, request, pk=None, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pk)
ret = conn._XMLDesc(0)
conn.close()
return Response({'return': str(ret)})
class VolumeViewSet(viewsets.ViewSet):
"""
A simple ViewSet for listing or retrieving Storage Volumes.
"""
serializer_class = VolumeSerializer
lookup_value_regex = "[^/]+"
def list(self, request, storage_pk=None, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, storage_pk)
state = conn.is_active()
if state:
conn.refresh()
volume_queryset = conn.update_volumes()
else:
volume_queryset = None
conn.close()
serializer = VolumeSerializer(volume_queryset, many=True, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, storage_pk=None, compute_pk=None, pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, storage_pk)
state = conn.is_active()
if state:
volume_queryset = conn.get_volume_details(pk)
else:
volume_queryset = None
conn.close()
serializer = VolumeSerializer(volume_queryset, many=False, context={'request': request})
return Response(serializer.data)
def create(self, request, storage_pk=None, compute_pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, storage_pk)
serializer = VolumeSerializer(data=request.data)
if serializer.is_valid():
state = conn.is_active()
if state:
conn.refresh()
ret = conn.create_volume(
serializer.validated_data['name'],
serializer.validated_data['size'],
serializer.validated_data['type'],
serializer.validated_data['meta_prealloc'],
int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID),
int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID),
)
conn.close()
return Response({'status': 'Volume: ' + ret + ' is created'})
else:
return Response({'status': 'Pool is not active'})
else:
return Response({'status': 'Data is not right for create volume'})
def destroy(self, request, storage_pk=None, compute_pk=None, pk=None):
compute = get_object_or_404(Compute, pk=compute_pk)
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, storage_pk)
if conn.is_active():
conn.del_volume(pk)
conn.close()
return Response({'status': 'Volume: ' + pk + ' is deleted'})
else:
return Response({'status': 'Pool is not active'})

View file

@ -1,3 +1,47 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
# Create your models here.
class Storages(models.Model):
name = models.CharField(_('name'), max_length=20, error_messages={'required': _('No pool name has been entered')})
status = models.IntegerField(_('status'))
type = models.CharField(_('type'), max_length=100)
size = models.IntegerField(_('size'))
volumes = models.IntegerField(_('volumes'))
def __str__(self):
return f'{self.name}'
class Meta:
managed = False
class Volume(models.Model):
name = models.CharField(_('name'), max_length=128)
type = models.CharField(_('format'), max_length=12, choices=(('qcow2', 'qcow2 (recommended)'), ('qcow', 'qcow'), ('raw', 'raw')))
allocation = models.IntegerField(_('allocation'))
size = models.IntegerField(_('size'))
def __str__(self):
return f'{self.name}'
class Meta:
managed = False
verbose_name_plural = "Volumes"
class Storage(models.Model):
state = models.IntegerField(_('state'))
size = models.IntegerField(_('size'))
free = models.IntegerField(_('free'))
status = models.CharField(_('status'), max_length=128)
path = models.CharField(_('path'), max_length=128)
type = models.CharField(_('type'), max_length=128)
autostart = models.BooleanField(_('autostart'))
volumes = models.ForeignKey(Volume, on_delete=models.DO_NOTHING)
def __str__(self):
return f'{self.path}'
class Meta:
managed = False

View file

@ -45,7 +45,7 @@ class wvmCreate(wvmConnect):
return util.get_xml_path(self.get_cap_xml(), "/capabilities/guest/os_type")
def get_host_arch(self):
"""Get guest capabilities"""
"""Get host architecture"""
return util.get_xml_path(self.get_cap_xml(), "/capabilities/host/cpu/arch")
def create_volume(self, storage, name, size, image_format, metadata=False, disk_owner_uid=0, disk_owner_gid=0):
@ -160,7 +160,7 @@ class wvmCreate(wvmConnect):
nwfilter,
graphics,
virtio,
listen_addr,
listener_addr,
video="vga",
console_pass="random",
mac=None,
@ -332,7 +332,7 @@ class wvmCreate(wvmConnect):
xml += """<input type='tablet'/>"""
xml += f"""
<graphics type='{graphics}' port='-1' autoport='yes' {console_pass} listen='{listen_addr}'/>
<graphics type='{graphics}' port='-1' autoport='yes' {console_pass} listen='{listener_addr}'/>
<console type='pty'/> """
if qemu_ga and virtio:
@ -345,4 +345,4 @@ class wvmCreate(wvmConnect):
</video>
</devices>
</domain>"""
self._defineXML(xml)
return self._defineXML(xml)

View file

@ -130,12 +130,12 @@ class wvmInstances(wvmConnect):
def graphics_listen(self, name):
inst = self.get_instance(name)
listen_addr = util.get_xml_path(inst.XMLDesc(0), "/domain/devices/graphics/@listen")
if listen_addr is None:
listen_addr = util.get_xml_path(inst.XMLDesc(0), "/domain/devices/graphics/listen/@address")
if listen_addr is None:
listener_addr = util.get_xml_path(inst.XMLDesc(0), "/domain/devices/graphics/@listen")
if listener_addr is None:
listener_addr = util.get_xml_path(inst.XMLDesc(0), "/domain/devices/graphics/listen/@address")
if listener_addr is None:
return "None"
return listen_addr
return listener_addr
def graphics_port(self, name):
inst = self.get_instance(name)
@ -253,6 +253,9 @@ class wvmInstance(wvmConnect):
else:
return self.get_vcpu()
def get_vcpu_mode(self):
return util.get_xml_path(self._XMLDesc(0), "/domain/cpu/@current")
def get_arch(self):
return util.get_xml_path(self._XMLDesc(0), "/domain/os/type/@arch")
@ -979,15 +982,15 @@ class wvmInstance(wvmConnect):
telnet_port = service_port
return telnet_port
def get_console_listen_addr(self):
listen_addr = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/@listen")
if listen_addr is None:
listen_addr = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/listen/@address")
if listen_addr is None:
def get_console_listener_addr(self):
listener_addr = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/@listen")
if listener_addr is None:
listener_addr = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/listen/@address")
if listener_addr is None:
return "127.0.0.1"
return listen_addr
return listener_addr
def set_console_listen_addr(self, listen_addr):
def set_console_listener_addr(self, listener_addr):
xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE)
root = ElementTree.fromstring(xml)
console_type = self.get_console_type()
@ -1001,9 +1004,9 @@ class wvmInstance(wvmConnect):
listen = graphic.find("listen[@type='address']")
if listen is None:
return False
if listen_addr:
graphic.set("listen", listen_addr)
listen.set("address", listen_addr)
if listener_addr:
graphic.set("listen", listener_addr)
listen.set("address", listener_addr)
else:
try:
graphic.attrib.pop("listen")

View file

@ -40,7 +40,12 @@ class wvmNetworks(wvmConnect):
net_bridge = util.get_xml_path(net.XMLDesc(0), "/network/forward/interface/@dev")
net_forward = util.get_xml_path(net.XMLDesc(0), "/network/forward/@mode")
networks.append({"name": network, "status": net_status, "device": net_bridge, "forward": net_forward})
networks.append({
"name": network,
"status": net_status,
"device": net_bridge,
"forward": net_forward
})
return networks

View file

@ -124,10 +124,10 @@ class wvmStorage(wvmConnect):
return self.pool.UUIDString()
def start(self):
self.pool.create(0)
return self.pool.create(0)
def stop(self):
self.pool.destroy()
return self.pool.destroy()
def delete(self):
self.pool.undefine()
@ -224,7 +224,27 @@ class wvmStorage(wvmConnect):
return util.get_xml_path(vol_xml, "/volume/@type")
def refresh(self):
self.pool.refresh(0)
return self.pool.refresh(0)
def get_volume_details(self, volname):
with contextlib.suppress(Exception):
self.refresh()
vols = self.get_volumes()
return [{"name": volname,
"size": self.get_volume_size(volname),
"allocation": self.get_volume_allocation(volname),
"type": self.get_volume_format_type(volname)} for volname in vols]
def get_volume_details(self, volname):
with contextlib.suppress(Exception):
self.refresh()
return {
"name": volname,
"size": self.get_volume_size(volname),
"allocation": self.get_volume_allocation(volname),
"type": self.get_volume_format_type(volname),
}
def update_volumes(self):
with contextlib.suppress(Exception):

View file

@ -22,10 +22,12 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"django_bootstrap5",
"django_icons",
"django_otp",
"django_otp.plugins.otp_totp",
"drf_yasg",
"accounts",
"admin",
"appsettings",
@ -40,6 +42,7 @@ INSTALLED_APPS = [
"virtsecrets",
"logs",
"qr_code",
"rest_framework",
]
MIDDLEWARE = [
@ -211,7 +214,7 @@ SOCKETIO_PUBLIC_PORT = 6081
SOCKETIO_PUBLIC_PATH = "socket.io/"
# List of console listen addresses
QEMU_CONSOLE_LISTEN_ADDRESSES = (
QEMU_CONSOLE_LISTENER_ADDRESSES = (
("127.0.0.1", "Localhost"),
("0.0.0.0", "All interfaces"),
)

37
webvirtcloud/urls-api.py Normal file
View file

@ -0,0 +1,37 @@
from django.urls import include, path
from rest_framework_nested import routers
from computes.api.viewsets import ComputeArchitecturesView, ComputeViewSet
from networks.api.viewsets import NetworkViewSet
from interfaces.api.viewsets import InterfaceViewSet
from storages.api.viewsets import StorageViewSet, VolumeViewSet
from instances.api.viewsets import FlavorViewSet, \
InstancesViewSet, \
InstanceViewSet, \
MigrateViewSet, \
CreateInstanceViewSet
router = routers.SimpleRouter()
router.register(r'computes', ComputeViewSet)
router.register(r'migrate', MigrateViewSet, basename='instance-migrate')
router.register(r'flavor', FlavorViewSet, basename='instance-flavor')
router.register(r'instances', InstancesViewSet, basename='instance')
compute_router = routers.NestedSimpleRouter(router, r'computes', lookup='compute')
compute_router.register(r'instances', InstanceViewSet, basename='compute-instance')
compute_router.register(r'instances/create/(?P<arch>[^/.]+)/(?P<machine>[^/.]+)', CreateInstanceViewSet, basename='instance-create')
compute_router.register(r'networks', NetworkViewSet, basename='compute-network')
compute_router.register(r'interfaces', InterfaceViewSet, basename='compute-interface')
compute_router.register(r'storages', StorageViewSet, basename='compute-storage')
compute_router.register(r'archs', ComputeArchitecturesView, basename='compute-archs')
storage_router = routers.NestedSimpleRouter(compute_router, r'storages', lookup='storage')
storage_router.register(r'volumes', VolumeViewSet, basename='compute-storage-volumes')
urlpatterns = [
path('', include(router.urls)),
path('', include(compute_router.urls)),
path('', include(storage_router.urls)),
]

View file

@ -1,9 +1,26 @@
from django.conf import settings
from django.urls import include, path, re_path
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from appsettings.views import appsettings
from console.views import console
from django.conf import settings
from django.urls import include, path
from instances.views import index
schema_view = get_schema_view(
openapi.Info(
title="Webvirtcloud REST-API",
default_version='v1',
description="Webvirtcloud REST API",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="catborise@gmail.com"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path("", index, name="index"),
path("admin/", include(("admin.urls", "admin"), namespace="admin")),
@ -15,6 +32,11 @@ urlpatterns = [
path("instances/", include("instances.urls")),
path("i18n/", include("django.conf.urls.i18n")),
path("logs/", include("logs.urls")),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('api/v1/', include("webvirtcloud.urls-api")),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]
if settings.DEBUG:
@ -26,3 +48,4 @@ if settings.DEBUG:
]
except ImportError:
pass