"""Datastore Cluster (StoragePod) + Storage DRS via SOAP.
A *datastore cluster* in vSphere UI is a ``vim.StoragePod`` in VMODL. SDRS
config (automation level, IO load balancing, space-utilization thresholds,
per-VM overrides) lives in ``StorageDrsConfigSpec`` and is applied via
``StorageResourceManager.ConfigureStorageDrsForPod_Task``.
Top-level surfaces here:
- ``list_`` / ``get`` / ``get_or_none`` — read existing datastore clusters
- ``create`` — ``Folder.CreateStoragePod`` under the datastore folder
- ``delete`` — ``StoragePod.Destroy_Task``
- ``add_datastore`` / ``remove_datastore`` — move a datastore in/out
- ``sdrs_get`` / ``sdrs_set`` — pod-wide SDRS config
- ``sdrs_vm_override_*`` — per-VM SDRS automation/intra-VM rules
"""
from pyVmomi import vim
from saltext.vcf.utils import vim as soap
def _resolve_pod(opts, name_or_id, profile=None):
content = soap.content(opts, profile=profile)
container = content.viewManager.CreateContainerView(content.rootFolder, [vim.StoragePod], True)
try:
for pod in container.view:
if name_or_id in (pod._moId, pod.name): # noqa: SLF001
return pod
finally:
container.Destroy()
raise LookupError(f"datastore cluster {name_or_id!r} not found")
def _resolve_datastore(opts, name_or_id, profile=None):
content = soap.content(opts, profile=profile)
container = content.viewManager.CreateContainerView(content.rootFolder, [vim.Datastore], True)
try:
for ds in container.view:
if name_or_id in (ds._moId, ds.name): # noqa: SLF001
return ds
finally:
container.Destroy()
raise LookupError(f"datastore {name_or_id!r} not found")
def _resolve_datacenter(opts, name_or_id, profile=None):
content = soap.content(opts, profile=profile)
for dc in content.rootFolder.childEntity:
if isinstance(dc, vim.Datacenter) and name_or_id in (dc._moId, dc.name): # noqa: SLF001
return dc
raise LookupError(f"datacenter {name_or_id!r} not found")
# ---------------------------------------------------------------------------
# Pod CRUD
# ---------------------------------------------------------------------------
[docs]
def list_(opts, profile=None):
"""Return summary records for every datastore cluster in vCenter."""
content = soap.content(opts, profile=profile)
container = content.viewManager.CreateContainerView(content.rootFolder, [vim.StoragePod], True)
try:
return [
{
"moid": p._moId, # noqa: SLF001
"name": p.name,
"datastore_count": len(p.childEntity or []),
}
for p in container.view
]
finally:
container.Destroy()
def get(opts, name_or_id, profile=None):
pod = _resolve_pod(opts, name_or_id, profile=profile)
return {
"moid": pod._moId, # noqa: SLF001
"name": pod.name,
"datastores": [ds.name for ds in (pod.childEntity or [])],
}
def get_or_none(opts, name_or_id, profile=None):
try:
return get(opts, name_or_id, profile=profile)
except LookupError:
return None
[docs]
def create(opts, name, datacenter, profile=None):
"""Create an empty datastore cluster under *datacenter*'s datastore folder."""
dc = _resolve_datacenter(opts, datacenter, profile=profile)
pod = dc.datastoreFolder.CreateStoragePod(name=name)
return pod._moId # noqa: SLF001
def delete(opts, name_or_id, profile=None):
pod = _resolve_pod(opts, name_or_id, profile=profile)
task = pod.Destroy_Task()
return task._moId # noqa: SLF001
# ---------------------------------------------------------------------------
# Pod membership
# ---------------------------------------------------------------------------
[docs]
def add_datastore(opts, pod, datastore, profile=None):
"""Move *datastore* into *pod*. Synchronous (no task)."""
pod_ref = _resolve_pod(opts, pod, profile=profile)
ds_ref = _resolve_datastore(opts, datastore, profile=profile)
pod_ref.parent.MoveIntoFolder_Task = pod_ref.parent.MoveIntoFolder_Task # nudge attr resolution
pod_ref.MoveInto_Task = getattr(pod_ref, "MoveInto_Task", None)
# vSphere uses Folder.MoveIntoFolder_Task for both regular folders and
# StoragePods (StoragePod inherits from Folder).
task = pod_ref.MoveIntoFolder_Task(list=[ds_ref])
return task._moId # noqa: SLF001
[docs]
def remove_datastore(opts, pod, datastore, datacenter, profile=None):
"""Move *datastore* out of *pod* back to *datacenter*'s datastore folder."""
dc = _resolve_datacenter(opts, datacenter, profile=profile)
ds_ref = _resolve_datastore(opts, datastore, profile=profile)
task = dc.datastoreFolder.MoveIntoFolder_Task(list=[ds_ref])
return task._moId # noqa: SLF001
# ---------------------------------------------------------------------------
# SDRS config
# ---------------------------------------------------------------------------
def _sdrs_mgr(opts, profile=None):
content = soap.content(opts, profile=profile)
return content.storageResourceManager
def sdrs_get(opts, pod, profile=None):
pod_ref = _resolve_pod(opts, pod, profile=profile)
cfg = pod_ref.podStorageDrsEntry.storageDrsConfig.podConfig
return {
"enabled": bool(cfg.enabled),
"automation_level": str(cfg.defaultVmBehavior),
"io_load_balance_enabled": bool(cfg.ioLoadBalanceEnabled),
"space_utilization_threshold": (
int(cfg.spaceLoadBalanceConfig.spaceUtilizationThreshold)
if cfg.spaceLoadBalanceConfig
else None
),
}
[docs]
def sdrs_set(
opts,
pod,
enabled=None,
automation_level=None,
io_load_balance_enabled=None,
space_utilization_threshold=None,
profile=None,
):
"""Update the pod-wide SDRS config. Only non-None fields are touched."""
pod_ref = _resolve_pod(opts, pod, profile=profile)
pod_cfg = vim.storageDrs.PodConfigSpec()
if enabled is not None:
pod_cfg.enabled = bool(enabled)
if automation_level is not None:
pod_cfg.defaultVmBehavior = automation_level
if io_load_balance_enabled is not None:
pod_cfg.ioLoadBalanceEnabled = bool(io_load_balance_enabled)
if space_utilization_threshold is not None:
slb = vim.storageDrs.SpaceLoadBalanceConfig()
slb.spaceUtilizationThreshold = int(space_utilization_threshold)
pod_cfg.spaceLoadBalanceConfig = slb
spec = vim.storageDrs.ConfigSpec(podConfigSpec=pod_cfg)
mgr = _sdrs_mgr(opts, profile=profile)
task = mgr.ConfigureStorageDrsForPod_Task(pod=pod_ref, spec=spec, modify=True)
return task._moId # noqa: SLF001
# ---------------------------------------------------------------------------
# Per-VM SDRS overrides
# ---------------------------------------------------------------------------
def sdrs_vm_override_list(opts, pod, profile=None):
pod_ref = _resolve_pod(opts, pod, profile=profile)
out = []
for entry in pod_ref.podStorageDrsEntry.storageDrsConfig.vmConfig or []:
out.append(
{
"vm_moid": entry.vm._moId if entry.vm else None, # noqa: SLF001
"enabled": bool(getattr(entry, "enabled", True)),
"behavior": str(entry.behavior) if entry.behavior else None,
"intra_vm_affinity": bool(getattr(entry, "intraVmAffinity", True)),
}
)
return out
[docs]
def sdrs_vm_override_set(
opts,
pod,
vm_moid,
behavior=None,
enabled=None,
intra_vm_affinity=None,
profile=None,
):
"""Add / replace the SDRS override for *vm_moid* on *pod*.
*behavior*: ``manual`` | ``automated``.
"""
pod_ref = _resolve_pod(opts, pod, profile=profile)
info = vim.storageDrs.VmConfigInfo(vm=vim.VirtualMachine(vm_moid, None))
if behavior is not None:
info.behavior = behavior
if enabled is not None:
info.enabled = bool(enabled)
if intra_vm_affinity is not None:
info.intraVmAffinity = bool(intra_vm_affinity)
spec = vim.storageDrs.ConfigSpec(
vmConfigSpec=[vim.storageDrs.VmConfigSpec(operation="edit", info=info)]
)
mgr = _sdrs_mgr(opts, profile=profile)
task = mgr.ConfigureStorageDrsForPod_Task(pod=pod_ref, spec=spec, modify=True)
return task._moId # noqa: SLF001
[docs]
def sdrs_rule_list(opts, pod, profile=None):
"""Return SDRS VM affinity/anti-affinity rules on *pod*."""
pod_ref = _resolve_pod(opts, pod, profile=profile)
out = []
for rule in pod_ref.podStorageDrsEntry.storageDrsConfig.podConfig.rule or []:
kind = (
"vm-anti-affinity"
if isinstance(rule, vim.cluster.AntiAffinityRuleSpec)
else "vm-affinity"
)
out.append(
{
"key": rule.key,
"name": rule.name,
"kind": kind,
"enabled": bool(rule.enabled),
"mandatory": bool(rule.mandatory),
"vm_moids": [v._moId for v in (rule.vm or [])], # noqa: SLF001
}
)
return out
def sdrs_rule_get(opts, pod, name, profile=None):
for rule in sdrs_rule_list(opts, pod, profile=profile):
if rule["name"] == name:
return rule
raise LookupError(f"SDRS rule {name!r} not found on {pod!r}")
def sdrs_rule_get_or_none(opts, pod, name, profile=None):
try:
return sdrs_rule_get(opts, pod, name, profile=profile)
except LookupError:
return None
def _sdrs_rule_apply(opts, pod, rule, operation, profile=None):
pod_ref = _resolve_pod(opts, pod, profile=profile)
rule_spec = vim.cluster.RuleSpec(operation=operation, info=rule)
if operation == "edit":
rule_spec.removeKey = rule.key
pod_cfg = vim.storageDrs.PodConfigSpec(rule=[rule_spec])
spec = vim.storageDrs.ConfigSpec(podConfigSpec=pod_cfg)
mgr = _sdrs_mgr(opts, profile=profile)
task = mgr.ConfigureStorageDrsForPod_Task(pod=pod_ref, spec=spec, modify=True)
return task._moId # noqa: SLF001
[docs]
def sdrs_rule_create_vm_anti_affinity(
opts, pod, name, vm_moids, *, enabled=True, mandatory=False, profile=None
):
"""Create an SDRS VM anti-affinity rule on *pod* (keeps VMDKs on different datastores)."""
rule = vim.cluster.AntiAffinityRuleSpec(
name=name,
enabled=enabled,
mandatory=mandatory,
vm=[vim.VirtualMachine(m, None) for m in vm_moids],
)
return _sdrs_rule_apply(opts, pod, rule, "add", profile=profile)
[docs]
def sdrs_rule_create_vm_affinity(
opts, pod, name, vm_moids, *, enabled=True, mandatory=False, profile=None
):
"""Create an SDRS VM affinity rule on *pod* (keeps VMDKs on the same datastore)."""
rule = vim.cluster.AffinityRuleSpec(
name=name,
enabled=enabled,
mandatory=mandatory,
vm=[vim.VirtualMachine(m, None) for m in vm_moids],
)
return _sdrs_rule_apply(opts, pod, rule, "add", profile=profile)
[docs]
def sdrs_rule_delete(opts, pod, name, profile=None):
"""Delete an SDRS rule by name. Returns the vim.Task moId."""
pod_ref = _resolve_pod(opts, pod, profile=profile)
target = None
for rule in pod_ref.podStorageDrsEntry.storageDrsConfig.podConfig.rule or []:
if rule.name == name:
target = rule
break
if target is None:
raise LookupError(f"SDRS rule {name!r} not found")
pod_cfg = vim.storageDrs.PodConfigSpec(
rule=[vim.cluster.RuleSpec(operation="remove", removeKey=target.key)]
)
spec = vim.storageDrs.ConfigSpec(podConfigSpec=pod_cfg)
mgr = _sdrs_mgr(opts, profile=profile)
task = mgr.ConfigureStorageDrsForPod_Task(pod=pod_ref, spec=spec, modify=True)
return task._moId # noqa: SLF001
def sdrs_vm_override_remove(opts, pod, vm_moid, profile=None):
pod_ref = _resolve_pod(opts, pod, profile=profile)
spec = vim.storageDrs.ConfigSpec(
vmConfigSpec=[
vim.storageDrs.VmConfigSpec(
operation="remove", removeKey=vim.VirtualMachine(vm_moid, None)
)
]
)
mgr = _sdrs_mgr(opts, profile=profile)
task = mgr.ConfigureStorageDrsForPod_Task(pod=pod_ref, spec=spec, modify=True)
return task._moId # noqa: SLF001