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

refactor(instances): one way to reach a VM, its console and its devices

The .vv console file is built from the VM found by its UUID, with the
console page's getters, instead of looking the VM up by name five times;
a console without a listen address or password now gives 127.0.0.1 and
an empty password instead of "None". Removed methods nothing calls: the
by-name getters and power methods of wvmInstances, managed save, the vCPU
mode getter (it read the wrong attribute), the old NIC editor and its
bridge lookup. CD-ROM media and NIC hotplug use _definitions() like the
other device edits, the disk editor takes the bus prefix from
util.vol_dev_type, and an unreadable disk is logged, not printed.
This commit is contained in:
catborise 2026-10-09 18:12:40 +03:00
parent 0ea5f0ff18
commit f1e83455e4
5 changed files with 31 additions and 201 deletions

View file

@ -540,6 +540,7 @@ A rollback leaves the clone on a detached HEAD at the restored commit; the updat
- The VM page shows each NIC's own QoS and each CD-ROM's own pool (they showed the previous device's), and a NIC without a source or model no longer breaks it.
- A volume of an LVM or iSCSI pool can be attached to a VM, and bridge and direct NICs can be boot devices. The NIC model `default` lets libvirt choose, and `rt18139` is `rtl8139`: a VM with either did not start; a migration corrects the default NIC type setting.
- Cloning a UEFI VM no longer defines a storage pool on the host's NVRAM directory; the clone gets its own copy of the VM's UEFI variables.
- The console .vv file finds the VM by its UUID; without a listen address or password it gives 127.0.0.1 and an empty password instead of "None".
- A running VM without a guest agent shows the IPv4 address the host learned by ARP (the lookup always failed before).
- After a live migration the VM's NVRAM file is deleted on the source, when it is in libvirt's `/var/lib/libvirt/qemu/nvram` and the destination does not share that directory (a VM created there later with the same name would start with those UEFI variables).
- gstfsd finds the guest's operating system the way libguestfs inspects it, so a root on LVM works, and it always answers (an error when it finds no or several operating systems). Update `/usr/local/bin/gstfsd` on every compute (see [Adding a Compute Node](#adding-a-compute-node), step 3).

View file

@ -52,17 +52,20 @@ class CanOpenConsoleTestCase(TestCase):
def test_owner_can_download_vv_file(self):
self.client.force_login(self.owner)
with patch("instances.views.wvmInstances") as mock_conn_cls, patch("instances.models.wvmInstance") as wvm:
wvm.return_value.instance.name.return_value = "cons-vm"
conn = mock_conn_cls.return_value
conn.graphics_type.return_value = "vnc"
conn.graphics_listen.return_value = "127.0.0.1"
conn.graphics_port.return_value = "5900"
conn.domain_name.return_value = "cons-vm"
conn.graphics_passwd.return_value = "secret"
with patch("instances.models.wvmInstance") as wvm:
proxy = wvm.return_value.cached_reads.return_value.__enter__.return_value
proxy.instance.name.return_value = "cons-vm"
proxy.host = "192.0.2.7"
proxy.get_console_type.return_value = "vnc"
proxy.get_console_listener_addr.return_value = "0.0.0.0"
proxy.get_console_port.return_value = "5900"
proxy.get_console_passwd.return_value = "secret"
res = self.client.get(reverse("instances:getvvfile", args=[self.instance.id]))
self.assertEqual(res.status_code, 200)
self.assertIn(b"[virt-viewer]", res.content)
# the VM found by its UUID; a console on all addresses is reached through the host
for line in (b"type=vnc", b"host=192.0.2.7", b"port=5900", b"title=cons-vm", b"password=secret"):
self.assertIn(line + b"\n", res.content)
def test_owner_can_get_vdi_url(self):
self.client.force_login(self.owner)

View file

@ -42,7 +42,7 @@ from logs.views import addlogmsg
from webvirtcloud.middleware import error_text
from vrtManager import util
from vrtManager.create import wvmCreate
from vrtManager.instance import USAGE_MAX_WINDOW, wvmInstances
from vrtManager.instance import USAGE_MAX_WINDOW
from vrtManager.interface import wvmInterface
from vrtManager.storage import wvmStorage
from vrtManager.util import randomPasswd
@ -2046,15 +2046,6 @@ def getvvfile(request, pk):
# The .vv file contains the VNC password: same rule as the console.
if not utils.can_open_console(request.user, instance):
raise PermissionDenied
conn = wvmInstances(
instance.compute.hostname,
instance.compute.login,
instance.compute.password,
instance.compute.type,
)
# The host's current name for this UUID; the stored name may be stale.
name = instance.proxy.instance.name()
msg = _("Send console.vv file")
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
response = HttpResponse(
@ -2064,15 +2055,15 @@ def getvvfile(request, pk):
reason=None,
charset="utf-8",
)
response.writelines("[virt-viewer]\n")
response.writelines("type=" + conn.graphics_type(name) + "\n")
if conn.graphics_listen(name) == "0.0.0.0":
response.writelines("host=" + conn.host + "\n")
else:
response.writelines("host=" + conn.graphics_listen(name) + "\n")
response.writelines("port=" + conn.graphics_port(name) + "\n")
response.writelines("title=" + conn.domain_name(name) + "\n")
response.writelines("password=" + conn.graphics_passwd(name) + "\n")
# the VM found by its UUID; the stored name may be stale
with instance.proxy.cached_reads() as proxy:
listen = proxy.get_console_listener_addr()
response.writelines("[virt-viewer]\n")
response.writelines(f"type={proxy.get_console_type()}\n")
response.writelines(f"host={proxy.host if listen == '0.0.0.0' else listen}\n")
response.writelines(f"port={proxy.get_console_port()}\n")
response.writelines(f"title={proxy.instance.name()}\n")
response.writelines(f"password={proxy.get_console_passwd() or ''}\n")
response.writelines("enable-usbredir=1\n")
response.writelines("disable-effects=all\n")
response.writelines("secure-attention=ctrl+alt+ins\n")

View file

@ -73,56 +73,6 @@ def _rate(before, after, seconds, scale=1):
class wvmInstances(wvmConnect):
def get_instance_status(self, name):
inst = self.get_instance(name)
return inst.info()[0]
def get_instance_memory(self, name):
inst = self.get_instance(name)
mem = util.get_xml_path(inst.XMLDesc(0), "/domain/currentMemory")
return int(mem) // 1024
def get_instance_vcpu(self, name):
inst = self.get_instance(name)
cur_vcpu = util.get_xml_path(inst.XMLDesc(0), "/domain/vcpu/@current")
return cur_vcpu or util.get_xml_path(inst.XMLDesc(0), "/domain/vcpu")
def get_instance_managed_save_image(self, name):
inst = self.get_instance(name)
return inst.hasManagedSaveImage(0)
def get_uuid(self, name):
inst = self.get_instance(name)
return inst.UUIDString()
def start(self, name):
dom = self.get_instance(name)
dom.create()
def shutdown(self, name):
dom = self.get_instance(name)
dom.shutdown()
def force_shutdown(self, name):
dom = self.get_instance(name)
dom.destroy()
def managedsave(self, name):
dom = self.get_instance(name)
dom.managedSave(0)
def managed_save_remove(self, name):
dom = self.get_instance(name)
dom.managedSaveRemove(0)
def suspend(self, name):
dom = self.get_instance(name)
dom.suspend()
def resume(self, name):
dom = self.get_instance(name)
dom.resume()
def moveto(self, conn, name, live, unsafe, offline, autoconverge=False, compress=False, uri=None, timeout=None):
"""Migrates the VM of conn (a wvmInstance on the source) here.
@ -382,43 +332,6 @@ class wvmInstances(wvmConnect):
f"{error}; the VM is now defined on both hosts, remove it from the destination: {undo_error}"
) from error
def graphics_type(self, name):
inst = self.get_instance(name)
console_type = util.get_xml_path(
inst.XMLDesc(0), "/domain/devices/graphics/@type"
)
return "None" if console_type is None else console_type
def graphics_listen(self, name):
inst = self.get_instance(name)
listener_addr = util.get_xml_path(
inst.XMLDesc(0), "/domain/devices/graphics/@listen"
)
if listener_addr is None:
listener_addr = util.get_xml_path(
inst.XMLDesc(0), "/domain/devices/graphics/listen/@address"
)
return "None" if listener_addr is None else listener_addr
def graphics_port(self, name):
inst = self.get_instance(name)
console_port = util.get_xml_path(
inst.XMLDesc(0), "/domain/devices/graphics/@port"
)
return "None" if console_port is None else console_port
def domain_name(self, name):
inst = self.get_instance(name)
domname = util.get_xml_path(inst.XMLDesc(0), "/domain/name")
return "NoName" if domname is None else domname
def graphics_passwd(self, name):
inst = self.get_instance(name)
password = util.get_xml_path(
inst.XMLDesc(VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@passwd"
)
return "None" if password is None else password
class wvmInstance(wvmConnect):
# set by cached_reads(): libvirt answers reused within the block
@ -472,12 +385,6 @@ class wvmInstance(wvmConnect):
def force_shutdown(self):
self.instance.destroy()
def managedsave(self):
self.instance.managedSave(0)
def managed_save_remove(self):
self.instance.managedSaveRemove(0)
def suspend(self):
self.instance.suspend()
@ -548,9 +455,6 @@ class wvmInstance(wvmConnect):
cur_vcpu = util.get_xml_path(self._XMLDesc(0), "/domain/vcpu/@current")
return int(cur_vcpu) if cur_vcpu else self.get_vcpu()
def get_vcpu_mode(self):
return util.get_xml_path(self._XMLDesc(0), "/domain/cpu/@current")
def get_arch(self):
return util.get_xml_path(self._XMLDesc(0), "/domain/os/type/@arch")
@ -788,7 +692,7 @@ class wvmInstance(wvmConnect):
else:
volume = src_file
except Exception as e:
print(f"Exception: {e}")
logger.warning("Disk %s of %s not fully read: %s", dev, self.instance.name(), e)
finally:
result.append(
{
@ -966,9 +870,10 @@ class wvmInstance(wvmConnect):
The live and the persistent definition are updated separately from
their own device XML, so neither is rewritten from the other.
"""
definitions = [("persistent", PERSISTENT_XML, VIR_DOMAIN_AFFECT_CONFIG)]
if self.instance.isActive(): # running or paused
definitions.insert(0, ("running", VIR_DOMAIN_XML_SECURE, VIR_DOMAIN_AFFECT_LIVE))
definitions = [
("running" if affect == VIR_DOMAIN_AFFECT_LIVE else "persistent", xml_flags, affect)
for xml_flags, affect in self._definitions()
]
devices = []
for label, xml_flags, _ in definitions:
tree = etree.fromstring(self._XMLDesc(xml_flags))
@ -1143,7 +1048,7 @@ class wvmInstance(wvmConnect):
disk = target.getparent()
if target.get("bus") != target_bus:
prefix = {"virtio": "vd", "ide": "hd", "fdc": "fd"}.get(target_bus, "sd")
prefix = util.vol_dev_type(target_bus) or "sd"
used = set(tree.xpath("./devices/disk/target/@dev")) - {target_dev}
new_dev = target_dev if target_dev.startswith(prefix) else next(
(prefix + letter for letter in string.ascii_lowercase if prefix + letter not in used),
@ -1949,18 +1854,6 @@ class wvmInstance(wvmConnect):
return clone.UUIDString()
def get_bridge_name(self, source, source_type="net"):
if source_type == "iface":
iface = self.get_iface(source)
bridge_name = iface.name()
else:
net = self.get_network(source)
try:
bridge_name = net.bridgeName()
except libvirtError:
bridge_name = None
return bridge_name
def add_network(
self, mac_address, source, source_type="net", model="virtio", nwfilter=None
):
@ -1988,9 +1881,8 @@ class wvmInstance(wvmConnect):
etree.SubElement(iface, "filterref", filter=nwfilter)
xml_iface = etree.tostring(iface).decode()
if self.instance.isActive(): # running or paused
self.instance.attachDeviceFlags(xml_iface, VIR_DOMAIN_AFFECT_LIVE)
self.instance.attachDeviceFlags(xml_iface, VIR_DOMAIN_AFFECT_CONFIG)
for _, affect in self._definitions():
self.instance.attachDeviceFlags(xml_iface, affect)
def _definitions(self):
"""(XMLDesc flags, affect flag) of the definitions a device edit applies to:
@ -2050,63 +1942,6 @@ class wvmInstance(wvmConnect):
self._defineXML(etree.tostring(tree).decode())
def change_network_oldway(self, network_data):
"""
change network firsh version...
will be removed if new one works as expected for all scenarios
"""
xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE)
tree = ElementTree.fromstring(xml)
for num, interface in enumerate(tree.findall("devices/interface")):
net_mac = network_data.get("net-mac-" + str(num))
if net_mac is None:
continue
net_source = network_data.get("net-source-" + str(num))
net_source_type = network_data.get("net-source-" + str(num) + "-type")
net_filter = network_data.get("net-nwfilter-" + str(num))
net_model = network_data.get("net-model-" + str(num))
source = interface.find("source")
if interface.get("type") == "bridge":
bridge_name = self.get_bridge_name(net_source, net_source_type)
source.set("bridge", bridge_name)
elif interface.get("type") in ["network", "direct"]:
if net_source_type == "net":
source.set("network", net_source)
elif net_source_type == "iface":
source.set("dev", net_source)
else:
raise util.OperationError(
"Unknown network type: {}".format(net_source_type)
)
else:
raise util.OperationError(
"Unknown network type: {}".format(interface.get("type"))
)
source = interface.find("model")
if net_model != "default":
source.attrib["type"] = net_model
else:
interface.remove(source)
source = interface.find("mac")
source.set("address", net_mac)
source = interface.find("filterref")
if net_filter:
if source is not None:
source.set("filter", net_filter)
else:
element = ElementTree.Element("filterref")
element.attrib["filter"] = net_filter
interface.append(element)
else:
if source is not None:
interface.remove(source)
new_xml = ElementTree.tostring(tree).decode()
self._defineXML(new_xml)
def set_link_state(self, mac_address, state):
for xml_flags, affect in self._definitions():
nic = self._nic(xml_flags, mac_address)

View file

@ -250,7 +250,7 @@ class AuthorizationMatrixTestCase(TestCase):
# Mock libvirt so the views would succeed if authorization let them through.
with patch("datasource.views.wvmInstance", MagicMock()), patch(
"datasource.views.get_hostname_by_ip", return_value="host"
), patch("instances.views.wvmInstances", MagicMock()):
), patch("instances.models.wvmInstance", MagicMock()):
for name in CONSOLE_ENDPOINTS:
url = self._url(name, params[name])
response = self._request(self.viewer, url)