1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-24 23:25:24 +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

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

View file

@ -1,17 +1,22 @@
Django==3.2.14 Django==3.2.15
django_bootstrap5==21.2 django_bootstrap5==22.1
django-icons==21.1 django-icons==22.1
django-login-required-middleware==0.8 django-login-required-middleware==0.8
django-otp==1.1.3 django-otp==1.1.3
django-qr-code==2.3.0 django-qr-code==2.3.0
gunicorn==20.1.0 gunicorn==20.1.0
libsass==0.21.0 libsass==0.21.0
libvirt-python==8.5.0 libvirt-python==8.6.0
lxml==4.9.1 lxml==4.9.1
qrcode==7.3.1 qrcode==7.3.1
rwlock==0.0.7 rwlock==0.0.7
websockify==0.10.0 websockify==0.10.0
zipp==3.6.0 zipp==3.6.0
ldap3==2.9.1 ldap3==2.9.1
python-socketio==5.7.0 python-engineio==4.3.4
eventlet==0.33.1 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 connport = 22
connuser = instance.compute.login connuser = instance.compute.login
conntype = instance.compute.type 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_port = conn.get_console_port()
console_socket = conn.get_console_socket() console_socket = conn.get_console_socket()
except Exception as e: except Exception as e:

View file

@ -45,7 +45,7 @@ echowarn() {
# DESCRIPTION: Echo debug information to stdout. # DESCRIPTION: Echo debug information to stdout.
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
echodebug() { echodebug() {
if [ $_ECHO_DEBUG -eq $BS_TRUE ]; then if [ "${_ECHO_DEBUG}" -eq "${BS_TRUE}" ]; then
printf "${BC} * DEBUG${EC}: %s\n" "$@"; printf "${BC} * DEBUG${EC}: %s\n" "$@";
fi fi
} }
@ -154,8 +154,7 @@ __gather_linux_system_info() {
DISTRO_VERSION="" DISTRO_VERSION=""
# Let's test if the lsb_release binary is available # Let's test if the lsb_release binary is available
rv=$(lsb_release >/dev/null 2>&1) if lsb_release >/dev/null 2>&1; then
if [ $? -eq 0 ]; then
DISTRO_NAME=$(lsb_release -si) DISTRO_NAME=$(lsb_release -si)
if [ "x$(echo "$DISTRO_NAME" | grep RedHat)" != "x" ]; then if [ "x$(echo "$DISTRO_NAME" | grep RedHat)" != "x" ]; then
# Let's convert CamelCase to Camel Case # Let's convert CamelCase to Camel Case
@ -208,7 +207,7 @@ __gather_linux_system_info() {
;; ;;
arch ) n="Arch Linux" ;; arch ) n="Arch Linux" ;;
centos ) n="CentOS" ;; centos ) n="CentOS" ;;
almalinux ) n="AlmaLinux" ;; almalinux ) n="AlmaLinux" ;;
debian ) n="Debian" ;; debian ) n="Debian" ;;
ubuntu ) n="Ubuntu" ;; ubuntu ) n="Ubuntu" ;;
fedora ) n="Fedora" ;; fedora ) n="Fedora" ;;
@ -248,7 +247,7 @@ __gather_linux_system_info() {
;; ;;
esac esac
;; ;;
* ) n="${n}" ; * ) ;;
esac esac
DISTRO_NAME=$n DISTRO_NAME=$n
DISTRO_VERSION=$v DISTRO_VERSION=$v
@ -779,8 +778,7 @@ if [ "$INSTALL_FUNC" = "null" ]; then
exit 1 exit 1
else else
echoinfo "Running ${INSTALL_FUNC}()" echoinfo "Running ${INSTALL_FUNC}()"
$INSTALL_FUNC if ! $INSTALL_FUNC; then
if [ $? -ne 0 ]; then
echoerror "Failed to run ${INSTALL_FUNC}()!!!" echoerror "Failed to run ${INSTALL_FUNC}()!!!"
exit 1 exit 1
fi fi
@ -803,8 +801,7 @@ if [ "$POST_INSTALL_FUNC" = "null" ]; then
exit 1 exit 1
else else
echoinfo "Running ${POST_INSTALL_FUNC}()" echoinfo "Running ${POST_INSTALL_FUNC}()"
$POST_INSTALL_FUNC if ! $POST_INSTALL_FUNC; then
if [ $? -ne 0 ]; then
echoerror "Failed to run ${POST_INSTALL_FUNC}()!!!" echoerror "Failed to run ${POST_INSTALL_FUNC}()!!!"
exit 1 exit 1
fi fi
@ -827,8 +824,7 @@ if [ "$DAEMONS_RUNNING_FUNC" = "null" ]; then
exit 1 exit 1
else else
echoinfo "Running ${DAEMONS_RUNNING_FUNC}()" echoinfo "Running ${DAEMONS_RUNNING_FUNC}()"
$DAEMONS_RUNNING_FUNC if ! $DAEMONS_RUNNING_FUNC; then
if [ $? -ne 0 ]; then
echoerror "Failed to run ${DAEMONS_RUNNING_FUNC}()!!!" echoerror "Failed to run ${DAEMONS_RUNNING_FUNC}()!!!"
exit 1 exit 1
fi fi

View file

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

View file

@ -75,10 +75,7 @@ backlog = 2048
def get_workers(): def get_workers():
procs = os.sysconf('SC_NPROCESSORS_ONLN') procs = os.sysconf('SC_NPROCESSORS_ONLN')
if procs > 0: return procs * 2 + 1 if procs > 0 else 3
return procs * 2 + 1
else:
return 3
workers = get_workers() workers = get_workers()

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 django.utils.translation import gettext_lazy as _
from appsettings.models import AppSettings 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): 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()) 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) keymap_choices = [('auto', 'Auto')] + list((c, c) for c in QEMU_KEYMAPS)
self.fields['type'] = forms.ChoiceField(choices=type_choices) 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) self.fields['keymap'] = forms.ChoiceField(choices=keymap_choices)
class NewVMForm(forms.Form): class NewVMForm(forms.ModelForm):
name = forms.CharField(error_messages={'required': _('No Virtual Machine name has been entered')}, max_length=64) # name = forms.CharField(error_messages={'required': _('No Virtual Machine name has been entered')}, max_length=64)
firmware = forms.CharField(max_length=50, required=False) # firmware = forms.CharField(max_length=50, required=False)
vcpu = forms.IntegerField(error_messages={'required': _('No VCPU has been entered')}) # vcpu = forms.IntegerField(error_messages={'required': _('No VCPU has been entered')})
vcpu_mode = forms.CharField(max_length=20, required=False) # vcpu_mode = forms.CharField(max_length=20, required=False)
disk = forms.IntegerField(required=False) # disk = forms.IntegerField(required=False)
memory = forms.IntegerField(error_messages={'required': _('No RAM size has been entered')}) # memory = forms.IntegerField(error_messages={'required': _('No RAM size has been entered')})
networks = forms.CharField(error_messages={'required': _('No Network pool has been choosen')}) # networks = forms.CharField(error_messages={'required': _('No Network pool has been choosen')})
nwfilter = forms.CharField(required=False) # nwfilter = forms.CharField(required=False)
storage = forms.CharField(max_length=20, required=False) # storage = forms.CharField(max_length=20, required=False)
template = forms.CharField(required=False) # template = forms.CharField(required=False)
images = forms.CharField(required=False) # images = forms.CharField(required=False)
cache_mode = forms.CharField(error_messages={'required': _('Please select HDD cache mode')}) # cache_mode = forms.CharField(error_messages={'required': _('Please select HDD cache mode')})
hdd_size = forms.IntegerField(required=False) # hdd_size = forms.IntegerField(required=False)
meta_prealloc = forms.BooleanField(required=False) # meta_prealloc = forms.BooleanField(required=False)
virtio = forms.BooleanField(required=False) # virtio = forms.BooleanField(required=False)
qemu_ga = forms.BooleanField(required=False) # qemu_ga = forms.BooleanField(required=False)
mac = forms.CharField(required=False) # mac = forms.CharField(required=False)
console_pass = forms.CharField(required=False, empty_value="", widget=forms.PasswordInput()) # console_pass = forms.CharField(required=False, empty_value="", widget=forms.PasswordInput())
graphics = forms.CharField(error_messages={'required': _('Please select a graphics type')}) # graphics = forms.CharField(error_messages={'required': _('Please select a graphics type')})
video = forms.CharField(error_messages={'required': _('Please select a video driver')}) # 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) # 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): def clean_name(self):
name = self.cleaned_data['name'] 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.db import models
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from libvirt import VIR_DOMAIN_XML_SECURE 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 computes.models import Compute
from vrtManager.instance import wvmInstance from vrtManager.instance import wvmInstance
@ -150,8 +153,8 @@ class Instance(models.Model):
return self.proxy.get_console_keymap() return self.proxy.get_console_keymap()
@cached_property @cached_property
def console_listen_address(self): def console_listener_address(self):
return self.proxy.get_console_listen_addr() return self.proxy.get_console_listener_addr()
@cached_property @cached_property
def guest_agent(self): def guest_agent(self):
@ -206,6 +209,50 @@ class Instance(models.Model):
return self.proxy.get_image_formats() 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): class PermissionSet(models.Model):
""" """
Dummy model for holding set of permissions we need to be automatically added by Django Dummy model for holding set of permissions we need to be automatically added by Django

View file

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

View file

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

View file

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

View file

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

View file

@ -67,12 +67,12 @@ def instance(request, pk):
console_form = ConsoleForm( console_form = ConsoleForm(
initial={ initial={
"type": instance.console_type, "type": instance.console_type,
"listen_on": instance.console_listen_address, "listen_on": instance.console_listener_address,
"password": instance.console_passwd, "password": instance.console_passwd,
"keymap": instance.console_keymap, "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 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 allow_admin_or_not_template = request.user.is_superuser or request.user.is_staff or not instance.is_template
try: try:
@ -344,7 +344,7 @@ def destroy(request, pk):
except Exception: except Exception:
userinstance = UserInstance(is_delete=request.user.is_superuser) 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: if instance.proxy.get_status() == 1:
instance.proxy.force_shutdown() instance.proxy.force_shutdown()
@ -390,7 +390,7 @@ def migrate(request, pk):
target_host = Compute.objects.get(id=compute_id) target_host = Compute.objects.get(id=compute_id)
try: 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: except libvirtError as err:
messages.error(request, err) messages.error(request, err)
@ -1239,7 +1239,7 @@ def update_console(request, pk):
addlogmsg(request.user.username, instance.compute.name, instance.name, msg) addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
if "listen_on" in form.changed_data: 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") msg = _("Set VNC listen address")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg) 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_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
default_disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID) default_disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
default_scsi_disk_model = app_settings.INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER 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() mac_auto = util.randomMAC()
disk_devices = conn.get_disk_device_types(arch, machine) disk_devices = conn.get_disk_device_types(arch, machine)
disk_buses = conn.get_disk_bus_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, volumes=volume_list,
networks=data["networks"], networks=data["networks"],
virtio=data["virtio"], virtio=data["virtio"],
listen_addr=data["listener_addr"], listener_addr=data["listener_addr"],
nwfilter=data["nwfilter"], nwfilter=data["nwfilter"],
graphics=data["graphics"], graphics=data["graphics"],
video=data["video"], video=data["video"],
@ -1568,6 +1568,7 @@ def create_instance(request, compute_id, arch, machine):
conn.close() conn.close()
except libvirtError as lib_err: except libvirtError as lib_err:
messages.error(request, lib_err) messages.error(request, lib_err)
return render(request, "create_instance_w2.html", locals()) 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.db import models
from django.utils.translation import gettext_lazy as _
# Create your models here. # 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.db import models
from django.utils.translation import gettext_lazy as _
# Create your models here. # 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.db import models
from django.utils.translation import gettext_lazy as _
# Create your models here. # 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

@ -181,10 +181,7 @@ class IPint(object):
if isinstance(data, INT_TYPES): if isinstance(data, INT_TYPES):
self.ip = int(data) self.ip = int(data)
if ipversion == 0: if ipversion == 0:
if self.ip <= MAX_IPV4_ADDRESS: ipversion = 4 if self.ip <= MAX_IPV4_ADDRESS else 6
ipversion = 4
else:
ipversion = 6
if ipversion == 4: if ipversion == 4:
if self.ip > MAX_IPV4_ADDRESS: if self.ip > MAX_IPV4_ADDRESS:
raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, self.ip)) raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, self.ip))

View file

@ -1,4 +1,5 @@
import string import string
import contextlib
from vrtManager import util from vrtManager import util
from vrtManager.connection import wvmConnect from vrtManager.connection import wvmConnect
@ -30,19 +31,13 @@ class wvmCreate(wvmConnect):
""" """
Function return all images on all storages Function return all images on all storages
""" """
images = list() images = []
storages = self.get_storages(only_actives=True) storages = self.get_storages(only_actives=True)
for storage in storages: for storage in storages:
stg = self.get_storage(storage) stg = self.get_storage(storage)
try: with contextlib.suppress(Exception):
stg.refresh(0) stg.refresh(0)
except Exception: images.extend(img for img in stg.listVolumes() if not img.lower().endswith(".iso"))
pass
for img in stg.listVolumes():
if img.lower().endswith(".iso"):
pass
else:
images.append(img)
return images return images
def get_os_type(self): def get_os_type(self):
@ -50,7 +45,7 @@ class wvmCreate(wvmConnect):
return util.get_xml_path(self.get_cap_xml(), "/capabilities/guest/os_type") return util.get_xml_path(self.get_cap_xml(), "/capabilities/guest/os_type")
def get_host_arch(self): def get_host_arch(self):
"""Get guest capabilities""" """Get host architecture"""
return util.get_xml_path(self.get_cap_xml(), "/capabilities/host/cpu/arch") 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): def create_volume(self, storage, name, size, image_format, metadata=False, disk_owner_uid=0, disk_owner_gid=0):
@ -58,10 +53,7 @@ class wvmCreate(wvmConnect):
stg = self.get_storage(storage) stg = self.get_storage(storage)
storage_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type") storage_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type")
if storage_type == "dir": if storage_type == "dir":
if image_format in ("qcow", "qcow2"): name += f".{image_format}" if image_format in ("qcow", "qcow2") else ".img"
name += "." + image_format
else:
name += ".img"
alloc = 0 alloc = 0
else: else:
image_format = 'raw' image_format = 'raw'
@ -87,28 +79,19 @@ class wvmCreate(wvmConnect):
</target> </target>
</volume>""" </volume>"""
stg.createXML(xml, metadata) stg.createXML(xml, metadata)
try:
with contextlib.suppress(Exception):
stg.refresh(0) stg.refresh(0)
except:
pass
vol = stg.storageVolLookupByName(name) vol = stg.storageVolLookupByName(name)
return vol.path() return vol.path()
def get_volume_format_type(self, path): def get_volume_format_type(self, path):
vol = self.get_volume_by_path(path) vol = self.get_volume_by_path(path)
vol_type = util.get_xml_path(vol.XMLDesc(0), "/volume/target/format/@type") vol_type = util.get_xml_path(vol.XMLDesc(0), "/volume/target/format/@type")
if vol_type == "unknown" or vol_type == "iso": return "raw" if vol_type in ["unknown", "iso"] else vol_type or "raw"
return "raw"
if vol_type:
return vol_type
else:
return "raw"
def get_volume_path(self, volume, pool=None): def get_volume_path(self, volume, pool=None):
if not pool: storages = [pool] if pool else self.get_storages(only_actives=True)
storages = self.get_storages(only_actives=True)
else:
storages = [pool]
for storage in storages: for storage in storages:
stg = self.get_storage(storage) stg = self.get_storage(storage)
if stg.info()[0] != 0: if stg.info()[0] != 0:
@ -124,10 +107,7 @@ class wvmCreate(wvmConnect):
def clone_from_template(self, clone, template, storage=None, metadata=False, disk_owner_uid=0, disk_owner_gid=0): def clone_from_template(self, clone, template, storage=None, metadata=False, disk_owner_uid=0, disk_owner_gid=0):
vol = self.get_volume_by_path(template) vol = self.get_volume_by_path(template)
if not storage: stg = self.get_storage(storage) if storage else vol.storagePoolLookupByVolume()
stg = vol.storagePoolLookupByVolume()
else:
stg = self.get_storage(storage)
storage_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type") storage_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type")
format = util.get_xml_path(vol.XMLDesc(0), "/volume/target/format/@type") format = util.get_xml_path(vol.XMLDesc(0), "/volume/target/format/@type")
@ -180,7 +160,7 @@ class wvmCreate(wvmConnect):
nwfilter, nwfilter,
graphics, graphics,
virtio, virtio,
listen_addr, listener_addr,
video="vga", video="vga",
console_pass="random", console_pass="random",
mac=None, mac=None,
@ -226,12 +206,8 @@ class wvmCreate(wvmConnect):
if caps["features"]: if caps["features"]:
xml += """<features>""" xml += """<features>"""
if "acpi" in caps["features"]: for feat in [x for x in ("acpi", "apic", "pae",) if x in caps["features"]]:
xml += """<acpi/>""" xml += f"""<{feat}/>"""
if "apic" in caps["features"]:
xml += """<apic/>"""
if "pae" in caps["features"]:
xml += """<pae/>"""
if firmware.get("secure", "no") == "yes": if firmware.get("secure", "no") == "yes":
xml += """<smm state="on"/>""" xml += """<smm state="on"/>"""
xml += """</features>""" xml += """</features>"""
@ -240,9 +216,7 @@ class wvmCreate(wvmConnect):
xml += """<cpu mode='host-model'/>""" xml += """<cpu mode='host-model'/>"""
elif vcpu_mode == "host-passthrough": elif vcpu_mode == "host-passthrough":
xml += """<cpu mode='host-passthrough'/>""" xml += """<cpu mode='host-passthrough'/>"""
elif vcpu_mode == "": elif vcpu_mode != "":
pass
else:
xml += f"""<cpu mode='custom' match='exact' check='none'> xml += f"""<cpu mode='custom' match='exact' check='none'>
<model fallback='allow'>{vcpu_mode}</model>""" <model fallback='allow'>{vcpu_mode}</model>"""
xml += """</cpu>""" xml += """</cpu>"""
@ -306,7 +280,7 @@ class wvmCreate(wvmConnect):
xml += """<target dev='hd%s' bus='%s'/>""" % (hd_disk_letters.pop(0), volume.get("bus")) xml += """<target dev='hd%s' bus='%s'/>""" % (hd_disk_letters.pop(0), volume.get("bus"))
elif volume.get("bus") == "fdc": elif volume.get("bus") == "fdc":
xml += """<target dev='fd%s' bus='%s'/>""" % (fd_disk_letters.pop(0), volume.get("bus")) xml += """<target dev='fd%s' bus='%s'/>""" % (fd_disk_letters.pop(0), volume.get("bus"))
elif volume.get("bus") == "sata" or volume.get("bus") == "scsi": elif volume.get("bus") in ["sata", "scsi"]:
xml += """<target dev='sd%s' bus='%s'/>""" % (sd_disk_letters.pop(0), volume.get("bus")) xml += """<target dev='sd%s' bus='%s'/>""" % (sd_disk_letters.pop(0), volume.get("bus"))
else: else:
xml += """<target dev='sd%s'/>""" % sd_disk_letters.pop(0) xml += """<target dev='sd%s'/>""" % sd_disk_letters.pop(0)
@ -345,21 +319,20 @@ class wvmCreate(wvmConnect):
if console_pass == "random": if console_pass == "random":
console_pass = "passwd='" + util.randomPasswd() + "'" console_pass = "passwd='" + util.randomPasswd() + "'"
else: elif console_pass != "":
if not console_pass == "": console_pass = "passwd='" + console_pass + "'"
console_pass = "passwd='" + console_pass + "'"
if "usb" in dom_caps["disk_bus"]: if "usb" in dom_caps["disk_bus"]:
xml += """<input type='mouse' bus='{}'/>""".format("virtio" if virtio else "usb") xml += f"""<input type='mouse' bus='{"virtio" if virtio else "usb"}'/>"""
xml += """<input type='keyboard' bus='{}'/>""".format("virtio" if virtio else "usb") xml += f"""<input type='keyboard' bus='{"virtio" if virtio else "usb"}'/>"""
xml += """<input type='tablet' bus='{}'/>""".format("virtio" if virtio else "usb") xml += f"""<input type='tablet' bus='{"virtio" if virtio else "usb"}'/>"""
else: else:
xml += """<input type='mouse'/>""" xml += """<input type='mouse'/>"""
xml += """<input type='keyboard'/>""" xml += """<input type='keyboard'/>"""
xml += """<input type='tablet'/>""" xml += """<input type='tablet'/>"""
xml += f""" 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'/> """ <console type='pty'/> """
if qemu_ga and virtio: if qemu_ga and virtio:
@ -372,4 +345,4 @@ class wvmCreate(wvmConnect):
</video> </video>
</devices> </devices>
</domain>""" </domain>"""
self._defineXML(xml) return self._defineXML(xml)

View file

@ -21,12 +21,11 @@ class wvmHostDetails(wvmConnect):
freemem = self.wvm.getMemoryStats(-1, 0) freemem = self.wvm.getMemoryStats(-1, 0)
if isinstance(freemem, dict): if isinstance(freemem, dict):
free = (freemem["buffers"] + freemem["free"] + freemem["cached"]) * 1024 free = (freemem["buffers"] + freemem["free"] + freemem["cached"]) * 1024
percent = abs(100 - ((free * 100) // all_mem)) percent = abs(100 - free * 100 // all_mem)
usage = all_mem - free usage = all_mem - free
mem_usage = {"total": all_mem, "usage": usage, "percent": percent} return {"total": all_mem, "usage": usage, "percent": percent}
else: else:
mem_usage = {"total": None, "usage": None, "percent": None} return {"total": None, "usage": None, "percent": None}
return mem_usage
def get_cpu_usage(self): def get_cpu_usage(self):
""" """
@ -35,30 +34,30 @@ class wvmHostDetails(wvmConnect):
prev_idle = 0 prev_idle = 0
prev_total = 0 prev_total = 0
cpu = self.wvm.getCPUStats(-1, 0) cpu = self.wvm.getCPUStats(-1, 0)
if isinstance(cpu, dict): if not isinstance(cpu, dict):
for num in range(2):
idle = self.wvm.getCPUStats(-1, 0)["idle"]
total = sum(self.wvm.getCPUStats(-1, 0).values())
diff_idle = idle - prev_idle
diff_total = total - prev_total
diff_usage = (1000 * (diff_total - diff_idle) / diff_total + 5) / 10
prev_total = total
prev_idle = idle
if num == 0:
time.sleep(1)
else:
if diff_usage < 0:
diff_usage = 0
else:
return {"usage": None} return {"usage": None}
for num in range(2):
idle = self.wvm.getCPUStats(-1, 0)["idle"]
total = sum(self.wvm.getCPUStats(-1, 0).values())
diff_idle = idle - prev_idle
diff_total = total - prev_total
diff_usage = (1000 * (diff_total - diff_idle) /
diff_total + 5) / 10
prev_total = total
prev_idle = idle
if num == 0:
time.sleep(1)
else:
diff_usage = max(diff_usage, 0)
return {"usage": diff_usage} return {"usage": diff_usage}
def get_node_info(self): def get_node_info(self):
""" """
Function return host server information: hostname, cpu, memory, ... Function return host server information: hostname, cpu, memory, ...
""" """
info = list() info = [self.wvm.getHostname()] # hostname
info.append(self.wvm.getHostname()) # hostname
info.append(self.wvm.getInfo()[0]) # architecture info.append(self.wvm.getInfo()[0]) # architecture
info.append(self.wvm.getInfo()[1] * 1048576) # memory info.append(self.wvm.getInfo()[1] * 1048576) # memory
info.append(self.wvm.getInfo()[2]) # cpu core count info.append(self.wvm.getInfo()[2]) # cpu core count

View file

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

View file

@ -57,7 +57,7 @@ class wvmInterface(wvmConnect):
try: try:
xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE) xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE)
return util.get_xml_path(xml, "/interface/start/@mode") return util.get_xml_path(xml, "/interface/start/@mode")
except: except Exception:
return None return None
def is_active(self): def is_active(self):
@ -65,10 +65,7 @@ class wvmInterface(wvmConnect):
def get_mac(self): def get_mac(self):
mac = self.iface.MACString() mac = self.iface.MACString()
if mac: return mac or None
return mac
else:
return None
def get_type(self): def get_type(self):
xml = self._XMLDesc() xml = self._XMLDesc()
@ -78,11 +75,8 @@ class wvmInterface(wvmConnect):
try: try:
xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE) xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE)
ipaddr = util.get_xml_path(xml, "/interface/protocol[@family='ipv4']/ip/@address") ipaddr = util.get_xml_path(xml, "/interface/protocol[@family='ipv4']/ip/@address")
if ipaddr: return "static" if ipaddr else "dhcp"
return "static" except Exception:
else:
return "dhcp"
except:
return None return None
def get_ipv4(self): def get_ipv4(self):
@ -92,17 +86,14 @@ class wvmInterface(wvmConnect):
if not int_ipv4_ip or not int_ipv4_mask: if not int_ipv4_ip or not int_ipv4_mask:
return None return None
else: else:
return int_ipv4_ip + "/" + int_ipv4_mask return f"{int_ipv4_ip}/{int_ipv4_mask}"
def get_ipv6_type(self): def get_ipv6_type(self):
try: try:
xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE) xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE)
ipaddr = util.get_xml_path(xml, "/interface/protocol[@family='ipv6']/ip/@address") ipaddr = util.get_xml_path(xml, "/interface/protocol[@family='ipv6']/ip/@address")
if ipaddr: return "static" if ipaddr else "dhcp"
return "static" except Exception:
else:
return "dhcp"
except:
return None return None
def get_ipv6(self): def get_ipv6(self):
@ -112,40 +103,39 @@ class wvmInterface(wvmConnect):
if not int_ipv6_ip or not int_ipv6_mask: if not int_ipv6_ip or not int_ipv6_mask:
return None return None
else: else:
return int_ipv6_ip + "/" + int_ipv6_mask return f"{int_ipv6_ip}/{int_ipv6_mask}"
def get_bridge(self): def get_bridge(self):
bridge = None bridge = None
if self.get_type() == "bridge": if self.get_type() != "bridge":
bridge = util.get_xml_path(self._XMLDesc(), "/interface/bridge/interface/@name")
for iface in self.get_bridge_slave_ifaces():
if iface.get("state") == "up" and iface.get("speed") != "unknown":
bridge = iface.get("name")
return bridge
return bridge
else:
return None return None
bridge = util.get_xml_path(self._XMLDesc(), "/interface/bridge/interface/@name")
for iface in self.get_bridge_slave_ifaces():
if iface.get("state") == "up" and iface.get("speed") != "unknown":
bridge = iface.get("name")
return bridge
return bridge
def get_bridge_slave_ifaces(self): def get_bridge_slave_ifaces(self):
ifaces = list() if self.get_type() != "bridge":
if self.get_type() == "bridge":
tree = ElementTree.fromstring(self._XMLDesc())
for iface in tree.findall("./bridge/"):
address = state = speed = None
name = iface.get("name")
if_type = iface.get("type")
link = iface.find("link")
if link is not None:
state = link.get("state")
speed = link.get("speed")
mac = iface.find("mac")
if mac is not None:
address = mac.get("address")
ifaces.append({"name": name, "type": if_type, "state": state, "speed": speed, "mac": address})
return ifaces
else:
return None return None
ifaces = []
tree = ElementTree.fromstring(self._XMLDesc())
for iface in tree.findall("./bridge/"):
address = state = speed = None
name = iface.get("name")
if_type = iface.get("type")
link = iface.find("link")
if link is not None:
state = link.get("state")
speed = link.get("speed")
mac = iface.find("mac")
if mac is not None:
address = mac.get("address")
ifaces.append({"name": name, "type": if_type, "state": state, "speed": speed, "mac": address})
return ifaces
def get_details(self): def get_details(self):
mac = self.get_mac() mac = self.get_mac()
itype = self.get_type() itype = self.get_type()

View file

@ -23,10 +23,8 @@ def network_size(subnet, dhcp=None):
if addr.version() == 6: if addr.version() == 6:
mask = mask.lstrip("/") if "/" in mask else mask mask = mask.lstrip("/") if "/" in mask else mask
dhcp_pool = [IP(addr[0].strCompressed() + hex(256)), IP(addr[0].strCompressed() + hex(512 - 1))] dhcp_pool = [IP(addr[0].strCompressed() + hex(256)), IP(addr[0].strCompressed() + hex(512 - 1))]
if dhcp:
return gateway, mask, dhcp_pool return (gateway, mask, dhcp_pool) if dhcp else (gateway, mask, None)
else:
return gateway, mask, None
class wvmNetworks(wvmConnect): class wvmNetworks(wvmConnect):
@ -42,7 +40,12 @@ class wvmNetworks(wvmConnect):
net_bridge = util.get_xml_path(net.XMLDesc(0), "/network/forward/interface/@dev") 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") 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 return networks
def define_network(self, xml): def define_network(self, xml):
@ -137,7 +140,7 @@ class wvmNetwork(wvmConnect):
def get_bridge_device(self): def get_bridge_device(self):
try: try:
return self.net.bridgeName() return self.net.bridgeName()
except: except Exception:
return util.get_xml_path(self._XMLDesc(0), "/network/forward/interface/@dev") return util.get_xml_path(self._XMLDesc(0), "/network/forward/interface/@dev")
def start(self): def start(self):
@ -153,7 +156,7 @@ class wvmNetwork(wvmConnect):
return self.net.update(command, section, parentIndex, xml, flags) return self.net.update(command, section, parentIndex, xml, flags)
def get_ip_networks(self): def get_ip_networks(self):
ip_networks = dict() ip_networks = {}
xml = self._XMLDesc(0) xml = self._XMLDesc(0)
if util.get_xml_path(xml, "/network/ip") is None: if util.get_xml_path(xml, "/network/ip") is None:
return ip_networks return ip_networks
@ -164,10 +167,10 @@ class wvmNetwork(wvmConnect):
netmask_str = ip.get("netmask") netmask_str = ip.get("netmask")
prefix = ip.get("prefix") prefix = ip.get("prefix")
family = ip.get("family", "ipv4") family = ip.get("family", "ipv4")
base = 32 if family == "ipv4" else 128
if prefix: if prefix:
prefix = int(prefix) prefix = int(prefix)
binstr = (prefix * "1") + ((base - prefix) * "0") base = 32 if family == "ipv4" else 128
binstr = prefix * "1" + (base - prefix) * "0"
netmask_str = str(IP(int(binstr, base=2))) netmask_str = str(IP(int(binstr, base=2)))
if netmask_str: if netmask_str:
@ -183,8 +186,7 @@ class wvmNetwork(wvmConnect):
def get_network_mac(self): def get_network_mac(self):
xml = self._XMLDesc(0) xml = self._XMLDesc(0)
mac = util.get_xml_path(xml, "/network/mac/@address") return util.get_xml_path(xml, "/network/mac/@address")
return mac
def get_network_forward(self): def get_network_forward(self):
xml = self._XMLDesc(0) xml = self._XMLDesc(0)
@ -201,22 +203,15 @@ class wvmNetwork(wvmConnect):
dhcpstart = util.get_xml_path(xml, "/network/ip[@family='ipv6']/dhcp/range[1]/@start") dhcpstart = util.get_xml_path(xml, "/network/ip[@family='ipv6']/dhcp/range[1]/@start")
dhcpend = util.get_xml_path(xml, "/network/ip[@family='ipv6']/dhcp/range[1]/@end") dhcpend = util.get_xml_path(xml, "/network/ip[@family='ipv6']/dhcp/range[1]/@end")
if not dhcpstart or not dhcpend: return None if not dhcpstart or not dhcpend else [IP(dhcpstart), IP(dhcpend)]
return None
return [IP(dhcpstart), IP(dhcpend)]
def get_dhcp_range_start(self, family="ipv4"): def get_dhcp_range_start(self, family="ipv4"):
dhcp = self.get_dhcp_range(family) dhcp = self.get_dhcp_range(family)
if not dhcp: return dhcp[0] if dhcp else None
return None
return dhcp[0]
def get_dhcp_range_end(self, family="ipv4"): def get_dhcp_range_end(self, family="ipv4"):
dhcp = self.get_dhcp_range(family) dhcp = self.get_dhcp_range(family)
if not dhcp: return dhcp[1] if dhcp else None
return None
return dhcp[1]
def can_pxe(self): def can_pxe(self):
xml = self._XMLDesc(0) xml = self._XMLDesc(0)
@ -226,21 +221,19 @@ class wvmNetwork(wvmConnect):
return bool(util.get_xml_path(xml, "/network/ip/dhcp/bootp/@file")) return bool(util.get_xml_path(xml, "/network/ip/dhcp/bootp/@file"))
def get_dhcp_host_addr(self, family="ipv4"): def get_dhcp_host_addr(self, family="ipv4"):
result = list() result = []
tree = etree.fromstring(self._XMLDesc(0)) tree = etree.fromstring(self._XMLDesc(0))
for ipdhcp in tree.findall("./ip"): for ipdhcp in tree.findall("./ip"):
if family == "ipv4": if family == "ipv4":
if ipdhcp.get("family") is None: if ipdhcp.get("family") is not None:
hosts = ipdhcp.findall("./dhcp/host")
for host in hosts:
host_ip = host.get("ip")
mac = host.get("mac")
name = host.get("name", "")
result.append({"ip": host_ip, "mac": mac, "name": name})
return result
else:
continue continue
hosts = ipdhcp.findall("./dhcp/host")
for host in hosts:
host_ip = host.get("ip")
mac = host.get("mac")
name = host.get("name", "")
result.append({"ip": host_ip, "mac": mac, "name": name})
return result
if family == "ipv6": if family == "ipv6":
hosts = tree.xpath("./ip[@family='ipv6']/dhcp/host") hosts = tree.xpath("./ip[@family='ipv6']/dhcp/host")
for host in hosts: for host in hosts:
@ -272,9 +265,9 @@ class wvmNetwork(wvmConnect):
for h in hosts: for h in hosts:
if h.get("ip") == ip: if h.get("ip") == ip:
if family == "ipv4": if family == "ipv4":
new_xml = '<host mac="{}" name="{}" ip="{}"/>'.format(h.get("mac"), h.get("name"), ip) new_xml = f'<host mac="{h.get("mac")}" name="{h.get("name")}" ip="{ip}"/>'
if family == "ipv6": if family == "ipv6":
new_xml = '<host id="{}" name="{}" ip="{}"/>'.format(h.get("id"), h.get("name"), ip) new_xml = f'<host id="{h.get("id")}" name="{h.get("name")}" ip="{ip}"/>'
self.update( self.update(
VIR_NETWORK_UPDATE_COMMAND_DELETE, VIR_NETWORK_UPDATE_COMMAND_DELETE,
@ -298,12 +291,7 @@ class wvmNetwork(wvmConnect):
compare_var = "id" compare_var = "id"
parent_index = self.parent_count - 1 parent_index = self.parent_count - 1
new_host_xml = etree.fromstring(new_xml) new_host_xml = etree.fromstring(new_xml)
host = next((h for h in hosts if h.get(compare_var) == mac_duid), None)
host = None
for h in hosts:
if h.get(compare_var) == mac_duid:
host = h
break
if host is None: if host is None:
self.update( self.update(
VIR_NETWORK_UPDATE_COMMAND_ADD_LAST, VIR_NETWORK_UPDATE_COMMAND_ADD_LAST,
@ -326,7 +314,7 @@ class wvmNetwork(wvmConnect):
) )
def get_qos(self): def get_qos(self):
qos_values = dict() qos_values = {}
tree = etree.fromstring(self._XMLDesc(0)) tree = etree.fromstring(self._XMLDesc(0))
qos = tree.xpath("/network/bandwidth") qos = tree.xpath("/network/bandwidth")
if qos: if qos:
@ -348,13 +336,10 @@ class wvmNetwork(wvmConnect):
return qos_values return qos_values
def set_qos(self, direction, average, peak, burst): def set_qos(self, direction, average, peak, burst):
if direction == "inbound": if direction not in ("inbound","outbound"):
xml = f"<inbound average='{average}' peak='{peak}' burst='{burst}'/>"
elif direction == "outbound":
xml = f"<outbound average='{average}' peak='{peak}' burst='{burst}'/>"
else:
raise Exception("Direction must be inbound or outbound") raise Exception("Direction must be inbound or outbound")
xml = f"<{direction} average='{average}' peak='{peak}' burst='{burst}'/>"
tree = etree.fromstring(self._XMLDesc(0)) tree = etree.fromstring(self._XMLDesc(0))
band = tree.xpath("/network/bandwidth") band = tree.xpath("/network/bandwidth")
@ -388,7 +373,7 @@ class wvmNetwork(wvmConnect):
self.leases = self.net.DHCPLeases() self.leases = self.net.DHCPLeases()
except Exception as e: except Exception as e:
self.leases = [] self.leases = []
raise "Error getting {} DHCP leases: {}".format(self, e) raise f"Error getting {self} DHCP leases: {e}" from e
def get_dhcp_leases(self): def get_dhcp_leases(self):
if self.leases is None: if self.leases is None:

View file

@ -47,11 +47,8 @@ class wvmNWFilter(wvmConnect):
return ElementTree.tostring(tree).decode() return ElementTree.tostring(tree).decode()
def get_filter_refs(self): def get_filter_refs(self):
refs = []
tree = ElementTree.fromstring(self._XMLDesc(0)) tree = ElementTree.fromstring(self._XMLDesc(0))
for ref in tree.findall("./filterref"): return [ref.get("filter") for ref in tree.findall("./filterref")]
refs.append(ref.get("filter"))
return refs
def get_rules(self): def get_rules(self):
rules = [] rules = []

View file

@ -1,3 +1,5 @@
import contextlib
from vrtManager import util from vrtManager import util
from vrtManager.connection import wvmConnect from vrtManager.connection import wvmConnect
@ -10,10 +12,7 @@ class wvmStorages(wvmConnect):
stg = self.get_storage(pool) stg = self.get_storage(pool)
stg_status = stg.isActive() stg_status = stg.isActive()
stg_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type") stg_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type")
if stg_status: stg_vol = len(stg.listVolumes()) if stg_status else None
stg_vol = len(stg.listVolumes())
else:
stg_vol = None
stg_size = stg.info()[1] stg_size = stg.info()[1]
storages.append( storages.append(
{"name": pool, "status": stg_status, "type": stg_type, "volumes": stg_vol, "size": stg_size} {"name": pool, "status": stg_status, "type": stg_type, "volumes": stg_vol, "size": stg_size}
@ -33,7 +32,7 @@ class wvmStorages(wvmConnect):
<format type='lvm2'/> <format type='lvm2'/>
</source>""" </source>"""
if stg_type == "logical": if stg_type == "logical":
target = "/dev/" + name target = f"/dev/{name}"
xml += f""" xml += f"""
<target> <target>
<path>{target}</path> <path>{target}</path>
@ -125,10 +124,10 @@ class wvmStorage(wvmConnect):
return self.pool.UUIDString() return self.pool.UUIDString()
def start(self): def start(self):
self.pool.create(0) return self.pool.create(0)
def stop(self): def stop(self):
self.pool.destroy() return self.pool.destroy()
def delete(self): def delete(self):
self.pool.undefine() self.pool.undefine()
@ -225,26 +224,30 @@ class wvmStorage(wvmConnect):
return util.get_xml_path(vol_xml, "/volume/@type") return util.get_xml_path(vol_xml, "/volume/@type")
def refresh(self): def refresh(self):
self.pool.refresh(0) return self.pool.refresh(0)
def update_volumes(self): def get_volume_details(self, volname):
try: try:
self.refresh() self.refresh()
except Exception: except Exception:
pass pass
vols = self.get_volumes()
vol_list = []
for volname in vols: return {
vol_list.append( "name": volname,
{ "size": self.get_volume_size(volname),
"name": volname, "allocation": self.get_volume_allocation(volname),
"size": self.get_volume_size(volname), "type": self.get_volume_format_type(volname),
"allocation": self.get_volume_allocation(volname), }
"type": self.get_volume_format_type(volname),
}
) def update_volumes(self):
return vol_list 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 create_volume(self, name, size, vol_fmt="qcow2", metadata=False, disk_owner_uid=0, disk_owner_gid=0): def create_volume(self, name, size, vol_fmt="qcow2", metadata=False, disk_owner_uid=0, disk_owner_gid=0):
size = int(size) * 1073741824 size = int(size) * 1073741824
@ -253,10 +256,7 @@ class wvmStorage(wvmConnect):
if vol_fmt == "unknown": if vol_fmt == "unknown":
vol_fmt = "raw" vol_fmt = "raw"
if storage_type == "dir": if storage_type == "dir":
if vol_fmt in ("qcow", "qcow2"): name += f".{vol_fmt}" if vol_fmt in ("qcow", "qcow2") else ".img"
name += "." + vol_fmt
else:
name += ".img"
alloc = 0 alloc = 0
xml = f""" xml = f"""
<volume> <volume>
@ -300,9 +300,9 @@ class wvmStorage(wvmConnect):
storage_type = self.get_type() storage_type = self.get_type()
if storage_type == "dir": if storage_type == "dir":
if vol_fmt in ["qcow", "qcow2"]: if vol_fmt in ["qcow", "qcow2"]:
target_file += "." + vol_fmt target_file += f".{vol_fmt}"
else: else:
suffix = "." + file_suffix suffix = f".{file_suffix}"
target_file += suffix if len(suffix) > 1 else "" target_file += suffix if len(suffix) > 1 else ""
xml = f""" xml = f"""

View file

@ -9,10 +9,7 @@ import lxml.etree as etree
def is_kvm_available(xml): def is_kvm_available(xml):
kvm_domains = get_xml_path(xml, "//domain/@type='kvm'") kvm_domains = get_xml_path(xml, "//domain/@type='kvm'")
if kvm_domains > 0: return kvm_domains > 0
return True
else:
return False
def randomMAC(): def randomMAC():
@ -26,40 +23,40 @@ def randomMAC():
def randomUUID(): def randomUUID():
"""Generate a random UUID.""" """Generate a random UUID."""
u = [secrets.randbelow(256) for ignore in range(0, 16)] u = [secrets.randbelow(256) for _ in range(16)]
u[6] = (u[6] & 0x0F) | (4 << 4) u[6] = u[6] & 15 | 4 << 4
u[8] = (u[8] & 0x3F) | (2 << 6) u[8] = u[8] & 63 | 2 << 6
return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2, "%02x" * 6]) % tuple(u) return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2, "%02x" * 6]) % tuple(u)
def randomPasswd(length=12, alphabet=string.ascii_letters + string.digits): def randomPasswd(length=12, alphabet=string.ascii_letters + string.digits):
"""Generate a random password""" """Generate a random password"""
return "".join([secrets.choice(alphabet) for i in range(length)]) return "".join([secrets.choice(alphabet) for _ in range(length)])
def get_max_vcpus(conn, type=None): def get_max_vcpus(conn, guest_type=None):
"""@param conn: libvirt connection to poll for max possible vcpus """@param conn: libvirt connection to poll for max possible vcpus
@type type: optional guest type (kvm, etc.)""" @param guest_type: optional guest type (kvm, etc.)"""
if type is None: if type is None:
type = conn.getType() guest_type = conn.getType()
try: try:
m = conn.getMaxVcpus(type.lower()) m = conn.getMaxVcpus(guest_type.lower())
except libvirt.libvirtError: except libvirt.libvirtError:
m = 32 m = 32
return m return m
def xml_escape(str): def xml_escape(xml_str):
"""Replaces chars ' " < > & with xml safe counterparts""" """Replaces chars ' " < > & with xml safe counterparts"""
if str is None: if xml_str is None:
return None return None
str = str.replace("&", "&amp;") xml_str = xml_str.replace("&", "&amp;")
str = str.replace("'", "&apos;") xml_str = xml_str.replace("'", "&apos;")
str = str.replace('"', "&quot;") xml_str = xml_str.replace('"', "&quot;")
str = str.replace("<", "&lt;") xml_str = xml_str.replace("<", "&lt;")
str = str.replace(">", "&gt;") xml_str = xml_str.replace(">", "&gt;")
return str return xml_str
def compareMAC(p, q): def compareMAC(p, q):
@ -108,10 +105,7 @@ def get_xpath(doc, path):
if ret is not None: if ret is not None:
if isinstance(ret, list): if isinstance(ret, list):
if len(ret) >= 1: if len(ret) >= 1:
if hasattr(ret[0], "text"): result = ret[0].text if hasattr(ret[0], "text") else ret[0]
result = ret[0].text
else:
result = ret[0]
else: else:
result = ret result = ret

View file

@ -7,28 +7,20 @@ register = template.Library()
@register.simple_tag @register.simple_tag
def app_active(request, app_name): def app_active(request, app_name):
if request.resolver_match.app_name == app_name: return "active" if request.resolver_match.app_name == app_name else ""
return "active"
return ""
@register.simple_tag @register.simple_tag
def view_active(request, view_name): def view_active(request, view_name):
if request.resolver_match.view_name == view_name: return "active" if request.resolver_match.view_name == view_name else ""
return "active"
return ""
@register.simple_tag @register.simple_tag
def class_active(request, pattern): def class_active(request, pattern):
if re.search(pattern, request.path): # Not sure why 'class="active"' returns class=""active""
# Not sure why 'class="active"' returns class=""active"" return "active" if re.search(pattern, request.path) else ""
return "active"
return ""
@register.simple_tag @register.simple_tag
def has_perm(user, permission_codename): def has_perm(user, permission_codename):
if user.has_perm(permission_codename): return bool(user.has_perm(permission_codename))
return True
return False

View file

@ -42,48 +42,49 @@ try:
def authenticate(self, request, username=None, password=None, **kwargs): def authenticate(self, request, username=None, password=None, **kwargs):
if not settings.LDAP_ENABLED: if not settings.LDAP_ENABLED:
return None return None
print("authenticate_ldap") print("authenticate_ldap")
# Get the user information from the LDAP if he can be authenticated # Get the user information from the LDAP if he can be authenticated
isAdmin = False isAdmin = False
isStaff = False isStaff = False
isTechnician = False isTechnician = False
requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_ADMINS) requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_ADMINS)
isAdmin = requeteLdap is not None
isStaff = requeteLdap is not None
if requeteLdap is None: if requeteLdap is None:
requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_STAFF) requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_STAFF)
if requeteLdap is None: isStaff = requeteLdap is not None
requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_TECHNICIANS)
if requeteLdap is None: if requeteLdap is None:
requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_USERS) requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_TECHNICIANS)
if requeteLdap is None: isTechnician = requeteLdap is not None
print("User does not belong to any search group. Check LDAP_SEARCH_GROUP_FILTER in settings.")
return None if requeteLdap is None:
else: requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_USERS)
isTechnician = True
else: if requeteLdap is None:
isStaff = True print("User does not belong to any search group. Check LDAP_SEARCH_GROUP_FILTER in settings.")
else: return None
isAdmin = True
isStaff = True
techniciansGroup = Group.objects.get(name='Technicians') techniciansGroup = Group.objects.get(name='Technicians')
try: try:
user = User.objects.get(username=username) user = User.objects.get(username=username)
attributes = UserAttributes.objects.get(user=user) attributes = UserAttributes.objects.get(user=user)
user.is_staff = isStaff user.is_staff = isStaff
user.is_superuser = isAdmin user.is_superuser = isAdmin
if isTechnician is False and user.groups.filter(name='Technicians').exists(): if not isTechnician and user.groups.filter(name='Technicians').exists():
user.groups.remove(techniciansGroup) user.groups.remove(techniciansGroup)
elif isTechnician is True and user.groups.filter(name='Technicians').exists() is False: elif isTechnician and not user.groups.filter(name='Technicians').exists():
user.groups.add(techniciansGroup) user.groups.add(techniciansGroup)
else: else:
print("The user is already in the Technicians group") print("The user is already in the Technicians group")
user.save() user.save()
# TODO VERIFY # TODO VERIFY
except User.DoesNotExist: except User.DoesNotExist:
print("authenticate-create new user: {}".format(username)) print(f"authenticate-create new user: {username}")
user = User(username=username) user = User(username=username)
user.first_name = requeteLdap[1] user.first_name = requeteLdap[1]
user.last_name = requeteLdap[2] user.last_name = requeteLdap[2]
@ -93,7 +94,7 @@ try:
user.is_superuser = isAdmin user.is_superuser = isAdmin
user.set_password(uuid.uuid4().hex) user.set_password(uuid.uuid4().hex)
user.save() user.save()
if isTechnician is True: if isTechnician:
user.groups.add(techniciansGroup) user.groups.add(techniciansGroup)
maxInstances = 1 maxInstances = 1
maxCpus = 1 maxCpus = 1

View file

@ -22,10 +22,12 @@ INSTALLED_APPS = [
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"rest_framework",
"django_bootstrap5", "django_bootstrap5",
"django_icons", "django_icons",
"django_otp", "django_otp",
"django_otp.plugins.otp_totp", "django_otp.plugins.otp_totp",
"drf_yasg",
"accounts", "accounts",
"admin", "admin",
"appsettings", "appsettings",
@ -40,6 +42,7 @@ INSTALLED_APPS = [
"virtsecrets", "virtsecrets",
"logs", "logs",
"qr_code", "qr_code",
"rest_framework",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -211,7 +214,7 @@ SOCKETIO_PUBLIC_PORT = 6081
SOCKETIO_PUBLIC_PATH = "socket.io/" SOCKETIO_PUBLIC_PATH = "socket.io/"
# List of console listen addresses # List of console listen addresses
QEMU_CONSOLE_LISTEN_ADDRESSES = ( QEMU_CONSOLE_LISTENER_ADDRESSES = (
("127.0.0.1", "Localhost"), ("127.0.0.1", "Localhost"),
("0.0.0.0", "All interfaces"), ("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 appsettings.views import appsettings
from console.views import console from console.views import console
from django.conf import settings
from django.urls import include, path
from instances.views import index 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 = [ urlpatterns = [
path("", index, name="index"), path("", index, name="index"),
path("admin/", include(("admin.urls", "admin"), namespace="admin")), path("admin/", include(("admin.urls", "admin"), namespace="admin")),
@ -15,6 +32,11 @@ urlpatterns = [
path("instances/", include("instances.urls")), path("instances/", include("instances.urls")),
path("i18n/", include("django.conf.urls.i18n")), path("i18n/", include("django.conf.urls.i18n")),
path("logs/", include("logs.urls")), 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: if settings.DEBUG:
@ -26,3 +48,4 @@ if settings.DEBUG:
] ]
except ImportError: except ImportError:
pass pass