mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
feat(instances): a VM's network devices as cards, each with its QoS limits
Settings > Network was a table whose fields were read-only inputs cut off at phone width, an unlabelled "+", three icon buttons per NIC and a separate QoS table keyed by MAC. The owner chose card layout from three rendered options, with the QoS limits folding out like the device details: - A card per NIC: a VM's only NIC spans the row, two or more sit side by side from md. Each has its name and target, the link state as a switch, its source and IPv4 addresses, and "Device details" (type, MAC, model, NWFilter, IPv6) in a <details>. - "QoS limits" in a second <details>, its summary naming the limited directions: a form per inbound or outbound limit, with Save and Delete limit, and the units. - Labelled Edit, Add QoS and Delete in the card footer; "Add network device" by the heading. The edit and add-QoS dialogs are titled and labelled, and the edit dialog's fields have labels. The limits come from each NIC of the persistent definition, so the QoS table, Instance.config_qos and wvmInstance.get_all_qos are removed.
This commit is contained in:
parent
0f778fe9f0
commit
a8f445d580
11 changed files with 184 additions and 311 deletions
|
|
@ -168,11 +168,6 @@ class Instance(models.Model):
|
|||
edit does not post the live value back over it."""
|
||||
return self.proxy.get_disk_devices(config=True)
|
||||
|
||||
@cached_property
|
||||
def config_qos(self):
|
||||
"""QoS of the persistent definition, which the QoS form changes."""
|
||||
return self.proxy.get_all_qos(config=True)
|
||||
|
||||
@cached_property
|
||||
def config_vcpu(self) -> int:
|
||||
"""vCPUs of the persistent definition, which the CPU form changes."""
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap_icons %}
|
||||
{% if request.user.is_superuser %}
|
||||
<button type="button" class="btn btn-primary float-end" data-bs-toggle="modal" data-bs-target="#addInstanceNetwork">
|
||||
<span aria-hidden="true">{% bs_icon 'plus'%} </span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addInstanceNetwork">{% bs_icon 'plus' %} {% trans "Add network device" %}</button>
|
||||
|
||||
<!-- Modal pool -->
|
||||
<div class="modal fade" id="addInstanceNetwork" tabindex="-1" role="dialog" aria-labelledby="addInstanceNetworkLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Add Instance Network" %}</h5>
|
||||
<h5 class="modal-title" id="addInstanceNetworkLabel">{% trans "Add Instance Network" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'instances:add_network' instance.id %}" method="post" role="form" aria-label="Add instance network form">{% csrf_token %}
|
||||
|
|
|
|||
26
instances/templates/instances/nic_qos_form.html
Normal file
26
instances/templates/instances/nic_qos_form.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{% load i18n %}
|
||||
{# one QoS limit of a NIC: its values to change, or to delete #}
|
||||
<form class="mt-2" style="max-width: 32rem" action="{% url 'instances:set_qos' instance.id %}" method="post" aria-label="{% blocktrans %}{{ direction }} QoS form{% endblocktrans %}">{% csrf_token %}
|
||||
<input type="hidden" name="net-mac" value="{{ network.mac }}">
|
||||
<input type="hidden" name="qos_direction" value="{{ direction }}">
|
||||
<div class="fw-semibold mb-1">{% if direction == "inbound" %}{% trans "Inbound" %}{% else %}{% trans "Outbound" %}{% endif %}</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-4">
|
||||
<label class="form-label small" for="qos-{{ nic }}-{{ direction }}-average">{% trans "Average" %}</label>
|
||||
<input class="form-control form-control-sm" id="qos-{{ nic }}-{{ direction }}-average" name="qos_average" value="{{ limit.average|default:'' }}" required pattern="[0-9]+">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small" for="qos-{{ nic }}-{{ direction }}-peak">{% trans "Peak" %}</label>
|
||||
<input class="form-control form-control-sm" id="qos-{{ nic }}-{{ direction }}-peak" name="qos_peak" value="{{ limit.peak|default:'' }}" pattern="[0-9]+">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small" for="qos-{{ nic }}-{{ direction }}-burst">{% trans "Burst" %}</label>
|
||||
<input class="form-control form-control-sm" id="qos-{{ nic }}-{{ direction }}-burst" name="qos_burst" value="{{ limit.burst|default:'' }}" pattern="[0-9]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-sm btn-primary" name="set_qos">{% trans "Save" %}</button>
|
||||
<button type="submit" class="btn btn-sm btn-danger" name="unset_qos" formaction="{% url 'instances:unset_qos' instance.id %}" formnovalidate
|
||||
data-confirm="{% blocktrans with mac=network.mac %}Remove the {{ direction }} limit of {{ mac }}? A running VM keeps it until it is powered off and on.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">{% trans "Delete limit" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -296,246 +296,127 @@
|
|||
</section>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="network">
|
||||
<div class="mb-3">
|
||||
{% trans "Add a network device" %}
|
||||
{% include 'add_instance_network_block.html' %}
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-lg-12 mt-3">
|
||||
<h5 class="fw-bold">{% trans "Network Devices" %}</h5>
|
||||
<div class="table-responsive">
|
||||
<table align-middle class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Name' %}</th>
|
||||
<th scope="col" class="d-none d-table-cell d-sm-table-cell" colspan="2">{% trans 'Info' %}</th>
|
||||
<th scope="colgroup" colspan="3" class="col-sm-2">{% trans 'Actions' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for network in instance.config_networks %}
|
||||
<tr>
|
||||
<td rowspan="2">eth{{ forloop.counter0 }}({{ network.target|default:"no target" }})
|
||||
<form action="{% url 'instances:set_link_state' instance.id %}" method="post" aria-label="set instance link state form">
|
||||
{% csrf_token %}
|
||||
<input name="mac" value="{{ network.mac }}" hidden/>
|
||||
<input name="set_link_state" value="{{ network.state }}" hidden/>
|
||||
<input type="checkbox" {% if network.state == 'up' %} checked{% endif %} onclick='submit();' />
|
||||
<strong>{% trans 'active' %}</strong>
|
||||
<small>{{ network.type }}</small>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">{% trans 'MAC' %}</span>
|
||||
<input type="text" class="form-control" value="{{ network.mac }}" aria-label="mac" aria-describedby="mac" readonly>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none d-table-cell">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">{% trans 'Filter' %}</span>
|
||||
<input type="text" class="form-control" value="{{ network.filterref|default:'None' }}" aria-label="filter" aria-describedby="filter" readonly>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">{% trans 'Source' %}</span>
|
||||
<input type="text" class="form-control" value="{{ network.nic }}" aria-label="source" aria-describedby="source" readonly>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{# a NIC that is a host device is changed in the XML editor #}
|
||||
{% if network.type != "hostdev" %}
|
||||
<form action="{% url 'instances:change_network' instance.id %}" method="post" name="edit_network{{ forloop.counter0 }}" role="form">{% csrf_token %}
|
||||
<button class="btn btn-sm btn-primary"
|
||||
type="button"
|
||||
title="{% trans "Edit NIC" %}"
|
||||
data-bs-target="#editInstanceNetwork{{ forloop.counter0 }}"
|
||||
data-bs-toggle="modal">
|
||||
<span aria-hidden="true">{% bs_icon 'pencil-square' %} </span>
|
||||
</button>
|
||||
|
||||
<div class="modal fade" id="editInstanceNetwork{{ forloop.counter0 }}" role="dialog" aria-labelledby="editInstanceNetworkLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Edit Instance Network" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<label class="col-form-label">{% trans "MAC" %}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" name="net-old-mac-{{ forloop.counter0 }}" value="{{ network.mac }}" readonly/>
|
||||
<input class="form-control" type="text" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-form-label">{% trans "Net Source" %}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" value="{{ network.nic }}" readonly/>
|
||||
<select class="form-select" name="net-source-{{ forloop.counter0 }}">
|
||||
{% for c_net in networks_host %}
|
||||
<option value="net:{{ c_net }}" {% if c_net == network.nic %} selected {% endif %}>{% trans 'Network' %} {{ c_net }}</option>
|
||||
{% endfor %}
|
||||
{% for c_iface in interfaces_host %}
|
||||
<option value="iface:{{ c_iface }}" {% if c_iface == network.nic %} selected {% endif %}>{% trans 'Interface' %} {{ c_iface }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-form-label">{% trans "NWFilter" %}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" value="{{ network.filterref }}" readonly/>
|
||||
<select class="form-select" name="net-nwfilter-{{ forloop.counter0 }}">
|
||||
<option value="">{% trans "None" %}</option>
|
||||
{% for c_filters in nwfilters_host %}
|
||||
<option value="{{ c_filters }}" {% if c_filters == network.filterref %} selected {% endif %}>{{ c_filters }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="col-form-label">{% trans "Model" %} </label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" value="{{ network.model }}" readonly/>
|
||||
<select class="form-select" name="net-model-{{ forloop.counter0 }}">
|
||||
{% for model in net_models_host %}
|
||||
<option value="{{ model }}" {% if model == network.model %} selected {% endif %}>{{ model }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
|
||||
<button class="btn btn-primary" name="change_network" title="{% trans "Apply network changes" %}">{% trans "Apply" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# a card per NIC, the only one across the row; its QoS limits fold out like its device details #}
|
||||
<section aria-labelledby="nics-heading">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
|
||||
<h2 class="h6 mb-0" id="nics-heading">{% trans "Network devices" %} <span class="text-body-secondary fw-normal">· {{ instance.config_networks|length }}</span></h2>
|
||||
{% include 'add_instance_network_block.html' %}
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
{% for network in instance.config_networks %}
|
||||
<div class="col-12{% if instance.config_networks|length > 1 %} col-md-6{% endif %}">
|
||||
<article class="card h-100" aria-labelledby="nic-{{ forloop.counter0 }}">
|
||||
<div class="card-body small">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
|
||||
<h3 class="h6 mb-0" id="nic-{{ forloop.counter0 }}">{% bs_icon 'ethernet' %} eth{{ forloop.counter0 }} <span class="text-body-secondary fw-normal">· {{ network.target|default:_("no target") }}</span></h3>
|
||||
<form action="{% url 'instances:set_link_state' instance.id %}" method="post" aria-label="{% trans "Link state form" %}">{% csrf_token %}
|
||||
<input type="hidden" name="mac" value="{{ network.mac }}">
|
||||
<input type="hidden" name="set_link_state" value="{{ network.state }}">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="link-{{ forloop.counter0 }}"{% if network.state == 'up' %} checked{% endif %} onchange="this.form.submit()">
|
||||
<label class="form-check-label" for="link-{{ forloop.counter0 }}">{% if network.state == 'up' %}{% trans "Link up" %}{% else %}{% trans "Link down" %}{% endif %}</label>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="{% url 'instances:set_qos' instance.id %}" method="post" name="add_qos{{ forloop.counter0 }}" role="form" aria-label="Add network qos form">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}" hidden/>
|
||||
{% include 'add_network_qos.html' with id=forloop.counter0 %}
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
{% if network.type != "hostdev" %}
|
||||
<form action="{% url 'instances:delete_network' instance.id %}" method="post" name="delete_network" role="form">{% csrf_token %}
|
||||
<button class="btn btn-sm btn-danger" value="{{ network.mac }}" name="delete_network" title="{% trans "Delete Device" %}"
|
||||
data-confirm="{% blocktrans with mac=network.mac %}Remove network card {{ mac }}? A running VM loses its connection through it at once.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">
|
||||
{% bs_icon 'trash' %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">{% trans 'IPv4' %}</span>
|
||||
<input type="text" class="form-control" value="{% for ipv4 in network.ipv4|default:'unknown' %}{{ ipv4 }}{% endfor %}" aria-label="ipv4" aria-describedby="ipv4" readonly>
|
||||
</div>
|
||||
<p class="text-break mb-1"><span class="text-body-secondary">{% trans "Source" %}</span> <strong class="ms-1">{{ network.nic|default:"—" }}</strong></p>
|
||||
<p class="mb-2"><span class="text-body-secondary">{% trans "IPv4" %}</span> <span class="ms-1">{{ network.ipv4|join:", "|default:_("unknown") }}</span></p>
|
||||
<details>
|
||||
<summary class="text-body-secondary">{% trans "Device details" %}</summary>
|
||||
<dl class="row mb-0 mt-2" style="max-width: 32rem">
|
||||
<dt class="col-4 fw-normal text-body-secondary">{% trans "Type" %}</dt><dd class="col-8 mb-1">{{ network.type }}</dd>
|
||||
<dt class="col-4 fw-normal text-body-secondary">{% trans "MAC" %}</dt><dd class="col-8 mb-1 text-break">{{ network.mac }}</dd>
|
||||
<dt class="col-4 fw-normal text-body-secondary">{% trans "Model" %}</dt><dd class="col-8 mb-1">{{ network.model|default:"—" }}</dd>
|
||||
<dt class="col-4 fw-normal text-body-secondary">{% trans "NWFilter" %}</dt><dd class="col-8 mb-1 text-break">{{ network.filterref|default:_("None") }}</dd>
|
||||
<dt class="col-4 fw-normal text-body-secondary">{% trans "IPv6" %}</dt><dd class="col-8 mb-1 text-break">{{ network.ipv6|join:", "|default:_("unknown") }}</dd>
|
||||
</dl>
|
||||
</details>
|
||||
<details>
|
||||
<summary class="text-body-secondary">{% trans "QoS limits" %} · {% if network.inbound and network.outbound %}{% trans "inbound, outbound" %}{% elif network.inbound %}{% trans "inbound" %}{% elif network.outbound %}{% trans "outbound" %}{% else %}{% trans "none" %}{% endif %}</summary>
|
||||
{% if network.inbound %}{% include 'instances/nic_qos_form.html' with nic=forloop.counter0 direction="inbound" limit=network.inbound %}{% endif %}
|
||||
{% if network.outbound %}{% include 'instances/nic_qos_form.html' with nic=forloop.counter0 direction="outbound" limit=network.outbound %}{% endif %}
|
||||
<p class="text-body-secondary mt-2 mb-0">{% trans "Average and peak are in KiB/s, burst in KiB. A running VM gets a change after it is powered off and on." %}</p>
|
||||
</details>
|
||||
{% if network.type == 'direct' %}
|
||||
<p class="text-body-secondary border-top pt-2 mt-2 mb-0">{% bs_icon 'info-circle' %} {% trans "In most configurations, macvtap does not work for host to guest network communication" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer bg-transparent d-flex flex-wrap gap-2">
|
||||
{# a NIC that is a host device is changed in the XML editor #}
|
||||
{% if network.type != "hostdev" %}
|
||||
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#editInstanceNetwork{{ forloop.counter0 }}">{% trans "Edit" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#AddQos{{ forloop.counter0 }}">{% trans "Add QoS" %}</button>
|
||||
{% if network.type != "hostdev" %}
|
||||
<form action="{% url 'instances:delete_network' instance.id %}" method="post" aria-label="{% trans "Delete network device form" %}">{% csrf_token %}
|
||||
<button class="btn btn-sm btn-danger" value="{{ network.mac }}" name="delete_network"
|
||||
data-confirm="{% blocktrans with mac=network.mac %}Remove network card {{ mac }}? A running VM loses its connection through it at once.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">{% trans "Delete" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% if network.type != "hostdev" %}
|
||||
<form action="{% url 'instances:change_network' instance.id %}" method="post" name="edit_network{{ forloop.counter0 }}">{% csrf_token %}
|
||||
<div class="modal fade" id="editInstanceNetwork{{ forloop.counter0 }}" tabindex="-1" aria-labelledby="editInstanceNetworkLabel{{ forloop.counter0 }}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editInstanceNetworkLabel{{ forloop.counter0 }}">{% blocktrans with nic=forloop.counter0 %}Edit eth{{ nic }}{% endblocktrans %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">{% trans 'IPv6' %}</span>
|
||||
<input type="text" class="form-control" value="{% for ipv6 in network.ipv6|default:'unknown' %}{{ ipv6 }}{% endfor %}" aria-label="ipv6" aria-describedby="ipv6" readonly>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="net-old-mac-{{ forloop.counter0 }}" value="{{ network.mac }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="net-mac-{{ forloop.counter0 }}">{% trans "MAC" %}</label>
|
||||
<input class="form-control" type="text" id="net-mac-{{ forloop.counter0 }}" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="net-source-{{ forloop.counter0 }}">{% trans "Net Source" %}</label>
|
||||
<select class="form-select" id="net-source-{{ forloop.counter0 }}" name="net-source-{{ forloop.counter0 }}">
|
||||
{% for c_net in networks_host %}
|
||||
<option value="net:{{ c_net }}"{% if c_net == network.nic %} selected{% endif %}>{% trans 'Network' %} {{ c_net }}</option>
|
||||
{% endfor %}
|
||||
{% for c_iface in interfaces_host %}
|
||||
<option value="iface:{{ c_iface }}"{% if c_iface == network.nic %} selected{% endif %}>{% trans 'Interface' %} {{ c_iface }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="net-nwfilter-{{ forloop.counter0 }}">{% trans "NWFilter" %}</label>
|
||||
<select class="form-select" id="net-nwfilter-{{ forloop.counter0 }}" name="net-nwfilter-{{ forloop.counter0 }}">
|
||||
<option value="">{% trans "None" %}</option>
|
||||
{% for c_filters in nwfilters_host %}
|
||||
<option value="{{ c_filters }}"{% if c_filters == network.filterref %} selected{% endif %}>{{ c_filters }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="net-model-{{ forloop.counter0 }}">{% trans "Model" %}</label>
|
||||
<select class="form-select" id="net-model-{{ forloop.counter0 }}" name="net-model-{{ forloop.counter0 }}">
|
||||
{% for model in net_models_host %}
|
||||
<option value="{{ model }}"{% if model == network.model %} selected{% endif %}>{{ model }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-2">{% trans 'Model' %}</span>
|
||||
<input type="text" class="form-control px-2" value="{{ network.model }}" aria-label="model" aria-describedby="model" readonly>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
|
||||
<button class="btn btn-primary" name="change_network">{% trans "Apply" %}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-1 bg-primary text-white" colspan="9">
|
||||
{% if network.type == 'direct' %}
|
||||
<small>{% trans 'In most configurations, macvtap does not work for host to guest network communication' %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if instance.config_qos %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-sm-10">
|
||||
<p><strong>{% trans "QoS Configuration" %}</strong></p>
|
||||
</div>
|
||||
<div class="col-12 col-sm-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2">{% trans "MAC" %}/{% trans "Direction" %}</th>
|
||||
<th scope="col" class="col-2">{% trans "Average" %}</th>
|
||||
<th scope="col" class="col-3">{% trans "Peak" %}</th>
|
||||
<th scope="col" class="col-3">{% trans "Burst" %}</th>
|
||||
<th scope="col" class="col-2">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q, attrs in instance.config_qos.items %}
|
||||
{% for att in attrs %}
|
||||
<tr class="d-flex">
|
||||
<form action="{% url 'instances:set_qos' instance.id %}" method="post" role="form" aria-label="Instance QoS configuration form">
|
||||
{% csrf_token %}
|
||||
<td class="col-2"><label class="col-form-label">{{ q }} {{ att.direction | capfirst }}</label></td>
|
||||
<td class="col-2"><input id="qos_average" class="form-control" name="qos_average"
|
||||
value="{{ att.average|default:'' }}"/>
|
||||
</td>
|
||||
<td class="col-3"><input id="qos_peak" class="form-control" name="qos_peak"
|
||||
value="{{ att.peak|default:'' }}"/>
|
||||
</td>
|
||||
<td class="col-3"><input id="qos_burst" class="form-control" name="qos_burst"
|
||||
value="{{ att.burst|default:'' }}"/>
|
||||
</td>
|
||||
<td class="col-sm-2">
|
||||
<input name="qos_direction" value="{{ att.direction }}" hidden/>
|
||||
<input name="net-mac" value="{{ q }}" hidden/>
|
||||
<button type="submit" class="btn btn-sm btn-primary"
|
||||
name="set_qos" data-bs-toggle="modal"
|
||||
title="{% trans "Edit QoS" %}">
|
||||
<span>{% bs_icon 'save' %} </span>
|
||||
</button>
|
||||
</form>
|
||||
<form action="{% url 'instances:unset_qos' instance.id %}" method="post" role="form" aria-label="Instance QoS configuration form">
|
||||
{% csrf_token %}
|
||||
<input name="qos_direction" value="{{ att.direction }}" hidden/>
|
||||
<input name="net-mac" value="{{ q }}" hidden/>
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
name="unset_qos"
|
||||
title="{% trans "Delete QoS" %}" data-confirm="{% blocktrans with direction=att.direction mac=q %}Remove the {{ direction }} limit of {{ mac }}? A running VM keeps it until it is powered off and on.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">
|
||||
{% bs_icon 'trash' %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form action="{% url 'instances:set_qos' instance.id %}" method="post" name="add_qos{{ forloop.counter0 }}" aria-label="{% trans "Add network QoS form" %}">{% csrf_token %}
|
||||
<input type="hidden" name="net-mac" value="{{ network.mac }}">
|
||||
{% include 'add_network_qos.html' with id=forloop.counter0 %}
|
||||
</form>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="small text-body-secondary mb-0">{% trans "The VM has no network device." %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</section>
|
||||
</div>
|
||||
{% if show_hostdevs %}
|
||||
<div role="tabpanel" class="tab-pane" id="hostdevs">
|
||||
|
|
|
|||
|
|
@ -602,9 +602,9 @@ class LiveDataLossTestCase(TestCase):
|
|||
|
||||
def row():
|
||||
page = html.fromstring(self.client.get(reverse("instances:instance", args=[inst.id])).content)
|
||||
(label,) = page.xpath("//label[@class='col-form-label'][contains(., 'Inbound')]")
|
||||
fields = label.xpath("ancestor::tr[1]//input[@name]") # the browser posts the row's inputs
|
||||
return {f.get("name"): f.get("value") for f in fields}
|
||||
# the browser posts the inputs of the NIC's inbound limit form
|
||||
(form,) = page.xpath("//form[input[@name='qos_direction'][@value='inbound']]")
|
||||
return {f.get("name"): f.get("value") for f in form.xpath(".//input[@name!='csrfmiddlewaretoken']")}
|
||||
|
||||
self.post("set_qos", inst, {**row(), "qos_average": "1000"})
|
||||
form = row()
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ class PendingEditsTestCase(TestCase):
|
|||
compute = Compute.objects.create(name="pe", hostname="127.0.0.1:1", login="root", password="", type=1)
|
||||
self.instance = Instance.objects.create(compute=compute, name="pe-vm", uuid="aaaaaaaa-1111-2222-3333-444444444444")
|
||||
|
||||
def page(self, live=(), config=(), qos=({}, {}), status=1, **getters):
|
||||
"""live and config: disks; qos and getters (get_vcpu=(2, 4)): values of
|
||||
def page(self, live=(), config=(), nics=((), ()), status=1, **getters):
|
||||
"""live and config: disks; nics and getters (get_vcpu=(2, 4)): values of
|
||||
the live and the persistent definition."""
|
||||
with patch("instances.models.wvmInstance") as wvm, patch.object(
|
||||
Compute, "status", new_callable=PropertyMock, return_value=True
|
||||
|
|
@ -44,12 +44,12 @@ class PendingEditsTestCase(TestCase):
|
|||
getattr(proxy, getter).side_effect = lambda config=False, values=values: values[config]
|
||||
proxy.get_max_cpus.return_value = [1, 2, 3, 4]
|
||||
proxy.get_vcpus.return_value = {}
|
||||
for getter in ("get_networks", "get_ifaces", "get_storages", "get_media_devices", "get_net_devices",
|
||||
for getter in ("get_networks", "get_ifaces", "get_storages", "get_media_devices",
|
||||
"get_snapshot", "get_snapshot_jobs"):
|
||||
getattr(proxy, getter).return_value = []
|
||||
proxy.get_hostdevs.return_value = ([], [], [])
|
||||
proxy.get_disk_devices.side_effect = lambda **kw: config if kw.get("config") else live
|
||||
proxy.get_all_qos.side_effect = lambda **kw: qos[1] if kw.get("config") else qos[0]
|
||||
proxy.get_net_devices.side_effect = lambda config=False: [dict(nic) for nic in nics[config]]
|
||||
proxy.get_console_type.return_value = "vnc"
|
||||
proxy.get_video_models.return_value = ["vga", "virtio"]
|
||||
proxy.get_cache_modes.return_value = {"default": "Default", "none": "Disabled"}
|
||||
|
|
@ -70,13 +70,24 @@ class PendingEditsTestCase(TestCase):
|
|||
self.assertEqual(vda.xpath("string(.//input[@name='vol_serial']/@value)"), "")
|
||||
|
||||
def test_the_qos_form_shows_the_pending_definition(self):
|
||||
mac = "52:54:10:00:00:01"
|
||||
inbound = {"direction": "inbound", "average": "500", "peak": "0", "burst": "0", "floor": None}
|
||||
doc = self.page(qos=({mac: [inbound]}, {mac: [{**inbound, "average": "1000"}, {**inbound, "direction": "outbound"}]}))
|
||||
nic = {"type": "network", "mac": "52:54:10:00:00:01", "nic": "default", "state": "up", "model": "virtio",
|
||||
"ipv4": None, "ipv6": None, "filterref": "", "target": "vnet0", "outbound": [],
|
||||
"inbound": {"average": "500", "peak": "0", "burst": "0"}}
|
||||
pending = {**nic, "inbound": {"average": "1000", "peak": "0", "burst": "0"}, "outbound": nic["inbound"]}
|
||||
doc = self.page(nics=([nic], [pending]))
|
||||
# the pending inbound value, and the pending outbound limit has a form
|
||||
self.assertEqual(doc.xpath("//input[@name='qos_average']/@value"), ["1000", "500"])
|
||||
# the pending outbound has a row
|
||||
self.assertEqual([" ".join(t.split()) for t in doc.xpath("//label[@class='col-form-label']/text()")],
|
||||
[f"{mac} Inbound", f"{mac} Outbound"])
|
||||
self.assertEqual(doc.xpath("//form[.//input[@name='qos_average']]//input[@name='qos_direction']/@value"),
|
||||
["inbound", "outbound"])
|
||||
|
||||
def test_one_nic_card_spans_the_row_and_more_sit_side_by_side(self):
|
||||
nic = {"type": "network", "mac": "52:54:10:00:00:01", "nic": "default", "state": "up", "model": "virtio",
|
||||
"ipv4": None, "ipv6": None, "filterref": "", "target": "", "inbound": [], "outbound": []}
|
||||
second = {**nic, "mac": "52:54:10:00:00:02"}
|
||||
for nics, wide in (([nic], ["col-12"]), ([nic, second], ["col-12 col-md-6"] * 2)):
|
||||
with self.subTest(count=len(nics)):
|
||||
doc = self.page(nics=(nics, nics))
|
||||
self.assertEqual(doc.xpath("//div[@id='network']//div[article]/@class"), wide)
|
||||
|
||||
def test_one_disk_card_spans_the_row_and_more_sit_side_by_side(self):
|
||||
for disks, wide in (([disk("vda", "default", "")], ["col-12"]),
|
||||
|
|
|
|||
|
|
@ -1012,7 +1012,7 @@ class InstancesTestCase(TestCase):
|
|||
self.assertEqual(self.instance.networks[0]["state"], "down")
|
||||
|
||||
def test_set_unset_qos(self):
|
||||
self.assertEqual(len(self.instance.config_qos.keys()), 0)
|
||||
self.assertFalse(self.instance.config_networks[0]["inbound"])
|
||||
net_mac = self.instance.networks[0]["mac"]
|
||||
response = self.client.post(
|
||||
reverse("instances:set_qos", args=[self.instance.id]),
|
||||
|
|
@ -1027,8 +1027,8 @@ class InstancesTestCase(TestCase):
|
|||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del self.instance.config_qos
|
||||
self.assertEqual(len(self.instance.config_qos.keys()), 1)
|
||||
del self.instance.config_networks
|
||||
self.assertEqual(self.instance.config_networks[0]["inbound"]["average"], "1")
|
||||
|
||||
response = self.client.post(
|
||||
reverse("instances:unset_qos", args=[self.instance.id]),
|
||||
|
|
@ -1039,8 +1039,8 @@ class InstancesTestCase(TestCase):
|
|||
HTTP_REFERER=reverse("index"),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
del self.instance.config_qos
|
||||
self.assertEqual(len(self.instance.config_qos.keys()), 0)
|
||||
del self.instance.config_networks
|
||||
self.assertFalse(self.instance.config_networks[0]["inbound"])
|
||||
|
||||
# test on running instance
|
||||
# self.instance.proxy.start()
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap_icons %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="#AddQos{{ id }}" type="button" class="btn btn-sm btn-success float-end" data-bs-toggle="modal" title="add qos">
|
||||
<span aria-hidden="true">{% bs_icon 'qr-code' %} </span>
|
||||
</a>
|
||||
|
||||
<!-- Modal pool -->
|
||||
<div class="modal fade" id="AddQos{{ id }}" tabindex="-1" role="dialog" aria-labelledby="AddQosLabel{{ id }}"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Add QoS for Network" %}</h5>
|
||||
<h5 class="modal-title" id="AddQosLabel{{ id }}">{% trans "Add QoS for Network" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
|
|
|||
11
static/icon_cache/ethernet_None_None_None.svg
Normal file
11
static/icon_cache/ethernet_None_None_None.svg
Normal 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-ethernet" viewBox="0 0 16 16">
|
||||
|
||||
|
||||
<path d="M14 13.5v-7a.5.5 0 0 0-.5-.5H12V4.5a.5.5 0 0 0-.5-.5h-1v-.5A.5.5 0 0 0 10 3H6a.5.5 0 0 0-.5.5V4h-1a.5.5 0 0 0-.5.5V6H2.5a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5M3.75 11h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m2 0h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m1.75.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zM9.75 11h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m1.75.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25z"/>
|
||||
|
||||
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z"/>
|
||||
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
|
|
@ -2883,48 +2883,6 @@ class wvmInstance(wvmConnect):
|
|||
def set_memory(self, size, flags=0):
|
||||
self.instance.setMemoryFlags(size, flags)
|
||||
|
||||
def get_all_qos(self, config=False):
|
||||
qos_values = dict()
|
||||
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML if config else 0))
|
||||
qos = tree.xpath("/domain/devices/interface")
|
||||
|
||||
for q in qos:
|
||||
bound_list = list()
|
||||
mac = q.xpath("mac/@address")
|
||||
band = q.find("bandwidth")
|
||||
if band is not None:
|
||||
in_qos = band.find("inbound")
|
||||
if in_qos is not None:
|
||||
in_av = in_qos.get("average")
|
||||
in_peak = in_qos.get("peak")
|
||||
in_burst = in_qos.get("burst")
|
||||
in_floor = in_qos.get("floor")
|
||||
bound_list.append(
|
||||
{
|
||||
"direction": "inbound",
|
||||
"average": in_av,
|
||||
"peak": in_peak,
|
||||
"floor": in_floor,
|
||||
"burst": in_burst,
|
||||
}
|
||||
)
|
||||
|
||||
out_qos = band.find("outbound")
|
||||
if out_qos is not None:
|
||||
out_av = out_qos.get("average")
|
||||
out_peak = out_qos.get("peak")
|
||||
out_burst = out_qos.get("burst")
|
||||
bound_list.append(
|
||||
{
|
||||
"direction": "outbound",
|
||||
"average": out_av,
|
||||
"peak": out_peak,
|
||||
"burst": out_burst,
|
||||
}
|
||||
)
|
||||
qos_values[mac[0]] = bound_list
|
||||
return qos_values
|
||||
|
||||
def set_qos(self, mac, direction, average, peak, burst):
|
||||
if direction not in ("inbound", "outbound"):
|
||||
raise ValueError("Direction must be inbound or outbound")
|
||||
|
|
|
|||
|
|
@ -110,10 +110,8 @@ class DeviceEditsTestCase(unittest.TestCase):
|
|||
p._defineXML.assert_called_once()
|
||||
p.wvm.defineXML.assert_not_called()
|
||||
|
||||
def test_qos_is_read_from_either_definition_and_an_edit_keeps_the_floor(self):
|
||||
def test_a_qos_edit_keeps_the_floor(self):
|
||||
p = proxy()
|
||||
self.assertEqual(p.get_all_qos(config=True)["52:54:10:00:00:01"][0]["floor"], "4")
|
||||
self.assertEqual(p.get_all_qos(), {}) # the running VM has none
|
||||
p.set_qos("52:54:10:00:00:01", "inbound", 10, 20, 30)
|
||||
inbound = defined(p).find("devices/interface[@type='network']/bandwidth/inbound")
|
||||
self.assertEqual(inbound.attrib, {"average": "10", "peak": "20", "burst": "30", "floor": "4"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue