mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
The machine types, volumes, templates and disk buses load when their parent changes. A slow answer for an earlier choice could arrive last and fill the list for the newer one, and while a list loaded it still held the previous choice's items, which could be submitted. Each list now empties when its load starts, and a newer load aborts the older request, so only the answer for the current choice fills it. Chosen volumes stay: they are found by name in any pool. Fixes #177 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
613 lines
37 KiB
HTML
613 lines
37 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>
|
|
{% 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 %}
|