1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

feat(instances): a VM's disks as cards, its CD-ROM drives above them

Settings > Disk was a bold title with an unlabelled "+", a CD-ROM row
in an input group, and a table whose actions were four alike icons.
The owner chose this layout from three rendered options:

- Removable media: each CD-ROM drive a card with its device, bus and
  image, the ISO select and Mount (or Unmount), and Detach; "Add
  CD-ROM" by the heading.
- Disks: a card each; one disk spans the row, two or more sit side by
  side from md, and the device details are capped at 32rem so values
  stay near their labels. The capacity large,
  the used size, the pool and the source path, the device's settings
  under "Device details" (they were in a popover), and labelled Edit,
  Move, Detach and Delete (danger) in its footer; "Add disk" by the
  heading.
- While the VM runs, "Add CD-ROM" adds the drive on the SCSI bus, and
  a SCSI drive can be detached: IDE and SATA drives cannot be plugged
  into or out of a running VM, SCSI ones can. Adding a drive was
  disabled while the VM ran. One line says so, and why detaching other
  drives and detaching or deleting disks waits for a shutdown, instead
  of each button alone being greyed.

The forms, their fields and actions are unchanged. The edit dialog of each
disk is titled with its device, and its fields' ids carry the disk's
number: the dialogs of two disks had the same ids, and the title was not
the dialog's label.
This commit is contained in:
catborise 2026-10-29 22:54:56 +03:00
parent 935a0e9bdc
commit 0f778fe9f0
8 changed files with 169 additions and 207 deletions

View file

@ -1,9 +1,7 @@
{% load i18n %}
{% load bootstrap_icons %}
{% if request.user.is_superuser %}
<a href="#addvol" type="button" class="btn btn-primary float-end" data-bs-toggle="modal" title="{% trans "Add Volume" %}">
<span aria-hidden="true">{% bs_icon 'plus'%} </span>
</a>
<a href="#addvol" type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal">{% bs_icon 'plus' %} {% trans "Add disk" %}</a>
<!-- Modal pool -->
<div class="modal fade" id="addvol" tabindex="-1" role="dialog" aria-labelledby="addInstanceVolumeLabel" aria-hidden="true">

View file

@ -1,16 +1,14 @@
{% load i18n %}
{% load bootstrap_icons %}
{% if request.user.is_superuser %}
<button href="#editvol{{ id }}" type="button" class="btn btn-sm btn-secondary" data-bs-toggle="modal" title="{% trans "Edit Volume" %}">
{% bs_icon 'pencil-square' %}
</button>
<button href="#editvol{{ id }}" type="button" class="btn btn-sm btn-secondary" data-bs-toggle="modal">{% trans "Edit" %}</button>
<!-- Modal pool -->
<div class="modal fade" id="editvol{{ id }}" tabindex="-1" role="dialog" aria-labelledby="editInstanceVolumeLabel{{ id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Edit Instance Volume" %}</h5>
<h5 class="modal-title" id="editInstanceVolumeLabel{{ id }}">{% blocktrans with dev=disk.dev %}Edit {{ dev }}{% endblocktrans %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
@ -36,8 +34,8 @@
<div class="tab-content">
<div class="tab-pane active" role="tabpanel" id="VirtualDisk{{ id }}">
<div class="mb-3">
<label class="form-label" for="vol_path">{% trans 'Volume Path' %}</label>
<input class="form-control" id="vol_path" name="vol_path" value="{{ disk.path }}"/>
<label class="form-label" for="vol_path{{ id }}">{% trans 'Volume Path' %}</label>
<input class="form-control" id="vol_path{{ id }}" name="vol_path" value="{{ disk.path }}"/>
</div>
<div class="mb-3 form-check form-switch">
<label class="form-check-label">{% trans 'Readonly' %}</label>
@ -51,32 +49,32 @@
<div class="tab-pane" role="tabpanel" id="PerformanceVolume{{ id }}">
<div class="mb-3">
<label class="form-label" for="vol_cache">{% trans 'Cache mode' %}</label>
<select class="form-select" id="vol_cache" name="vol_cache">
<label class="form-label" for="vol_cache{{ id }}">{% trans 'Cache mode' %}</label>
<select class="form-select" id="vol_cache{{ id }}" name="vol_cache">
{% for key, val in instance.cache_modes %}
<option value="{{ key }}" {% if key == disk.cache %}selected{% endif %}>{{ val }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label" id="vol_io_mode">{% trans 'IO mode' %}</label>
<select class="form-select" name="vol_io_mode">
<label class="form-label" for="vol_io_mode{{ id }}">{% trans 'IO mode' %}</label>
<select class="form-select" id="vol_io_mode{{ id }}" name="vol_io_mode">
{% for key, val in instance.io_modes %}
<option value="{{ key }}" {% if key == disk.io %}selected{% endif %}>{{ val }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label" for="vol_discard_mode">{% trans 'Discard mode' %}</label>
<select class="form-select" id="vol_discard_mode" name="vol_discard_mode">
<label class="form-label" for="vol_discard_mode{{ id }}">{% trans 'Discard mode' %}</label>
<select class="form-select" id="vol_discard_mode{{ id }}" name="vol_discard_mode">
{% for key, val in instance.discard_modes %}
<option value="{{ key }}" {% if key == disk.discard %}selected{% endif %}>{{ val }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label" for="vol_detect_zeroes">{% trans 'Detect zeroes' %}</label>
<select class="form-select" id="vol_detect_zeroes" name="vol_detect_zeroes">
<label class="form-label" for="vol_detect_zeroes{{ id }}">{% trans 'Detect zeroes' %}</label>
<select class="form-select" id="vol_detect_zeroes{{ id }}" name="vol_detect_zeroes">
{% for key, val in instance.detect_zeroes_modes %}
<option value="{{ key }}" {% if key == disk.detect_zeroes %}selected{% endif %}>{{ val }}</option>
{% endfor %}
@ -87,20 +85,20 @@
<div class="tab-pane" role="tabpanel" id="AdvancedVolume{{ id }}">
<div class="mb-3">
<label class="form-label" for="vol_bus">{% trans 'Bus' %}</label>
<select class="form-select" id="vol_Bus" name="vol_bus" {% if instance.status != 5 %} disabled {% endif %}>
<label class="form-label" for="vol_bus{{ id }}">{% trans 'Bus' %}</label>
<select class="form-select" id="vol_bus{{ id }}" name="vol_bus" {% if instance.status != 5 %} disabled {% endif %}>
{% for bus in bus_host %}
<option value="{{ bus }}" {% if bus == disk.bus %}selected{% endif %}>{{ bus }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label" for="vol_serial">{% trans 'Serial Number' %}</label>
<input class="form-control" id="vol_serial" name="vol_serial" type="text" value="{{ disk.serial }}"/>
<label class="form-label" for="vol_serial{{ id }}">{% trans 'Serial Number' %}</label>
<input class="form-control" id="vol_serial{{ id }}" name="vol_serial" type="text" value="{{ disk.serial }}"/>
</div>
<div class="mb-3">
<label class="form-label" for="vol_format">{% trans 'Storage Format' %}</label>
<input class="form-control" id="vol_format" name="vol_format" type="text" value="{{ disk.format }}"/>
<label class="form-label" for="vol_format{{ id }}">{% trans 'Storage Format' %}</label>
<input class="form-control" id="vol_format{{ id }}" name="vol_format" type="text" value="{{ disk.format }}"/>
</div>
</div>

View file

@ -2,9 +2,7 @@
{% load bootstrap_icons %}
{% load operations %}
{# the Move button of a disk and its dialog: another storage pool of this host #}
<button type="button" class="btn btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#movedisk{{ id }}" title="{% trans "Move to another pool" %}">
{% bs_icon 'arrow-left-right' %}
</button>
<button type="button" class="btn btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#movedisk{{ id }}" title="{% trans "Move to another pool" %}">{% trans "Move" %}</button>
<div class="modal fade" id="movedisk{{ id }}" tabindex="-1" aria-labelledby="movedisk{{ id }}-label" aria-hidden="true">
<div class="modal-dialog">
<form class="modal-content text-wrap" action="{% url 'instances:move_disk' instance.id %}" method="post">

View file

@ -170,191 +170,130 @@
<div class="clearfix"></div>
</div>
<div role="tabpanel" class="tab-pane" id="disks">
<form action="{% url 'instances:add_cdrom' instance.id %}" method="post" role="form" aria-label="Add CD-ROM form">{% csrf_token %}
<p class="fw-bold">
{% trans "Instance Media" %}
<button class="btn btn-primary float-end"
type="submit"
title="{% trans 'Add CD-ROM' %}"
{% if instance.status != 5 %} disabled {% endif %}>
<span>{% bs_icon 'plus' %} </span>
</button>
</p>
</form>
<div class="clearfix"></div>
{# its CD-ROM drives, then its disks as cards; the forms are those of the actions #}
{% if instance.status != 5 %}
<p class="small text-body-secondary">{% bs_icon 'lock' %} {% trans "The VM is running: CD-ROM drives added now use the SCSI bus. Shut it down to detach other CD-ROM drives, or to detach or delete disks." %}</p>
{% endif %}
<section class="mb-4" aria-labelledby="media-heading">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<h2 class="h6 mb-0" id="media-heading">{% trans "Removable media" %}</h2>
<form action="{% url 'instances:add_cdrom' instance.id %}" method="post" aria-label="{% trans "Add CD-ROM form" %}">{% csrf_token %}
<button class="btn btn-sm btn-secondary" type="submit">{% bs_icon 'plus' %} {% trans "Add CD-ROM" %}</button>
</form>
</div>
{% for cd in instance.media %}
<div class="row mt-2">
<div class="col-sm-12">
{% if not cd.image %}
<form action="{% url 'instances:mount_iso' instance.id %}" method="post">
{% csrf_token %}
<div class="input-group">
<a class="btn btn-secondary"
role="button"
name="details"
title="{% trans 'Details' %}"
tabindex="0"
data-trigger="focus"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-content="<strong>{% trans 'Bus' %}:</strong> {{ cd.bus }} <br/>
<strong>{% trans 'Device' %}:</strong> {{ cd.dev }}">
{% trans "CD-ROM" %} {{ forloop.counter }}
</a>
<select name="media" class="form-select">
{% if instance.media_iso %}
{% for iso in instance.media_iso %}
<option value="{{ iso }}">{{ iso }}</option>
{% endfor %}
{% else %}
<option value="none">{% trans "None" %}</option>
{% endif %}
</select>
{% if instance.media_iso %}
<button type="submit" class="btn btn-sm btn-primary" name="mount_iso" value="{{ cd.dev }}">{% trans "Mount" %}</button>
{% else %}
<button class="btn btn-sm btn-primary disabled" disabled>{% trans "Mount" %}</button>
{% endif %}
{% if instance.status == 5 %}
<button type="submit" class="btn btn-sm btn-danger float-end" formaction="{% url 'instances:detach_cdrom' instance.id cd.dev %}" formmethod="post" title="{% trans "Detach CD-ROM (remove device)" %}">
{% bs_icon 'dash-circle' %}
</button>
{% endif %}
<div class="card mb-2">
<form class="card-body" method="post" action="{% if cd.image %}{% url 'instances:unmount_iso' instance.id %}{% else %}{% url 'instances:mount_iso' instance.id %}{% endif %}">
{% csrf_token %}
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-2">
<div class="d-flex align-items-center gap-3">
<span class="fs-4 text-body-secondary" aria-hidden="true">{% bs_icon 'disc' %}</span>
<div>
<div class="fw-semibold">{% trans "CD-ROM" %} {{ forloop.counter }}</div>
<div class="small text-body-secondary">{{ cd.dev }} · {{ cd.bus }} · {% if cd.image %}{{ cd.image }}{% else %}{% trans "No media" %}{% endif %}</div>
</div>
</form>
</div>
{% if not cd.image %}
<button type="submit" class="btn btn-sm btn-secondary" formaction="{% url 'instances:detach_cdrom' instance.id cd.dev %}" formmethod="post"{% if instance.status != 5 and cd.bus != "scsi" %} disabled{% endif %}>{% trans "Detach" %}</button>
{% endif %}
</div>
{% if cd.image %}
<input type="hidden" name="path" value="{{ cd.path }}">
<button type="submit" class="btn btn-sm btn-primary" value="{{ cd.dev }}" name="umount_iso">{% bs_icon 'eject' %} {% trans "Unmount" %}</button>
{% else %}
<form action="{% url 'instances:unmount_iso' instance.id %}" method="post">
{% csrf_token %}
<div class="input-group">
<a class="btn btn-secondary"
role="button"
name="details"
title="{% trans 'Details' %}"
tabindex="0"
data-trigger="focus"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-content="<strong>{% trans 'Bus' %}:</strong> {{ cd.bus }} <br/>
<strong>{% trans 'Device' %}:</strong> {{ cd.dev }}">
{% trans "CD-ROM" %} {{ forloop.counter }}
</a>
<input type="text" class="form-control" value="{{ cd.image }}" disabled/>
<input type="hidden" name="path" value="{{ cd.path }}">
<button type="submit" class="btn btn-sm btn-primary" value="{{ cd.dev }}" name="umount_iso">{% trans "Unmount" %}</button>
</div>
</form>
<label class="form-label small" for="media-{{ cd.dev }}">{% trans "ISO image" %}</label>
<div class="row g-2">
<div class="col-12 col-sm">
<select name="media" class="form-select form-select-sm" id="media-{{ cd.dev }}">
{% for iso in instance.media_iso %}
<option value="{{ iso }}">{{ iso }}</option>
{% empty %}
<option value="none">{% trans "None" %}</option>
{% endfor %}
</select>
</div>
<div class="col-12 col-sm-auto d-grid">
<button type="submit" class="btn btn-sm btn-primary" name="mount_iso" value="{{ cd.dev }}"{% if not instance.media_iso %} disabled{% endif %}>{% trans "Mount" %}</button>
</div>
</div>
{% endif %}
</div>
</form>
</div>
{% empty %}
<div class="offset-sm-3 col-sm-6">
<div class="bg-warning rounded shadow-sm">{% trans 'There is not any CD-ROM device.' %}</div>
</div>
<p class="small text-body-secondary mb-0">{% trans "The VM has no CD-ROM drive." %}</p>
{% endfor %}
<div class="clearfix"></div>
<div class="mb-3">
<span class="fw-bold">{% trans "Instance Volume" %}</span>
</section>
<section aria-labelledby="disks-heading">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<h2 class="h6 mb-0" id="disks-heading">{% trans "Disks" %} <span class="text-body-secondary fw-normal">· {{ instance.config_disks|length }}</span></h2>
{% include 'add_instance_volume.html' %}
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th scope="col">{% trans "Device" %}</th>
<th scope="col">{% trans "Used" %}</th>
<th scope="col">{% trans "Capacity" %}</th>
<th scope="col">{% trans "Storage" %}</th>
<th scope="col">{% trans "Source" %}</th>
<th scope="col">{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for disk in instance.config_disks %}
<tr>
<td>
<button type="submit" class="btn btn-sm btn-secondary"
name="details{{ forloop.counter0 }}"
title="{% trans 'Details' %}"
tabindex="1"
data-trigger="focus"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-content="<strong>{% trans 'Bus' %}:</strong> {{ disk.bus }} <br/>
<strong>{% trans 'Format' %}:</strong> {{ disk.format }} <br/>
<strong>{% trans 'Cache' %}:</strong> {{ disk.cache }} <br/>
<strong>{% trans 'Serial' %}:</strong> {{ disk.serial }} <br/>
<strong>{% trans 'Readonly' %}:</strong> {{ disk.readonly }} <br/>
<strong>{% trans 'Shareable' %}:</strong> {{ disk.shareable }}</br>
<strong>{% trans 'IO Mode' %}:</strong> {{ disk.io }} <br/>
<strong>{% trans 'Discard' %}:</strong> {{ disk.discard }} <br/>
<strong>{% trans 'Detect Zeroes' %}:</strong> {{ disk.detect_zeroes }}">
<span>{% bs_icon 'info' %} </span>
</button>
{{ disk.dev }}
</td>
{% if disk.storage is None %}
<td colspan="4">
<div class="alert alert-danger">
{% trans "Error getting disk info" %}
</div>
</td>
{% else %}
<td>{{ disk.used | filesizeformat}}</td>
<td>{{ disk.size | filesizeformat }}</td>
<td>{{ disk.storage }}</td>
<td>{{ disk.path }}</td>
{% endif %}
<td class="text-nowrap">
<form class="d-inline" action="{% url 'instances:edit_volume' instance.id %}" method="post" role="form" aria-label="Edit instance volume form">
{% csrf_token %}
<input type="hidden" name="path" value="{{ disk.path }}">
<input type="hidden" name="dev" value="{{ disk.dev }}">
<input type="hidden" name="storage" value="{{ disk.storage }}">
<input type="hidden" name="name" value="{{ disk.image }}">
{% include 'instances/edit_instance_volume.html' with id=forloop.counter0 %}
</form>
{% if request.user.is_superuser %}{% include 'instances/move_disk.html' with id=forloop.counter0 %}{% endif %}
<form class="d-inline" action="{% url 'instances:detach_vol' instance.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="path" value="{{ disk.path }}">
<input type="hidden" name="dev" value="{{ disk.dev }}">
<input type="hidden" name="storage" value="{{ disk.storage }}">
<input type="hidden" name="name" value="{{ disk.image }}">
{% if instance.status == 5 %}
<button type="submit" class="btn btn-sm btn-secondary" value="{{ disk.dev }}" title="{% trans "Detach" %}" data-confirm="{% blocktrans with dev=disk.dev %}Detach {{ dev }}? Its file is kept.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">
{% bs_icon 'eject' %}
</button>
{% else %}
<button class="btn btn-sm btn-secondary disabled" disabled value="{{ disk.dev }}" title="{% trans "Detach" %}">
{% bs_icon 'eject' %}
</button>
{% endif %}
</form>
<form class="d-inline" action="{% url 'instances:delete_vol' instance.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="path" value="{{ disk.path }}">
<input type="hidden" name="dev" value="{{ disk.dev }}">
<input type="hidden" name="storage" value="{{ disk.storage }}">
<input type="hidden" name="name" value="{{ disk.image }}">
{% if instance.status == 5 %}
<button type="submit" class="btn btn-sm btn-secondary" title="{% trans "Delete" %}" data-confirm="{% blocktrans with dev=disk.dev %}Delete {{ dev }}? It is detached and its file and data are removed.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">
{% bs_icon 'trash' %}
</button>
{% else %}
<button class="btn btn-sm btn-secondary disabled" disabled title="{% trans "Delete" %}">
{% bs_icon 'trash' %}
</button>
{% endif %}
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row g-3">
{% for disk in instance.config_disks %}
<div class="col-12{% if instance.config_disks|length > 1 %} col-md-6{% endif %}">
<article class="card h-100" aria-labelledby="disk-{{ disk.dev }}">
<div class="card-body small">
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
<h3 class="h6 mb-0" id="disk-{{ disk.dev }}">{% bs_icon 'device-hdd' %} {{ disk.dev }}</h3>
<span class="text-body-secondary">{{ disk.bus }} · {{ disk.format }}</span>
</div>
{% if disk.storage is None %}
<div class="alert alert-danger mb-0">{% trans "Error getting disk info" %}</div>
{% else %}
<div class="text-body-secondary">{% trans "Capacity" %}</div>
<p class="fs-4 fw-semibold lh-sm mb-0">{{ disk.size|filesizeformat }}</p>
<p class="text-body-secondary mb-2">{% blocktrans with used=disk.used|filesizeformat %}{{ used }} used{% endblocktrans %}</p>
<div class="border-top pt-2">
<div class="mb-1"><span class="text-body-secondary">{% trans "Storage pool" %}</span> <strong class="ms-1">{{ disk.storage }}</strong></div>
<div class="text-body-secondary">{% trans "Source" %}</div>
<p class="text-break mb-2">{{ disk.path }}</p>
</div>
{% endif %}
<details>
<summary class="text-body-secondary">{% trans "Device details" %}</summary>
<dl class="row mb-0 mt-2" style="max-width: 32rem">
<dt class="col-6 fw-normal text-body-secondary">{% trans "Cache" %}</dt><dd class="col-6 mb-1">{{ disk.cache }}</dd>
<dt class="col-6 fw-normal text-body-secondary">{% trans "IO mode" %}</dt><dd class="col-6 mb-1">{{ disk.io }}</dd>
<dt class="col-6 fw-normal text-body-secondary">{% trans "Discard" %}</dt><dd class="col-6 mb-1">{{ disk.discard }}</dd>
<dt class="col-6 fw-normal text-body-secondary">{% trans "Detect zeroes" %}</dt><dd class="col-6 mb-1">{{ disk.detect_zeroes }}</dd>
<dt class="col-6 fw-normal text-body-secondary">{% trans "Serial" %}</dt><dd class="col-6 mb-1">{{ disk.serial|default:"—" }}</dd>
<dt class="col-6 fw-normal text-body-secondary">{% trans "Read-only" %}</dt><dd class="col-6 mb-1">{{ disk.readonly|yesno }}</dd>
<dt class="col-6 fw-normal text-body-secondary">{% trans "Shareable" %}</dt><dd class="col-6 mb-0">{{ disk.shareable|yesno }}</dd>
</dl>
</details>
</div>
<div class="card-footer bg-transparent d-flex flex-wrap gap-2">
<form class="d-inline" action="{% url 'instances:edit_volume' instance.id %}" method="post" aria-label="{% trans "Edit instance volume form" %}">
{% csrf_token %}
<input type="hidden" name="path" value="{{ disk.path }}">
<input type="hidden" name="dev" value="{{ disk.dev }}">
<input type="hidden" name="storage" value="{{ disk.storage }}">
<input type="hidden" name="name" value="{{ disk.image }}">
{% include 'instances/edit_instance_volume.html' with id=forloop.counter0 %}
</form>
{% if request.user.is_superuser %}{% include 'instances/move_disk.html' with id=forloop.counter0 %}{% endif %}
<form class="d-inline" action="{% url 'instances:detach_vol' instance.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="path" value="{{ disk.path }}">
<input type="hidden" name="dev" value="{{ disk.dev }}">
<input type="hidden" name="storage" value="{{ disk.storage }}">
<input type="hidden" name="name" value="{{ disk.image }}">
<button type="submit" class="btn btn-sm btn-secondary" value="{{ disk.dev }}"{% if instance.status != 5 %} disabled{% endif %} data-confirm="{% blocktrans with dev=disk.dev %}Detach {{ dev }}? Its file is kept.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">{% trans "Detach" %}</button>
</form>
<form class="d-inline" action="{% url 'instances:delete_vol' instance.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="path" value="{{ disk.path }}">
<input type="hidden" name="dev" value="{{ disk.dev }}">
<input type="hidden" name="storage" value="{{ disk.storage }}">
<input type="hidden" name="name" value="{{ disk.image }}">
<button type="submit" class="btn btn-sm btn-danger"{% if instance.status != 5 %} disabled{% endif %} data-confirm="{% blocktrans with dev=disk.dev %}Delete {{ dev }}? It is detached and its file and data are removed.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">{% trans "Delete" %}</button>
</form>
</div>
</article>
</div>
{% endfor %}
</div>
<div class="clearfix"></div>
</section>
</div>
<div role="tabpanel" class="tab-pane" id="network">
<div class="mb-3">

View file

@ -22,8 +22,8 @@ class NewDiskNameTestCase(TestCase):
compute = Compute.objects.create(name="nd", hostname="127.0.0.1:1", login="root", password="", type=1)
self.instance = Instance.objects.create(compute=compute, name="nd-vm", uuid="aaaaaaaa-5555-2222-3333-444444444444")
def attached_name(self, view, data, live=((), ()), config=((), ())):
"""The name view attaches with; live and config are (disks, media) of each definition."""
def attach(self, view, data, live=((), ()), config=((), ()), status=1):
"""The attach_disk call of view; live and config are (disks, media) of each definition."""
with patch("instances.models.wvmInstance") as wvm, patch("instances.views.wvmStorage") as storage, patch(
"instances.views.wvmCreate"
) as create:
@ -33,6 +33,7 @@ class NewDiskNameTestCase(TestCase):
proxy.get_disk_bus_types.return_value = ["virtio", "sata", "scsi"]
proxy.get_cache_modes.return_value = {"default": "", "none": ""}
proxy.get_machine_type.return_value = "q35"
proxy.get_status.return_value = status
storage.return_value.get_volumes.return_value = ["data.qcow2"]
storage.return_value.get_type.return_value = "dir"
storage.return_value.get_volume_type.return_value = "file"
@ -40,7 +41,7 @@ class NewDiskNameTestCase(TestCase):
create.return_value.create_volume.return_value = "/pool/data.qcow2"
self.client.post(reverse(f"instances:{view}", args=[self.instance.id]), data)
proxy.attach_disk.assert_called_once()
return proxy.attach_disk.call_args.args[0]
return proxy.attach_disk.call_args
VIEWS = (
("add_new_vol", {"storage": "default", "name": "data", "format": "qcow2", "size": "1", "cache": "default"}),
@ -60,4 +61,9 @@ class NewDiskNameTestCase(TestCase):
for view, data in self.VIEWS:
for bus, live, config, expected in cases:
with self.subTest(view=view, bus=bus, expected=expected):
self.assertEqual(self.attached_name(view, {**data, "bus": bus}, live, config), expected)
self.assertEqual(self.attach(view, {**data, "bus": bus}, live, config).args[0], expected)
def test_a_cdrom_of_a_running_vm_is_on_the_bus_that_can_be_hotplugged(self):
for status, bus in ((1, "scsi"), (3, "scsi"), (5, "sata")): # running, paused, shut off
with self.subTest(status=status):
self.assertEqual(self.attach("add_cdrom", {}, status=status).kwargs["target_bus"], bus)

View file

@ -78,6 +78,14 @@ class PendingEditsTestCase(TestCase):
self.assertEqual([" ".join(t.split()) for t in doc.xpath("//label[@class='col-form-label']/text()")],
[f"{mac} Inbound", f"{mac} Outbound"])
def test_one_disk_card_spans_the_row_and_more_sit_side_by_side(self):
for disks, wide in (([disk("vda", "default", "")], ["col-12"]),
([disk("vda", "default", ""), disk("vdb", "default", "")], ["col-12 col-md-6"] * 2)):
with self.subTest(count=len(disks)):
doc = self.page(live=disks, config=disks)
self.assertEqual(doc.xpath("//div[@id='disks']//div[article[starts-with(@aria-labelledby, 'disk-')]]/@class"),
wide)
def test_a_disk_with_a_pending_source_is_not_deleted(self):
# running: vda is A, its definition for the next start uses B
with patch("instances.models.wvmInstance") as wvm, patch("instances.views.wvmStorage") as storage:
@ -99,8 +107,8 @@ class PendingEditsTestCase(TestCase):
def test_buttons_shown_as_disabled_cannot_submit(self):
# running: the CPU, options and video forms wait for a shutdown
doc = self.page(live=[disk("vda", "default", "")], config=[disk("vda", "default", "")])
self.assertGreater(len(doc.xpath("//button[@disabled]")), 5)
shown = doc.xpath("//button[contains(concat(' ', @class, ' '), ' disabled ')]")
self.assertGreater(len(shown), 5)
self.assertEqual([b for b in shown if b.get("disabled") is None], [])
for action in ("resizevm_cpu", "change_options", "set_video_model", "detach_vol", "delete_vol"):
with self.subTest(action):

View file

@ -1462,7 +1462,11 @@ def detach_vol(request, pk):
@serialize_instance_mutation
def add_cdrom(request, pk):
instance = get_instance(request.user, pk, perm_type="change")
bus = request.POST.get("bus", "ide" if instance.machine == "pc" else "sata")
if instance.status == 5:
default_bus = "ide" if instance.machine == "pc" else "sata"
else: # IDE and SATA drives cannot be plugged into a running VM, SCSI ones can
default_bus = "scsi"
bus = request.POST.get("bus", default_bus)
invalid = invalid_disk_options(instance, bus=bus)
if invalid:
return reject_disk_options(request, pk, invalid)

View file

@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-disc" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
<path d="M10 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0M8 4a4 4 0 0 0-4 4 .5.5 0 0 1-1 0 5 5 0 0 1 5-5 .5.5 0 0 1 0 1m4.5 3.5a.5.5 0 0 1 .5.5 5 5 0 0 1-5 5 .5.5 0 0 1 0-1 4 4 0 0 0 4-4 .5.5 0 0 1 .5-.5"/>
</svg>

After

Width:  |  Height:  |  Size: 440 B