diff --git a/README.md b/README.md index 700843f5..149f8eee 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/instances/test_console_rule.py b/instances/test_console_rule.py index cdf07069..45f5970a 100644 --- a/instances/test_console_rule.py +++ b/instances/test_console_rule.py @@ -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) diff --git a/instances/views.py b/instances/views.py index b8d214bc..fa85f56e 100755 --- a/instances/views.py +++ b/instances/views.py @@ -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") diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 0424dafc..af630eb7 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -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) diff --git a/webvirtcloud/test_authorization_matrix.py b/webvirtcloud/test_authorization_matrix.py index 37fb81a6..0ffd56d7 100644 --- a/webvirtcloud/test_authorization_matrix.py +++ b/webvirtcloud/test_authorization_matrix.py @@ -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)