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 f004552148 feat(instances): a new VM can get a cloud-init seed with its hostname and SSH keys
Cloud images read their first-boot settings with cloud-init. The panel
served them only over HTTP, which a VM reaches only with the network
and a reverse DNS name for its address. A new VM can now carry them
itself: Cloud-init seed (Advanced) on the create form, or cloud_init in
the API.

The job writes a NoCloud seed, an ISO labelled cidata, into the disk's
directory pool as cidata-<uuid>.iso and puts it in the VM's CD-ROM
drive. Its meta-data holds the VM's UUID as instance-id and its name as
hostname, written as JSON so a VM named 123 or yes keeps a string
hostname; its user-data is what the HTTP datasource serves, with the
creating user's SSH keys. The ISO is built with pycdlib and written
through a libvirt stream, as the NVRAM copy is, now one helper for both.

A seed needs a CD-ROM bus and a directory pool, else the create is
refused. A failed create deletes the seed with the new disk, recovery
names one left behind, and deleting the VM with its disks deletes its
seed, found in the pools by name even when ejected. A clone's drive is
left empty: the seed is the source VM's identity.
2026-10-25 19:57:15 +03:00

618 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; }
.create-vm { max-width: 56rem; }
.source-option { cursor: pointer; }
/* the chosen card by its border only: the Bootswatch dark themes keep light "subtle" backgrounds */
.source-option:has(input:checked) { border-color: var(--bs-primary) !important; box-shadow: inset 0 0 0 1px var(--bs-primary); }
.source-option:has(input:focus-visible) { outline: 2px solid var(--bs-primary); outline-offset: 2px; }
.row-list { list-style: none; padding-left: 0; }
.row-list li { overflow-wrap: anywhere; }
</style>
{% endblock %}
{% block page_heading %}
{% blocktrans with host=compute.name %}New instance on {{ host }} {% endblocktrans %}
{% endblock page_heading %}
{% block content %}
{% include 'pleasewaitdialog.html' %}
<div class="create-vm mx-auto">
{% 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" %}
</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"{% error_attrs vm_form.vcpu_mode "vm_vcpu_mode" %}>
<option value="">{% trans 'no-mode' %}</option>
{% 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="row-list 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"></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="row-list 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"></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="card mb-3" aria-labelledby="advanced-heading">
<div class="card-body">
<h2 class="h5 card-title mb-0" id="advanced-heading">
<button class="btn p-0 border-0 text-body fw-medium w-100 text-start focus-ring disclosure" type="button" data-bs-toggle="collapse" data-bs-target="#advanced"
aria-expanded="false" aria-controls="advanced">
{% bs_icon 'chevron-down' %} {% trans "Advanced" %}
</button>
</h2>
<div class="collapse" id="advanced">
<div class="row g-3 pt-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>
</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 %}