mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 19:56:48 +00:00
feat(interfaces): the interface pages on Bootstrap's cards, tables and dialogs
As the storage and network pages; plan reviewed with Codex. - The list is grouped by type (bridges, bonds, VLANs, ethernet; a type the host has none of is left out), three cards a row, with a search of their names, MACs and networks (table_filter.js, which now takes any element with data-search as a row); a card per interface: its link as the kernel reports it (up green, down grey, another state or unknown told as such, no red card for a link that is merely down), MAC, MTU, speed when known, the members it reports and the libvirt networks that use it, linked. - An interface's page: an Interface card (type, libvirt's own state where it manages interfaces, link, start mode, MAC, MTU, speed, STP, networks using it); on a host whose libvirt changes interfaces, a card per family with how it is addressed and its addresses; the members of a bridge, bond or VLAN with their link and speed, or that none is reported; for a bridge, the running VMs' cards on it, directly or through a network using it, with their tap and MAC, linked (wvmInterface.get_vms: one domain listing). Not the bridge's delay: netcf tells it in seconds, the udev backend in hundredths. Start or Stop in its heading, Delete in its menu, disabled while it runs, on hosts that can change interfaces only. - A host whose libvirt only reads interfaces says so once, with that it does not tell their addresses, instead of "None" on every page. - The New interface dialog is one form, as the New pool and New network ones (choice_dialog.js, radio_card.html): Bridge or Ethernet, a bridge's member from the devices it can take, an ethernet interface's device from the ones the host does not define yet, a static address's fields with its choice; a refused interface, or one libvirt refuses, opens it again with what was entered; a made one opens its page. - networks_by_device maps the libvirt networks to their bridges or interfaces on the page's own connection.
This commit is contained in:
parent
680dd5dad4
commit
050d071ff9
13 changed files with 532 additions and 308 deletions
|
|
@ -1,150 +1,168 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap_icons %}
|
||||
{% load form_fields %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="#AddInterface" type="button" class="btn btn-success float-end" title="{% trans "Create New Interface" %}" data-bs-toggle="modal">
|
||||
<span aria-hidden="true">{% bs_icon 'plus' %} </span>
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#AddInterface">
|
||||
<span aria-hidden="true">{% bs_icon 'plus-lg' %}</span> {% trans "New interface" %}
|
||||
</button>
|
||||
|
||||
<!-- Modal Secret -->
|
||||
<div class="modal fade" id="AddInterface" tabindex="-1" role="dialog" aria-labelledby="AddInterface" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
{# one form: a card per type, the fields of the chosen one (the others hidden and disabled, so not sent) #}
|
||||
<div class="modal fade" id="AddInterface" tabindex="-1" aria-labelledby="AddInterfaceLabel" aria-hidden="true"
|
||||
{% if form.errors %}data-open{% endif %}>
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable modal-fullscreen-sm-down">
|
||||
<form method="post" class="modal-content" id="interface-form">{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Create New Interface" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<div>
|
||||
<h5 class="modal-title" id="AddInterfaceLabel">{% trans "New interface" %}</h5>
|
||||
<div class="small text-body-secondary">{% blocktrans with host=compute.name %}On {{ host }}{% endblocktrans %}</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
|
||||
</div>
|
||||
<form method="post" action="" role="form" aria-label="Create New Interface form">{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<label for="iface_name" class="col-sm-3 col-form-label">{% trans "Name" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input id="iface_name" type="text" name="name" class="form-control" placeholder="br0" maxlength="15" required pattern="[A-Za-z0-9_.\-]+"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="iface_start_mode" class="col-sm-3 col-form-label">{% trans "Start mode" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select id="iface_start_mode" name="start_mode" class="form-select">
|
||||
<option value="none">{% trans "none" %}</option>
|
||||
<option value="onboot">{% trans "onboot" %}</option>
|
||||
<option value="hotplug">{% trans "hotplug" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="iface_netdev" class="col-sm-3 col-form-label">{% trans "Device" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select id="iface_netdev" name="netdev" class="form-select">
|
||||
{% for dev in netdevs %}
|
||||
<option value="{{ dev }}">{{ dev }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="itype" class="col-sm-3 col-form-label">{% trans "Type" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="itype" class="form-select" id="itype">
|
||||
<option value="bridge">{% trans "bridge" %}</option>
|
||||
<option value="ethernet">{% trans "ethernet" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row bridge_name_form_group">
|
||||
<label for="iface_stp" class="col-sm-3 col-form-label">{% trans "STP" %}</label>
|
||||
<div class="modal-body">
|
||||
{% include "instances/form_errors.html" %}
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend class="form-label fs-6">{% trans "Type" %}</legend>
|
||||
<div class="row row-cols-1 row-cols-sm-2 g-2">
|
||||
{% trans "Bridge" as title %}{% trans "A new bridge over a device of the host; VMs can join it." as help %}
|
||||
{% include "radio_card.html" with name="itype" value="bridge" chosen=if_kind %}
|
||||
{% trans "Ethernet" as title %}{% trans "A device of the host, configured and started by libvirt." as help %}
|
||||
{% include "radio_card.html" with name="itype" value="ethernet" chosen=if_kind %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset data-kinds="bridge">
|
||||
<legend class="visually-hidden">{% trans "Bridge" %}</legend>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-6">
|
||||
<label for="iface_name" class="form-label">{% trans "Bridge name" %}</label>
|
||||
<input id="iface_name" type="text" name="name" class="form-control font-monospace{% if if_kind == "bridge" %}{{ form.name|invalid }}{% endif %}"
|
||||
value="{% if if_kind == "bridge" %}{{ form.name.value|default:"" }}{% endif %}" placeholder="br0" maxlength="15" required pattern="[A-Za-z0-9_.\-]+"
|
||||
{% if if_kind == "bridge" %}{% error_attrs form.name "iface_name" %}{% endif %}>
|
||||
{% if if_kind == "bridge" %}{% field_error form.name "iface_name" %}{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label for="iface_netdev" class="form-label">{% trans "Member device" %}</label>
|
||||
<select id="iface_netdev" name="netdev" class="form-select{{ form.netdev|invalid }}" required {% error_attrs form.netdev "iface_netdev" "iface_netdev_help" %}>
|
||||
{# a device taken meanwhile is chosen again, not swapped for the first one #}
|
||||
{% if form.netdev.value and form.netdev.value not in members %}<option value="" selected>{% trans "Choose a device" %}</option>{% endif %}
|
||||
{% for dev in members %}<option value="{{ dev }}"{% if dev == form.netdev.value %} selected{% endif %}>{{ dev }}</option>{% endfor %}
|
||||
</select>
|
||||
<div class="form-text" id="iface_netdev_help">{% trans "Devices in no bridge, bond or VLAN." %}</div>
|
||||
{% field_error form.netdev "iface_netdev" %}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label for="iface_stp" class="form-label">{% trans "Spanning tree (STP)" %}</label>
|
||||
<select id="iface_stp" name="stp" class="form-select">
|
||||
<option value="on">{% trans "on" %}</option>
|
||||
<option value="off">{% trans "off" %}</option>
|
||||
<option value="off"{% if form.stp.value == "off" %} selected{% endif %}>{% trans "Off" %}</option>
|
||||
<option value="on"{% if form.stp.value == "on" %} selected{% endif %}>{% trans "On" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row bridge_name_form_group">
|
||||
<label for="iface_delay" class="col-sm-3 col-form-label">{% trans "Delay" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input id="iface_delay" type="text" name="delay" class="form-control" value="0" maxlength="3" required pattern="[0-9]+"/>
|
||||
<label for="iface_delay" class="form-label">{% trans "Forward delay (s)" %}</label>
|
||||
<input id="iface_delay" type="number" name="delay" min="0" class="form-control{{ form.delay|invalid }}" value="{{ form.delay.value|default:"0" }}" required
|
||||
{% error_attrs form.delay "iface_delay" %}>
|
||||
{% field_error form.delay "iface_delay" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-tabs" aria-label="IP types">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#ipv4tab" type="button" role="tab" aria-controls="ipv4tab" aria-selected="true">
|
||||
{% trans "IPv4" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#ipv6tab" type="button" role="tab" aria-controls="ipv6tab" aria-selected="false">
|
||||
{% trans "IPv6" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="alert alert-warning small">
|
||||
{% bs_icon 'exclamation-triangle' %} {% trans "The device's own addresses go: the bridge takes the ones below, and connections through the device drop while it is rebuilt." %}
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="ipv4tab">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<label for="ipv4" class="col-sm-3 col-form-label">{% trans "IPv4 Mode" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="ipv4_type" class="form-select" id="ipv4">
|
||||
<option value="dhcp">{% trans "DHCP" %}</option>
|
||||
<option value="static">{% trans "Static" %}</option>
|
||||
<option value="none">{% trans "No configuration" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row static_ipv4_form_group">
|
||||
<label for="iface_ipv4_addr" class="col-sm-3 col-form-label">{% trans "IPv4 Address" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input id="iface_ipv4_addr" type="text" class="form-control" name="ipv4_addr" maxlength="31" pattern="[0-9\.\/]+|^$">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row static_ipv4_form_group">
|
||||
<label for="iface_ipv4_gw" class="col-sm-3 col-form-label">{% trans "IPv4 Gateway" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input id="iface_ipv4_gw" type="text" class="form-control" name="ipv4_gw" maxlength="16" pattern="[0-9\.]+|^$">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="ipv6tab">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<label for="ipv6" class="col-sm-3 col-form-label">{% trans "IPv6 Mode" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="ipv6_type" class="form-select" id="ipv6">
|
||||
<option value="none">{% trans "No configuration" %}</option>
|
||||
<option value="dhcp">{% trans "DHCP" %}</option>
|
||||
<option value="static">{% trans "Static" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row static_ipv6_form_group">
|
||||
<label for="iface_ipv6_addr" class="col-sm-3 col-form-label">{% trans "IPv6 Address" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input id="iface_ipv6_addr" type="text" class="form-control" name="ipv6_addr" maxlength="100" pattern="[0-9a-fA-F\:\/]+|^$">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row static_ipv6_form_group">
|
||||
<label for="iface_ipv6_gw" class="col-sm-3 col-form-label">{% trans "IPv6 Gateway" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input id="iface_ipv6_gw" type="text" class="form-control" name="ipv6_gw" maxlength="100" pattern="[0-9a-fA-F\:]+|^$">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset data-kinds="ethernet">
|
||||
<legend class="visually-hidden">{% trans "Ethernet" %}</legend>
|
||||
{% if ethernet_devices %}
|
||||
<div class="mb-3">
|
||||
<label for="iface_device" class="form-label">{% trans "Device" %}</label>
|
||||
<select id="iface_device" name="name" class="form-select{% if if_kind == "ethernet" %}{{ form.name|invalid }}{% endif %}" required
|
||||
{% if if_kind == "ethernet" %}{% error_attrs form.name "iface_device" %}{% endif %}>
|
||||
{% if if_kind == "ethernet" and form.name.value and form.name.value not in ethernet_devices %}<option value="" selected>{% trans "Choose a device" %}</option>{% endif %}
|
||||
{% for dev in ethernet_devices %}<option value="{{ dev }}"{% if dev == form.name.value %} selected{% endif %}>{{ dev }}</option>{% endfor %}
|
||||
</select>
|
||||
{% if if_kind == "ethernet" %}{% field_error form.name "iface_device" %}{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary small">{% trans "The host has every device defined already; a device's page shows it." %}</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="iface_ipv4" class="form-label">IPv4</label>
|
||||
<select id="iface_ipv4" name="ipv4_type" class="form-select" data-static="#iface_ipv4_static">
|
||||
<option value="none"{% if form.ipv4_type.value == "none" %} selected{% endif %}>{% trans "None" %}</option>
|
||||
<option value="dhcp"{% if form.ipv4_type.value == "dhcp" or not form.is_bound and "dhcp" == "dhcp" %} selected{% endif %}>DHCP</option>
|
||||
<option value="static"{% if form.ipv4_type.value == "static" %} selected{% endif %}>{% trans "Static" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<fieldset class="row g-3 mb-3" id="iface_ipv4_static">
|
||||
<legend class="visually-hidden">IPv4</legend>
|
||||
<div class="col-sm-6">
|
||||
<label for="iface_ipv4_addr" class="form-label">{% trans "Address/prefix" %}</label>
|
||||
<input id="iface_ipv4_addr" name="ipv4_addr" class="form-control font-monospace{{ form.ipv4_addr|invalid }}" value="{{ form.ipv4_addr.value|default:"" }}"
|
||||
placeholder="192.0.2.10/24" required {% error_attrs form.ipv4_addr "iface_ipv4_addr" %}>
|
||||
{% field_error form.ipv4_addr "iface_ipv4_addr" %}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label for="iface_ipv4_gw" class="form-label">{% trans "Gateway" %}</label>
|
||||
<input id="iface_ipv4_gw" name="ipv4_gw" class="form-control font-monospace{{ form.ipv4_gw|invalid }}" value="{{ form.ipv4_gw.value|default:"" }}"
|
||||
placeholder="192.0.2.1" {% error_attrs form.ipv4_gw "iface_ipv4_gw" %}>
|
||||
{% field_error form.ipv4_gw "iface_ipv4_gw" %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="mb-3">
|
||||
<label for="iface_ipv6" class="form-label">IPv6</label>
|
||||
<select id="iface_ipv6" name="ipv6_type" class="form-select" data-static="#iface_ipv6_static">
|
||||
<option value="none"{% if form.ipv6_type.value == "none" %} selected{% endif %}>{% trans "None" %}</option>
|
||||
<option value="dhcp"{% if form.ipv6_type.value == "dhcp" or not form.is_bound and "none" == "dhcp" %} selected{% endif %}>DHCP</option>
|
||||
<option value="static"{% if form.ipv6_type.value == "static" %} selected{% endif %}>{% trans "Static" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<fieldset class="row g-3 mb-3" id="iface_ipv6_static">
|
||||
<legend class="visually-hidden">IPv6</legend>
|
||||
<div class="col-sm-6">
|
||||
<label for="iface_ipv6_addr" class="form-label">{% trans "Address/prefix" %}</label>
|
||||
<input id="iface_ipv6_addr" name="ipv6_addr" class="form-control font-monospace{{ form.ipv6_addr|invalid }}" value="{{ form.ipv6_addr.value|default:"" }}"
|
||||
placeholder="2001:db8::10/64" required {% error_attrs form.ipv6_addr "iface_ipv6_addr" %}>
|
||||
{% field_error form.ipv6_addr "iface_ipv6_addr" %}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label for="iface_ipv6_gw" class="form-label">{% trans "Gateway" %}</label>
|
||||
<input id="iface_ipv6_gw" name="ipv6_gw" class="form-control font-monospace{{ form.ipv6_gw|invalid }}" value="{{ form.ipv6_gw.value|default:"" }}"
|
||||
placeholder="2001:db8::1" {% error_attrs form.ipv6_gw "iface_ipv6_gw" %}>
|
||||
{% field_error form.ipv6_gw "iface_ipv6_gw" %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="mb-3">
|
||||
<label for="iface_start_mode" class="form-label">{% trans "Starts" %}</label>
|
||||
<select id="iface_start_mode" name="start_mode" class="form-select">
|
||||
<option value="onboot"{% if form.start_mode.value == "onboot" %} selected{% endif %}>{% trans "On boot" %}</option>
|
||||
<option value="hotplug"{% if form.start_mode.value == "hotplug" %} selected{% endif %}>{% trans "When plugged in" %}</option>
|
||||
<option value="none"{% if form.start_mode.value == "none" %} selected{% endif %}>{% trans "Only by hand" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="create">
|
||||
{% trans "Create" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||
<button type="submit" class="btn btn-primary" name="create">{% trans "Create interface" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{% static 'js/choice_dialog.js' %}"></script>
|
||||
<script>
|
||||
// a static address's fields show with its choice (after the page's
|
||||
// scripts, Bootstrap's among them)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var form = document.getElementById('interface-form');
|
||||
form.querySelectorAll('select[data-static]').forEach(function (method) {
|
||||
var fields = form.querySelector(method.dataset.static);
|
||||
function show() { fields.hidden = fields.disabled = method.value !== 'static'; }
|
||||
method.addEventListener('change', show);
|
||||
show();
|
||||
});
|
||||
choiceDialog(document.getElementById('AddInterface'), 'itype', function () {});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -4,80 +4,174 @@
|
|||
|
||||
{% block title %}{% trans "Interface" %} - {{ iface }}{% endblock %}
|
||||
|
||||
{% block page_heading %}{% trans "Interface" %}: {{ iface }}{% endblock page_heading %}
|
||||
{% block page_heading %}{{ compute.name }} - {% trans "Interface" %}: {{ iface }}{% endblock page_heading %}
|
||||
|
||||
{% block page_heading_extra %}
|
||||
{% if changeable and info %}
|
||||
{# the interface's own actions where the host's libvirt can change interfaces: start or stop, and delete a stopped one #}
|
||||
<div class="btn-group" role="group" aria-label="{% blocktrans %}Actions for interface {{ iface }}{% endblocktrans %}">
|
||||
<form method="post" class="btn-group">{% csrf_token %}
|
||||
{% if state %}
|
||||
<button type="submit" class="btn btn-outline-secondary" name="stop"
|
||||
data-confirm="{% blocktrans with name=iface %}Stop interface {{ name }}? The host and its VMs lose their connections through it at once; if you reach the host through it, you lose the host.{% endblocktrans %}"
|
||||
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'stop-fill' %} {% trans "Stop" %}</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-outline-secondary" name="start">{% bs_icon 'play-fill' %} {% trans "Start" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false" aria-label="{% trans "More actions" %}">{% bs_icon 'three-dots' %}</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li><form method="post">{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item text-danger" name="delete"{% if state %} disabled{% endif %}
|
||||
data-confirm="{% blocktrans with name=iface %}Delete interface {{ name }}? Its definition is removed from the host; a bridge's members are defined again on their own, its addresses going to its only member.{% endblocktrans %}"
|
||||
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% if state %}{% trans "Delete (stop it first)" %}{% else %}{% trans "Delete interface" %}{% endif %}</button>
|
||||
</form></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock page_heading_extra %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include "computes/host_nav.html" with section="interfaces" detail=True %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "computes/host_nav.html" with section="interfaces" detail=True %}
|
||||
|
||||
{% if changeable is False %}{% include 'interfaces_unchangeable.html' %}{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% if info %}
|
||||
<dl class="ms-3 row">
|
||||
<dt class="col-4">{% trans "Interface" %}</dt>
|
||||
<dd class="col-8"><strong>{{ iface }}</strong></dd>
|
||||
<dt class="col-4">{% trans "IPv4" %}{% if info.ipv4.method %} ({{ info.ipv4.method }}){% endif %}</dt>
|
||||
<dd class="col-8">{{ info.ipv4.addresses|join:", "|default:"–" }}</dd>
|
||||
<dt class="col-4">{% trans "IPv6" %}{% if info.ipv6.method %} ({{ info.ipv6.method }}){% endif %}</dt>
|
||||
<dd class="col-8">{{ info.ipv6.addresses|join:", "|default:"–" }}</dd>
|
||||
<dt class="col-4">{% trans "MAC" %}</dt>
|
||||
<dd class="col-8">{{ info.mac|default:"–" }}</dd>
|
||||
<dt class="col-4">{% trans "Interface Type" %}</dt>
|
||||
<dd class="col-8">{{ info.type }}</dd>
|
||||
<dt class="col-4">{% trans "Boot Mode" %}</dt>
|
||||
<dd class="col-8">{{ info.start_mode|default:"–" }}</dd>
|
||||
<dt class="col-4">{% trans "State" %}</dt>
|
||||
<dd class="col-8">
|
||||
{% if changeable %}
|
||||
<form action="" method="post" role="form" aria-label="Interface start/stop/delete form">{% csrf_token %}
|
||||
{% if state == 0 %}
|
||||
<input type="submit" class="btn btn-sm btn-primary" name="start" value="{% trans "Start" %}">
|
||||
<input type="submit" class="btn btn-sm btn-danger" name="delete" value="{% trans "Delete" %}" data-confirm="{% blocktrans with name=iface %}Delete interface {{ name }}? Its definition is removed from the host; a bridge's members are defined again on their own, its addresses going to its only member.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">
|
||||
{% else %}
|
||||
<input type="submit" class="btn btn-sm btn-secondary" name="stop" value="{% trans "Stop" %}" data-confirm="{% blocktrans with name=iface %}Stop interface {{ name }}? The host and its VMs lose their connections through it at once; if you reach the host through it, you lose the host.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">
|
||||
{% endif %}
|
||||
</form>
|
||||
{% else %}
|
||||
{% if state == 0 %}{% trans "Inactive" %}{% else %}{% trans "Active" %}{% endif %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
{% if info %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">{% trans "Interface" %}</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Type" %}</span><span class="badge text-bg-light border">{{ info.type|capfirst }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "State" %}</span>
|
||||
<span class="d-flex flex-wrap justify-content-end gap-1">
|
||||
{# libvirt's own state where it manages the interface; the link as the kernel reports it everywhere #}
|
||||
{% if changeable %}{% if state %}<span class="badge text-bg-success">{% trans "Active" %}</span>{% else %}<span class="badge text-bg-secondary">{% trans "Inactive" %}</span>{% endif %}{% endif %}
|
||||
{% include "link_badge.html" with link=info.link %}
|
||||
</span>
|
||||
</li>
|
||||
{% if info.start_mode %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Starts" %}</span>
|
||||
<span>{% if info.start_mode == "onboot" %}{% trans "On boot" %}{% elif info.start_mode == "hotplug" %}{% trans "When plugged in" %}{% else %}{% trans "Only by hand" %}{% endif %}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if info.members %}
|
||||
<div class="col-sm-12">
|
||||
<h5>{% trans 'Members' %}</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'MAC' %}</th>
|
||||
<th scope="col">{% trans 'Name' %}</th>
|
||||
<th scope="col">{% trans 'Type' %}</th>
|
||||
<th scope="col">{% trans 'Speed' %}</th>
|
||||
<th scope="col">{% trans 'State' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in info.members %}
|
||||
<tr>
|
||||
<td>{{ member.mac|default:"–" }}</td>
|
||||
<td>{{ member.name }}</td>
|
||||
<td>{{ member.type }}</td>
|
||||
<td>{{ member.speed|default:"–" }}</td>
|
||||
<td>{{ member.link|default:"–" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "MAC" %}</span><span class="font-monospace text-break text-end">{{ info.mac|default:"–" }}</span>
|
||||
</li>
|
||||
{% if info.mtu %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "MTU" %}</span><span>{{ info.mtu }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if info.speed %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Speed" %}</span><span>{{ info.speed }} Mb/s</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if info.stp %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Spanning tree" %}</span>
|
||||
{# not its delay: netcf tells it in seconds, the udev backend in hundredths #}
|
||||
<span>{% if info.stp == "on" %}{% trans "On" %}{% else %}{% trans "Off" %}{% endif %}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if networks %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Used by" %}</span>
|
||||
<span class="text-end">{% for net in networks %}<a href="{% url 'network' compute.id net %}">{% blocktrans %}network {{ net }}{% endblocktrans %}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% if changeable %}
|
||||
<div class="row row-cols-1 row-cols-md-2 g-3 mb-3">
|
||||
{% for family, addressing in info.addressing %}
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">{{ family }}</div>
|
||||
<ul class="list-group list-group-flush flex-grow-1">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Configured" %}</span>
|
||||
<span>{% if addressing.method == "dhcp" %}DHCP{% elif addressing.method == "autoconf" %}{% trans "Automatic (SLAAC)" %}{% elif addressing.method == "static" %}{% trans "Static" %}{% else %}{% trans "None" %}{% endif %}</span>
|
||||
</li>
|
||||
{% for address in addressing.addresses %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Address" %}</span><span class="font-monospace text-break text-end">{{ address }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if info.type == "bridge" or info.type == "bond" or info.type == "vlan" %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">{% if info.type == "vlan" %}{% trans "Device" %}{% else %}{% trans "Members" %}{% endif %}</div>
|
||||
{% if info.members %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Name" %}</th>
|
||||
<th scope="col">{% trans "Type" %}</th>
|
||||
<th scope="col">{% trans "MAC" %}</th>
|
||||
<th scope="col">{% trans "Link" %}</th>
|
||||
<th scope="col">{% trans "Speed" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in info.members %}
|
||||
<tr>
|
||||
<td class="font-monospace text-nowrap">{{ member.name }}</td>
|
||||
<td>{{ member.type|default:"–" }}</td>
|
||||
<td class="font-monospace text-nowrap">{{ member.mac|default:"–" }}</td>
|
||||
<td>{% include "link_badge.html" with link=member.link %}</td>
|
||||
<td class="text-nowrap">{% if member.speed %}{{ member.speed }} Mb/s{% else %}–{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-body small text-body-secondary">{% trans "libvirt reports no members." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if info.type == "bridge" %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">{% trans "Connected VM interfaces" %}</div>
|
||||
{% if vms %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "VM" %}</th>
|
||||
<th scope="col">{% trans "Tap" %}</th>
|
||||
<th scope="col">{% trans "VM's MAC" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vm in vms %}
|
||||
<tr>
|
||||
<td>{% if vm.id %}<a href="{% url 'instances:instance' vm.id %}">{{ vm.vm }}</a>{% else %}{{ vm.vm }}{% endif %}</td>
|
||||
<td class="font-monospace">{{ vm.tap|default:"–" }}</td>
|
||||
<td class="font-monospace text-nowrap">{{ vm.mac|default:"–" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-body small text-body-secondary">{% trans "No running VM is on this bridge." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -12,71 +12,64 @@
|
|||
{% endblock page_heading_extra %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include "computes/host_nav.html" with section="interfaces" %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "computes/host_nav.html" with section="interfaces" %}
|
||||
|
||||
{% if changeable is False %}{% include 'interfaces_unchangeable.html' %}{% endif %}
|
||||
<div class="row">
|
||||
{% if not ifaces_all %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning shadow-sm">
|
||||
{% bs_icon 'exclamation-triangle' %} <strong>{% trans "Warning" %}:</strong> {% trans "Hypervisor doesn't have any Interfaces" %}
|
||||
{% if not ifaces_all %}
|
||||
<div class="alert alert-warning shadow-sm">
|
||||
{% bs_icon 'exclamation-triangle' %} {% trans "libvirt lists no interfaces of this host." %}
|
||||
</div>
|
||||
{% else %}
|
||||
<input id="filter" class="form-control w-auto mb-3" type="search" placeholder="{% trans "Search interfaces" %}" aria-label="{% trans "Search interfaces" %}">
|
||||
{# a card per interface, as the networks', under its type; a type's heading shows while any of its cards does #}
|
||||
{% for title, ifaces in iface_groups %}{% if ifaces %}
|
||||
<section class="searchable">
|
||||
<h2 class="h5 mb-3" data-group-header>{{ title }} <span class="badge text-bg-light border align-middle">{{ ifaces|length }}</span></h2>
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-4">
|
||||
{% for iface in ifaces %}
|
||||
<div class="col" data-search="{{ iface.name }} {{ iface.mac|default:"" }}{% for net in iface.networks %} {{ net }}{% endfor %}">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex flex-wrap align-items-center gap-2">
|
||||
<a class="fw-semibold me-auto text-break" href="{% url 'interface' compute.id iface.name %}">{{ iface.name }}</a>
|
||||
{% include "link_badge.html" with link=iface.link %}
|
||||
</div>
|
||||
<ul class="list-group list-group-flush flex-grow-1">
|
||||
{# the same rows on every card of a type, a dash for what is not known #}
|
||||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "MAC" %}</span>
|
||||
<span class="font-monospace text-break text-end">{{ iface.mac|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 "MTU" %}</span><span>{{ iface.mtu|default:"–" }}</span>
|
||||
</li>
|
||||
{% if iface.type == "ethernet" or iface.type == "bond" %}
|
||||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Speed" %}</span><span>{% if iface.speed %}{{ iface.speed }} Mb/s{% else %}–{% endif %}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if iface.type != "ethernet" %}
|
||||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Members" %}</span>
|
||||
<span>{% if iface.members %}{% blocktrans count counter=iface.members|length %}{{ counter }} reported{% plural %}{{ counter }} reported{% endblocktrans %}{% else %}–{% endif %}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item py-1 d-flex justify-content-between align-items-baseline gap-3">
|
||||
<span class="text-body-secondary">{% trans "Used by" %}</span>
|
||||
<span class="text-end">{% for net in iface.networks %}<a href="{% url 'network' compute.id net %}">{% blocktrans %}network {{ net }}{% endblocktrans %}</a>{% if not forloop.last %}, {% endif %}{% empty %}–{% endfor %}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% for iface in ifaces_all %}
|
||||
<div class="col-12 col-sm-4">
|
||||
<div class="mb-3 card {% if iface.state == 1 %}border-success{% else %}border-danger{% endif %} shadow-sm">
|
||||
<div class="card-header {% if iface.state == 1 %}bg-success-subtle{% else %}bg-danger-subtle{% endif %}">
|
||||
<h6 class="my-0 card-title">
|
||||
<a class="card-link {% if iface.state == 1 %}text-success-emphasis{% else %}text-danger-emphasis{% endif %}" href="{% url 'interface' compute.id iface.name %}">{{ iface.name }}</a>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="ps-0 pe-0 card-body">
|
||||
<div class="row">
|
||||
<div class="col-3 col-sm-4">
|
||||
<p><strong>{% trans "Type" %}:</strong></p>
|
||||
<p><strong>{% trans "MAC" %}:</strong></p>
|
||||
</div>
|
||||
<div class="col-9 col-sm-8">
|
||||
<p>{{ iface.type }}</p>
|
||||
<p>{{ iface.mac }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}{% endfor %}
|
||||
<div role="status">
|
||||
<div id="no-match" class="text-center text-body-secondary py-4" hidden>{% trans "No interface matches the search." %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#itype').change(function (eventObject) {
|
||||
if ($(this).val() == 'bridge') {
|
||||
$('.bridge_name_form_group').show();
|
||||
} else {
|
||||
$('.bridge_name_form_group').hide();
|
||||
}
|
||||
}).change();
|
||||
$('#ipv4').change(function (eventObject) {
|
||||
if ($(this).val() == 'static') {
|
||||
$('.static_ipv4_form_group').show();
|
||||
} else {
|
||||
$('.static_ipv4_form_group').hide();
|
||||
}
|
||||
}).change();
|
||||
$('#ipv6').change(function (eventObject) {
|
||||
if ($(this).val() == 'static') {
|
||||
$('.static_ipv6_form_group').show();
|
||||
} else {
|
||||
$('.static_ipv6_form_group').hide();
|
||||
}
|
||||
}).change();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% if ifaces_all %}<script src="{% static 'js/table_filter.js' %}"></script>{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap_icons %}
|
||||
<div class="alert alert-info shadow-sm">
|
||||
{% bs_icon 'info-circle' %} {% trans "This host's libvirt does not support changing network interfaces. Configure them directly on the host, for example with NetworkManager." %}
|
||||
<div class="alert alert-secondary small">
|
||||
{% bs_icon 'info-circle' %} {% trans "This host's libvirt only reads network interfaces and does not tell their addresses: change them on the host, for example with NetworkManager." %}
|
||||
</div>
|
||||
|
|
|
|||
1
interfaces/templates/link_badge.html
Normal file
1
interfaces/templates/link_badge.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
{% load i18n %}{# a link state as libvirt reports it: up, down, another (dormant, lowerlayerdown...) or none #}{% if link == "up" %}<span class="badge text-bg-success">{% trans "Link up" %}</span>{% elif link == "down" %}<span class="badge text-bg-secondary">{% trans "Link down" %}</span>{% elif link %}<span class="badge text-bg-light border">{% blocktrans %}Link {{ link }}{% endblocktrans %}</span>{% else %}<span class="badge text-bg-light border">{% trans "Link unknown" %}</span>{% endif %}
|
||||
|
|
@ -8,7 +8,7 @@ from computes.models import Compute
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from django.urls import reverse
|
||||
from lxml import etree, html
|
||||
from lxml import etree
|
||||
from vrtManager.interface import wvmInterfaces
|
||||
|
||||
from interfaces.forms import AddInterface
|
||||
|
|
@ -334,6 +334,24 @@ class CreateViewTestCase(TestCase):
|
|||
)
|
||||
self.assertEqual(conn.create_iface.called, created)
|
||||
|
||||
def test_a_device_taken_meanwhile_is_chosen_again(self):
|
||||
# not swapped for the first device the dialog still offers
|
||||
admin = get_user_model().objects.create_superuser("iface_admin3", "c@example.com", "x")
|
||||
compute = Compute.objects.create(name="iface-c3", hostname="127.0.0.1:1", login="root", password="", type=1)
|
||||
self.client.force_login(admin)
|
||||
for select, values in (("iface_netdev", {"netdev": "eth1"}), ("iface_device", {"itype": "ethernet", "name": "eth1", "netdev": "eth0"})):
|
||||
with self.subTest(**values), patch("interfaces.views.wvmInterfaces") as conn_cls:
|
||||
conn = conn_cls.return_value
|
||||
conn.get_ifaces_info.return_value = []
|
||||
conn.can_change_interfaces.return_value = True
|
||||
conn.get_net_devices.return_value = ["eth0", "eth1"]
|
||||
conn.get_member_candidates.return_value = ["eth0"]
|
||||
page = self.client.post(reverse("interfaces", args=[compute.id]),
|
||||
{**BASE, **values, "ipv6_type": "none", "create": ""}).content.decode()
|
||||
self.assertFalse(conn.create_iface.called)
|
||||
chosen = etree.HTML(page).xpath(f"//select[@id='{select}']/option[@selected]/@value")
|
||||
self.assertEqual(chosen, [""])
|
||||
|
||||
def test_unreadable_host_devices_are_reported_not_blamed_on_the_input(self):
|
||||
admin = get_user_model().objects.create_superuser("iface_admin2", "b@example.com", "x")
|
||||
compute = Compute.objects.create(name="iface-c2", hostname="127.0.0.1:1", login="root", password="", type=1)
|
||||
|
|
@ -353,11 +371,25 @@ class CreateViewTestCase(TestCase):
|
|||
self.assertFalse(any("not on this host" in m for m in messages), messages)
|
||||
|
||||
|
||||
class InterfaceListTestCase(TestCase):
|
||||
def test_the_interfaces_are_grouped_by_type(self):
|
||||
self.client.force_login(get_user_model().objects.create_superuser("iface_list", "l@example.com", "x"))
|
||||
compute = Compute.objects.create(name="iface-list", hostname="127.0.0.1:1", login="root", password="", type=1)
|
||||
ifaces = [{"name": name, "type": kind, "mac": None, "link": "up", "members": []}
|
||||
for name, kind in (("br0", "bridge"), ("eth0", "ethernet"), ("eth1", "ethernet"))]
|
||||
with patch("interfaces.views.wvmInterfaces") as conn_cls:
|
||||
conn_cls.return_value.get_ifaces_info.return_value = ifaces
|
||||
conn_cls.return_value.can_change_interfaces.return_value = False
|
||||
page = etree.HTML(self.client.get(reverse("interfaces", args=[compute.id])).content.decode())
|
||||
# no heading for a type the host has none of
|
||||
self.assertEqual([" ".join(h.itertext()).split() for h in page.xpath("//h2")], [["Bridges", "1"], ["Ethernet", "2"]])
|
||||
|
||||
|
||||
class UnchangeableHostTestCase(TestCase):
|
||||
"""A host whose libvirt cannot change interfaces (udev backend) gets no
|
||||
create form and no start/stop/delete, only a note; posts change nothing."""
|
||||
|
||||
NOTE = "does not support changing network interfaces"
|
||||
NOTE = "only reads network interfaces"
|
||||
|
||||
def setUp(self):
|
||||
self.client.force_login(get_user_model().objects.create_superuser("iface_ro", "ro@example.com", "x"))
|
||||
|
|
@ -397,17 +429,19 @@ class UnchangeableHostTestCase(TestCase):
|
|||
return conn, response
|
||||
|
||||
def test_the_interface_page_shows_the_state_without_actions(self):
|
||||
for state, shown in ((1, "Active"), (0, "Inactive")):
|
||||
# libvirt's own state is told where it manages the interface only; the link always
|
||||
for state in (1, 0):
|
||||
with self.subTest(state=state):
|
||||
_conn, response = self.interface_page(False, state)
|
||||
conn, response = self.interface_page(False, state)
|
||||
page = response.content.decode()
|
||||
self.assertNotIn('name="start"', page)
|
||||
self.assertNotIn('name="stop"', page)
|
||||
state_cell = html.fromstring(page).xpath("//dt[normalize-space()='State']/following-sibling::dd[1]")[0]
|
||||
self.assertEqual(state_cell.text_content().strip(), shown)
|
||||
self.assertNotIn(">Active<", page)
|
||||
self.assertIn(self.NOTE, page)
|
||||
_conn, response = self.interface_page(True, 0)
|
||||
self.assertIn('name="start"', response.content.decode())
|
||||
page = response.content.decode()
|
||||
self.assertIn('name="start"', page)
|
||||
self.assertIn(">Inactive<", page)
|
||||
|
||||
def test_a_host_that_could_not_be_read_offers_nothing(self):
|
||||
# not knowing whether the host can change interfaces is not a yes
|
||||
|
|
|
|||
|
|
@ -39,4 +39,4 @@ class LiveInterfacesTestCase(TestCase):
|
|||
)
|
||||
page = self.client.get(reverse("interfaces", args=[compute.id])).content.decode()
|
||||
self.assertEqual("#AddInterface" in page, changeable)
|
||||
self.assertEqual("does not support changing network interfaces" in page, not changeable)
|
||||
self.assertEqual("only reads network interfaces" in page, not changeable)
|
||||
|
|
|
|||
|
|
@ -90,3 +90,30 @@ class ReadTests(SimpleTestCase):
|
|||
xml = "<interface type='ethernet' name='eth0'><protocol family='ipv4'><ip address='192.0.2.10'/></protocol></interface>"
|
||||
conn.iface = MagicMock(**{"name.return_value": "eth0", "MACString.return_value": "", "XMLDesc.return_value": xml})
|
||||
self.assertEqual(conn.get_info()["ipv4"]["addresses"], ["192.0.2.10"])
|
||||
|
||||
|
||||
class BridgeUseTests(SimpleTestCase):
|
||||
def test_networks_and_running_vms_on_a_bridge(self):
|
||||
from vrtManager.interface import networks_by_device
|
||||
|
||||
nets = [MagicMock(**{"name.return_value": name, "XMLDesc.return_value": xml}) for name, xml in (
|
||||
("default", "<network><bridge name='virbr0'/></network>"),
|
||||
("lan", "<network><forward mode='bridge'/><bridge name='br0'/></network>"),
|
||||
("direct", "<network><forward mode='bridge'><interface dev='eth0'/><interface dev='eth1'/></forward></network>"),
|
||||
("sriov", "<network><forward mode='hostdev'><pf dev='eth2'/></forward></network>"))]
|
||||
self.assertEqual(networks_by_device(MagicMock(**{"listAllNetworks.return_value": nets})),
|
||||
{"virbr0": ["default"], "br0": ["lan"], "eth0": ["direct"], "eth1": ["direct"], "eth2": ["sriov"]})
|
||||
doms = [MagicMock(**{"name.return_value": name, "XMLDesc.return_value": xml}) for name, xml in (
|
||||
("web", "<domain><devices><interface type='bridge'><mac address='52:54:00:00:00:01'/><source bridge='br0'/>"
|
||||
"<target dev='vnet3'/></interface></devices></domain>"),
|
||||
("db", "<domain><devices><interface type='network'><mac address='52:54:00:00:00:02'/><source network='lan'/>"
|
||||
"<target dev='vnet7'/></interface></devices></domain>"),
|
||||
("other", "<domain><devices><interface type='network'><source network='default'/><target dev='vnet9'/>"
|
||||
"</interface></devices></domain>"))]
|
||||
conn = wvmInterface.__new__(wvmInterface)
|
||||
conn.iface = MagicMock(**{"name.return_value": "br0"})
|
||||
conn.wvm = MagicMock(**{"listAllDomains.return_value": doms})
|
||||
self.assertEqual(conn.get_vms(["lan"]), [
|
||||
{"vm": "db", "tap": "vnet7", "mac": "52:54:00:00:00:02"},
|
||||
{"vm": "web", "tap": "vnet3", "mac": "52:54:00:00:00:01"},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ from django.urls import reverse
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from libvirt import libvirtError
|
||||
from computes.models import Compute
|
||||
from instances.models import Instance
|
||||
from interfaces.forms import AddInterface
|
||||
from logs.audit import audit
|
||||
from vrtManager.interface import wvmInterface, wvmInterfaces
|
||||
from vrtManager.interface import networks_by_device, wvmInterface, wvmInterfaces
|
||||
from admin.decorators import superuser_only
|
||||
|
||||
|
||||
|
|
@ -19,7 +20,18 @@ def interfaces(request, compute_id):
|
|||
try:
|
||||
conn = wvmInterfaces(compute.hostname, compute.login, compute.password, compute.type)
|
||||
ifaces_all = conn.get_ifaces_info()
|
||||
defined_names = [iface["name"] for iface in ifaces_all]
|
||||
changeable = conn.can_change_interfaces()
|
||||
used_by = networks_by_device(conn.wvm)
|
||||
for iface in ifaces_all:
|
||||
iface["networks"] = used_by.get(iface["name"], [])
|
||||
# the list by type, in libvirt's four, a type the host has none of left out
|
||||
iface_groups = [(title, [iface for iface in ifaces_all if iface["type"] == kind])
|
||||
for kind, title in (("bridge", _("Bridges")), ("bond", _("Bonds")),
|
||||
("vlan", _("VLANs")), ("ethernet", _("Ethernet")))]
|
||||
form = AddInterface()
|
||||
# the new interface dialog's type: the one sent back after a refusal
|
||||
if_kind = request.POST.get("itype", "bridge")
|
||||
|
||||
# the host's devices are only the create form's choices
|
||||
netdevs, members, netdev_error = [], [], None
|
||||
|
|
@ -27,6 +39,7 @@ def interfaces(request, compute_id):
|
|||
try:
|
||||
netdevs = conn.get_net_devices()
|
||||
members = conn.get_member_candidates(netdevs)
|
||||
ethernet_devices = [dev for dev in members if dev not in defined_names]
|
||||
except Exception as err: # a host without the node device driver too
|
||||
netdev_error = err
|
||||
messages.error(request, _("The network devices of this host could not be read: %(error)s") % {"error": err})
|
||||
|
|
@ -35,19 +48,21 @@ def interfaces(request, compute_id):
|
|||
# interfaces; without the host's devices nothing can be checked
|
||||
if request.method == "POST" and "create" in request.POST and changeable and netdev_error is None:
|
||||
form = AddInterface(request.POST, netdevs=netdevs, members=members,
|
||||
defined=[iface["name"] for iface in ifaces_all])
|
||||
defined=defined_names)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
with audit(request, compute, f"Interface {data['name']}", "add"):
|
||||
conn.create_iface(
|
||||
data["name"], data["itype"], data["start_mode"], data["netdev"],
|
||||
data["ipv4_type"], data["ipv4_addr"], data["ipv4_gw"],
|
||||
data["ipv6_type"], data["ipv6_addr"], data["ipv6_gw"],
|
||||
data["stp"], data["delay"],
|
||||
)
|
||||
return HttpResponseRedirect(reverse("interfaces", args=[compute_id]))
|
||||
for msg_err in form.errors.values():
|
||||
messages.error(request, msg_err.as_text())
|
||||
try:
|
||||
with audit(request, compute, f"Interface {data['name']}", "add"):
|
||||
conn.create_iface(
|
||||
data["name"], data["itype"], data["start_mode"], data["netdev"],
|
||||
data["ipv4_type"], data["ipv4_addr"], data["ipv4_gw"],
|
||||
data["ipv6_type"], data["ipv6_addr"], data["ipv6_gw"],
|
||||
data["stp"], data["delay"],
|
||||
)
|
||||
except libvirtError as err: # shown in the dialog, which opens again
|
||||
form.add_error(None, str(err))
|
||||
else:
|
||||
return HttpResponseRedirect(reverse("interface", args=[compute_id, data["name"]]))
|
||||
conn.close()
|
||||
except libvirtError as lib_err:
|
||||
messages.error(request, lib_err)
|
||||
|
|
@ -86,7 +101,14 @@ def interface(request, compute_id, iface):
|
|||
return HttpResponseRedirect(here)
|
||||
|
||||
info = conn.get_info()
|
||||
info["addressing"] = [("IPv4", info["ipv4"]), ("IPv6", info["ipv6"])]
|
||||
state = conn.is_active()
|
||||
networks = networks_by_device(conn.wvm).get(iface, [])
|
||||
if info["type"] == "bridge":
|
||||
vms = conn.get_vms(networks)
|
||||
vm_ids = dict(Instance.objects.filter(compute=compute, name__in=[vm["vm"] for vm in vms]).values_list("name", "id"))
|
||||
for vm in vms:
|
||||
vm["id"] = vm_ids.get(vm["vm"])
|
||||
conn.close()
|
||||
except libvirtError as lib_err:
|
||||
messages.error(request, lib_err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue