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:
parent
0ea5f0ff18
commit
f1e83455e4
5 changed files with 31 additions and 201 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue