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

fix(virtsecrets): a secret in use is not deleted, and a password is stored as typed

- A secret is in use when a VM names it in a disk, a host device or its
  vTPM, or a pool in its source, by UUID or by its usage within the usage
  type, as they run and as saved. The vTPM, host devices and pools' saved
  definitions were not checked before.
- A Ceph key is taken in base64, as `ceph auth get-key` prints it; any other
  value is a password or passphrase stored as typed, spaces included.
- A new secret takes its value at once; a value refused leaves no secret.
- TLS and vTPM secrets can be created; the usage has no length limit.
- Every POST redirects; the list is read once and only with its users; a
  secret, VM or pool gone meanwhile is left out.
- domain_trees() is shared by the in-use checks of secrets, network filters,
  networks and pools; the storage page's Ceph secret picker reads the same
  listing.
This commit is contained in:
catborise 2026-10-29 09:29:20 +03:00
parent 0014b5f240
commit a95ec40f69
17 changed files with 429 additions and 249 deletions

View file

@ -843,11 +843,27 @@ class wvmConnect(object):
def get_iface(self, name):
return self.wvm.interfaceLookupByName(name)
def get_secrets(self):
return self.wvm.listSecrets()
def get_secret(self, uuid):
return self.wvm.secretLookupByUUIDString(uuid)
def get_secrets_info(self):
"""Every secret of the host: UUID, usage type and usage (a volume
path, an iSCSI target, a Ceph or TLS or vTPM name), whether it is
ephemeral and private. One listing, one XML read per secret; a secret
gone meanwhile is left out."""
secrets = []
for secret in self.wvm.listAllSecrets(0):
try:
tree = etree.fromstring(secret.XMLDesc(0))
except libvirtError as err:
if err.get_error_code() != libvirt.VIR_ERR_NO_SECRET:
raise
continue
secrets.append({
"uuid": secret.UUIDString(),
"usage_type": tree.xpath("string(usage/@type)") or "none",
"usage": tree.xpath("string(usage/*)"),
"ephemeral": tree.get("ephemeral") == "yes",
"private": tree.get("private") == "yes",
})
return sorted(secrets, key=lambda secret: (secret["usage_type"], secret["usage"]))
def get_storage(self, name):
return self.wvm.storagePoolLookupByName(name)
@ -968,16 +984,27 @@ class wvmConnect(object):
instances.append(name)
return instances
def domains_using(self, match, running_only=False):
"""Names of the domains with a definition, parsed, that match(tree)
holds for: the live and the persistent one of each domain, or with
running_only the live one of each running domain."""
def domain_trees(self, running_only=False):
"""Each domain's name with its definitions, parsed: the live one, and
for a persistent domain the one saved for its next boot too, read
whether it runs or not (it may stop meanwhile); with running_only the
live one of each running domain. A domain gone meanwhile is left out."""
listing = libvirt.VIR_CONNECT_LIST_DOMAINS_ACTIVE if running_only else 0
flags = (0,) if running_only else (0, libvirt.VIR_DOMAIN_XML_INACTIVE)
return sorted(
dom.name() for dom in self.wvm.listAllDomains(listing)
if any(match(etree.fromstring(dom.XMLDesc(f))) for f in flags)
)
for dom in self.wvm.listAllDomains(listing):
try:
xmls = [dom.XMLDesc(0)]
if not running_only and dom.isPersistent():
xmls.append(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
except libvirtError as err:
if err.get_error_code() != libvirt.VIR_ERR_NO_DOMAIN:
raise
continue
yield dom.name(), [etree.fromstring(xml) for xml in xmls]
def domains_using(self, match, running_only=False):
"""Names of the domains with a definition (domain_trees) that
match(tree) holds for."""
return sorted(name for name, trees in self.domain_trees(running_only) if any(map(match, trees)))
def paths_in_use(self, skip_uuid=None, own=()):
"""Paths (names for network disks) a domain uses as a disk or a

View file

@ -2,7 +2,6 @@ import copy
import re
from functools import cached_property
from libvirt import VIR_DOMAIN_XML_INACTIVE, VIR_ERR_NO_DOMAIN, libvirtError
from lxml import etree
from vrtManager import util
@ -43,24 +42,15 @@ class wvmNWFilters(wvmConnect):
return sorted(filters, key=lambda nwfilter: nwfilter["name"])
def get_users(self):
"""The VMs whose interfaces reference each filter, by filter name: as
they run and, for a persistent VM that runs, as saved for its next
boot. One listing, one or two XML reads per VM."""
"""The VMs whose interfaces reference each filter, by filter name, in
any of their definitions (domain_trees)."""
users = {}
for dom in self.wvm.listAllDomains(0):
try:
xmls = [dom.XMLDesc(0)]
if dom.isActive() and dom.isPersistent():
xmls.append(dom.XMLDesc(VIR_DOMAIN_XML_INACTIVE))
except libvirtError as err: # a VM undefined meanwhile uses none
if err.get_error_code() != VIR_ERR_NO_DOMAIN:
raise
continue
for vm, trees in self.domain_trees():
names = set()
for xml in xmls:
names.update(etree.fromstring(xml).xpath("devices/interface/filterref/@filter"))
for tree in trees:
names.update(tree.xpath("devices/interface/filterref/@filter"))
for name in names:
users.setdefault(name, []).append(dom.name())
users.setdefault(name, []).append(vm)
return users
def create_nwfilter(self, xml):

View file

@ -1,37 +1,118 @@
import base64
from libvirt import VIR_ERR_NO_STORAGE_POOL, VIR_STORAGE_XML_INACTIVE, libvirtError
from lxml import etree
from vrtManager import util
from vrtManager.connection import wvmConnect
# the element of <usage> that holds the value, per usage type
USAGE_ELEMENTS = {"ceph": "name", "volume": "volume", "iscsi": "target"}
# the element of <usage> that holds the usage, per usage type
USAGE_ELEMENTS = {"ceph": "name", "volume": "volume", "iscsi": "target", "tls": "name", "vtpm": "name"}
# the usage type a reference by usage means: the type the <secret> gives, or
# for a pool's <auth> the auth type; a disk's encryption passphrase is a volume's
REF_USAGE_TYPES = {"ceph": "ceph", "iscsi": "iscsi", "chap": "iscsi", "passphrase": "volume"}
def value_bytes(usage_type, value):
"""The bytes a value typed on the page stands for: a Ceph key as
`ceph auth get-key` prints it, in base64; any other secret's password or
passphrase as it is typed."""
if usage_type != "ceph":
return value.encode()
try:
return base64.b64decode(value, validate=True)
except ValueError: # not base64, or not ASCII; the message never holds the value
raise util.OperationError("A Ceph key is base64 text, as `ceph auth get-key` prints it") from None
def secrets_named(tree, path, by_usage):
"""UUIDs of the secrets the <secret> elements at path name, by UUID or by
usage; by_usage maps (usage type, usage) to a UUID."""
uuids = set()
for ref in tree.xpath(path):
if ref.get("uuid"):
uuids.add(ref.get("uuid"))
else:
ref_type = ref.get("type") or ref.getparent().get("type")
uuids.add(by_usage.get((REF_USAGE_TYPES.get(ref_type), ref.get("usage"))))
return uuids
class wvmSecrets(wvmConnect):
def create_secret(self, ephemeral, private, secret_type, data):
# built with lxml: data is text, whatever characters it holds
secret = etree.Element("secret", ephemeral=ephemeral, private=private)
usage = etree.SubElement(secret, "usage", type=secret_type)
etree.SubElement(usage, USAGE_ELEMENTS[secret_type]).text = data
self.wvm.secretDefineXML(etree.tostring(secret, encoding="unicode"))
def pool_trees(self):
"""Each pool's name with its definitions, parsed: the live one, and
for a persistent pool the one saved for its next start too, read
whether it is active or not (it may stop meanwhile). A pool gone
meanwhile is left out."""
for pool in self.wvm.listAllStoragePools(0):
try:
xmls = [pool.XMLDesc(0)]
if pool.isPersistent():
xmls.append(pool.XMLDesc(VIR_STORAGE_XML_INACTIVE))
except libvirtError as err:
if err.get_error_code() != VIR_ERR_NO_STORAGE_POOL:
raise
continue
yield pool.name(), [etree.fromstring(xml) for xml in xmls]
def get_users(self, secrets):
"""The VMs and pools that name each of secrets (get_secrets_info), by
UUID: a VM's disks, host devices and vTPM, a pool's source, in any of
their definitions. Not seen: a LUKS volume's own encryption, the TLS
settings of qemu.conf, snapshots."""
by_usage = {(secret["usage_type"], secret["usage"]): secret["uuid"] for secret in secrets}
users = {}
def add(kind, name, uuids):
for uuid in uuids - {None}:
users.setdefault(uuid, {"vms": [], "pools": []})[kind].append(name)
for name, trees in self.domain_trees():
uuids = set()
for tree in trees:
uuids.update(secrets_named(tree, "devices//secret", by_usage))
uuids.update(tree.xpath("devices/tpm/backend/encryption/@secret"))
add("vms", name, uuids)
for name, trees in self.pool_trees():
uuids = set()
for tree in trees:
uuids.update(secrets_named(tree, "source//secret", by_usage))
add("pools", name, uuids)
return users
def create_secret(self, usage_type, usage, value="", ephemeral=False, private=False):
"""A secret for usage, with value if one is given: a Ceph key that is
not base64 defines nothing, and a value libvirt refuses undefines the
new secret again."""
value = value_bytes(usage_type, value) if value else None
# built with lxml: the usage is text, whatever characters it holds
secret = etree.Element("secret", ephemeral="yes" if ephemeral else "no", private="yes" if private else "no")
usage_element = etree.SubElement(secret, "usage", type=usage_type)
etree.SubElement(usage_element, USAGE_ELEMENTS[usage_type]).text = usage
defined = self.wvm.secretDefineXML(etree.tostring(secret, encoding="unicode"))
if value is not None:
try:
defined.setValue(value)
except libvirtError:
defined.undefine()
raise
def set_secret_value(self, uuid, value):
"""value: the new value as base64 text."""
secrt = self.get_secret(uuid)
# validate: a value that is not base64 is refused, not decoded to other bytes
secrt.setValue(base64.b64decode(value, validate=True))
"""value as typed on the page; its bytes follow the secret's usage
type (value_bytes)."""
secret = self.wvm.secretLookupByUUIDString(uuid)
usage_type = etree.fromstring(secret.XMLDesc(0)).xpath("string(usage/@type)")
secret.setValue(value_bytes(usage_type, value))
def delete_secret(self, uuid):
"""Refused while a VM disk or a pool names it, by UUID or by usage:
without it that VM or pool would no longer start."""
secrt = self.get_secret(uuid)
refs = "//secret[@uuid=$uuid or @usage=$usage]"
usage = secrt.usageID() or ""
vms = self.domains_using(lambda domain: domain.xpath(f"devices/disk{refs}", uuid=uuid, usage=usage))
pools = sorted(pool.name() for pool in self.wvm.listAllStoragePools()
if etree.fromstring(pool.XMLDesc(0)).xpath(f"source{refs}", uuid=uuid, usage=usage))
if vms or pools:
users = "; ".join(f"{kind} {', '.join(names)}" for kind, names in (("VMs", vms), ("pools", pools)) if names)
raise util.OperationError(f"Secret {uuid} is used by {users}; remove it from them first")
secrt.undefine()
"""Refused while a VM or a pool names it (get_users): without it that
VM or pool would no longer start."""
secret = self.wvm.secretLookupByUUIDString(uuid)
if users := self.get_users(self.get_secrets_info()).get(uuid):
named = []
if users["vms"]:
named.append(f"VMs {', '.join(users['vms'])}")
if users["pools"]:
named.append(f"pools {', '.join(users['pools'])}")
raise util.OperationError(f"Secret {uuid} is used by {'; '.join(named)}; remove it from them first")
secret.undefine()