1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/templates/create_instance_w2.html
catborise 005fa59bf0 feat(instances): a new VM can have UEFI Secure Boot and a TPM 2.0
Windows 11 does not install without Secure Boot and a TPM 2.0, and the new
VM form offered neither: its firmware list was BIOS and one UEFI loader
guessed from file names, which on RHEL never matched the Secure Boot one.

The firmware choices are now BIOS, UEFI and UEFI Secure Boot, as the
host's libvirt reports them (its firmware features from libvirt 12.1,
before that its loaders), and libvirt picks the firmware itself: UEFI
without Secure Boot, or with Microsoft's keys enrolled. A TPM 2.0 switch,
shown when the host has swtpm, adds an emulated TPM. The create job
refuses a firmware or a TPM the host does not offer, and the API takes
both. The path-guessing helpers are gone.
2026-11-01 16:18:29 +03:00

620 lines
38 KiB
HTML

{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load operations %}
{% load django_bootstrap5 %}
{% load form_fields %}
{% load bootstrap_icons %}
{% block body_attrs %} data-error-focus="manual"{% if refused %} data-refused="1"{% endif %}{% endblock %}
{% block title %}{% trans "Create new instance" %}{% endblock %}
{% block style %}
<link href="{% static "css/tom-select.bootstrap5.min.css" %}" rel="stylesheet">
<style>
/* the chosen images and networks are listed below the pickers, not in them */
#image-control + .ts-wrapper .ts-control > .item, #network-control + .ts-wrapper .ts-control > .item { display: none; }
</style>
{% endblock %}
{% block page_heading %}
{% blocktrans with host=compute.name %}New instance on {{ host }} {% endblocktrans %}
{% endblock page_heading %}
{% block content %}
{% include 'pleasewaitdialog.html' %}
<div>
{% include "create_steps.html" with step=3 %}
{% if vm_form %}
<form method="post" id="create-vm-form" aria-label="{% trans "Create virtual machine" %}"
{% if template_restore %}data-template-pool="{{ template_restore.pool }}" data-template="{{ template_restore.template }}"{% endif %}>
{% csrf_token %}{% submission_key %}
{% if submitted_lists %}{{ submitted_lists|json_script:"submitted-lists" }}{% endif %}
{% include "instances/form_errors.html" with form=vm_form %}
<section class="card mb-3" aria-labelledby="size-heading">
<div class="card-body">
<h2 class="h5 card-title" id="size-heading">{% trans "Name and size" %}</h2>
<div class="mb-3">
<label for="vm_name" class="form-label">{% trans "Name" %}</label>
<input id="vm_name" type="text" class="form-control{{ vm_form.name|invalid }}" name="name"
value="{{ vm_form.name.value|default_if_none:"" }}" maxlength="64" required pattern="[a-zA-Z0-9._\-]+"
autocomplete="off" spellcheck="false"{% error_attrs vm_form.name "vm_name" "vm_name_help" %}>
<div id="vm_name_help" class="form-text">{% trans "Letters, digits, dot, dash and underscore." %}</div>
{% field_error vm_form.name "vm_name" %}
</div>
<div class="mb-3">
<div class="d-flex align-items-center mb-1">
<span class="form-label mb-0" id="presets-label">{% trans "Presets" %}</span>
<button type="button" class="btn btn-link btn-sm ms-auto" data-bs-toggle="modal" data-bs-target="#flavors-dialog">
{% trans "Manage flavors" %}
</button>
</div>
{% include 'create_presets.html' %}
<div id="preset-status" class="visually-hidden" role="status"></div>
</div>
<div class="row g-3">
<div class="col-6">
<label for="vm_vcpu" class="form-label">{% trans "vCPUs" %}</label>
<input id="vm_vcpu" type="number" min="1" inputmode="numeric" class="form-control{{ vm_form.vcpu|invalid }}" name="vcpu"
value="{{ vm_form.vcpu.value|default_if_none:"" }}" required{% error_attrs vm_form.vcpu "vm_vcpu" %}>
{% field_error vm_form.vcpu "vm_vcpu" %}
</div>
<div class="col-6">
<label for="vm_memory" class="form-label">{% trans "Memory (MiB)" %}</label>
<input id="vm_memory" type="number" min="1" inputmode="numeric" class="form-control{{ vm_form.memory|invalid }}" name="memory"
value="{{ vm_form.memory.value|default_if_none:"" }}" required{% error_attrs vm_form.memory "vm_memory" %}>
{% field_error vm_form.memory "vm_memory" %}
</div>
{% if firmwares %}
<div class="col-md-6">
<label for="vm_firmware" class="form-label">{% trans "Firmware" %}</label>
<select id="vm_firmware" class="form-select{{ vm_form.firmware|invalid }}" name="firmware"{% error_attrs vm_form.firmware "vm_firmware" %}>
{% for frm in firmwares %}
<option value="{{ frm }}"{% if frm == vm_form.firmware.value %} selected{% endif %}>{{ frm }}</option>
{% endfor %}
</select>
{% field_error vm_form.firmware "vm_firmware" %}
{% if tpm_support %}
<div class="form-check mt-2">
<input id="vm_tpm" class="form-check-input" type="checkbox" name="tpm" value="true"{% if vm_form.tpm.value %} checked{% endif %} aria-describedby="vm_tpm_help">
<label for="vm_tpm" class="form-check-label">{% trans "TPM 2.0" %}</label>
<div id="vm_tpm_help" class="form-text">{% trans "Windows 11 needs it, with UEFI Secure Boot" %}</div>
</div>
{% endif %}
</div>
{% endif %}
{% if dom_caps.cpu_modes %}
<div class="col-md-6">
<label for="vm_vcpu_mode" class="form-label">{% trans "CPU mode" %}</label>
<select id="vm_vcpu_mode" class="form-select{{ vm_form.vcpu_mode|invalid }}" name="vcpu_mode" required{% error_attrs vm_form.vcpu_mode "vm_vcpu_mode" %}>
{% if not vm_form.vcpu_mode.value %}<option value="" selected disabled>{% trans "Choose…" %}</option>{% endif %}
{% for mode in dom_caps.cpu_modes %}
{% if mode == 'custom' %}
<optgroup label="{% trans "Custom CPU Models" %}">
{% for model in dom_caps.cpu_custom_models %}
<option value="{{ model }}"{% if model == vm_form.vcpu_mode.value %} selected{% endif %}>{{ model }}</option>
{% endfor %}
</optgroup>
{% else %}
<option value="{{ mode }}"{% if mode == vm_form.vcpu_mode.value %} selected{% endif %}>{% trans mode %}</option>
{% endif %}
{% endfor %}
</select>
{% field_error vm_form.vcpu_mode "vm_vcpu_mode" %}
</div>
{% endif %}
</div>
</div>
</section>
<section class="card mb-3" aria-labelledby="disk-heading">
<div class="card-body">
<h2 class="h5 card-title" id="disk-heading">{% trans "Disk" %}</h2>
<fieldset class="mb-3">
<legend class="form-label fs-6">{% trans "Disk source" %}</legend>
<div class="row g-2">
{% for value, title, help in disk_sources %}
<div class="col-md-4">
<label class="source-option border rounded p-2 h-100 d-flex gap-2">
<input class="form-check-input flex-shrink-0 mt-1" type="radio" name="disk_source" value="{{ value }}"{% if value == disk_source %} checked{% endif %}>
<span><span class="d-block fw-semibold">{{ title }}</span><span class="d-block small text-body-secondary">{{ help }}</span></span>
</label>
</div>
{% endfor %}
</div>
</fieldset>
<fieldset id="source-new" class="disk-source"{% if disk_source != "new" %} hidden disabled{% endif %}>
<legend class="visually-hidden">{% trans "New disk" %}</legend>
<div class="row g-3">
<div class="col-md-6">
<label for="new_storage" class="form-label">{% trans "Storage pool" %}</label>
<select id="new_storage" class="form-select{{ vm_form.storage|invalid }}" name="storage"{% error_attrs vm_form.storage "new_storage" %}>
{% for storage in storages %}
<option value="{{ storage }}"{% if storage == vm_form.storage.value %} selected{% endif %}>{{ storage }}</option>
{% endfor %}
</select>
{% field_error vm_form.storage "new_storage" %}
</div>
<div class="col-md-6">
<label for="vm_hdd_size" class="form-label">{% trans "Size (GiB)" %}</label>
<input id="vm_hdd_size" type="number" min="1" inputmode="numeric" class="form-control{{ vm_form.hdd_size|invalid }}" name="hdd_size"
value="{{ vm_form.hdd_size.value|default_if_none:"" }}" required{% error_attrs vm_form.hdd_size "vm_hdd_size" %}>
{% field_error vm_form.hdd_size "vm_hdd_size" %}
</div>
<div class="col-12">
<div class="form-check">
<input id="new_meta_prealloc" class="form-check-input" type="checkbox" name="meta_prealloc" value="true"{% if vm_form.meta_prealloc.value %} checked{% endif %}>
<label for="new_meta_prealloc" class="form-check-label">{% trans "Preallocate metadata (qcow2)" %}</label>
</div>
</div>
</div>
</fieldset>
<fieldset id="source-existing" class="disk-source"{% if disk_source != "existing" %} hidden disabled{% endif %}>
<legend class="visually-hidden">{% trans "Existing volumes" %}</legend>
<input id="images" name="images" type="hidden" value="">
<div class="row g-3">
<div class="col-md-6">
<label for="storage-control" class="form-label">{% trans "Storage pool" %}</label>
<select class="form-select" id="storage-control" name="storage-control">
<option value disabled selected>{% trans "Select pool" %}...</option>
{% for storage in storages %}<option value="{{ storage }}">{{ storage }}</option>{% endfor %}
</select>
</div>
<div class="col-md-6">
<label for="image-control" class="form-label">{% trans "Volumes" %}</label>
<select class="form-select{{ vm_form.images|invalid }}" id="image-control" multiple aria-label="{% trans "Volumes" %}"{% error_attrs vm_form.images "image-control" %}></select>
{% field_error vm_form.images "image-control" %}
</div>
</div>
<ul id="img-list" class="list-unstyled mt-2 mb-0" aria-label="{% trans "Chosen volumes" %}"></ul>
<template id="disk-row">
<li class="d-flex flex-wrap align-items-center gap-2 py-1">
<span class="row-label font-monospace"></span>
<select class="form-select form-select-sm w-auto disk-device" aria-label="{% trans "Device" %}">
{% for dev in disk_devices %}<option value="{{ dev }}">{% trans dev %}</option>{% endfor %}
</select>
<select class="form-select form-select-sm w-auto disk-bus" aria-label="{% trans "Bus" %}">
{% for bus in disk_buses %}<option value="{{ bus }}"{% if bus == default_bus %} selected{% endif %}>{% trans bus %}</option>{% endfor %}
</select>
<span class="row-item text-break"></span>
<button type="button" class="btn btn-sm btn-link ms-auto row-remove" aria-label="{% trans "Remove" %}">{% bs_icon 'x-lg' %}</button>
</li>
</template>
</fieldset>
<fieldset id="source-template" class="disk-source"{% if disk_source != "template" %} hidden disabled{% endif %}>
<legend class="visually-hidden">{% trans "Clone from template" %}</legend>
<div class="row g-3">
<div class="col-md-6">
<label for="template_pool" class="form-label">{% trans "Template pool" %}</label>
<select class="form-select" id="template_pool" name="template_pool">
<option value disabled selected>{% trans "Select pool" %}...</option>
{% for storage in storages %}<option value="{{ storage }}">{{ storage }}</option>{% endfor %}
</select>
</div>
<div class="col-md-6">
<label for="template" class="form-label">{% trans "Template volume" %}</label>
<select id="template" class="form-select{{ vm_form.template|invalid }}" name="template" required{% error_attrs vm_form.template "template" %}></select>
{% field_error vm_form.template "template" %}
</div>
<div class="col-md-6">
<label for="template_storage" class="form-label">{% trans "Copy into pool" %}</label>
<select id="template_storage" class="form-select{{ vm_form.storage|invalid }}" name="storage"{% error_attrs vm_form.storage "template_storage" %}>
{% for storage in storages %}
<option value="{{ storage }}"{% if storage == vm_form.storage.value %} selected{% endif %}>{{ storage }}</option>
{% endfor %}
</select>
{% field_error vm_form.storage "template_storage" %}
</div>
<div class="col-md-6 align-self-end">
<div class="form-check">
<input id="template_meta_prealloc" class="form-check-input" type="checkbox" name="meta_prealloc" value="true"{% if vm_form.meta_prealloc.value %} checked{% endif %}>
<label for="template_meta_prealloc" class="form-check-label">{% trans "Preallocate metadata (qcow2)" %}</label>
</div>
</div>
</div>
</fieldset>
</div>
</section>
<section class="card mb-3" aria-labelledby="network-heading">
<div class="card-body">
<h2 class="h5 card-title" id="network-heading">{% trans "Networks" %}</h2>
<label for="network-control" class="form-label">{% trans "Add networks" %}</label>
<select class="form-select{{ vm_form.networks|invalid }}" id="network-control" multiple aria-label="{% trans "Networks" %}"{% error_attrs vm_form.networks "network-control" %}>
{% for network in networks %}<option value="{{ network }}">{{ network }}</option>{% endfor %}
</select>
{% field_error vm_form.networks "network-control" %}
<input id="networks" type="hidden" name="networks" value="">
<input id="vm_mac" type="hidden" name="mac" value="">
<ul id="net-list" class="list-unstyled mt-2 mb-0" aria-label="{% trans "Network interfaces" %}"></ul>
<template id="network-row">
<li class="d-flex flex-wrap align-items-center gap-2 py-1">
<span class="row-label font-monospace"></span>
<span class="row-item text-break"></span>
<input type="text" class="form-control form-control-sm w-auto font-monospace" maxlength="17" size="17" required
autocomplete="off" autocapitalize="none" spellcheck="false"
pattern="[a-fA-F0-9:]+" aria-label="{% trans "MAC address" %}">
<button type="button" class="btn btn-sm btn-link ms-auto row-remove" aria-label="{% trans "Remove" %}">{% bs_icon 'x-lg' %}</button>
</li>
</template>
{% field_error vm_form.mac "vm_mac" %}
</div>
</section>
<section class="accordion mb-3" aria-labelledby="advanced-heading">
<div class="accordion-item">
<h2 class="accordion-header" id="advanced-heading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#advanced"
aria-expanded="false" aria-controls="advanced">
{% trans "Advanced" %}
</button>
</h2>
<div class="accordion-collapse collapse" id="advanced">
<div class="accordion-body">
<div class="row g-3">
<div class="col-md-6">
<label for="vm_cache_mode" class="form-label">{% trans "Disk cache mode" %}</label>
<select id="vm_cache_mode" class="form-select{{ vm_form.cache_mode|invalid }}" name="cache_mode"{% error_attrs vm_form.cache_mode "vm_cache_mode" %}>
{% for mode, name in cache_modes %}
<option value="{{ mode }}"{% if mode == vm_form.cache_mode.value %} selected{% endif %}>{% trans name %}</option>
{% endfor %}
</select>
{% field_error vm_form.cache_mode "vm_cache_mode" %}
</div>
<div class="col-md-6">
<label for="vm_net_model" class="form-label">{% trans "NIC type" %}</label>
<select id="vm_net_model" class="form-select{{ vm_form.net_model|invalid }}" name="net_model"{% error_attrs vm_form.net_model "vm_net_model" %}>
{% for model in net_models_host %}
<option value="{{ model }}"{% if model == vm_form.net_model.value %} selected{% endif %}>{{ model }}</option>
{% endfor %}
</select>
{% field_error vm_form.net_model "vm_net_model" %}
</div>
<div class="col-md-6">
<label for="vm_nwfilter" class="form-label">{% trans "Network filter" %}</label>
<select id="vm_nwfilter" class="form-select{{ vm_form.nwfilter|invalid }}" name="nwfilter"{% error_attrs vm_form.nwfilter "vm_nwfilter" %}>
<option value="">{% trans "None" %}</option>
{% for nwfilter in nwfilters %}
<option value="{{ nwfilter }}"{% if nwfilter == vm_form.nwfilter.value %} selected{% endif %}>{{ nwfilter }}</option>
{% endfor %}
</select>
{% field_error vm_form.nwfilter "vm_nwfilter" %}
</div>
<div class="col-md-6">
<label for="vm_video" class="form-label">{% trans "Video" %}</label>
<select id="vm_video" class="form-select{{ vm_form.video|invalid }}" name="video"{% error_attrs vm_form.video "vm_video" %}>
{% if not videos %}<option value="vga">vga</option><option value="cirrus">cirrus</option>{% endif %}
{% for video in videos %}
<option value="{{ video }}"{% if video == vm_form.video.value %} selected{% endif %}>{{ video }}</option>
{% endfor %}
</select>
{% field_error vm_form.video "vm_video" %}
</div>
<div class="col-md-6">
<label for="vm_listener_addr" class="form-label">{% trans "Console access" %}</label>
<select id="vm_listener_addr" class="form-select{{ vm_form.listener_addr|invalid }}" name="listener_addr"{% error_attrs vm_form.listener_addr "vm_listener_addr" %}>
{% for addr, label in listener_addr %}
<option value="{{ addr }}"{% if addr == vm_form.listener_addr.value %} selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% field_error vm_form.listener_addr "vm_listener_addr" %}
</div>
<div class="col-md-6">
<label for="vm_add_cdrom" class="form-label">{% trans "CD-ROM" %}</label>
<select id="vm_add_cdrom" class="form-select{{ vm_form.add_cdrom|invalid }}" name="add_cdrom"{% error_attrs vm_form.add_cdrom "vm_add_cdrom" %}>
<option value="None"{% if vm_form.add_cdrom.value == "None" %} selected{% endif %}>{% trans "None" %}</option>
{% for bus in cdrom_buses %}
<option value="{{ bus }}"{% if bus == vm_form.add_cdrom.value %} selected{% endif %}>{% trans bus %}</option>
{% endfor %}
</select>
{% field_error vm_form.add_cdrom "vm_add_cdrom" %}
</div>
<div class="col-md-6">
<label for="vm_add_input" class="form-label">{% trans "Input device" %}</label>
<select id="vm_add_input" class="form-select{{ vm_form.add_input|invalid }}" name="add_input"{% error_attrs vm_form.add_input "vm_add_input" %}>
<option value="None"{% if vm_form.add_input.value == "None" %} selected{% endif %}>{% trans "None" %}</option>
{% for bus in input_device_buses %}
<option value="{{ bus }}"{% if bus == vm_form.add_input.value %} selected{% endif %}>{% trans bus %}</option>
{% endfor %}
</select>
{% field_error vm_form.add_input "vm_add_input" %}
</div>
<div class="col-md-6 d-flex flex-column justify-content-end">
<div class="form-check">
<input id="vm_qemu_ga" class="form-check-input" type="checkbox" name="qemu_ga" value="true"{% if vm_form.qemu_ga.value %} checked{% endif %}>
<label for="vm_qemu_ga" class="form-check-label">{% trans "Guest agent" %}</label>
</div>
<div class="form-check">
<input id="vm_cloud_init" class="form-check-input" type="checkbox" name="cloud_init" value="true"{% if vm_form.cloud_init.value %} checked{% endif %} aria-describedby="vm_cloud_init_help">
<label for="vm_cloud_init" class="form-check-label">{% trans "Cloud-init seed" %}</label>
<div id="vm_cloud_init_help" class="form-text">{% trans "The hostname and your SSH keys on the CD-ROM, for cloud images" %}</div>
</div>
{% if virtio_support %}
<div class="form-check">
<input id="vm_virtio" class="form-check-input" type="checkbox" name="virtio" value="{{ virtio_support }}"{% if vm_form.virtio.value %} checked{% endif %}>
<label for="vm_virtio" class="form-check-label">{% trans "VirtIO devices" %}</label>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
<div class="d-flex flex-wrap gap-2 justify-content-end mb-4">
<a class="btn btn-link" href="{% url 'instances' compute.id %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-primary" name="create" value="1" formnovalidate>{% trans "Create VM" %}</button>
</div>
</form>
{% endif %}
</div>
<div class="modal fade" id="flavors-dialog" tabindex="-1" aria-labelledby="flavors-dialog-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title h5" id="flavors-dialog-title">{% trans "Flavors" %}</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
</div>
{% include 'create_flavors.html' %}
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script src="{% static 'js/tom-select.complete.min.js' %}"></script>
<script>
const COMPUTE = {{ compute_id }};
let images_control, networks_control;
// the volume and network pickers: each chosen item is a row below its picker
function make_pickers() {
images_control = new TomSelect('#image-control', {
plugins: ['checkbox_options'],
placeholder: '{{ _("Add volume")|escapejs }}',
hidePlaceholder: false,
onItemAdd: function (value) { if (!row_for("#img-list", value)) add_disk_row(value); },
onItemRemove: function (value) { remove_row("#img-list", value); },
});
networks_control = new TomSelect('#network-control', {
plugins: ['checkbox_options'],
placeholder: '{{ _("Add network")|escapejs }}',
hidePlaceholder: false,
onItemAdd: function (value) { if (!row_for("#net-list", value)) add_network_row(value, ""); },
onItemRemove: function (value) { remove_row("#net-list", value); },
});
}
$(document).ready(function () {
if (!document.querySelector("#create-vm-form")) return;
make_pickers();
document.querySelectorAll('input[name="disk_source"]').forEach(function (radio) {
radio.addEventListener("change", function () {
show_source(this.value);
if (this.value !== "new") clear_presets(); // its size is not sent
});
});
show_source(document.querySelector('input[name="disk_source"]:checked').value);
$("#create-vm-form").on("click", ".preset", function () { apply_preset(this); });
$("#flavors-dialog").on("submit", "form", function (event) {
event.preventDefault();
$.post(this.action, $(this).serialize(), show_flavors).fail(() => this.submit());
});
["#vm_vcpu", "#vm_memory", "#vm_hdd_size"].forEach(function (sel) {
document.querySelector(sel).addEventListener("input", clear_presets);
});
document.querySelector("#storage-control").addEventListener("change", function () { load_volumes(this.value); });
document.querySelector("#template_pool").addEventListener("change", function () { template_pool_chosen(this.value); });
if (document.body.dataset.refused) restore_submitted();
else add_first_network();
});
// a new VM starts with one interface: on "default" if the host has it
function add_first_network() {
const names = Object.keys(networks_control.options);
if (names.length) networks_control.addItem(names.includes("default") ? "default" : names[0]);
}
// Only the chosen disk source's fields are shown and sent: the others
// are disabled, so the browser neither submits nor validates them.
function show_source(source) {
document.querySelectorAll(".disk-source").forEach(function (fieldset) {
const on = fieldset.id === "source-" + source;
fieldset.hidden = !on;
fieldset.disabled = !on;
});
images_control[source === "existing" ? "enable" : "disable"]();
}
// A preset fills the size fields and picks a new disk; editing one of
// them afterwards leaves no preset marked as applied.
function apply_preset(button) {
document.querySelector("#vm_vcpu").value = button.dataset.vcpu;
document.querySelector("#vm_memory").value = button.dataset.memory;
document.querySelector("#vm_hdd_size").value = button.dataset.disk;
document.querySelector('input[name="disk_source"][value="new"]').checked = true;
show_source("new");
clear_presets();
button.setAttribute("aria-pressed", "true");
button.classList.replace("text-body", "btn-primary");
document.querySelector("#preset-status").textContent =
`${button.dataset.label}: ${button.dataset.vcpu} vCPU, ${button.dataset.memory} MiB, new ${button.dataset.disk} GiB disk`;
}
function clear_presets() {
document.querySelectorAll(".preset").forEach(function (b) {
b.setAttribute("aria-pressed", "false");
b.classList.replace("btn-primary", "text-body");
});
}
// Adding or deleting a flavor stays on the page: its reply replaces the
// presets and the dialog's list and form, and the dialog keeps the focus.
function show_flavors(reply) {
const parts = new DOMParser().parseFromString(reply, "text/html");
["#presets", "#flavors"].forEach(sel => document.querySelector(sel).replaceWith(parts.querySelector(sel)));
document.querySelector("#flavors .is-invalid, #flavors input:not([type=hidden])").focus();
}
// Disk and network rows: one per chosen volume or network, cloned from
// the page's row templates; the fields the server reads are derived
// from them in sync_lists.
function new_row(list, template, item) {
const row = document.getElementById(template).content.firstElementChild.cloneNode(true);
row.dataset.item = item;
row.querySelector(".row-item").textContent = item;
document.querySelector(list).append(row);
return row;
}
function add_disk_row(volume) {
const row = new_row("#img-list", "disk-row", volume);
const bus = row.querySelector(".disk-bus");
row.querySelector(".disk-device").addEventListener("change", function () {
load_buses(bus, this.value);
});
row.querySelector(".row-remove").addEventListener("click", function () { images_control.removeItem(volume); });
sync_lists();
return row;
}
// a network row without a MAC gets a random one
function add_network_row(network, mac) {
const row = new_row("#net-list", "network-row", network);
const input = row.querySelector("input");
input.value = mac;
input.addEventListener("input", sync_lists);
row.querySelector(".row-remove").addEventListener("click", function () { networks_control.removeItem(network); });
if (!mac) random_mac(input);
sync_lists();
return row;
}
function row_for(list, item) {
return [...document.querySelector(list).children].find(row => row.dataset.item === item);
}
function remove_row(list, item) {
row_for(list, item)?.remove();
sync_lists();
}
// volumes, networks and their MACs in row order; device<i> and bus<i>
// belong to the i-th volume
function sync_lists() {
const disks = [...document.querySelector("#img-list").children];
const nics = [...document.querySelector("#net-list").children];
disks.forEach(function (row, i) {
row.querySelector(".row-label").textContent = "hdd" + i;
row.querySelector(".disk-device").name = "device" + i;
row.querySelector(".disk-bus").name = "bus" + i;
});
nics.forEach(function (row, i) {
row.querySelector(".row-label").textContent = "eth" + i;
});
document.querySelector("#images").value = disks.map(row => row.dataset.item).join(",");
document.querySelector("#networks").value = nics.map(row => row.dataset.item).join(",");
document.querySelector("#vm_mac").value = nics.map(row => row.querySelector("input").value).join(",");
}
// a MAC typed while the request was out stays
function random_mac(input) {
$.getJSON("{% url 'instances:random_mac_address' %}", function (data) {
if (!input.value) input.value = data['mac'];
sync_lists();
});
}
function volumes_url(pool) {
return "/computes/" + COMPUTE + "/storage/" + encodeURIComponent(pool) + "/volumes";
}
// A dependent list is empty while it loads, and a newer load aborts the
// older one: an answer for the previous choice never fills it.
function load_list(list, url, fill) {
if (list.request) list.request.abort();
list.request = $.getJSON(url, fill);
return list.request;
}
function load_volumes(pool) {
images_control.clearOptions(); // keeps the chosen volumes
return load_list(images_control, volumes_url(pool), function (data) {
data['vols'].forEach(item => images_control.addOption({value: item, text: item}));
});
}
// the copy goes into the template's pool unless the user picks another
function template_pool_chosen(pool) {
select_value(document.querySelector("#template_storage"), pool);
return load_templates(pool);
}
function load_templates(pool) {
const list = document.querySelector("#template");
list.replaceChildren();
return load_list(list, volumes_url(pool), function (data) {
list.replaceChildren(...data['vols'].map(item => new Option(item, item)));
});
}
function load_buses(bus_select, disk_type) {
const url = "/computes/" + COMPUTE + "/archs/{{ arch|escapejs }}/machines/{{ machine|escapejs }}/disks/" + disk_type + "/buses";
bus_select.replaceChildren();
return load_list(bus_select, url, function (data) {
bus_select.replaceChildren(...data['bus'].map(item => new Option(item, item)));
});
}
// a value the loaded list no longer has is kept, not swapped for the first
function select_value(select, value) {
if (![...select.options].some(option => option.value === value)) select.add(new Option(value, value));
select.value = value;
}
// A refused form comes back as it was sent: its networks, volumes and
// template loaded first, any Advanced field with an error shown, then
// the focus on the first error.
function restore_submitted() {
const form = document.querySelector("#create-vm-form");
const element = document.getElementById("submitted-lists");
const lists = element ? JSON.parse(element.textContent) : {networks: [], disks: [], pool: ""};
lists.networks.forEach(function (nic) {
networks_control.addOption({value: nic.name, text: nic.name});
add_network_row(nic.name, nic.mac);
networks_control.addItem(nic.name, true);
});
const loads = [];
if (lists.pool) {
select_value(document.querySelector("#storage-control"), lists.pool);
loads.push(load_volumes(lists.pool).then(function () {
return $.when(...lists.disks.map(function (disk) {
images_control.addOption({value: disk.volume, text: disk.volume});
const row = add_disk_row(disk.volume);
images_control.addItem(disk.volume, true);
const bus = row.querySelector(".disk-bus");
select_value(row.querySelector(".disk-device"), disk.device);
return load_buses(bus, disk.device).then(() => select_value(bus, disk.bus));
}));
}));
}
if (form.dataset.templatePool) {
select_value(document.querySelector("#template_pool"), form.dataset.templatePool);
loads.push(load_templates(form.dataset.templatePool).then(function () {
select_value(document.querySelector("#template"), form.dataset.template);
}));
}
const advanced = document.querySelector("#advanced");
if (advanced.querySelector(".is-invalid")) bootstrap.Collapse.getOrCreateInstance(advanced, {toggle: false}).show();
$.when(...loads).then(focusFirstError);
}
</script>
{% endblock %}