1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/forms.py
catborise f1f52aac42 refactor: the API's serializers and the new VM form no longer need models
Networks, Interfaces, Storages, Storage, Volume, MigrateInstance and
CreateInstance were unmanaged models with no table, there only so that
ModelSerializers and the new VM ModelForm could take their fields. They
made content types and migrations, and their DO_NOTHING foreign keys
added reverse accessors that meant nothing (Compute.host,
Compute.target_host, Instance.source_host).

The serializers are plain Serializers and NewVMForm a Form, field for
field as the models gave them (types, lengths, required, defaults, the
minimum of 1 for vCPUs, memory and disk size; the form's labels are
still derived from the field names). Compared field by field before and
after, the API schema changes only where the models were wrong:
- no int64 bounds on the listing fields;
- the interface fields lose the titles that named the wrong column
  (type was "Status", state "Device", mac "Forward");
- a pool's volumes is a list of objects (it was described as an
  integer) and a volume's allocation an integer (it was untyped).
One migration per app deletes the models; none touches a table.
2026-11-06 13:46:27 +03:00

152 lines
6.5 KiB
Python

import re
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from .models import Flavor
from .utils import nic_macs
class FlavorForm(forms.ModelForm):
# the units the create form fills in from a flavor
memory = forms.IntegerField(label=_("Memory (MiB)"), min_value=1)
vcpu = forms.IntegerField(label=_("vCPUs"), min_value=1)
disk = forms.IntegerField(label=_("Disk (GiB)"), min_value=1)
class Meta:
model = Flavor
fields = "__all__"
class ConsoleForm(forms.Form):
console_type = forms.ChoiceField(
label=_("Console type"), required=False, widget=forms.Select(attrs={"class": "form-select"})
)
password_action = forms.ChoiceField(
label=_("Password"),
choices=[
("keep", _("Keep the password")),
("generate", _("Generate a new one")),
("set", _("Set a new one")),
("none", _("No password")),
],
widget=forms.RadioSelect(attrs={"class": "form-check-input"}),
)
password = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(attrs={"class": "form-control", "autocomplete": "new-password"}),
required=False,
strip=False,
)
def __init__(self, *args, instance, is_superuser, **kwargs):
# the VM's settings as the initial values: a save changes only the fields edited
self.instance = instance
self.is_superuser = is_superuser
listener = instance.config_console_listener_address
kwargs["initial"] = {
"console_type": instance.config_console_type,
"listen_on": listener,
"password_action": "keep",
"keymap": instance.config_console_keymap or "auto",
}
super(ConsoleForm, self).__init__(*args, **kwargs)
types = [("vnc", "VNC")]
if settings.SERIAL_CONSOLE_ENABLED:
types.append(("pty", _("Serial (xterm; the guest needs a getty on ttyS0)")))
self.fields["console_type"].choices = types
# the VNC settings are shown and saved while the console is VNC; a
# switch to Serial ignores them
if "pty" in (instance.config_console_type, self.data.get("console_type")):
del self.fields["password_action"], self.fields["password"]
return
keymap_choices = [("auto", _("Auto"))] + list((c, c) for c in settings.QEMU_KEYMAPS)
# Only administrators choose the listen address: beyond localhost the
# console is reachable from the network. A disabled field ignores
# posted values.
listeners = list(settings.QEMU_CONSOLE_LISTENER_ADDRESSES)
# an address set elsewhere (the XML editor) is offered, so a save keeps it
if listener and listener not in dict(listeners):
listeners.append((listener, listener))
self.fields["listen_on"] = forms.ChoiceField(
label=_("Listen on"),
choices=listeners,
disabled=not is_superuser,
required=is_superuser,
widget=forms.Select(attrs={"class": "form-select"}),
)
self.fields["keymap"] = forms.ChoiceField(
label=_("Keymap"), choices=keymap_choices, widget=forms.Select(attrs={"class": "form-select"})
)
def clean(self):
data = super().clean()
action = data.get("password_action")
if action == "set":
password = data.get("password", "")
if not password:
self.add_error("password", _("Enter the new password."))
# VNC uses 8 bytes; libvirt refuses a longer password
elif len(password.encode()) > 8:
self.add_error("password", _("A VNC password takes at most 8 bytes."))
# A console that listens beyond localhost (set by an administrator)
# keeps its password unless an administrator removes it.
if action == "none" and not self.is_superuser and not self.instance.proxy.console_listens_locally():
self.add_error(
"password_action",
_("The console listens beyond localhost; only an administrator can remove its password."),
)
return data
class NewVMForm(forms.Form):
"""The new VM form (create_instance_w2.html); the API takes CreateInstanceSerializer."""
name = forms.CharField(max_length=64)
firmware = forms.CharField(max_length=64)
# an emulated TPM 2.0, as Windows 11 needs
tpm = forms.BooleanField(required=False)
vcpu = forms.IntegerField(min_value=1)
# a mode of libvirt's (host-model, host-passthrough, maximum) or a CPU model's name
vcpu_mode = forms.CharField(max_length=64)
disk = forms.IntegerField(required=False)
memory = forms.IntegerField(min_value=1)
networks = forms.CharField(max_length=256)
nwfilter = forms.CharField(max_length=256, required=False)
net_model = forms.CharField(max_length=256, required=False)
storage = forms.CharField(max_length=256, required=False)
template = forms.CharField(max_length=256, required=False)
images = forms.CharField(max_length=256, required=False)
cache_mode = forms.CharField(max_length=16)
hdd_size = forms.IntegerField(min_value=1, required=False)
meta_prealloc = forms.BooleanField(required=False)
virtio = forms.BooleanField(required=False, initial=True)
qemu_ga = forms.BooleanField(required=False)
mac = forms.CharField(max_length=255, required=False) # one per NIC, comma separated
add_cdrom = forms.CharField(max_length=16)
# a NoCloud seed on a CD-ROM: the hostname and the creating user's SSH keys
cloud_init = forms.BooleanField(required=False)
add_input = forms.CharField(max_length=16)
video = forms.CharField(max_length=16)
listener_addr = forms.ChoiceField(choices=settings.QEMU_CONSOLE_LISTENER_ADDRESSES)
def clean_name(self):
name = self.cleaned_data["name"]
have_symbol = re.match("^[a-zA-Z0-9._-]+$", name)
if not have_symbol:
raise forms.ValidationError(
_(
"The name of the virtual machine must not contain any special characters"
)
)
return name
def clean(self):
# one MAC per NIC at most, each a MAC address, as the job checks
cleaned = super().clean()
try:
nic_macs(cleaned.get("mac") or "", cleaned.get("networks") or "")
except ValueError as err:
self.add_error("mac", str(err))
return cleaned