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

fix(nwfilters): a filter in use is not deleted, and a change replaces no other filter

Plan reviewed with Codex; libvirt's behaviour checked on a host.

- A filter is not deleted while a VM's interface references it, as the
  VM runs or as saved for its next boot (a stopped VM too), or while
  another filter references it: libvirt refuses only one a running VM
  uses, and a VM using a filter that references a deleted one does not
  start. The VMs are read from one listing, one or two XML reads each,
  instead of a connection per VM; the refusal names them.
- Create refuses a name or UUID the host has, clone a name in use or
  one outside the panel's names, and editing the XML must keep the
  filter's name and UUID: each would replace another filter or make a
  copy. Entered XML is read for these checks without expanding entities
  or taking a DTD (util.parse_xml, shared with the network editor),
  and a rule is one <rule> element.
- A rule or reference is deleted only while it reads as the page showed
  it, not by index and three attributes: a rule alike in those or a
  filter changed meanwhile loses nothing.
- A reference is added only to another filter of the host not
  referenced yet; the picker offers those.
- References keep their parameters, in order (a name may repeat); a
  rule shows every protocol element it has, a rule without one too.
- One change a POST (two would both start from the page's reading,
  the second undoing the first); every change redirects and reports its
  refusal as a message, a POST for a filter gone meanwhile going to the
  list; the catch-all that showed any error goes, and so does
  wvmInstance.get_filterrefs, which only the old delete check used. The list reads each filter's
  XML once from one listing; a filter's page reads its XML once and
  only the other filters' names; the per-filter XML dialog of the list
  goes (a filter's page shows its XML). The XML editors send their text
  untouched too.
This commit is contained in:
catborise 2026-10-29 00:09:15 +03:00
parent 65c5a251ba
commit bc2b70544a
13 changed files with 502 additions and 352 deletions

View file

@ -142,11 +142,11 @@ class NetworkChangeTests(SimpleTestCase):
net.net.update.assert_not_called()
def test_the_xml_editor_expands_no_entity(self):
# a file of the panel's host named by an external entity is not read into the check
# a file of the panel's host named by an external entity is not read: no DTD is taken
net = self.network()
xml = ('<!DOCTYPE network [<!ENTITY name SYSTEM "file:///etc/hostname">]>'
"<network><name>&name;</name></network>")
with self.assertRaisesRegex(OperationError, "must define network lab"):
with self.assertRaisesRegex(OperationError, "may not declare a DTD"):
net.edit_network(xml)
def test_the_xml_editor_takes_an_encoding_declaration(self):

View file

@ -50,8 +50,8 @@
<label class="input-group-text" for="nwfilter_select">{% trans "Filter" %}</label>
<select id="nwfilter_select" name="nwfilters_select" class="form-select">
<option value="" selected>{% trans 'None' %}</option>
{% for nwf in nwfilters_all %}
<option value="{{ nwf.name }}">{{ nwf.name }}</option>
{% for nwf in ref_choices %}
<option value="{{ nwf }}">{{ nwf }}</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-success float-end" name="add_nwfilter_ref" title="{% trans "Add" %}">
@ -78,11 +78,12 @@
{% for ref in refs %}
<tr>
<th scope="row">{{ forloop.counter }}</th>
<td>{{ ref }}</td>
<td>{{ ref.filter }}{% for param, value in ref.parameters %} <span class="font-monospace small">{{ param }}={{ value }}</span>{% endfor %}</td>
<td style="width:30px;">
<form action="" method="post" role="form" aria-label="Delete NWFilter form">{% csrf_token %}
<input type="hidden" name="ref" value="{{ ref }}">
<button type="submit" class="btn btn-sm btn-danger" name="del_nwfilter_ref" title="{% trans "Delete" %}" data-confirm="{% blocktrans with ref=ref %}Remove the reference to {{ ref }}? Its rules no longer apply through this filter.{% endblocktrans %}" onclick="return confirm(this.dataset.confirm)">
<input type="hidden" name="index" value="{{ forloop.counter0 }}">
<input type="hidden" name="ref" value="{{ ref.text }}">
<button type="submit" class="btn btn-sm btn-danger" name="del_nwfilter_ref" title="{% trans "Delete" %}" 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>
@ -120,13 +121,11 @@
<td>{{ rule.direction }}</td>
<td>{{ rule.priority }}</td>
<td>{{ rule.statematch }}</td>
<td>{{ rule.directives }}</td>
<td class="font-monospace small">{% for protocol in rule.protocols %}<div>{{ protocol.name }}{% for attribute, value in protocol.attributes %} {{ attribute }}={{ value }}{% endfor %}</div>{% endfor %}</td>
<td>
<form action="" method="post" role="form" aria-label="Delete NWFilter rule">{% csrf_token %}
<input type="hidden" name="index" value="{{ rule.index }}">
<input type="hidden" name="action" value="{{ rule.action }}">
<input type="hidden" name="direction" value="{{ rule.direction }}">
<input type="hidden" name="priority" value="{{ rule.priority }}">
<input type="hidden" name="index" value="{{ forloop.counter0 }}">
<input type="hidden" name="rule" value="{{ rule.text }}">
<button type="submit" class="btn btn-sm btn-danger" name="del_nwfilter_rule" title="{% trans 'Delete' %}" 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>
@ -146,6 +145,7 @@
editor.getSession().setMode("ace/mode/xml");
var edit_input = $('input[name="edit_xml"]');
edit_input.val(editor.getSession().getValue());
editor.getSession().on("change",function () {
edit_input.val(editor.getSession().getValue());
});
@ -154,6 +154,7 @@
rule_editor.getSession().setMode("ace/mode/xml");
var rule_input = $('input[name="nwfilterrule_xml"]');
rule_input.val(rule_editor.getSession().getValue());
rule_editor.getSession().on("change",function () {
rule_input.val(rule_editor.getSession().getValue());
});

View file

@ -29,7 +29,7 @@
<th scope="col" data-sort-method="number">#</th>
<th scope="col">{% trans "UUID" %}</th>
<th scope="col">{% trans "Name" %}</th>
<th scope="colgroup" data-sort-method="none" colspan="3">{% trans "Action" %}</th>
<th scope="colgroup" data-sort-method="none" colspan="2">{% trans "Action" %}</th>
</tr>
</thead>
<tbody class="searchable">
@ -38,29 +38,6 @@
<th scope="row">{{ forloop.counter }}</th>
<td><a class="link-primary" href="{% url 'nwfilter' compute.id nwfilter.name %} ">{{ nwfilter.uuid }}</a></td>
<td>{{ nwfilter.name }}</td>
<td>
<div class="modal fade" id="Show{{ forloop.counter }}" tabindex="-1" role="dialog" aria-labelledby="showNWFilter" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Details of NWFilter" %}: <span class="text-danger">{{ nwfilter.name }}</span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<textarea rows="10" class="w-100" aria-label="{% trans "Details of NWFilter" %}">
{{ nwfilter.xml }}
</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
</div>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /.modal -->
<button class="btn btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#Show{{ forloop.counter }}" title="{% trans "Show" %}">
{% bs_icon 'eye' %}
</button>
</td>
<td>
<div class="modal fade" id="Clone{{ forloop.counter }}" tabindex="-1" role="dialog" aria-labelledby="addNwFilterLabel" aria-hidden="true">
<div class="modal-dialog">
@ -123,6 +100,7 @@
editor.getSession().setMode("ace/mode/xml");
var input = $('input[name="nwfilter_xml"]');
input.val(editor.getSession().getValue());
editor.getSession().on("change",function () {
input.val(editor.getSession().getValue());
})

View file

@ -1,7 +1,7 @@
"""Network filter changes write rows on the Logs page. libvirt's test driver
has no filters, so the connection is a mock (as in test_redefine.py)."""
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from computes.models import Compute
from django.contrib.auth import get_user_model
@ -11,6 +11,8 @@ from django.urls import reverse
from libvirt import libvirtError
from logs.models import Logs
from lxml import etree
from vrtManager.nwfilters import element_text
UUID = "6d1f7a10-0000-4000-8000-00000000c0de"
XML = (f"<filter name='web' chain='root'><uuid>{UUID}</uuid><filterref filter='clean-traffic'/>"
@ -31,16 +33,16 @@ class FilterRowTests(TestCase):
nwfilter.XMLDesc.return_value = XML
nwfilter.name.return_value = "web"
nwfilter.UUIDString.return_value = UUID
self.wvm.listNWFilters.return_value = ["web"]
self.wvm.listNWFilters.return_value = ["web", "clean-traffic", "no-spoofing"]
self.wvm.listAllNWFilters.return_value = [nwfilter]
self.wvm.listAllDomains.return_value = []
self.compute = Compute.objects.create(name="c1", hostname="localhost", login="", password="", type=4)
self.client.force_login(get_user_model().objects.create_superuser("audit-admin", "a@example.com", "pw"))
Logs.objects.all().delete() # the login's row
self.list_url = reverse("nwfilters", args=[self.compute.id])
self.url = reverse("nwfilter", args=[self.compute.id, "web"])
@patch("nwfilters.views.wvmInstances")
def test_filter_list_actions(self, instances):
instances.return_value.get_instances.return_value = []
def test_filter_list_actions(self):
# the list is read again, so the new filter shows
self.assertRedirects(self.client.post(self.list_url, {"create_nwfilter": "", "nwfilter_xml": NEW}),
self.list_url, fetch_redirect_response=False)
@ -61,34 +63,35 @@ class FilterRowTests(TestCase):
self.client.post(self.list_url, {"create_nwfilter": "", "nwfilter_xml": other})
self.wvm.nwfilterDefineXML.assert_not_called()
self.assertEqual(rows(), [
("Network filter web: add: name already in use", False),
("Network filter other: add: UUID already in use", False),
("Network filter web: add: A network filter with this name already exists", False),
("Network filter other: add: A network filter with this UUID already exists", False),
])
def test_an_unparsable_filter(self):
self.wvm.nwfilterDefineXML.side_effect = libvirtError("XML error")
response = self.client.post(self.list_url, {"create_nwfilter": "", "nwfilter_xml": "<filter"})
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, self.list_url, fetch_redirect_response=False)
[(message, success)] = rows()
self.assertFalse(success)
self.assertTrue(message.startswith("Network filter (no name): add: "), message)
self.assertTrue(message.startswith("Network filter (no name): add: The XML is not valid"), message)
self.wvm.nwfilterDefineXML.assert_not_called()
@patch("nwfilters.views.wvmInstance")
@patch("nwfilters.views.wvmInstances")
def test_a_filter_in_use_is_not_deleted(self, instances, instance):
instances.return_value.get_instances.return_value = ["web-1"]
instance.return_value.get_filterrefs.return_value = ["web"]
def test_a_filter_in_use_is_not_deleted(self):
vm = MagicMock(**{"name.return_value": "web-1", "isActive.return_value": False, "XMLDesc.return_value":
"<domain><devices><interface type='network'><filterref filter='web'/></interface></devices></domain>"})
self.wvm.listAllDomains.return_value = [vm]
self.client.post(self.list_url, {"del_nwfilter": "", "nwfiltername": "web"})
self.wvm.nwfilterLookupByName.return_value.undefine.assert_not_called()
self.assertEqual(rows(), [("Network filter web: delete: in use by web-1", False)])
self.assertEqual(rows(), [("Network filter web: delete: Filter web is used by VM web-1", False)])
def test_filter_page_actions(self):
self.client.post(self.url, {"edit_nwfilter": "", "edit_xml": XML})
self.client.post(self.url, {"add_nwfilter_rule": "",
"nwfilterrule_xml": "<rule action='accept' direction='in' priority='500'><all/></rule>"})
self.client.post(self.url, {"del_nwfilter_rule": "", "index": "0", "action": "drop", "direction": "in", "priority": "900"})
tree = etree.fromstring(XML)
self.client.post(self.url, {"del_nwfilter_rule": "", "index": "0", "rule": element_text(tree.find("rule"))})
self.client.post(self.url, {"add_nwfilter_ref": "", "nwfilters_select": "no-spoofing"})
self.client.post(self.url, {"del_nwfilter_ref": "", "ref": "clean-traffic"})
self.client.post(self.url, {"del_nwfilter_ref": "", "index": "0", "ref": element_text(tree.find("filterref"))})
self.assertEqual(rows(), [
("Network filter web: change XML", True),
("Network filter web: add rule", True),

View file

@ -0,0 +1,182 @@
"""What the panel reads of the host's network filters and what it refuses
to change: which VMs use a filter, as they run and as saved; a filter in use
or referenced is not deleted; a create, clone or edit replaces no other
filter; references and rules as libvirt keeps them. libvirt's test driver has
no filters, so the connection is a mock."""
from unittest.mock import MagicMock
import libvirt
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.nwfilters import wvmNWFilter, wvmNWFilters
from vrtManager.util import OperationError
UUID = "6d1f7a10-0000-4000-8000-00000000c0de"
WEB = (f"<filter name='web' chain='root'><uuid>{UUID}</uuid>"
"<filterref filter='clean-traffic'><parameter name='IP' value='192.0.2.10'/>"
"<parameter name='IP' value='192.0.2.11'/></filterref>"
"<rule action='accept' direction='inout' priority='500'><arp match='no' arpsrcipaddr='192.0.2.1'/>"
"<arp opcode='request'/></rule>"
"<rule action='drop' direction='in'/></filter>")
def nwfilter(name, xml, uuid=UUID):
return MagicMock(**{"name.return_value": name, "UUIDString.return_value": uuid, "XMLDesc.return_value": xml})
def vm(name, xml, saved=None, active=False, persistent=True):
def describe(flags=0):
return saved if flags else xml
return MagicMock(**{"name.return_value": name, "XMLDesc.side_effect": describe,
"isActive.return_value": active, "isPersistent.return_value": persistent})
def using(*names):
refs = "".join(f"<interface type='network'><filterref filter='{name}'/></interface>" for name in names)
return f"<domain><devices>{refs}</devices></domain>"
def error(code):
err = libvirt.libvirtError("refused")
err.get_error_code = lambda: code
return err
def filters(*listed, domains=()):
conn = wvmNWFilters.__new__(wvmNWFilters)
conn.wvm = MagicMock(**{"listAllNWFilters.return_value": list(listed), "listAllDomains.return_value": list(domains),
"listNWFilters.return_value": [f.name() for f in listed]})
return conn
def one(xml=WEB, names=("web", "clean-traffic", "no-spoofing")):
conn = wvmNWFilter.__new__(wvmNWFilter)
conn.nwfilter = nwfilter("web", xml)
conn.wvm = MagicMock(**{"listNWFilters.return_value": list(names)})
return conn
def defined(conn):
return etree.fromstring(conn.wvm.nwfilterDefineXML.call_args.args[0])
class UsersTests(SimpleTestCase):
def test_a_filter_is_used_as_the_vm_runs_and_as_saved(self):
conn = filters(domains=[
vm("stopped", using("a")),
vm("running", using("b", "b"), saved=using("c"), active=True), # c only from its next boot
vm("transient", using("d"), saved=using("x"), active=True, persistent=False),
])
self.assertEqual(conn.get_users(), {"a": ["stopped"], "b": ["running"], "c": ["running"], "d": ["transient"]})
stopped, running, transient = conn.wvm.listAllDomains.return_value
self.assertEqual((stopped.XMLDesc.call_count, running.XMLDesc.call_count, transient.XMLDesc.call_count), (1, 2, 1))
def test_a_vm_gone_meanwhile_uses_nothing_other_errors_are_raised(self):
gone = vm("gone", "")
gone.XMLDesc.side_effect = error(libvirt.VIR_ERR_NO_DOMAIN)
self.assertEqual(filters(domains=[gone, vm("web-1", using("a"))]).get_users(), {"a": ["web-1"]})
gone.XMLDesc.side_effect = error(libvirt.VIR_ERR_OPERATION_FAILED)
with self.assertRaises(libvirt.libvirtError):
filters(domains=[gone]).get_users()
class DeleteTests(SimpleTestCase):
def test_a_filter_a_vm_or_another_filter_uses_is_kept(self):
target = nwfilter("clean-traffic", "<filter name='clean-traffic'/>", uuid="other")
for conn, why in ((filters(target, domains=[vm("web-1", using("clean-traffic"))]), "used by VM web-1"),
(filters(target, nwfilter("web", WEB)), "referenced by filter web")):
with self.subTest(why=why), self.assertRaisesRegex(OperationError, why):
conn.delete_nwfilter("clean-traffic")
conn.wvm.nwfilterLookupByName.return_value.undefine.assert_not_called()
def test_an_unused_filter_is_undefined_libvirt_may_still_refuse(self):
conn = filters(nwfilter("web", WEB))
conn.delete_nwfilter("web")
conn.wvm.nwfilterLookupByName.return_value.undefine.assert_called_once_with()
conn.wvm.nwfilterLookupByName.return_value.undefine.side_effect = error(libvirt.VIR_ERR_OPERATION_INVALID)
with self.assertRaises(libvirt.libvirtError): # a VM started meanwhile: libvirt refuses itself
conn.delete_nwfilter("web")
class IdentityTests(SimpleTestCase):
def test_a_create_replaces_no_filter(self):
conn = filters(nwfilter("web", WEB))
for xml, why in ((WEB, "name already exists"),
(WEB.replace("name='web'", "name='other'"), "UUID already exists"),
("<rule/>", "one <filter>"),
("<filter name='a b'/>", "letters, digits"),
('<!DOCTYPE filter [<!ENTITY n "web">]><filter name="&n;"/>', "DTD"),
("", "not valid")):
with self.subTest(why=why), self.assertRaisesRegex(OperationError, why):
conn.create_nwfilter(xml)
conn.wvm.nwfilterDefineXML.assert_not_called()
conn.create_nwfilter("<filter name='other' chain='root'/>")
conn.wvm.nwfilterDefineXML.assert_called_once_with("<filter name='other' chain='root'/>")
def test_a_clone_keeps_rules_and_parameters_with_a_uuid_of_its_own(self):
conn = filters(nwfilter("web", WEB))
conn.wvm.nwfilterLookupByName.return_value.XMLDesc.return_value = WEB
for name, why in (("web", "already exists"), ("web copy", "letters, digits"), ("", "letters, digits")):
with self.subTest(name=name), self.assertRaisesRegex(OperationError, why):
conn.clone_nwfilter("web", name)
conn.clone_nwfilter("web", "web-copy")
clone = defined(conn)
self.assertEqual((clone.get("name"), clone.find("uuid")), ("web-copy", None))
self.assertEqual(clone.xpath("filterref/parameter/@value"), ["192.0.2.10", "192.0.2.11"])
self.assertEqual(len(clone.findall("rule")), 2)
def test_an_edit_is_of_the_same_filter(self):
conn = one()
for xml, why in ((WEB.replace("name='web'", "name='other'"), "must define filter web"),
(WEB.replace(f"<uuid>{UUID}</uuid>", ""), "keep the filter's UUID"),
(WEB.replace(UUID, "11111111-2222-3333-4444-555555555555"), "keep the filter's UUID")):
with self.subTest(why=why), self.assertRaisesRegex(OperationError, why):
conn.edit(xml)
conn.wvm.nwfilterDefineXML.assert_not_called()
conn.edit(WEB.replace("priority='500'", "priority='400'"))
conn.wvm.nwfilterDefineXML.assert_called_once()
class ChangeTests(SimpleTestCase):
def test_a_reference_is_to_another_filter_of_the_host_not_yet_referenced(self):
for name in ("web", "clean-traffic", "missing"):
with self.subTest(name=name), self.assertRaisesRegex(OperationError, "cannot be referenced"):
one().add_ref(name)
conn = one()
conn.add_ref("no-spoofing")
self.assertEqual(defined(conn).xpath("filterref/@filter"), ["clean-traffic", "no-spoofing"])
self.assertEqual(one().get_ref_choices(), ["no-spoofing"])
def test_a_rule_is_one_rule_element(self):
with self.assertRaisesRegex(OperationError, "one <rule>"):
one().add_rule("<filter name='web'/>")
def test_a_change_keeps_the_rest_of_the_filter(self):
conn = one()
conn.add_rule("<rule action='accept' direction='in'><udp dstportstart='53'/></rule>")
tree = defined(conn)
self.assertEqual((tree.findtext("uuid"), len(tree.findall("rule"))), (UUID, 3))
self.assertEqual(tree.xpath("filterref/parameter/@value"), ["192.0.2.10", "192.0.2.11"])
self.assertEqual(len(etree.fromstring(conn.get_xml()).findall("rule")), 2, "the page's own copy is not changed")
class ReadTests(SimpleTestCase):
def test_rules_and_references_as_libvirt_keeps_them(self):
conn = one()
arp, drop = conn.get_rules()
# two elements of one protocol, a match and a negated one; a rule of no protocol
self.assertEqual([p["attributes"] for p in arp["protocols"]], [[("match", "no"), ("arpsrcipaddr", "192.0.2.1")],
[("opcode", "request")]])
self.assertEqual((drop["protocols"], drop["priority"]), ([], None))
[ref] = conn.get_refs()
self.assertEqual(ref["parameters"], [("IP", "192.0.2.10"), ("IP", "192.0.2.11")])
self.assertEqual(conn.nwfilter.XMLDesc.call_count, 1)
def test_the_list_reads_each_filter_once(self):
conn = filters(nwfilter("web", WEB), nwfilter("clean-traffic", "<filter name='clean-traffic' chain='root'/>", "x"))
info = conn.get_nwfilters_info()
self.assertEqual([(f["name"], f["rules"], f["refs"]) for f in info],
[("clean-traffic", 0, []), ("web", 2, ["clean-traffic"])])
for listed in conn.wvm.listAllNWFilters.return_value:
self.assertEqual(listed.XMLDesc.call_count, 1)

View file

@ -13,6 +13,7 @@ from django.test import TestCase
from django.urls import reverse
from libvirt import libvirtError
from lxml import etree, html
from vrtManager.nwfilters import element_text
UUID = "6d1f7a10-0000-4000-8000-00000000c0de"
XML = (f"<filter name='web' chain='root'><uuid>{UUID}</uuid><filterref filter='clean-traffic'/>"
@ -20,6 +21,11 @@ XML = (f"<filter name='web' chain='root'><uuid>{UUID}</uuid><filterref filter='c
"<rule action='drop' direction='in' priority='900'><all/></rule></filter>")
def text(xml, tag, index):
"""The text the page sends back for the tag element at index."""
return element_text(etree.fromstring(xml).findall(tag)[index])
class FilterRedefineTests(TestCase):
def setUp(self):
patcher = patch("vrtManager.connection.connection_manager.get_connection")
@ -29,10 +35,11 @@ class FilterRedefineTests(TestCase):
nwfilter.XMLDesc.return_value = XML
nwfilter.name.return_value = "web"
nwfilter.UUIDString.return_value = UUID
self.wvm.listNWFilters.return_value = ["web"]
self.wvm.listNWFilters.return_value = ["web", "clean-traffic", "no-spoofing"]
compute = Compute.objects.create(name="nwf", hostname="localhost", login="", password="", type=4)
self.client.force_login(get_user_model().objects.create_superuser("nwf", "nwf@example.com", "pw"))
self.url = reverse("nwfilter", args=[compute.id, "web"])
self.list_url = reverse("nwfilters", args=[compute.id])
def defined(self, data):
response = self.client.post(self.url, data)
@ -41,12 +48,12 @@ class FilterRedefineTests(TestCase):
return etree.fromstring(self.wvm.nwfilterDefineXML.call_args.args[0]), response
def test_a_rule_is_removed_in_place(self):
tree, _ = self.defined({"del_nwfilter_rule": "", "index": "1", "action": "drop", "direction": "in", "priority": "900"})
tree, _ = self.defined({"del_nwfilter_rule": "", "index": "1", "rule": text(XML, "rule", 1)})
self.assertEqual(tree.findtext("uuid"), UUID)
self.assertEqual([r.get("action") for r in tree.findall("rule")], ["accept"])
def test_a_reference_is_removed_in_place(self):
tree, _ = self.defined({"del_nwfilter_ref": "", "ref": "clean-traffic"})
tree, _ = self.defined({"del_nwfilter_ref": "", "index": "0", "ref": text(XML, "filterref", 0)})
self.assertEqual((tree.findtext("uuid"), tree.findall("filterref")), (UUID, []))
def test_a_rule_is_added_in_place(self):
@ -70,43 +77,59 @@ class FilterRedefineTests(TestCase):
tree, _ = self.defined({"edit_nwfilter": "", "edit_xml": edited})
self.assertEqual(tree.findtext("uuid"), UUID)
def test_one_change_a_post(self):
# two at once would both start from the page's reading, the second undoing the first
rule = "<rule action='accept' direction='in' priority='500'><udp dstportstart='53'/></rule>"
self.defined({"add_nwfilter_rule": "", "nwfilterrule_xml": rule, "add_nwfilter_ref": "", "nwfilters_select": "no-spoofing"})
def test_a_post_for_a_filter_gone_meanwhile_goes_to_the_list(self):
self.wvm.nwfilterLookupByName.side_effect = libvirtError("Network filter not found")
response = self.client.post(self.url, {"del_nwfilter_rule": "", "index": "0", "rule": ""})
self.assertRedirects(response, self.list_url, fetch_redirect_response=False)
self.wvm.nwfilterDefineXML.assert_not_called()
def test_a_refused_definition_is_reported_not_undone(self):
self.wvm.nwfilterDefineXML.side_effect = libvirtError("XML error: Invalid value for attribute 'action'")
_, response = self.defined({"del_nwfilter_rule": "", "index": "1", "action": "drop", "direction": "in", "priority": "900"})
_, response = self.defined({"del_nwfilter_rule": "", "index": "1", "rule": text(XML, "rule", 1)})
self.assertIn("Invalid value for attribute 'action'", " ".join(str(m) for m in get_messages(response.wsgi_request)))
ALIKE = (f"<filter name='web' chain='root'><uuid>{UUID}</uuid>"
"<rule action='accept' direction='in' priority='500'><tcp dstportstart='22'/></rule>"
"<rule action='accept' direction='in' priority='500'><tcp dstportstart='80'/></rule></filter>")
class RuleDeleteTests(TestCase):
"""Rules sharing action, direction and priority: the one clicked goes."""
"""Rules sharing action, direction and priority: the one clicked goes,
and only while it reads as the page showed it."""
def setUp(self):
patcher = patch("vrtManager.connection.connection_manager.get_connection")
self.wvm = patcher.start().return_value
self.addCleanup(patcher.stop)
nwfilter = self.wvm.nwfilterLookupByName.return_value
nwfilter.XMLDesc.return_value = (
f"<filter name='web' chain='root'><uuid>{UUID}</uuid>"
"<rule action='accept' direction='in' priority='500'><tcp dstportstart='22'/></rule>"
"<rule action='accept' direction='in' priority='500'><tcp dstportstart='80'/></rule></filter>")
nwfilter.XMLDesc.return_value = ALIKE
nwfilter.name.return_value = "web"
nwfilter.UUIDString.return_value = UUID
self.wvm.listNWFilters.return_value = ["web"]
compute = Compute.objects.create(name="nwf", hostname="localhost", login="", password="", type=4)
self.client.force_login(get_user_model().objects.create_superuser("nwf", "nwf@example.com", "pw"))
self.url = reverse("nwfilter", args=[compute.id, "web"])
def delete(self, index, priority="500"):
return self.client.post(self.url, {"del_nwfilter_rule": "", "index": index, "action": "accept",
"direction": "in", "priority": priority})
def delete(self, index, rule):
return self.client.post(self.url, {"del_nwfilter_rule": "", "index": index, "rule": rule})
def test_the_second_of_two_alike_rules_is_deleted(self):
self.delete("1")
# sent back by a browser, with other line breaks between its tags
self.delete("1", text(ALIKE, "rule", 1).replace("><", ">\r\n <"))
tree = etree.fromstring(self.wvm.nwfilterDefineXML.call_args.args[0])
self.assertEqual(tree.xpath("rule/*/@dstportstart"), ["22"])
def test_a_rule_that_is_no_longer_there_is_not_deleted(self):
for index, priority in (("2", "500"), ("1", "900")):
# gone, another one at its place (reordered), or not an index
for index, rule in (("2", text(ALIKE, "rule", 1)), ("0", text(ALIKE, "rule", 1)),
("-1", text(ALIKE, "rule", 1)), ("x", text(ALIKE, "rule", 0)),
("²", text(ALIKE, "rule", 0)), ("9" * 5000, text(ALIKE, "rule", 0))):
with self.subTest(index=index):
response = self.delete(index, priority)
response = self.delete(index, rule)
self.assertIn("reload the page", " ".join(str(m) for m in get_messages(response.wsgi_request)))
self.wvm.nwfilterDefineXML.assert_not_called()

View file

@ -1,7 +1,10 @@
from unittest.mock import patch
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from computes.models import Compute
from libvirt import libvirtError
class NWFiltersTestCase(TestCase):
@ -28,3 +31,10 @@ class NWFiltersTestCase(TestCase):
reverse("nwfilter", args=[self.compute.id, "clean-traffic"])
)
self.assertEqual(response.status_code, 403)
def test_a_list_post_to_a_host_not_reached_is_redirected(self):
self.client.force_login(User.objects.create_superuser("nwfilter-admin", "a@example.com", "pwd"))
url = reverse("nwfilters", args=[self.compute.id])
with patch("nwfilters.views.wvmNWFilters", side_effect=libvirtError("unable to connect")):
response = self.client.post(url, {"del_nwfilter": "", "nwfiltername": "web"})
self.assertRedirects(response, url, fetch_redirect_response=False)

View file

@ -3,209 +3,93 @@ from computes.models import Compute
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.utils.translation import gettext_noop as _
from django.urls import reverse
from libvirt import libvirtError
from logs.audit import audit, failed
from logs.audit import audit
from vrtManager import util
from vrtManager.instance import wvmInstance, wvmInstances
from vrtManager.nwfilters import wvmNWFilter, wvmNWFilters
@superuser_only
def nwfilters(request, compute_id):
"""
:param request:
:param compute_id:
:return:
"""
nwfilters_all = []
"""The host's network filters; one created, cloned or deleted, after
which the list is read again."""
compute = get_object_or_404(Compute, pk=compute_id)
nwfilters_all = []
try:
conn = wvmNWFilters(
compute.hostname,
compute.login,
compute.password,
compute.type
)
for nwf in conn.get_nwfilters():
nwfilters_all.append(conn.get_nwfilter_info(nwf))
conn = wvmNWFilters(compute.hostname, compute.login, compute.password, compute.type)
if request.method == "POST":
name = request.POST.get("nwfiltername", "")
if "create_nwfilter" in request.POST:
xml = request.POST.get("nwfilter_xml", "")
if xml:
name = uuid = None
try:
util.etree.fromstring(xml)
name = util.get_xml_path(xml, "/filter/@name")
uuid = util.get_xml_path(xml, "/filter/uuid")
except util.etree.ParseError:
pass
subject = f"Network filter {name or '(no name)'}"
try: # the row names the filter where the XML does
new_name = util.parse_xml(xml).get("name")
except util.OperationError:
new_name = None
with audit(request, compute, f"Network filter {new_name or '(no name)'}", "add"):
conn.create_nwfilter(xml)
elif "cln_nwfilter" in request.POST:
clone_name = request.POST.get("cln_name", "")
with audit(request, compute, f"Network filter {name}", f"clone as {clone_name}"):
conn.clone_nwfilter(name, clone_name)
elif "del_nwfilter" in request.POST:
with audit(request, compute, f"Network filter {name}", "delete"):
conn.delete_nwfilter(name)
return HttpResponseRedirect(request.get_full_path())
for nwf in nwfilters_all:
if name == nwf["name"]:
failed(request, compute, subject, "add", "name already in use")
messages.error(request, _("A network filter with this name already exists"))
break
if uuid == nwf["uuid"]:
failed(request, compute, subject, "add", "UUID already in use")
messages.error(request, _("A network filter with this UUID already exists"))
break
else:
try:
with audit(request, compute, subject, "add"):
conn.create_nwfilter(xml)
except libvirtError as lib_err:
messages.error(request, lib_err)
else:
return HttpResponseRedirect(request.get_full_path())
if "del_nwfilter" in request.POST:
name = request.POST.get("nwfiltername", "")
in_use = False
nwfilter = conn.get_nwfilter(name)
nwfilter_info = conn.get_nwfilter_info(name)
is_conn = wvmInstances(
compute.hostname,
compute.login,
compute.password,
compute.type
)
instances = is_conn.get_instances()
for inst in instances:
i_conn = wvmInstance(
compute.hostname,
compute.login,
compute.password,
compute.type,
inst,
)
dom_filterrefs = i_conn.get_filterrefs()
if name in dom_filterrefs:
in_use = True
msg = _(
"NWFilter is in use by %(instance)s. Cannot be deleted."
) % {"instance": inst}
messages.error(request, msg)
failed(request, compute, f"Network filter {name}", "delete", f"in use by {inst}")
i_conn.close()
break
is_conn.close()
if nwfilter and not in_use:
with audit(request, compute, f"Network filter {name}", "delete"):
nwfilter.undefine()
nwfilters_all.remove(nwfilter_info)
if "cln_nwfilter" in request.POST:
name = request.POST.get("nwfiltername", "")
cln_name = request.POST.get("cln_name", name + "-clone")
with audit(request, compute, f"Network filter {name}", f"clone as {cln_name}"):
conn.clone_nwfilter(name, cln_name)
nwfilters_all.append(conn.get_nwfilter_info(cln_name))
conn.close()
except libvirtError as lib_err:
nwfilters_all = conn.get_nwfilters_info()
except libvirtError as lib_err: # OperationError too: a name in use, a filter in use
messages.error(request, lib_err)
except Exception as err:
messages.error(request, err)
if request.method == "POST": # a host not reached as well: nothing was changed
return HttpResponseRedirect(request.get_full_path())
return render(
request,
"nwfilters.html",
{
"nwfilters": nwfilters_all,
"compute": compute,
},
)
return render(request, "nwfilters.html", {"nwfilters": nwfilters_all, "compute": compute})
@superuser_only
def nwfilter(request, compute_id, nwfltr):
"""
:param request:
:param compute_id:
:param nwfltr:
:return:
"""
nwfilters_all = []
"""A network filter of the host: its rules and references changed one at
a time, or its XML as a whole."""
compute = get_object_or_404(Compute, pk=compute_id)
try:
nwfilter = wvmNWFilter(
compute.hostname,
compute.login,
compute.password,
compute.type,
nwfltr
)
conn = wvmNWFilters(
compute.hostname,
compute.login,
compute.password,
compute.type
)
for nwf in conn.get_nwfilters():
nwfilters_all.append(conn.get_nwfilter_info(nwf))
uuid = nwfilter.get_uuid()
name = nwfilter.get_name()
xml = nwfilter.get_xml()
rules = nwfilter.get_rules()
refs = nwfilter.get_filter_refs()
nwfilter = wvmNWFilter(compute.hostname, compute.login, compute.password, compute.type, nwfltr)
if request.method == "POST":
subject = f"Network filter {name}"
if "edit_nwfilter" in request.POST:
new_xml = request.POST.get("edit_xml", "")
if new_xml:
post = request.POST
subject = f"Network filter {nwfltr}"
try:
if "edit_nwfilter" in post:
with audit(request, compute, subject, "change XML"):
conn.create_nwfilter(new_xml)
if "del_nwfilter_rule" in request.POST:
index = int(request.POST.get("index", ""))
action = request.POST.get("action", "")
direction = request.POST.get("direction", "")
priority = request.POST.get("priority", "")
with audit(request, compute, subject, "delete rule"):
conn.create_nwfilter(nwfilter.delete_rule(index, action, direction, priority))
if "del_nwfilter_ref" in request.POST:
ref_name = request.POST.get("ref")
with audit(request, compute, subject, f"delete reference {ref_name}"):
conn.create_nwfilter(nwfilter.delete_ref(ref_name))
if "add_nwfilter_rule" in request.POST:
rule_xml = request.POST.get("nwfilterrule_xml", "")
if not rule_xml:
return HttpResponseRedirect(request.get_full_path())
with audit(request, compute, subject, "add rule"):
conn.create_nwfilter(nwfilter.add_rule(rule_xml))
if "add_nwfilter_ref" in request.POST:
ref_name = request.POST.get("nwfilters_select", "")
if not ref_name:
return HttpResponseRedirect(request.get_full_path())
with audit(request, compute, subject, f"add reference {ref_name}"):
conn.create_nwfilter(nwfilter.add_ref(ref_name))
nwfilter.edit(post.get("edit_xml", ""))
elif "add_nwfilter_rule" in post:
with audit(request, compute, subject, "add rule"):
nwfilter.add_rule(post.get("nwfilterrule_xml", ""))
elif "del_nwfilter_rule" in post:
with audit(request, compute, subject, "delete rule"):
nwfilter.delete_rule(post.get("index", ""), post.get("rule", ""))
elif "add_nwfilter_ref" in post:
ref_name = post.get("nwfilters_select", "")
with audit(request, compute, subject, f"add reference {ref_name}"):
nwfilter.add_ref(ref_name)
elif "del_nwfilter_ref" in post:
with audit(request, compute, subject, "delete reference") as entry:
entry.action = f"delete reference {nwfilter.delete_ref(post.get('index', ''), post.get('ref', ''))}"
except libvirtError as err: # OperationError too: a filter changed meanwhile, XML refused
messages.error(request, err)
return HttpResponseRedirect(request.get_full_path())
conn.close()
nwfilter.close()
name = nwfilter.get_name()
uuid = nwfilter.get_uuid()
xml = nwfilter.get_xml()
rules = nwfilter.get_rules()
refs = nwfilter.get_refs()
ref_choices = nwfilter.get_ref_choices()
except libvirtError as lib_err:
messages.error(request, lib_err)
except Exception as error_msg:
messages.error(request, error_msg)
if request.method == "POST": # a filter gone meanwhile, a host not reached: nothing was changed
return HttpResponseRedirect(reverse("nwfilters", args=[compute_id]))
return render(request, "nwfilter.html", locals())

View file

@ -522,17 +522,6 @@ class wvmInstance(wvmConnect):
title = util.get_xml_path(self._XMLDesc(PERSISTENT_XML if config else 0), "/domain/title")
return title or ""
def get_filterrefs(self):
def filterrefs(ctx):
result = []
for net in ctx.xpath("/domain/devices/interface"):
filterref = net.xpath("filterref/@filter")
if filterref:
result.append(filterref[0])
return result
return util.get_xml_path(self._XMLDesc(0), func=filterrefs)
def get_description(self, config=False):
description = util.get_xml_path(self._XMLDesc(PERSISTENT_XML if config else 0), "/domain/description")
return description or ""

View file

@ -442,12 +442,7 @@ class wvmNetwork(wvmConnect):
def edit_network(self, new_xml):
"""Defines this network anew from new_xml: of the same name and, if it
names one, the same UUID, not another network."""
# read for its name and UUID only, its entities not expanded: libvirt gets the text itself
parser = etree.XMLParser(resolve_entities=False, no_network=True)
try:
tree = etree.fromstring(new_xml.encode(), parser) # bytes: the XML may declare its encoding
except etree.XMLSyntaxError as err:
raise util.OperationError(f"The XML is not valid: {err}") from None
tree = util.parse_xml(new_xml)
if tree.tag != "network" or tree.findtext("name") != self.net.name():
raise util.OperationError(f"The XML must define network {self.net.name()}")
if tree.findtext("uuid") not in (None, self.net.UUIDString()):

View file

@ -1,35 +1,119 @@
from xml.etree import ElementTree
import copy
import re
from functools import cached_property
from libvirt import VIR_DOMAIN_XML_INACTIVE, VIR_ERR_NO_DOMAIN, libvirtError
from lxml import etree
from vrtManager import util
from vrtManager.connection import wvmConnect
# the names the panel gives filters: letters, digits, dots, dashes and underscores
NAME = re.compile(r"[A-Za-z0-9._-]+")
def element_text(element):
"""An element as the page shows it and sends it back: a rule or reference
is changed only while it still reads the same."""
return same_text(etree.tostring(element, encoding="unicode", with_tail=False))
def same_text(text):
"""text without the space between its tags, which a browser may send
back with other line breaks."""
return re.sub(r">\s+<", "><", text.strip())
def filter_info(nwfilter, tree):
"""What a filter and its XML tell: name, UUID, chain, priority, how many
rules it has and the filters it references."""
return {"name": nwfilter.name(), "uuid": nwfilter.UUIDString(), "chain": tree.get("chain"),
"priority": tree.get("priority"), "rules": len(tree.findall("rule")),
"refs": [ref.get("filter") for ref in tree.findall("filterref")]}
class wvmNWFilters(wvmConnect):
def get_nwfilter_info(self, name):
nwfilter = self.get_nwfilter(name)
xml = nwfilter.XMLDesc(0)
uuid = nwfilter.UUIDString()
return {"name": name, "uuid": uuid, "xml": xml}
def get_nwfilters_info(self):
"""Every filter of the host (filter_info) by name: one listing, one XML
read per filter."""
filters = [filter_info(nwfilter, etree.fromstring(nwfilter.XMLDesc(0)))
for nwfilter in self.wvm.listAllNWFilters(0)]
return sorted(filters, key=lambda nwfilter: nwfilter["name"])
def get_users(self):
"""The VMs whose interfaces reference each filter, by filter name: as
they run and, for a persistent VM that runs, as saved for its next
boot. One listing, one or two XML reads per VM."""
users = {}
for dom in self.wvm.listAllDomains(0):
try:
xmls = [dom.XMLDesc(0)]
if dom.isActive() and dom.isPersistent():
xmls.append(dom.XMLDesc(VIR_DOMAIN_XML_INACTIVE))
except libvirtError as err: # a VM undefined meanwhile uses none
if err.get_error_code() != VIR_ERR_NO_DOMAIN:
raise
continue
names = set()
for xml in xmls:
names.update(etree.fromstring(xml).xpath("devices/interface/filterref/@filter"))
for name in names:
users.setdefault(name, []).append(dom.name())
return users
def create_nwfilter(self, xml):
"""Define a filter from the XML entered: one whose name or UUID the
host has is refused, as it would replace that filter."""
tree = util.parse_xml(xml)
if tree.tag != "filter":
raise util.OperationError("Enter one <filter> element")
if not NAME.fullmatch(tree.get("name") or ""):
raise util.OperationError("The filter's name may have letters, digits, dots, dashes and underscores")
for nwfilter in self.wvm.listAllNWFilters(0):
if nwfilter.name() == tree.get("name"):
raise util.OperationError("A network filter with this name already exists")
if nwfilter.UUIDString() == tree.findtext("uuid"):
raise util.OperationError("A network filter with this UUID already exists")
self.wvm.nwfilterDefineXML(xml)
def clone_nwfilter(self, name, cln_name):
nwfilter = self.get_nwfilter(name)
if nwfilter:
tree = ElementTree.fromstring(nwfilter.XMLDesc(0))
tree.set("name", cln_name)
uuid = tree.find("uuid")
tree.remove(uuid)
self.create_nwfilter(ElementTree.tostring(tree).decode())
def clone_nwfilter(self, name, clone_name):
"""Define a copy of filter name as clone_name, with its rules,
references and their parameters, and a UUID of its own."""
if not NAME.fullmatch(clone_name):
raise util.OperationError("The filter's name may have letters, digits, dots, dashes and underscores")
if clone_name in self.get_nwfilters():
raise util.OperationError(f"A network filter named {clone_name} already exists")
tree = etree.fromstring(self.get_nwfilter(name).XMLDesc(0))
tree.set("name", clone_name)
tree.remove(tree.find("uuid"))
self.wvm.nwfilterDefineXML(etree.tostring(tree, encoding="unicode"))
def delete_nwfilter(self, name):
"""Undefine a filter that no VM's interface and no other filter
references. libvirt refuses only one a running VM uses; a stopped VM,
or a VM using a filter that references it, would not start."""
vms = self.get_users().get(name, [])
if vms:
raise util.OperationError(f"Filter {name} is used by VM {', '.join(vms)}")
referencing = [nwfilter["name"] for nwfilter in self.get_nwfilters_info() if name in nwfilter["refs"]]
if referencing:
raise util.OperationError(f"Filter {name} is referenced by filter {', '.join(referencing)}")
self.get_nwfilter(name).undefine()
class wvmNWFilter(wvmConnect):
"""A filter of the host. A page reads its XML once (tree); a change is
the filter defined again with its UUID, which libvirt does in place, also
while a VM uses it, and refuses for XML it cannot parse or apply to the
running VMs' interfaces."""
def __init__(self, host, login, passwd, conn, nwfiltername):
wvmConnect.__init__(self, host, login, passwd, conn)
self.nwfilter = self.get_nwfilter(nwfiltername)
def _XMLDesc(self, flags):
return self.nwfilter.XMLDesc(flags)
@cached_property
def tree(self):
return etree.fromstring(self.nwfilter.XMLDesc(0))
def get_uuid(self):
return self.nwfilter.UUIDString()
@ -38,75 +122,82 @@ class wvmNWFilter(wvmConnect):
return self.nwfilter.name()
def get_xml(self):
"""With the UUID: defined again with it, libvirt changes the filter in
place, also while a VM uses it. It refuses XML it cannot parse or
apply to the running VMs' interfaces."""
return self._XMLDesc(0)
return etree.tostring(self.tree, encoding="unicode")
def get_filter_refs(self):
tree = ElementTree.fromstring(self._XMLDesc(0))
return [ref.get("filter") for ref in tree.findall("./filterref")]
def get_refs(self):
"""The filters it references, each with its parameters in order (one
name may repeat, giving a list) and its text."""
return [{"filter": ref.get("filter"), "text": element_text(ref),
"parameters": [(param.get("name"), param.get("value")) for param in ref.findall("parameter")]}
for ref in self.tree.findall("filterref")]
def get_rules(self):
"""Its rules: action, direction, priority, statematch, each protocol
element with its attributes (one rule may have several of one
protocol, a match and a negated one) and the rule's text."""
rules = []
tree = ElementTree.fromstring(self._XMLDesc(0))
for index, r in enumerate(tree.findall("./rule")):
rule_action = r.get("action")
rule_direction = r.get("direction")
rule_priority = r.get("priority")
rule_statematch = r.get("statematch")
rule_directives = r.find("./")
if rule_directives is not None:
rule_directives = ElementTree.tostring(rule_directives).decode()
rule_info = {
"index": index,
"action": rule_action,
"direction": rule_direction,
"priority": rule_priority,
"statematch": rule_statematch,
"directives": rule_directives,
}
rules.append(rule_info)
for rule in self.tree.findall("rule"):
protocols = [{"name": protocol.tag, "attributes": list(protocol.attrib.items())} for protocol in rule]
rules.append({"action": rule.get("action"), "direction": rule.get("direction"),
"priority": rule.get("priority"), "statematch": rule.get("statematch"),
"protocols": protocols, "text": element_text(rule)})
return rules
def delete_ref(self, name):
tree = ElementTree.fromstring(self._XMLDesc(0))
for ref in tree.findall("./filterref"):
if name == ref.get("filter"):
tree.remove(ref)
break
return ElementTree.tostring(tree).decode()
def get_ref_choices(self):
"""The filters a reference can be added to: not this one, nor one it
references already."""
taken = {self.get_name(), *(ref.get("filter") for ref in self.tree.findall("filterref"))}
return [name for name in sorted(self.get_nwfilters()) if name not in taken]
def delete_rule(self, index, action, direction, priority):
"""The filter without its rule at index (several rules may share
action, direction and priority); those must still match, so a filter
changed since the page was read loses no other rule."""
tree = ElementTree.fromstring(self._XMLDesc(0))
rules = tree.findall("./rule")
if index >= len(rules) or (rules[index].get("action"), rules[index].get("direction"),
rules[index].get("priority")) != (action, direction, priority):
raise ValueError("The filter has changed; reload the page and try again")
tree.remove(rules[index])
return ElementTree.tostring(tree).decode()
def _define(self, tree):
self.wvm.nwfilterDefineXML(etree.tostring(tree, encoding="unicode"))
def _without(self, tag, index, text):
"""The filter without its tag element at index, which must still read
as text: a filter changed since the page was read loses nothing else.
Returns the element removed."""
tree = copy.deepcopy(self.tree)
elements = tree.findall(tag)
try:
index = int(index)
except ValueError:
index = -1
if not 0 <= index < len(elements) or element_text(elements[index]) != same_text(text):
raise util.OperationError("The filter has changed; reload the page and try again")
tree.remove(elements[index])
self._define(tree)
return elements[index]
def edit(self, xml):
"""Define the filter anew from the XML entered: the same filter, of
its name and UUID."""
tree = util.parse_xml(xml)
if tree.tag != "filter" or tree.get("name") != self.get_name():
raise util.OperationError(f"The XML must define filter {self.get_name()}")
if tree.findtext("uuid") != self.get_uuid():
raise util.OperationError("The XML must keep the filter's UUID")
self.wvm.nwfilterDefineXML(xml)
def delete_rule(self, index, text):
self._without("rule", index, text)
def delete_ref(self, index, text):
"""Returns the name of the filter it referenced."""
return self._without("filterref", index, text).get("filter")
def add_ref(self, name):
tree = ElementTree.fromstring(self._XMLDesc(0))
element = ElementTree.Element("filterref")
element.attrib["filter"] = name
tree.append(element)
return ElementTree.tostring(tree).decode()
if name not in self.get_ref_choices():
raise util.OperationError(f"Filter {name} cannot be referenced: it is this one, referenced already or not on the host")
tree = copy.deepcopy(self.tree)
etree.SubElement(tree, "filterref", filter=name)
self._define(tree)
def add_rule(self, xml):
"""The filter with the rule added as entered; libvirt keeps one
protocol per rule, so it is never merged into another rule."""
tree = ElementTree.fromstring(self._XMLDesc(0))
rule = ElementTree.fromstring(xml)
rule = util.parse_xml(xml)
if rule.tag != "rule":
raise ValueError("Enter one <rule> element")
raise util.OperationError("Enter one <rule> element")
tree = copy.deepcopy(self.tree)
tree.append(rule)
return ElementTree.tostring(tree).decode()
self._define(tree)

View file

@ -10,16 +10,11 @@ if not settings.configured:
from vrtManager.connection import wvmConnect
from vrtManager.network import wvmNetwork
from vrtManager.nwfilters import wvmNWFilter
CAPS = """<capabilities><guest><arch name='x86_64'><wordsize>64</wordsize><emulator>/usr/bin/qemu-x86</emulator>
<machine canonical='pc-q35'>q35</machine></arch>
<arch name='aarch64'><emulator>/usr/bin/qemu-arm</emulator><machine>virt</machine></arch></guest></capabilities>"""
FILTER = """<filter name='f'><rule action='drop' direction='in' priority='1'><tcp/></rule>
<rule action='drop' direction='in' priority='2'><udp/></rule></filter>"""
class FakeConn:
def __init__(self):
self.defined = None
@ -41,20 +36,6 @@ class XPathValueTests(unittest.TestCase):
self.assertEqual(self.conn.get_machine_types(injected), [])
self.assertEqual(self.conn.get_capabilities(injected), {})
def test_nwfilter_rule_is_matched_by_its_attributes(self):
nwf = wvmNWFilter.__new__(wvmNWFilter)
nwf._XMLDesc = lambda flags: FILTER
# values with quotes or brackets are compared, not parsed as XPath
for action in ("drop'][@priority='2", "it's"):
with self.subTest(action=action), self.assertRaises(ValueError):
nwf.delete_rule(0, action, "in", "1")
kept = nwf.delete_rule(1, "drop", "in", "2")
self.assertEqual(kept.count("<rule "), 1)
self.assertIn("priority=\"1\"", kept)
# add_rule adds a rule of its own
added = nwf.add_rule("<rule action='drop' direction='in' priority='2'><icmp/></rule>")
self.assertEqual(added.count("<rule "), 3)
def test_qos_direction_must_be_known(self):
net = wvmNetwork.__new__(wvmNetwork)
net.wvm = FakeConn()

View file

@ -102,6 +102,19 @@ def compareMAC(p, q):
return 0
def parse_xml(xml):
"""XML a user entered, read for the panel's checks: its entities not
expanded, nothing fetched and no DTD; libvirt gets the text itself."""
parser = etree.XMLParser(resolve_entities=False, no_network=True)
try:
tree = etree.fromstring(xml.encode(), parser) # bytes: the XML may declare its encoding
except etree.XMLSyntaxError as err:
raise OperationError(f"The XML is not valid: {err}") from None
if tree.getroottree().docinfo.internalDTD is not None:
raise OperationError("The XML may not declare a DTD")
return tree
def get_xml_path(xml, path=None, func=None):
"""
Return the content from the passed xml xpath, or return the result