mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 20:26:48 +00:00
As the network and interface pages; plan reviewed with Codex. - The list is one table with a search: each filter's chain, priority, number of rules, the filters it references, and how many VMs and filters use it directly; a row's menu clones it (one shared dialog, the copy's name filled in) or deletes it, Delete disabled and saying so while it is in use. Without the VMs (a host that fails to list them) the list is not shown: it would offer to delete filters in use. - New filter is one dialog with the filter as XML in the Ace editor, from a starter filter, with what in, out and priority mean and a link to libvirt's format. - A filter's page: a Filter card (UUID, chain, priority, the VMs that use it directly, linked, and the filters that reference it, linked); its references with their parameters (one to a filter gone from the host marked as missing, not linked), removed per row, added from a select of the filters it can reference; its rules with the action as a badge, the direction as to or from the VM, the priority (500 where none is given), every protocol element with its attributes, a rule of no protocol as any traffic of the chain, and when it does not track connections; Add rule as an XML dialog; the XML, collapsed, saved in place. - The editors size themselves when shown and keep their label for screen readers; tables keep their hidden header text inside them, so a phone does not scroll the page sideways. add_nwf_rule.html goes (the dialog is on the page).
212 lines
13 KiB
HTML
212 lines
13 KiB
HTML
{% extends "base.html" %}
|
||
{% load i18n %}
|
||
{% load static %}
|
||
{% load bootstrap_icons %}
|
||
|
||
{% block title %}{% trans "Network filter" %} - {{ name }}{% endblock %}
|
||
|
||
{% block page_heading %}{{ compute.name }} - {% trans "Network filter" %}: {{ name }}{% endblock page_heading %}
|
||
|
||
{% block content %}
|
||
{% include "computes/host_nav.html" with section="nwfilters" detail=True %}
|
||
|
||
{% if uuid %}
|
||
<div class="card mb-3">
|
||
<div class="card-header">{% trans "Filter" %}</div>
|
||
<ul class="list-group list-group-flush">
|
||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||
<span class="text-body-secondary">{% trans "UUID" %}</span><span class="font-monospace text-break text-end">{{ uuid }}</span>
|
||
</li>
|
||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||
<span class="text-body-secondary">{% trans "Chain" %}</span><span class="font-monospace">{{ chain|default:"–" }}</span>
|
||
</li>
|
||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||
<span class="text-body-secondary">{% trans "Priority" %}</span><span>{% if priority is not None %}{{ priority }}{% else %}–{% endif %}</span>
|
||
</li>
|
||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||
<span class="text-body-secondary">{% trans "Directly used by" %}</span>
|
||
<span class="text-end">{% for vm in vms %}{% if vm.id %}<a href="{% url 'instances:instance' vm.id %}">{{ vm.name }}</a>{% else %}{{ vm.name }}{% endif %}{% if not forloop.last %}, {% endif %}{% empty %}–{% endfor %}</span>
|
||
</li>
|
||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||
<span class="text-body-secondary">{% trans "Referenced by" %}</span>
|
||
<span class="text-end">{% for other in referenced_by %}<a href="{% url 'nwfilter' compute.id other %}">{{ other }}</a>{% if not forloop.last %}, {% endif %}{% empty %}–{% endfor %}</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header d-flex flex-wrap align-items-center gap-2">
|
||
<span class="me-auto">{% trans "References" %}</span>
|
||
{% if ref_choices %}
|
||
<form method="post" class="d-flex gap-2">{% csrf_token %}
|
||
<select name="nwfilters_select" class="form-select form-select-sm w-auto" aria-label="{% trans "Filter to reference" %}" required>
|
||
{% for choice in ref_choices %}<option value="{{ choice }}">{{ choice }}</option>{% endfor %}
|
||
</select>
|
||
<button type="submit" class="btn btn-sm btn-primary text-nowrap" name="add_nwfilter_ref">{% bs_icon 'plus-lg' %} {% trans "Add" %}</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% if refs %}
|
||
<div class="table-responsive position-relative">
|
||
<table class="table table-hover align-middle mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">{% trans "Filter" %}</th>
|
||
<th scope="col">{% trans "Parameters" %}</th>
|
||
<th scope="col"><span class="visually-hidden">{% trans "Actions" %}</span></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for ref in refs %}
|
||
<tr>
|
||
<td class="text-nowrap">
|
||
{% if ref.filter in filter_names %}<a href="{% url 'nwfilter' compute.id ref.filter %}">{{ ref.filter }}</a>
|
||
{% else %}{{ ref.filter }} <span class="badge bg-danger-subtle text-danger-emphasis">{% trans "Missing" %}</span>{% endif %}
|
||
</td>
|
||
<td class="font-monospace small">{% for param, value in ref.parameters %}<div>{{ param }}={{ value }}</div>{% empty %}–{% endfor %}</td>
|
||
<td class="text-end">
|
||
<form method="post">{% csrf_token %}
|
||
<input type="hidden" name="index" value="{{ forloop.counter0 }}">
|
||
<input type="hidden" name="ref" value="{{ ref.text }}">
|
||
<button type="submit" class="btn btn-sm btn-outline-danger" name="del_nwfilter_ref" aria-label="{% blocktrans with ref=ref.filter %}Remove the reference to {{ ref }}{% endblocktrans %}"
|
||
data-confirm="{% blocktrans with ref=ref.filter %}Remove the reference to {{ ref }}? Its rules no longer apply through this filter.{% endblocktrans %}"
|
||
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %}</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="card-body small text-body-secondary">{% trans "It references no other filter." %}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header d-flex flex-wrap align-items-center gap-2">
|
||
<span class="me-auto">{% trans "Rules" %}</span>
|
||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#AddNWFilterRule">{% bs_icon 'plus-lg' %} {% trans "Add rule" %}</button>
|
||
</div>
|
||
{% if rules %}
|
||
<div class="table-responsive position-relative">
|
||
<table class="table table-hover align-middle mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">#</th>
|
||
<th scope="col">{% trans "Action" %}</th>
|
||
<th scope="col">{% trans "Direction" %}</th>
|
||
<th scope="col">{% trans "Priority" %}</th>
|
||
<th scope="col">{% trans "Matches" %}</th>
|
||
<th scope="col"><span class="visually-hidden">{% trans "Actions" %}</span></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for rule in rules %}
|
||
<tr>
|
||
<th scope="row">{{ forloop.counter }}</th>
|
||
<td>
|
||
{% if rule.action == "accept" %}<span class="badge bg-success-subtle text-success-emphasis">{% trans "Accept" %}</span>
|
||
{% elif rule.action == "drop" or rule.action == "reject" %}<span class="badge bg-danger-subtle text-danger-emphasis">{{ rule.action|capfirst }}</span>
|
||
{% else %}<span class="badge text-bg-light border">{{ rule.action|capfirst }}</span>{% endif %}
|
||
</td>
|
||
<td class="text-nowrap">{% if rule.direction == "in" %}{% trans "To the VM" %}{% elif rule.direction == "out" %}{% trans "From the VM" %}{% elif rule.direction == "inout" %}{% trans "Both ways" %}{% else %}{{ rule.direction }}{% endif %}</td>
|
||
<td>{% if rule.priority is not None %}{{ rule.priority }}{% else %}500{% endif %}</td>
|
||
<td class="small">
|
||
{% for protocol in rule.protocols %}
|
||
<div><span class="badge text-bg-light border">{{ protocol.name }}</span>{% for attribute, value in protocol.attributes %} <span class="font-monospace">{{ attribute }}={{ value }}</span>{% endfor %}</div>
|
||
{% empty %}
|
||
<span class="text-body-secondary">{% trans "Any traffic of the chain" %}</span>
|
||
{% endfor %}
|
||
{% if rule.statematch == "false" or rule.statematch == "0" %}<div class="text-body-secondary">{% trans "Without connection tracking" %}</div>{% endif %}
|
||
</td>
|
||
<td class="text-end">
|
||
<form method="post">{% csrf_token %}
|
||
<input type="hidden" name="index" value="{{ forloop.counter0 }}">
|
||
<input type="hidden" name="rule" value="{{ rule.text }}">
|
||
<button type="submit" class="btn btn-sm btn-outline-danger" name="del_nwfilter_rule" aria-label="{% blocktrans with number=forloop.counter %}Delete rule {{ number }}{% endblocktrans %}"
|
||
data-confirm="{% trans "Delete this rule? Interfaces that use the filter no longer get it." %}"
|
||
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %}</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="card-body small text-body-secondary">{% trans "The filter has no rules of its own." %}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<section class="accordion mb-3" aria-labelledby="xml-heading">
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header" id="xml-heading">
|
||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#nwfilter-xml" aria-expanded="false" aria-controls="nwfilter-xml">
|
||
{% trans "XML definition" %}
|
||
</button>
|
||
</h2>
|
||
<div class="accordion-collapse collapse" id="nwfilter-xml">
|
||
<form method="post" class="accordion-body" aria-label="{% trans "Change the filter's XML" %}">{% csrf_token %}
|
||
<p class="small text-body-secondary">{% trans "A change applies at once, also to the running VMs that use the filter; it keeps the filter's name and UUID." %}</p>
|
||
<input type="hidden" name="edit_xml">
|
||
<div class="xmlheight mb-3"><textarea id="edit_editor" aria-label="{% trans "Filter XML" %}">{{ xml }}</textarea></div>
|
||
<div class="d-flex justify-content-end">
|
||
<button type="submit" class="btn btn-primary" name="edit_nwfilter">{% trans "Save" %}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="modal fade" id="AddNWFilterRule" tabindex="-1" aria-labelledby="AddNWFilterRuleLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg modal-fullscreen-sm-down">
|
||
<form method="post" class="modal-content">{% csrf_token %}
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="AddNWFilterRuleLabel">{% trans "Add rule" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p class="small text-body-secondary" id="rule_editor_help">
|
||
{% trans "One rule: in is traffic to the VM, out from it; a lower priority applies first (500 if none is given)." %}
|
||
<a href="https://libvirt.org/formatnwfilter.html#supported-protocols" target="_blank" rel="noopener">{% trans "The protocols and their attributes" %}</a>
|
||
</p>
|
||
<input type="hidden" name="nwfilterrule_xml">
|
||
<div class="xmlheight"><textarea id="rule_editor" aria-label="{% trans "Rule XML" %}" aria-describedby="rule_editor_help"><rule action='accept' direction='in' priority='500'>
|
||
<tcp dstportstart='443'/>
|
||
</rule></textarea></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||
<button type="submit" class="btn btn-primary" name="add_nwfilter_rule">{% trans "Add rule" %}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% endblock %}
|
||
|
||
{% block script %}
|
||
{% if uuid %}
|
||
<script src="{% static 'js/ace/ace.js' %}"></script>
|
||
<script>
|
||
// each editor's text goes in its form's hidden field, the starter too if sent as it is,
|
||
// and it measures itself when its section or dialog is shown
|
||
function xmlEditor(textareaId, fieldName, shownBy, shownEvent) {
|
||
var textarea = document.getElementById(textareaId);
|
||
var form = textarea.closest("form");
|
||
var editor = ace.edit(textareaId);
|
||
editor.getSession().setMode("ace/mode/xml");
|
||
editor.setOption("textInputAriaLabel", textarea.getAttribute("aria-label"));
|
||
var field = form.elements[fieldName];
|
||
field.value = editor.getSession().getValue();
|
||
editor.getSession().on("change", function () {
|
||
field.value = editor.getSession().getValue();
|
||
});
|
||
document.getElementById(shownBy).addEventListener(shownEvent, () => editor.resize());
|
||
}
|
||
xmlEditor("edit_editor", "edit_xml", "nwfilter-xml", "shown.bs.collapse");
|
||
xmlEditor("rule_editor", "nwfilterrule_xml", "AddNWFilterRule", "shown.bs.modal");
|
||
</script>
|
||
{% endif %}
|
||
{% endblock %}
|