mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 12:16:49 +00:00
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.
203 lines
9.1 KiB
Python
203 lines
9.1 KiB
Python
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_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, 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)
|
|
|
|
@cached_property
|
|
def tree(self):
|
|
return etree.fromstring(self.nwfilter.XMLDesc(0))
|
|
|
|
def get_uuid(self):
|
|
return self.nwfilter.UUIDString()
|
|
|
|
def get_name(self):
|
|
return self.nwfilter.name()
|
|
|
|
def get_xml(self):
|
|
return etree.tostring(self.tree, encoding="unicode")
|
|
|
|
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 = []
|
|
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 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 _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):
|
|
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."""
|
|
rule = util.parse_xml(xml)
|
|
if rule.tag != "rule":
|
|
raise util.OperationError("Enter one <rule> element")
|
|
tree = copy.deepcopy(self.tree)
|
|
tree.append(rule)
|
|
self._define(tree)
|