1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/vrtManager/network.py
catborise 09d7b0c08a fix(networks): fixed addresses, failed creates and stopped networks behave
- A network created with fixed addresses gets one per address of its DHCP
  range for any subnet; the old prefix cut assumed a gateway ending in .1
  and built addresses such as 10.0.0.1.130. Fixed addresses need a /24 or
  smaller subnet.
- A new network that does not start (its subnet is in use) is undefined
  again, so the name is free for another try.
- Fixed addresses are changed and deleted on a stopped network too: the
  running network is changed only while it runs. A host with only a name
  is deleted as defined, not with an empty MAC, shows no "None", and has no
  edit button it could not use.
- The create dialog disables the fields it hides, so an IPv6 subnet typed
  and then unchecked is not created, and switching from MACVTAP back to
  BRIDGE restores the bridge label and the Open vSwitch option.
- Isolated networks show QoS; QoS save buttons no longer raise a script
  error; saving an unchanged fixed address says so; two messages lose a
  stray backslash and spaces.
2026-10-26 15:36:57 +03:00

413 lines
16 KiB
Python

from libvirt import (
VIR_NETWORK_SECTION_IP_DHCP_HOST,
VIR_NETWORK_UPDATE_AFFECT_CONFIG,
VIR_NETWORK_UPDATE_AFFECT_LIVE,
VIR_NETWORK_UPDATE_COMMAND_ADD_LAST,
VIR_NETWORK_UPDATE_COMMAND_DELETE,
VIR_NETWORK_UPDATE_COMMAND_MODIFY, libvirtError)
from lxml import etree
from vrtManager import util
import ipaddress
from vrtManager.connection import wvmConnect
def network_size(subnet, dhcp=None):
"""
Func return gateway, mask and dhcp pool.
"""
net = ipaddress.ip_network(subnet, strict=False)
gateway = str(net.network_address + 1)
if net.version == 4:
mask = str(net.netmask)
dhcp_pool = [str(net.network_address + 2), str(net.broadcast_address - 1)]
else:
mask = str(net.prefixlen)
dhcp_pool = [str(net.network_address + 256), str(net.network_address + 511)]
return (gateway, mask, dhcp_pool) if dhcp else (gateway, mask, None)
class wvmNetworks(wvmConnect):
def get_networks_info(self):
get_networks = self.get_networks()
networks = []
for network in get_networks:
net = self.get_network(network)
net_status = net.isActive()
try:
net_bridge = net.bridgeName()
except libvirtError:
net_bridge = util.get_xml_path(net.XMLDesc(0), "/network/forward/interface/@dev")
net_forward = util.get_xml_path(net.XMLDesc(0), "/network/forward/@mode")
networks.append({
"name": network,
"status": net_status,
"device": net_bridge,
"forward": net_forward
})
return networks
def create_network(
self,
name,
forward,
ipv4,
gateway,
mask,
dhcp4,
ipv6,
gateway6,
prefix6,
dhcp6,
bridge,
openvswitch,
fixed=False,
):
esc_name = util.xml_escape(str(name))
esc_forward = util.xml_escape(str(forward)) if forward else ""
esc_bridge = util.xml_escape(str(bridge)) if bridge else ""
esc_gateway = util.xml_escape(str(gateway)) if gateway else ""
esc_mask = util.xml_escape(str(mask)) if mask else ""
esc_gateway6 = util.xml_escape(str(gateway6)) if gateway6 else ""
esc_prefix6 = util.xml_escape(str(prefix6)) if prefix6 else ""
xml = f"""
<network>
<name>{esc_name}</name>"""
if forward in ["nat", "route", "bridge"]:
xml += f"""<forward mode='{esc_forward}'/>"""
if forward == "macvtap":
xml += f"""<forward mode='bridge'>
<interface dev='{esc_bridge}'/>
</forward>"""
else:
xml += """<bridge """
if forward in ["nat", "route", "none"]:
xml += """stp='on' delay='0'"""
if forward == "bridge":
xml += f"""name='{esc_bridge}'"""
xml += """/>"""
if openvswitch is True:
xml += """<virtualport type='openvswitch'/>"""
if forward not in ["bridge", "macvtap"]:
if ipv4:
xml += f"""<ip address='{esc_gateway}' netmask='{esc_mask}'>"""
if dhcp4:
dhcp4_0 = util.xml_escape(str(dhcp4[0]))
dhcp4_1 = util.xml_escape(str(dhcp4[1]))
xml += f"""<dhcp>
<range start='{dhcp4_0}' end='{dhcp4_1}' />"""
if fixed:
first, last = (int(ipaddress.IPv4Address(ip)) for ip in dhcp4)
for ip in range(first, last + 1):
xml += f"""<host mac='{util.randomMAC()}' ip='{ipaddress.IPv4Address(ip)}' />"""
xml += """</dhcp>"""
xml += """</ip>"""
if ipv6:
xml += f"""<ip family='ipv6' address='{esc_gateway6}' prefix='{esc_prefix6}'>"""
if dhcp6:
dhcp6_0 = util.xml_escape(str(dhcp6[0]))
dhcp6_1 = util.xml_escape(str(dhcp6[1]))
xml += f"""<dhcp>
<range start='{dhcp6_0}' end='{dhcp6_1}' />"""
xml += """</dhcp>"""
xml += """</ip>"""
xml += """</network>"""
net = self.wvm.networkDefineXML(xml)
try:
net.create()
except libvirtError: # e.g. the subnet is in use: the name stays free for another try
net.undefine()
raise
net.setAutostart(1)
class wvmNetwork(wvmConnect):
def __init__(self, host, login, passwd, conn, net):
wvmConnect.__init__(self, host, login, passwd, conn)
self.leases = None
self.net = self.get_network(net)
self.parent_count = len(self.get_ip_networks())
def get_name(self):
return self.net.name()
def _XMLDesc(self, flags):
return self.net.XMLDesc(flags)
def get_autostart(self):
return self.net.autostart()
def set_autostart(self, value):
self.net.setAutostart(value)
def is_active(self):
return self.net.isActive()
def get_uuid(self):
return self.net.UUIDString()
def get_bridge_device(self):
try:
return self.net.bridgeName()
except Exception:
return util.get_xml_path(self._XMLDesc(0), "/network/forward/interface/@dev")
def start(self):
self.net.create()
def _vms_on_it(self, running_only):
name = self.net.name()
return self.domains_using(
lambda domain: domain.xpath("devices/interface[@type='network']/source[@network=$name]", name=name),
running_only,
)
def stop(self):
"""Refused while a running VM is on it: that VM would be cut off."""
if vms := self._vms_on_it(running_only=True):
raise util.OperationError(f"Network {self.net.name()} is in use by the running VMs {', '.join(vms)}; "
"shut them down or remove their network cards first")
self.net.destroy()
def delete(self):
"""Refused while a VM's definition names it: that VM would no longer start."""
if vms := self._vms_on_it(running_only=False):
raise util.OperationError(f"VMs {', '.join(vms)} use network {self.net.name()} and would no longer "
"start; remove their network cards first")
self.net.undefine()
def update(self, command, section, xml, parentIndex):
"""Changes the network's configuration and, while it runs, the running
network too (libvirt refuses a live change on a stopped one)."""
flags = VIR_NETWORK_UPDATE_AFFECT_CONFIG | (VIR_NETWORK_UPDATE_AFFECT_LIVE if self.is_active() else 0)
return self.net.update(command, section, parentIndex, xml, flags)
def get_ip_networks(self):
ip_networks = {}
xml = self._XMLDesc(0)
if util.get_xml_path(xml, "/network/ip") is None:
return ip_networks
tree = etree.fromstring(xml)
ips = tree.findall(".ip")
for ip in ips:
address_str = ip.get("address")
netmask_str = ip.get("netmask")
prefix = ip.get("prefix")
family = ip.get("family", "ipv4")
mask = prefix if prefix is not None else netmask_str
if address_str and mask:
ret = ipaddress.ip_network(f"{address_str}/{mask}", strict=False)
elif address_str:
ret = ipaddress.ip_address(address_str)
else:
continue
ip_networks[family] = ret
return ip_networks
def get_network_mac(self):
xml = self._XMLDesc(0)
return util.get_xml_path(xml, "/network/mac/@address")
def get_network_forward(self):
xml = self._XMLDesc(0)
fw = util.get_xml_path(xml, "/network/forward/@mode")
forward_dev = util.get_xml_path(xml, "/network/forward/@dev")
return [fw, forward_dev]
def get_dhcp_range(self, family="ipv4"):
xml = self._XMLDesc(0)
if family == "ipv4":
dhcpstart = util.get_xml_path(xml, "/network/ip[not(@family='ipv6')]/dhcp/range[1]/@start")
dhcpend = util.get_xml_path(xml, "/network/ip[not(@family='ipv6')]/dhcp/range[1]/@end")
if family == "ipv6":
dhcpstart = util.get_xml_path(xml, "/network/ip[@family='ipv6']/dhcp/range[1]/@start")
dhcpend = util.get_xml_path(xml, "/network/ip[@family='ipv6']/dhcp/range[1]/@end")
return None if not dhcpstart or not dhcpend else [ipaddress.ip_address(dhcpstart), ipaddress.ip_address(dhcpend)]
def get_dhcp_range_start(self, family="ipv4"):
dhcp = self.get_dhcp_range(family)
return dhcp[0] if dhcp else None
def get_dhcp_range_end(self, family="ipv4"):
dhcp = self.get_dhcp_range(family)
return dhcp[1] if dhcp else None
def can_pxe(self):
xml = self._XMLDesc(0)
forward = self.get_network_forward()[0]
if forward and forward != "nat":
return True
return bool(util.get_xml_path(xml, "/network/ip/dhcp/bootp/@file"))
def get_dhcp_host_addr(self, family="ipv4"):
result = []
tree = etree.fromstring(self._XMLDesc(0))
for ipdhcp in tree.findall("./ip"):
if family == "ipv4":
if ipdhcp.get("family") is not None:
continue
hosts = ipdhcp.findall("./dhcp/host")
for host in hosts:
host_ip = host.get("ip")
mac = host.get("mac")
name = host.get("name", "")
result.append({"ip": host_ip, "mac": mac, "name": name})
return result
if family == "ipv6":
hosts = tree.xpath("./ip[@family='ipv6']/dhcp/host")
for host in hosts:
host_ip = host.get("ip")
host_id = host.get("id")
name = host.get("name", "")
result.append({"ip": host_ip, "id": host_id, "name": name})
return result
def modify_dhcp_range(self, range_start, range_end, family="ipv4"):
"""Refused while the network runs: the running network would keep the old range."""
if self.is_active():
raise util.OperationError(f"Stop network {self.net.name()} to change its DHCP range")
tree = etree.fromstring(self._XMLDesc(0))
ip = {"ipv4": "ip[not(@family='ipv6')]", "ipv6": "ip[@family='ipv6']"}.get(family)
dhcp_range = tree.xpath(f"{ip}/dhcp/range") if ip else []
if not dhcp_range:
raise util.OperationError(f"Network {self.net.name()} has no {family} DHCP range")
dhcp_range[0].set("start", range_start)
dhcp_range[0].set("end", range_end)
self.wvm.networkDefineXML(etree.tostring(tree).decode())
def delete_fixed_address(self, ip, family="ipv4"):
tree = etree.fromstring(self._XMLDesc(0))
if family == "ipv4":
hosts = tree.xpath("/network/ip[not(@family='ipv6')]/dhcp/host")
parent_index = self.parent_count - 2
if family == "ipv6":
hosts = tree.xpath("/network/ip[@family='ipv6']/dhcp/host")
parent_index = self.parent_count - 1
for h in hosts:
if h.get("ip") == ip:
# the host as it is: a host may have only a name and an address
new_xml = etree.tostring(h, with_tail=False).decode()
self.update(
VIR_NETWORK_UPDATE_COMMAND_DELETE,
VIR_NETWORK_SECTION_IP_DHCP_HOST,
new_xml,
parent_index,
)
break
def modify_fixed_address(self, name, address, mac_duid, family="ipv4"):
tree = etree.fromstring(self._XMLDesc(0))
name_esc = util.xml_escape(name) if name else ""
name_attr = f' name="{name_esc}"' if name else ""
mac_esc = util.xml_escape(mac_duid)
ip_addr = ipaddress.ip_address(address)
if family == "ipv4":
new_xml = f'<host mac="{mac_esc}"{name_attr} ip="{ip_addr}"/>'
hosts = tree.xpath("./ip[not(@family='ipv6')]/dhcp/host")
compare_var = "mac"
parent_index = self.parent_count - 2
if family == "ipv6":
new_xml = f'<host id="{mac_esc}"{name_attr} ip="{ip_addr}"/>'
hosts = tree.xpath("./ip[@family='ipv6']/dhcp/host")
compare_var = "id"
parent_index = self.parent_count - 1
new_host_xml = etree.fromstring(new_xml)
host = next((h for h in hosts if h.get(compare_var) == mac_duid), None)
if host is None:
self.update(
VIR_NETWORK_UPDATE_COMMAND_ADD_LAST,
VIR_NETWORK_SECTION_IP_DHCP_HOST,
new_xml,
parent_index,
)
else:
# change the host
if host.get("name") == new_host_xml.get("name") and host.get("ip") == new_host_xml.get("ip"):
return False
else:
self.update(
VIR_NETWORK_UPDATE_COMMAND_MODIFY,
VIR_NETWORK_SECTION_IP_DHCP_HOST,
new_xml,
parent_index,
)
def get_qos(self):
qos_values = {}
tree = etree.fromstring(self._XMLDesc(0))
qos = tree.xpath("/network/bandwidth")
if qos:
qos = qos[0]
in_qos = qos.find("inbound")
if in_qos is not None:
in_av = in_qos.get("average")
in_peak = in_qos.get("peak")
in_burst = in_qos.get("burst")
qos_values["inbound"] = {"average": in_av, "peak": in_peak, "burst": in_burst}
out_qos = qos.find("outbound")
if out_qos is not None:
out_av = out_qos.get("average")
out_peak = out_qos.get("peak")
out_burst = out_qos.get("burst")
qos_values["outbound"] = {"average": out_av, "peak": out_peak, "burst": out_burst}
return qos_values
def set_qos(self, direction, average, peak, burst):
if direction not in ("inbound", "outbound"):
raise ValueError("Direction must be inbound or outbound")
try:
average, peak, burst = int(average), int(peak), int(burst)
except ValueError:
raise ValueError("QoS rates must be whole numbers")
xml = f"<{direction} average='{average}' peak='{peak}' burst='{burst}'/>"
tree = etree.fromstring(self._XMLDesc(0))
band = tree.xpath("/network/bandwidth")
if len(band) == 0:
xml = f"<bandwidth>{xml}</bandwidth>"
tree.append(etree.fromstring(xml))
else:
direct = band[0].find(direction)
if direct is not None:
parent = direct.getparent()
parent.remove(direct)
parent.append(etree.fromstring(xml))
else:
band[0].append(etree.fromstring(xml))
new_xml = etree.tostring(tree).decode()
self.wvm.networkDefineXML(new_xml)
def unset_qos(self, direction):
# direction goes into the XPath below
if direction not in ("inbound", "outbound"):
raise ValueError("Direction must be inbound or outbound")
tree = etree.fromstring(self._XMLDesc(0))
for direct in tree.xpath(f"/network/bandwidth/{direction}"):
parent = direct.getparent()
parent.remove(direct)
self.wvm.networkDefineXML(etree.tostring(tree).decode())
def edit_network(self, new_xml):
self.wvm.networkDefineXML(new_xml)
def refresh_dhcp_leases(self):
try:
self.leases = self.net.DHCPLeases()
except Exception as e:
self.leases = []
raise util.OperationError(f"Error getting {self} DHCP leases: {e}") from e
def get_dhcp_leases(self):
if self.leases is None:
self.refresh_dhcp_leases()
return self.leases