diff --git a/README.md b/README.md index f917721..e00b1ff 100644 --- a/README.md +++ b/README.md @@ -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:///swagger +``` +```bash +http:///redoc +``` + ## Screenshots Instance Detail: diff --git a/computes/api/serializers.py b/computes/api/serializers.py new file mode 100644 index 0000000..d7cd096 --- /dev/null +++ b/computes/api/serializers.py @@ -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 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'] diff --git a/computes/api/viewsets.py b/computes/api/viewsets.py new file mode 100644 index 0000000..c7d8acc --- /dev/null +++ b/computes/api/viewsets.py @@ -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)) diff --git a/computes/urls.py b/computes/urls.py index d69509a..8938107 100644 --- a/computes/urls.py +++ b/computes/urls.py @@ -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//', nwfilter, name='nwfilter'), path('virtsecrets/', secrets, name='virtsecrets'), - # path('create/', create_instance_select_type, name='create_instance_select_type'), - # path('create/archs//machines//', create_instance, name='create_instance'), path('archs//machines/', views.get_compute_machine_types, name='machines'), path( 'archs//machines//disks//buses/', diff --git a/conf/requirements.txt b/conf/requirements.txt index 04908bf..d2bf6a7 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -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 \ No newline at end of file +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 diff --git a/console/novncd b/console/novncd index 4d3f94b..59ff686 100755 --- a/console/novncd +++ b/console/novncd @@ -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: diff --git a/dev/libvirt-bootstrap.sh b/dev/libvirt-bootstrap.sh index 8a3cdfe..603b189 100644 --- a/dev/libvirt-bootstrap.sh +++ b/dev/libvirt-bootstrap.sh @@ -45,7 +45,7 @@ echowarn() { # DESCRIPTION: Echo debug information to stdout. #------------------------------------------------------------------------------- echodebug() { - if [ $_ECHO_DEBUG -eq $BS_TRUE ]; then + if [ "${_ECHO_DEBUG}" -eq "${BS_TRUE}" ]; then printf "${BC} * DEBUG${EC}: %s\n" "$@"; fi } @@ -154,8 +154,7 @@ __gather_linux_system_info() { DISTRO_VERSION="" # Let's test if the lsb_release binary is available - rv=$(lsb_release >/dev/null 2>&1) - if [ $? -eq 0 ]; then + if lsb_release >/dev/null 2>&1; then DISTRO_NAME=$(lsb_release -si) if [ "x$(echo "$DISTRO_NAME" | grep RedHat)" != "x" ]; then # Let's convert CamelCase to Camel Case @@ -208,7 +207,7 @@ __gather_linux_system_info() { ;; arch ) n="Arch Linux" ;; centos ) n="CentOS" ;; - almalinux ) n="AlmaLinux" ;; + almalinux ) n="AlmaLinux" ;; debian ) n="Debian" ;; ubuntu ) n="Ubuntu" ;; fedora ) n="Fedora" ;; @@ -248,7 +247,7 @@ __gather_linux_system_info() { ;; esac ;; - * ) n="${n}" ; + * ) ;; esac DISTRO_NAME=$n DISTRO_VERSION=$v @@ -779,8 +778,7 @@ if [ "$INSTALL_FUNC" = "null" ]; then exit 1 else echoinfo "Running ${INSTALL_FUNC}()" - $INSTALL_FUNC - if [ $? -ne 0 ]; then + if ! $INSTALL_FUNC; then echoerror "Failed to run ${INSTALL_FUNC}()!!!" exit 1 fi @@ -803,8 +801,7 @@ if [ "$POST_INSTALL_FUNC" = "null" ]; then exit 1 else echoinfo "Running ${POST_INSTALL_FUNC}()" - $POST_INSTALL_FUNC - if [ $? -ne 0 ]; then + if ! $POST_INSTALL_FUNC; then echoerror "Failed to run ${POST_INSTALL_FUNC}()!!!" exit 1 fi @@ -827,8 +824,7 @@ if [ "$DAEMONS_RUNNING_FUNC" = "null" ]; then exit 1 else echoinfo "Running ${DAEMONS_RUNNING_FUNC}()" - $DAEMONS_RUNNING_FUNC - if [ $? -ne 0 ]; then + if ! $DAEMONS_RUNNING_FUNC; then echoerror "Failed to run ${DAEMONS_RUNNING_FUNC}()!!!" exit 1 fi diff --git a/dev/requirements.txt b/dev/requirements.txt index 3e8a789..0e8ec79 100644 --- a/dev/requirements.txt +++ b/dev/requirements.txt @@ -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 diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 90bd54e..7fab982 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -75,10 +75,7 @@ backlog = 2048 def get_workers(): procs = os.sysconf('SC_NPROCESSORS_ONLN') - if procs > 0: - return procs * 2 + 1 - else: - return 3 + return procs * 2 + 1 if procs > 0 else 3 workers = get_workers() diff --git a/instances/api/serializers.py b/instances/api/serializers.py new file mode 100644 index 0000000..7e0c0d9 --- /dev/null +++ b/instances/api/serializers.py @@ -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'] diff --git a/instances/api/viewsets.py b/instances/api/viewsets.py new file mode 100644 index 0000000..4960f07 --- /dev/null +++ b/instances/api/viewsets.py @@ -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) \ No newline at end of file diff --git a/instances/forms.py b/instances/forms.py index 77433ba..14e0a9f 100644 --- a/instances/forms.py +++ b/instances/forms.py @@ -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'] diff --git a/instances/migrations/0010_auto_20220722_0812.py b/instances/migrations/0010_auto_20220722_0812.py new file mode 100644 index 0000000..6eb70f6 --- /dev/null +++ b/instances/migrations/0010_auto_20220722_0812.py @@ -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'), + ), + ] diff --git a/instances/models.py b/instances/models.py index 147b099..cc15873 100644 --- a/instances/models.py +++ b/instances/models.py @@ -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 diff --git a/instances/templates/allinstances_index_grouped.html b/instances/templates/allinstances_index_grouped.html index bdeda51..5d270d4 100644 --- a/instances/templates/allinstances_index_grouped.html +++ b/instances/templates/allinstances_index_grouped.html @@ -30,9 +30,9 @@ {% trans "Connected" %} - {% if app_settings.VM_DRBD_STATUS == 'True' %} - - {% endif %} + {% if app_settings.VM_DRBD_STATUS == 'True' %} + + {% endif %} {{ compute.cpu_count }} {{ compute.ram_size|filesizeformat }} @@ -68,11 +68,11 @@ {% trans "Suspended" %} {% endif %} - {% if app_settings.VM_DRBD_STATUS == 'True' %} - - {% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}{% else %}{% endif %}{{ instance.drbd }} - - {% endif %} + {% if app_settings.VM_DRBD_STATUS == 'True' %} + + {% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}{% else %}{% endif %}{{ instance.drbd }} + + {% endif %} {{ instance.proxy.instance.info.3 }} {{ instance.cur_memory }} MB diff --git a/instances/templates/create_instance_w1.html b/instances/templates/create_instance_w1.html index b655201..93a915d 100644 --- a/instances/templates/create_instance_w1.html +++ b/instances/templates/create_instance_w1.html @@ -15,7 +15,7 @@ {% for field in form %} {% for error in field.errors %}
- {{ error|escape }} + {{ field.label }}: {{ error|escape }}
{% endfor %} {% endfor %} diff --git a/instances/templates/create_instance_w2.html b/instances/templates/create_instance_w2.html index 499dd3e..9c38670 100644 --- a/instances/templates/create_instance_w2.html +++ b/instances/templates/create_instance_w2.html @@ -19,7 +19,7 @@ {% for field in form %} {% for error in field.errors %}
- {{ error|escape }} + {{ field.label }}: {{ error|escape }}
{% endfor %} {% endfor %} diff --git a/instances/templates/instance.html b/instances/templates/instance.html index cb18556..e602431 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -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 () { diff --git a/instances/views.py b/instances/views.py index 449fc7b..ca998e3 100644 --- a/instances/views.py +++ b/instances/views.py @@ -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()) diff --git a/interfaces/api/serializers.py b/interfaces/api/serializers.py new file mode 100644 index 0000000..e9a7bea --- /dev/null +++ b/interfaces/api/serializers.py @@ -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'] + diff --git a/interfaces/api/viewsets.py b/interfaces/api/viewsets.py new file mode 100644 index 0000000..1b7a6e7 --- /dev/null +++ b/interfaces/api/viewsets.py @@ -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) + diff --git a/interfaces/models.py b/interfaces/models.py index 71a8362..c2834e8 100644 --- a/interfaces/models.py +++ b/interfaces/models.py @@ -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 diff --git a/networks/api/serializers.py b/networks/api/serializers.py new file mode 100644 index 0000000..8b26298 --- /dev/null +++ b/networks/api/serializers.py @@ -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'] diff --git a/networks/api/viewsets.py b/networks/api/viewsets.py new file mode 100644 index 0000000..892511d --- /dev/null +++ b/networks/api/viewsets.py @@ -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) + diff --git a/networks/models.py b/networks/models.py index 71a8362..c52ceff 100644 --- a/networks/models.py +++ b/networks/models.py @@ -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 diff --git a/storages/api/serializers.py b/storages/api/serializers.py new file mode 100644 index 0000000..2584616 --- /dev/null +++ b/storages/api/serializers.py @@ -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'] diff --git a/storages/api/viewsets.py b/storages/api/viewsets.py new file mode 100644 index 0000000..023aaf9 --- /dev/null +++ b/storages/api/viewsets.py @@ -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'}) diff --git a/storages/models.py b/storages/models.py index 71a8362..0af474b 100644 --- a/storages/models.py +++ b/storages/models.py @@ -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 diff --git a/vrtManager/IPy.py b/vrtManager/IPy.py index 3cddf29..099ac90 100644 --- a/vrtManager/IPy.py +++ b/vrtManager/IPy.py @@ -181,10 +181,7 @@ class IPint(object): if isinstance(data, INT_TYPES): self.ip = int(data) if ipversion == 0: - if self.ip <= MAX_IPV4_ADDRESS: - ipversion = 4 - else: - ipversion = 6 + ipversion = 4 if self.ip <= MAX_IPV4_ADDRESS else 6 if ipversion == 4: if self.ip > MAX_IPV4_ADDRESS: raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, self.ip)) diff --git a/vrtManager/create.py b/vrtManager/create.py index 61d5040..0044fe0 100644 --- a/vrtManager/create.py +++ b/vrtManager/create.py @@ -1,4 +1,5 @@ import string +import contextlib from vrtManager import util from vrtManager.connection import wvmConnect @@ -30,19 +31,13 @@ class wvmCreate(wvmConnect): """ Function return all images on all storages """ - images = list() + images = [] storages = self.get_storages(only_actives=True) for storage in storages: stg = self.get_storage(storage) - try: + with contextlib.suppress(Exception): stg.refresh(0) - except Exception: - pass - for img in stg.listVolumes(): - if img.lower().endswith(".iso"): - pass - else: - images.append(img) + images.extend(img for img in stg.listVolumes() if not img.lower().endswith(".iso")) return images 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") 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): @@ -58,10 +53,7 @@ class wvmCreate(wvmConnect): stg = self.get_storage(storage) storage_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type") if storage_type == "dir": - if image_format in ("qcow", "qcow2"): - name += "." + image_format - else: - name += ".img" + name += f".{image_format}" if image_format in ("qcow", "qcow2") else ".img" alloc = 0 else: image_format = 'raw' @@ -87,28 +79,19 @@ class wvmCreate(wvmConnect): """ stg.createXML(xml, metadata) - try: + + with contextlib.suppress(Exception): stg.refresh(0) - except: - pass vol = stg.storageVolLookupByName(name) return vol.path() def get_volume_format_type(self, path): vol = self.get_volume_by_path(path) 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: - return vol_type - else: - return "raw" + return "raw" if vol_type in ["unknown", "iso"] else vol_type or "raw" def get_volume_path(self, volume, pool=None): - if not pool: - storages = self.get_storages(only_actives=True) - else: - storages = [pool] + storages = [pool] if pool else self.get_storages(only_actives=True) for storage in storages: stg = self.get_storage(storage) 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): vol = self.get_volume_by_path(template) - if not storage: - stg = vol.storagePoolLookupByVolume() - else: - stg = self.get_storage(storage) + stg = self.get_storage(storage) if storage else vol.storagePoolLookupByVolume() storage_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type") format = util.get_xml_path(vol.XMLDesc(0), "/volume/target/format/@type") @@ -180,7 +160,7 @@ class wvmCreate(wvmConnect): nwfilter, graphics, virtio, - listen_addr, + listener_addr, video="vga", console_pass="random", mac=None, @@ -226,12 +206,8 @@ class wvmCreate(wvmConnect): if caps["features"]: xml += """""" - if "acpi" in caps["features"]: - xml += """""" - if "apic" in caps["features"]: - xml += """""" - if "pae" in caps["features"]: - xml += """""" + for feat in [x for x in ("acpi", "apic", "pae",) if x in caps["features"]]: + xml += f"""<{feat}/>""" if firmware.get("secure", "no") == "yes": xml += """""" xml += """""" @@ -240,9 +216,7 @@ class wvmCreate(wvmConnect): xml += """""" elif vcpu_mode == "host-passthrough": xml += """""" - elif vcpu_mode == "": - pass - else: + elif vcpu_mode != "": xml += f""" {vcpu_mode}""" xml += """""" @@ -306,7 +280,7 @@ class wvmCreate(wvmConnect): xml += """""" % (hd_disk_letters.pop(0), volume.get("bus")) elif volume.get("bus") == "fdc": xml += """""" % (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 += """""" % (sd_disk_letters.pop(0), volume.get("bus")) else: xml += """""" % sd_disk_letters.pop(0) @@ -345,21 +319,20 @@ class wvmCreate(wvmConnect): if console_pass == "random": console_pass = "passwd='" + util.randomPasswd() + "'" - else: - if not console_pass == "": - console_pass = "passwd='" + console_pass + "'" + elif console_pass != "": + console_pass = "passwd='" + console_pass + "'" if "usb" in dom_caps["disk_bus"]: - xml += """""".format("virtio" if virtio else "usb") - xml += """""".format("virtio" if virtio else "usb") - xml += """""".format("virtio" if virtio else "usb") + xml += f"""""" + xml += f"""""" + xml += f"""""" else: xml += """""" xml += """""" xml += """""" xml += f""" - + """ if qemu_ga and virtio: @@ -372,4 +345,4 @@ class wvmCreate(wvmConnect): """ - self._defineXML(xml) + return self._defineXML(xml) diff --git a/vrtManager/hostdetails.py b/vrtManager/hostdetails.py index 2996904..6ee97fb 100644 --- a/vrtManager/hostdetails.py +++ b/vrtManager/hostdetails.py @@ -21,12 +21,11 @@ class wvmHostDetails(wvmConnect): freemem = self.wvm.getMemoryStats(-1, 0) if isinstance(freemem, dict): 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 - mem_usage = {"total": all_mem, "usage": usage, "percent": percent} + return {"total": all_mem, "usage": usage, "percent": percent} else: - mem_usage = {"total": None, "usage": None, "percent": None} - return mem_usage + return {"total": None, "usage": None, "percent": None} def get_cpu_usage(self): """ @@ -35,30 +34,30 @@ class wvmHostDetails(wvmConnect): prev_idle = 0 prev_total = 0 cpu = self.wvm.getCPUStats(-1, 0) - if 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: + if not isinstance(cpu, dict): 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} def get_node_info(self): """ Function return host server information: hostname, cpu, memory, ... """ - info = list() - info.append(self.wvm.getHostname()) # hostname + info = [self.wvm.getHostname()] # hostname info.append(self.wvm.getInfo()[0]) # architecture info.append(self.wvm.getInfo()[1] * 1048576) # memory info.append(self.wvm.getInfo()[2]) # cpu core count diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 8212aab..b2a8dd2 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -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") diff --git a/vrtManager/interface.py b/vrtManager/interface.py index 14ddebc..a2eb777 100644 --- a/vrtManager/interface.py +++ b/vrtManager/interface.py @@ -57,7 +57,7 @@ class wvmInterface(wvmConnect): try: xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE) return util.get_xml_path(xml, "/interface/start/@mode") - except: + except Exception: return None def is_active(self): @@ -65,10 +65,7 @@ class wvmInterface(wvmConnect): def get_mac(self): mac = self.iface.MACString() - if mac: - return mac - else: - return None + return mac or None def get_type(self): xml = self._XMLDesc() @@ -78,11 +75,8 @@ class wvmInterface(wvmConnect): try: xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE) ipaddr = util.get_xml_path(xml, "/interface/protocol[@family='ipv4']/ip/@address") - if ipaddr: - return "static" - else: - return "dhcp" - except: + return "static" if ipaddr else "dhcp" + except Exception: return None def get_ipv4(self): @@ -92,17 +86,14 @@ class wvmInterface(wvmConnect): if not int_ipv4_ip or not int_ipv4_mask: return None else: - return int_ipv4_ip + "/" + int_ipv4_mask + return f"{int_ipv4_ip}/{int_ipv4_mask}" def get_ipv6_type(self): try: xml = self._XMLDesc(VIR_INTERFACE_XML_INACTIVE) ipaddr = util.get_xml_path(xml, "/interface/protocol[@family='ipv6']/ip/@address") - if ipaddr: - return "static" - else: - return "dhcp" - except: + return "static" if ipaddr else "dhcp" + except Exception: return None def get_ipv6(self): @@ -112,40 +103,39 @@ class wvmInterface(wvmConnect): if not int_ipv6_ip or not int_ipv6_mask: return None else: - return int_ipv6_ip + "/" + int_ipv6_mask + return f"{int_ipv6_ip}/{int_ipv6_mask}" def get_bridge(self): bridge = None - 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: + if self.get_type() != "bridge": 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): - ifaces = list() - 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: + if self.get_type() != "bridge": 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): mac = self.get_mac() itype = self.get_type() diff --git a/vrtManager/network.py b/vrtManager/network.py index 9a5a149..16aaccb 100644 --- a/vrtManager/network.py +++ b/vrtManager/network.py @@ -23,10 +23,8 @@ def network_size(subnet, dhcp=None): if addr.version() == 6: mask = mask.lstrip("/") if "/" in mask else mask dhcp_pool = [IP(addr[0].strCompressed() + hex(256)), IP(addr[0].strCompressed() + hex(512 - 1))] - if dhcp: - return gateway, mask, dhcp_pool - else: - return gateway, mask, None + + return (gateway, mask, dhcp_pool) if dhcp else (gateway, mask, None) class wvmNetworks(wvmConnect): @@ -42,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 def define_network(self, xml): @@ -137,7 +140,7 @@ class wvmNetwork(wvmConnect): def get_bridge_device(self): try: return self.net.bridgeName() - except: + except Exception: return util.get_xml_path(self._XMLDesc(0), "/network/forward/interface/@dev") def start(self): @@ -153,7 +156,7 @@ class wvmNetwork(wvmConnect): return self.net.update(command, section, parentIndex, xml, flags) def get_ip_networks(self): - ip_networks = dict() + ip_networks = {} xml = self._XMLDesc(0) if util.get_xml_path(xml, "/network/ip") is None: return ip_networks @@ -164,10 +167,10 @@ class wvmNetwork(wvmConnect): netmask_str = ip.get("netmask") prefix = ip.get("prefix") family = ip.get("family", "ipv4") - base = 32 if family == "ipv4" else 128 if 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))) if netmask_str: @@ -183,8 +186,7 @@ class wvmNetwork(wvmConnect): def get_network_mac(self): xml = self._XMLDesc(0) - mac = util.get_xml_path(xml, "/network/mac/@address") - return mac + return util.get_xml_path(xml, "/network/mac/@address") def get_network_forward(self): 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") dhcpend = util.get_xml_path(xml, "/network/ip[@family='ipv6']/dhcp/range[1]/@end") - if not dhcpstart or not dhcpend: - return None - - return [IP(dhcpstart), IP(dhcpend)] + return None if not dhcpstart or not dhcpend else [IP(dhcpstart), IP(dhcpend)] def get_dhcp_range_start(self, family="ipv4"): dhcp = self.get_dhcp_range(family) - if not dhcp: - return None - return dhcp[0] + return dhcp[0] if dhcp else None def get_dhcp_range_end(self, family="ipv4"): dhcp = self.get_dhcp_range(family) - if not dhcp: - return None - return dhcp[1] + return dhcp[1] if dhcp else None def can_pxe(self): xml = self._XMLDesc(0) @@ -226,21 +221,19 @@ class wvmNetwork(wvmConnect): return bool(util.get_xml_path(xml, "/network/ip/dhcp/bootp/@file")) def get_dhcp_host_addr(self, family="ipv4"): - result = list() + result = [] tree = etree.fromstring(self._XMLDesc(0)) - for ipdhcp in tree.findall("./ip"): if family == "ipv4": - if ipdhcp.get("family") is 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: + if ipdhcp.get("family") is not None: 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": hosts = tree.xpath("./ip[@family='ipv6']/dhcp/host") for host in hosts: @@ -272,9 +265,9 @@ class wvmNetwork(wvmConnect): for h in hosts: if h.get("ip") == ip: if family == "ipv4": - new_xml = ''.format(h.get("mac"), h.get("name"), ip) + new_xml = f'' if family == "ipv6": - new_xml = ''.format(h.get("id"), h.get("name"), ip) + new_xml = f'' self.update( VIR_NETWORK_UPDATE_COMMAND_DELETE, @@ -298,12 +291,7 @@ class wvmNetwork(wvmConnect): compare_var = "id" parent_index = self.parent_count - 1 new_host_xml = etree.fromstring(new_xml) - - host = None - for h in hosts: - if h.get(compare_var) == mac_duid: - host = h - break + host = next((h for h in hosts if h.get(compare_var) == mac_duid), None) if host is None: self.update( VIR_NETWORK_UPDATE_COMMAND_ADD_LAST, @@ -326,7 +314,7 @@ class wvmNetwork(wvmConnect): ) def get_qos(self): - qos_values = dict() + qos_values = {} tree = etree.fromstring(self._XMLDesc(0)) qos = tree.xpath("/network/bandwidth") if qos: @@ -348,13 +336,10 @@ class wvmNetwork(wvmConnect): return qos_values def set_qos(self, direction, average, peak, burst): - if direction == "inbound": - xml = f"" - elif direction == "outbound": - xml = f"" - else: + if direction not in ("inbound","outbound"): raise Exception("Direction must be inbound or outbound") + xml = f"<{direction} average='{average}' peak='{peak}' burst='{burst}'/>" tree = etree.fromstring(self._XMLDesc(0)) band = tree.xpath("/network/bandwidth") @@ -388,7 +373,7 @@ class wvmNetwork(wvmConnect): self.leases = self.net.DHCPLeases() except Exception as e: 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): if self.leases is None: diff --git a/vrtManager/nwfilters.py b/vrtManager/nwfilters.py index 627e571..8b8d1b9 100644 --- a/vrtManager/nwfilters.py +++ b/vrtManager/nwfilters.py @@ -47,11 +47,8 @@ class wvmNWFilter(wvmConnect): return ElementTree.tostring(tree).decode() def get_filter_refs(self): - refs = [] tree = ElementTree.fromstring(self._XMLDesc(0)) - for ref in tree.findall("./filterref"): - refs.append(ref.get("filter")) - return refs + return [ref.get("filter") for ref in tree.findall("./filterref")] def get_rules(self): rules = [] diff --git a/vrtManager/storage.py b/vrtManager/storage.py index c7b37f8..7c88886 100644 --- a/vrtManager/storage.py +++ b/vrtManager/storage.py @@ -1,3 +1,5 @@ +import contextlib + from vrtManager import util from vrtManager.connection import wvmConnect @@ -10,10 +12,7 @@ class wvmStorages(wvmConnect): stg = self.get_storage(pool) stg_status = stg.isActive() stg_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type") - if stg_status: - stg_vol = len(stg.listVolumes()) - else: - stg_vol = None + stg_vol = len(stg.listVolumes()) if stg_status else None stg_size = stg.info()[1] storages.append( {"name": pool, "status": stg_status, "type": stg_type, "volumes": stg_vol, "size": stg_size} @@ -33,7 +32,7 @@ class wvmStorages(wvmConnect): """ if stg_type == "logical": - target = "/dev/" + name + target = f"/dev/{name}" xml += f""" {target} @@ -125,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() @@ -225,26 +224,30 @@ 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 update_volumes(self): + def get_volume_details(self, volname): try: self.refresh() except Exception: pass - vols = self.get_volumes() - vol_list = [] - for volname in vols: - vol_list.append( - { - "name": volname, - "size": self.get_volume_size(volname), - "allocation": self.get_volume_allocation(volname), - "type": self.get_volume_format_type(volname), - } - ) - return vol_list + 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): + 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): size = int(size) * 1073741824 @@ -253,10 +256,7 @@ class wvmStorage(wvmConnect): if vol_fmt == "unknown": vol_fmt = "raw" if storage_type == "dir": - if vol_fmt in ("qcow", "qcow2"): - name += "." + vol_fmt - else: - name += ".img" + name += f".{vol_fmt}" if vol_fmt in ("qcow", "qcow2") else ".img" alloc = 0 xml = f""" @@ -300,9 +300,9 @@ class wvmStorage(wvmConnect): storage_type = self.get_type() if storage_type == "dir": if vol_fmt in ["qcow", "qcow2"]: - target_file += "." + vol_fmt + target_file += f".{vol_fmt}" else: - suffix = "." + file_suffix + suffix = f".{file_suffix}" target_file += suffix if len(suffix) > 1 else "" xml = f""" diff --git a/vrtManager/util.py b/vrtManager/util.py index 1f15b37..d618896 100644 --- a/vrtManager/util.py +++ b/vrtManager/util.py @@ -9,10 +9,7 @@ import lxml.etree as etree def is_kvm_available(xml): kvm_domains = get_xml_path(xml, "//domain/@type='kvm'") - if kvm_domains > 0: - return True - else: - return False + return kvm_domains > 0 def randomMAC(): @@ -26,40 +23,40 @@ def randomMAC(): def randomUUID(): """Generate a random UUID.""" - u = [secrets.randbelow(256) for ignore in range(0, 16)] - u[6] = (u[6] & 0x0F) | (4 << 4) - u[8] = (u[8] & 0x3F) | (2 << 6) + u = [secrets.randbelow(256) for _ in range(16)] + u[6] = u[6] & 15 | 4 << 4 + u[8] = u[8] & 63 | 2 << 6 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2, "%02x" * 6]) % tuple(u) def randomPasswd(length=12, alphabet=string.ascii_letters + string.digits): """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 - @type type: optional guest type (kvm, etc.)""" + @param guest_type: optional guest type (kvm, etc.)""" if type is None: - type = conn.getType() + guest_type = conn.getType() try: - m = conn.getMaxVcpus(type.lower()) + m = conn.getMaxVcpus(guest_type.lower()) except libvirt.libvirtError: m = 32 return m -def xml_escape(str): +def xml_escape(xml_str): """Replaces chars ' " < > & with xml safe counterparts""" - if str is None: + if xml_str is None: return None - str = str.replace("&", "&") - str = str.replace("'", "'") - str = str.replace('"', """) - str = str.replace("<", "<") - str = str.replace(">", ">") - return str + xml_str = xml_str.replace("&", "&") + xml_str = xml_str.replace("'", "'") + xml_str = xml_str.replace('"', """) + xml_str = xml_str.replace("<", "<") + xml_str = xml_str.replace(">", ">") + return xml_str def compareMAC(p, q): @@ -108,10 +105,7 @@ def get_xpath(doc, path): if ret is not None: if isinstance(ret, list): if len(ret) >= 1: - if hasattr(ret[0], "text"): - result = ret[0].text - else: - result = ret[0] + result = ret[0].text if hasattr(ret[0], "text") else ret[0] else: result = ret diff --git a/webvirtcloud/common_tags.py b/webvirtcloud/common_tags.py index 7197800..4b85df7 100644 --- a/webvirtcloud/common_tags.py +++ b/webvirtcloud/common_tags.py @@ -7,28 +7,20 @@ register = template.Library() @register.simple_tag def app_active(request, app_name): - if request.resolver_match.app_name == app_name: - return "active" - return "" + return "active" if request.resolver_match.app_name == app_name else "" @register.simple_tag def view_active(request, view_name): - if request.resolver_match.view_name == view_name: - return "active" - return "" + return "active" if request.resolver_match.view_name == view_name else "" @register.simple_tag def class_active(request, pattern): - if re.search(pattern, request.path): - # Not sure why 'class="active"' returns class=""active"" - return "active" - return "" + # Not sure why 'class="active"' returns class=""active"" + return "active" if re.search(pattern, request.path) else "" @register.simple_tag def has_perm(user, permission_codename): - if user.has_perm(permission_codename): - return True - return False + return bool(user.has_perm(permission_codename)) diff --git a/webvirtcloud/ldapbackend.py b/webvirtcloud/ldapbackend.py index 15c0834..0f7a797 100644 --- a/webvirtcloud/ldapbackend.py +++ b/webvirtcloud/ldapbackend.py @@ -42,48 +42,49 @@ try: def authenticate(self, request, username=None, password=None, **kwargs): if not settings.LDAP_ENABLED: - return None + return None print("authenticate_ldap") # Get the user information from the LDAP if he can be authenticated isAdmin = False isStaff = False isTechnician = False - + 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: requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_STAFF) - if requeteLdap is None: - requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_TECHNICIANS) - if requeteLdap is None: - requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_USERS) - if requeteLdap is None: - print("User does not belong to any search group. Check LDAP_SEARCH_GROUP_FILTER in settings.") - return None - else: - isTechnician = True - else: - isStaff = True - else: - isAdmin = True - isStaff = True + isStaff = requeteLdap is not None + + if requeteLdap is None: + requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_TECHNICIANS) + isTechnician = requeteLdap is not None + + if requeteLdap is None: + requeteLdap = self.get_LDAP_user(username, password, settings.LDAP_SEARCH_GROUP_FILTER_USERS) + + if requeteLdap is None: + print("User does not belong to any search group. Check LDAP_SEARCH_GROUP_FILTER in settings.") + return None techniciansGroup = Group.objects.get(name='Technicians') - + try: user = User.objects.get(username=username) attributes = UserAttributes.objects.get(user=user) user.is_staff = isStaff 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) - 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) else: print("The user is already in the Technicians group") user.save() # TODO VERIFY except User.DoesNotExist: - print("authenticate-create new user: {}".format(username)) + print(f"authenticate-create new user: {username}") user = User(username=username) user.first_name = requeteLdap[1] user.last_name = requeteLdap[2] @@ -93,7 +94,7 @@ try: user.is_superuser = isAdmin user.set_password(uuid.uuid4().hex) user.save() - if isTechnician is True: + if isTechnician: user.groups.add(techniciansGroup) maxInstances = 1 maxCpus = 1 diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index 9d050d8..bb37597 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -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"), ) diff --git a/webvirtcloud/urls-api.py b/webvirtcloud/urls-api.py new file mode 100644 index 0000000..40dab63 --- /dev/null +++ b/webvirtcloud/urls-api.py @@ -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[^/.]+)/(?P[^/.]+)', 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)), +] diff --git a/webvirtcloud/urls.py b/webvirtcloud/urls.py index 42785ac..81000e3 100644 --- a/webvirtcloud/urls.py +++ b/webvirtcloud/urls.py @@ -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\.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 +