from django.utils.translation import gettext_noop import contextlib import libvirt from vrtManager import util from vrtManager.connection import wvmConnect def volume_details(vol): """A volume's name, capacity (size), allocation and format, from its XML: one libvirt call, which refreshes the volume as info() does. Logical and iSCSI volumes have no format (None).""" def read(doc): return { "name": util.get_xpath(doc, "/volume/name"), "size": int(util.get_xpath(doc, "/volume/capacity")), "allocation": int(util.get_xpath(doc, "/volume/allocation")), "type": util.get_xpath(doc, "/volume/target/format/@type"), } return util.get_xml_path(vol.XMLDesc(0), func=read) class wvmStorages(wvmConnect): def get_storages_info(self): """Each pool's state, autostart, type, path (an RBD pool's Ceph pool), volume count, size, free space and the percent used. An active pool is refreshed first: libvirt measures its space only when it is refreshed, so two hosts sharing a pool's storage told different free space, each as of its own last refresh.""" get_storages = self.get_storages() storages = [] for pool in get_storages: stg = self.get_storage(pool) stg_status = stg.isActive() if stg_status: with contextlib.suppress(libvirt.libvirtError): # refused while a volume is cloned in it stg.refresh(0) xml = stg.XMLDesc(0) stg_vol = len(stg.listVolumes()) if stg_status else None _state, stg_size, _allocation, stg_free = stg.info() storages.append( { "name": pool, "status": stg_status, "autostart": stg.autostart(), "type": util.get_xml_path(xml, "/pool/@type"), "path": util.get_xml_path(xml, "/pool/target/path") or util.get_xml_path(xml, "/pool/source/name"), "volumes": stg_vol, "size": stg_size, "free": stg_free, "used_percent": (stg_size - stg_free) * 100 / stg_size if stg_size else 0, } ) return storages def define_storage(self, xml, flags): """Define the pool, start it with flags and set it to autostart. A pool that does not start is undefined again, so its name stays free for another try.""" stg = self.wvm.storagePoolDefineXML(xml, 0) try: stg.create(flags) except libvirt.libvirtError: stg.undefine() raise stg.setAutostart(1) return stg def get_pool_by_target(self, target): pool_names = self.get_storages() for pool_name in pool_names: stg = wvmStorage(self.host, self.login, self.passwd, self.conn, pool_name) if stg.get_target_path() == target: return self.get_storage(pool_name) if stg.get_type() == "rbd" and stg.get_source_name() == target: return self.get_storage(pool_name) return None def create_storage(self, stg_type, name, source, target): xml = f""" {name}""" if stg_type == "logical": xml += f""" {name} """ if stg_type == "logical": target = f"/dev/{name}" xml += f""" {target} """ # builds the dir pool's directory or the logical pool's volume group return self.define_storage(xml, libvirt.VIR_STORAGE_POOL_CREATE_WITH_BUILD) def create_storage_ceph( self, stg_type, name, ceph_pool, ceph_host, ceph_user, secret ): ceph_pool, ceph_host, ceph_user, secret = map(util.xml_escape, (ceph_pool, ceph_host, ceph_user, secret)) xml = f""" {name} {ceph_pool} """ self.define_storage(xml, 0) def create_storage_netfs( self, stg_type, name, netfs_host, source, source_format, target ): netfs_host, source_format = util.xml_escape(netfs_host), util.xml_escape(source_format) xml = f""" {name} {target} """ # builds the mount point return self.define_storage(xml, libvirt.VIR_STORAGE_POOL_CREATE_WITH_BUILD) class wvmStorage(wvmConnect): def __init__(self, host, login, passwd, conn, pool): wvmConnect.__init__(self, host, login, passwd, conn) self.pool = self.get_storage(pool) def get_name(self): return self.pool.name() def get_status(self): status = [ "Not running", "Initializing pool, not available", "Running normally", "Running degraded", ] try: return status[self.pool.info()[0]] except ValueError: return "Unknown" def get_size(self): return [self.pool.info()[1], self.pool.info()[3]] def _XMLDesc(self, flags): return self.pool.XMLDesc(flags) def _createXML(self, xml, flags): self.pool.createXML(xml, flags) def _createXMLFrom(self, xml, vol, flags): self.pool.createXMLFrom(xml, vol, flags) def _define(self, xml): return self.wvm.storagePoolDefineXML(xml, 0) def is_active(self): return self.pool.isActive() def get_uuid(self): return self.pool.UUIDString() def start(self): return self.pool.create(0) def stop(self): """Refused while it holds a disk of a running VM, named by the pool or by a volume's path: that VM would lose the disk.""" name = self.pool.name() paths = {vol.path() for vol in self.pool.listAllVolumes()} vms = self.domains_using( lambda domain: domain.xpath("devices/disk/source[@pool=$name]", name=name) or paths.intersection(self._source_paths(domain)), running_only=True, ) if vms: raise util.OperationError(gettext_noop("Pool %(name)s holds disks of the running VMs %(vms)s; shut them down first"), name=name, vms=', '.join(vms)) return self.pool.destroy() def delete(self): """Refused while a VM's definition names it for a disk: that VM would no longer start. A disk given by its file path does not need it.""" name = self.pool.name() if vms := self.domains_using(lambda domain: domain.xpath("devices/disk/source[@pool=$name]", name=name)): raise util.OperationError(gettext_noop("VMs %(vms)s have disks in pool %(name)s by its name and would no longer start; detach those disks first"), vms=', '.join(vms), name=name) self.pool.undefine() def get_autostart(self): return self.pool.autostart() def set_autostart(self, value): self.pool.setAutostart(value) def get_type(self): return util.get_xml_path(self._XMLDesc(0), "/pool/@type") def get_target_path(self): return util.get_xml_path(self._XMLDesc(0), "/pool/target/path") def get_source_name(self): return util.get_xml_path(self._XMLDesc(0), "/pool/source/name") def get_allocation(self): return int(util.get_xml_path(self._XMLDesc(0), "/pool/allocation")) def get_available(self): return int(util.get_xml_path(self._XMLDesc(0), "/pool/available")) def get_capacity(self): return int(util.get_xml_path(self._XMLDesc(0), "/pool/capacity")) def get_rbd_source(self): def hosts(doc): hosts_array = [] for host in doc.xpath("/pool/source/host"): name = host.get("name") if name: port = host.get("port") if port: hosts_array.append({"hostname": name, "hostport": port}) else: hosts_array.append({"hostname": name}) name = doc.get("name") auth = doc.xpath("/pool/source/auth") auth_type = auth[0].get("type") auth_user = auth[0].get("username") auth_uuid = auth[0].xpath("secret/@uuid")[0] return { "name": name, "auth_type": auth_type, "auth_user": auth_user, "auth_uuid": auth_uuid, "hosts": hosts_array, } return util.get_xml_path(self._XMLDesc(0), func=hosts) def get_volumes(self): return self.pool.listVolumes() def get_volume(self, name): return self.pool.storageVolLookupByName(name) def _vol_XMLDesc(self, name): vol = self.get_volume(name) return vol.XMLDesc(0) def del_volume(self, name): """libvirt deletes any volume; one a VM still needs is refused.""" vol = self.pool.storageVolLookupByName(name) if vol.path() in self.paths_in_use(): raise util.InUse(gettext_noop("Volume %(name)s is in use: a VM's disk, a backing file or a snapshot's file"), name=name) vol.delete(0) def get_volume_format_type(self, name): vol_xml = self._vol_XMLDesc(name) return util.get_xml_path(vol_xml, "/volume/target/format/@type") def get_volume_type(self, name): vol_xml = self._vol_XMLDesc(name) return util.get_xml_path(vol_xml, "/volume/@type") def refresh(self): return self.pool.refresh(0) def get_volume_details(self, volname): """The volume's details (volume_details), the pool refreshed first.""" with contextlib.suppress(libvirt.libvirtError): # refused while a volume is cloned in the pool self.refresh() return volume_details(self.get_volume(volname)) def update_volumes(self): """Every volume's details (volume_details) by name, the pool refreshed first: two calls and one per volume, where a lookup, info() and the XML of each took six. A volume deleted meanwhile is left out.""" with contextlib.suppress(libvirt.libvirtError): # refused while a volume is cloned in the pool self.refresh() volumes = [] for vol in self.pool.listAllVolumes(0): try: volumes.append(volume_details(vol)) except libvirt.libvirtError as err: if err.get_error_code() != libvirt.VIR_ERR_NO_STORAGE_VOL: raise return sorted(volumes, key=lambda volume: volume["name"]) def create_volume( self, name, size, vol_fmt="qcow2", metadata=False, disk_owner_uid=0, disk_owner_gid=0, ): size = int(size) * 1073741824 storage_type = self.get_type() alloc = size if vol_fmt == "unknown": vol_fmt = "raw" if storage_type == "dir": name += f".{vol_fmt}" if vol_fmt in ("qcow", "qcow2") else ".img" alloc = 0 xml = f""" {util.xml_escape(name)} {size} {alloc} {disk_owner_uid} {disk_owner_gid} 0644 """ if vol_fmt == "qcow2": xml += """ 1.1 """ xml += """ """ self._createXML(xml, metadata) return name def clone_name(self, target_file, vol_fmt, file_suffix="img"): """The name clone_volume gives a copy: a dir pool adds the format's suffix.""" if self.get_type() != "dir": return target_file if vol_fmt in ("qcow", "qcow2"): return f"{target_file}.{vol_fmt}" return f"{target_file}.{file_suffix}" if file_suffix else target_file def refuse_running_copy(self, name): """A volume a running guest writes is not copied: the copy would mix instants.""" path, pool = self.get_volume(name).path(), self.pool.name() # a disk names it by its path (an RBD image by pool/image) or by its pool and name uses = "devices/disk/source[@file=$p or @dev=$p or @name=$p or (@pool=$pool and @volume=$v)]" if vms := self.domains_using(lambda domain: domain.xpath(uses, p=path, pool=pool, v=name), running_only=True): raise util.OperationError(gettext_noop("The running VMs %(vms)s use volume %(name)s; shut them down to copy it"), vms=", ".join(vms), name=name) def clone_volume( self, name, target_file, vol_fmt=None, metadata=False, mode="0644", file_suffix="img", disk_owner_uid=0, disk_owner_gid=0, ): vol = self.get_volume(name) if not vol_fmt: vol_fmt = self.get_volume_format_type(name) target_file = self.clone_name(target_file, vol_fmt, file_suffix) xml = f""" {target_file} 0 0 {disk_owner_uid} {disk_owner_gid} {mode} """ if vol_fmt == "qcow2": xml += """ 1.1 """ xml += """ """ self._createXMLFrom(xml, vol, metadata) return target_file