mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
- 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.
413 lines
16 KiB
Python
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
|