sp-rand: remove iSCSI exports and targets.
Change-Id: I318bd65080bd1ee644aa034ec2fe0c9f041064f7
diff --git a/tools/sp-rand/src/sp_rand/cleanup.py b/tools/sp-rand/src/sp_rand/cleanup.py
index 3e1a464..80ff052 100644
--- a/tools/sp-rand/src/sp_rand/cleanup.py
+++ b/tools/sp-rand/src/sp_rand/cleanup.py
@@ -4,6 +4,7 @@
import argparse
import errno
+import itertools
import pathlib
from typing import NamedTuple, Tuple # noqa: H301
@@ -44,6 +45,67 @@
return conffile, prefix
+def remove_iscsi_targets_and_exports(cfg: Config, api: spapi.Api, prefix: str) -> None:
+ """Unexport volumes, remove the iSCSI target records."""
+ print("Querying the StorPool API for the iSCSI configuration")
+ iscsi = api.iSCSIConfig().iscsi
+
+ print("Looking for target records for our volumes")
+ targets = {
+ tgt.name: tgt.volume
+ for tgt in iscsi.targets.values()
+ if tgt.volume.startswith(prefix)
+ }
+
+ if targets:
+ print(f"Found {len(targets)} target records, looking for exports")
+ exports = list(
+ itertools.chain(
+ *(
+ [
+ (idata.name, exp.portalGroup, targets[exp.target])
+ for exp in idata.exports
+ if exp.target in targets
+ ]
+ for idata in iscsi.initiators.values()
+ )
+ )
+ )
+
+ if exports:
+ to_remove = []
+ for exp_name, exp_pg, exp_vol in exports:
+ print(f"Will remove the export for {exp_vol} to {exp_name} in {exp_pg}")
+ to_remove.append(
+ sptypes.iSCSIConfigCommand(
+ exportDelete=sptypes.iSCSICommandExportDelete(
+ initiator=exp_name, portalGroup=exp_pg, volumeName=exp_vol
+ )
+ )
+ )
+
+ print(f"Prepared for removing {len(to_remove)} exports")
+ if not cfg.noop:
+ api.iSCSIConfigChange(sptypes.iSCSIConfigChange(commands=to_remove))
+ else:
+ print("No iSCSI exports to remove")
+
+ to_remove = []
+ for vol in sorted(targets.values()):
+ print(f"Will remove the target record for volume {vol}")
+ to_remove.append(
+ sptypes.iSCSIConfigCommand(
+ deleteTarget=sptypes.iSCSICommandDeleteTarget(volumeName=vol)
+ )
+ )
+
+ print(f"Prepared for removing {len(to_remove)} targets")
+ if not cfg.noop:
+ api.iSCSIConfigChange(sptypes.iSCSIConfigChange(commands=to_remove))
+ else:
+ print("No iSCSI targets to remove, not looking for exports at all")
+
+
def remove_volumes(cfg: Config, api: spapi.Api, prefix: str) -> None:
"""Detach and remove any pertinent StorPool volumes and snapshots."""
print("Querying the StorPool API for attached volumes and snapshots")
@@ -73,6 +135,8 @@
sptypes.VolumesReassignWaitDesc(reassign=to_detach)
)
+ remove_iscsi_targets_and_exports(cfg, api, prefix)
+
print("Querying the StorPool API for existing volumes")
for name in [vol.name for vol in api.volumesList()]:
if not name.startswith(prefix):
diff --git a/tools/sp-rand/unit_tests/test_cleanup.py b/tools/sp-rand/unit_tests/test_cleanup.py
index 7c90d08..c54c53e 100644
--- a/tools/sp-rand/unit_tests/test_cleanup.py
+++ b/tools/sp-rand/unit_tests/test_cleanup.py
@@ -5,7 +5,7 @@
import pathlib
import tempfile
-from typing import DefaultDict, Dict, List, NamedTuple # noqa: H301
+from typing import DefaultDict, Dict, List, NamedTuple, Tuple # noqa: H301
import pytest
@@ -15,6 +15,39 @@
from sp_rand import defs as r_defs
+T_ATTACHED = {
+ False: ["something-else", "testvol-42"],
+ True: ["testvol-9000", "another.snapshot", "testvol-616", "some-testvol-12"],
+}
+
+T_EXPORTED = {
+ "beleriand": [
+ ("neighbors", "testvol-451"),
+ ("ours", "something-else"),
+ ("ours", "testvol-616"),
+ ],
+ "gondor": [("ours", "testvol-12")],
+}
+
+T_TARGETS = {
+ 1: {"name": "ouriqn:target-something-else", "volume": "something-else"},
+ 2: {"name": "ouriqn:target-testvol-12", "volume": "testvol-12"},
+ 3: {"name": "ouriqn:target-testvol-616", "volume": "testvol-616"},
+ 4: {"name": "neiqn:whee-testvol-451", "volume": "testvol-451"},
+}
+
+T_VOLS = {
+ False: ["something-else", "testvol-42", "testvol-451"],
+ True: [
+ "another.snapshot",
+ "testvol-616",
+ "testvol-9000",
+ "some-testvol-12",
+ "yet.another.thing",
+ ],
+}
+
+
def test_parse_file() -> None:
"""Make sure the name prefix file will be parsed correctly."""
with tempfile.TemporaryDirectory() as tempd_obj:
@@ -88,14 +121,22 @@
"""Mock the StorPool API."""
attached: Dict[bool, List[str]]
+ exported: Dict[str, List[Tuple[str, str]]]
invoked: DefaultDict[str, int]
+ targets: Dict[int, Dict[str, str]]
vols: Dict[bool, List[str]]
def __init__(
- self, attached: Dict[bool, List[str]], vols: Dict[bool, List[str]]
+ self,
+ attached: Dict[bool, List[str]],
+ exported: Dict[str, List[Tuple[str, str]]],
+ targets: Dict[int, Dict[str, str]],
+ vols: Dict[bool, List[str]],
) -> None:
"""Store deep copies of the passed definitions."""
self.attached = {key: list(value) for key, value in attached.items()}
+ self.exported = {key: list(value) for key, value in exported.items()}
+ self.targets = {key: dict(value) for key, value in targets.items()}
self.vols = {key: list(value) for key, value in vols.items()}
self.invoked = collections.defaultdict(lambda: 0)
@@ -116,6 +157,64 @@
for name in self.attached[True]
]
+ def iSCSIConfig(self) -> sptypes.iSCSIConfig:
+ """Return the current view of the iSCSI configuration."""
+ self.invoked["iSCSIConfig"] += 1
+ target_dict = {tgt["volume"]: tgt["name"] for tgt in self.targets.values()}
+
+ return sptypes.iSCSIConfig(
+ iscsi=sptypes.iSCSIConfigData(
+ baseName="just-us-here",
+ initiators={
+ idx: sptypes.iSCSIInitiator(
+ name=name,
+ username="",
+ secret="",
+ nets=[],
+ exports=[
+ sptypes.iSCSIExport(
+ portalGroup=exp[0], target=target_dict[exp[1]]
+ )
+ for exp in explist
+ ],
+ )
+ for idx, (name, explist) in enumerate(sorted(self.exported.items()))
+ },
+ portalGroups={},
+ targets={
+ tid: sptypes.iSCSITarget(
+ currentControllerId=65535,
+ name=tgt["name"],
+ volume=tgt["volume"],
+ )
+ for tid, tgt in self.targets.items()
+ },
+ )
+ )
+
+ def iSCSIConfigChange(self, change: sptypes.iSCSIConfigChange) -> None:
+ """Apply the "delete export" and "remove target" commands."""
+ self.invoked["iSCSIConfigChange"] += 1
+
+ for cmd in change.commands:
+ expdel = cmd.exportDelete
+ tgtdel = cmd.deleteTarget
+ if expdel is not None:
+ initiator = self.exported[expdel.initiator]
+ data = (expdel.portalGroup, expdel.volumeName)
+ assert data in initiator
+ initiator.remove(data)
+ elif tgtdel is not None:
+ found = [
+ tid
+ for tid, tgt in self.targets.items()
+ if tgt["volume"] == tgtdel.volumeName
+ ]
+ assert len(found) == 1
+ del self.targets[found[0]]
+ else:
+ raise ValueError(repr(cmd))
+
def volumesReassignWait(self, req: sptypes.VolumesReassignWaitDesc) -> None:
"""Update the current view of what is attached."""
self.invoked["volumesReassignWait"] += 1
@@ -160,43 +259,42 @@
def test_remove_volumes() -> None:
"""Make sure the StorPool volumes and snapshots will be removed."""
cfg = r_cleanup.Config.default()
- attached = {
- False: ["something-else", "testvol-42"],
- True: ["testvol-9000", "another.snapshot", "testvol-616", "some-testvol-12"],
- }
- vols = {
- False: ["something-else", "testvol-42", "testvol-451"],
- True: [
- "another.snapshot",
- "testvol-616",
- "testvol-9000",
- "some-testvol-12",
- "yet.another.thing",
- ],
- }
- mock_api = MockApi(attached=attached, vols=vols)
+ mock_api = MockApi(
+ attached=T_ATTACHED, exported=T_EXPORTED, targets=T_TARGETS, vols=T_VOLS
+ )
r_cleanup.remove_volumes(cfg, mock_api, "testvol-")
assert mock_api.attached == {
key: [name for name in value if not name.startswith("testvol-")]
- for key, value in attached.items()
+ for key, value in T_ATTACHED.items()
+ }
+ assert mock_api.exported == {
+ initiator: [item for item in exports if not item[1].startswith("testvol-")]
+ for initiator, exports in T_EXPORTED.items()
+ }
+ assert mock_api.targets == {
+ tid: tgt
+ for tid, tgt in T_TARGETS.items()
+ if not tgt["volume"].startswith("testvol-")
}
assert mock_api.vols == {
key: [name for name in value if not name.startswith("testvol-")]
- for key, value in vols.items()
+ for key, value in T_VOLS.items()
}
assert mock_api.invoked == {
"attachmentsList": 1,
+ "iSCSIConfig": 1,
+ "iSCSIConfigChange": 2,
"volumesReassignWait": 1,
"volumesList": 1,
"snapshotsList": 1,
"volumeDelete": len(
- [name for name in vols[False] if name.startswith("testvol-")]
+ [name for name in T_VOLS[False] if name.startswith("testvol-")]
),
"snapshotDelete": len(
- [name for name in vols[True] if name.startswith("testvol-")]
+ [name for name in T_VOLS[True] if name.startswith("testvol-")]
),
}
@@ -204,29 +302,20 @@
def test_remove_volumes_noop() -> None:
"""Make sure the StorPool volumes and snapshots will be removed."""
cfg = r_cleanup.Config.default()._replace(noop=True)
- attached = {
- False: ["something-else", "testvol-42"],
- True: ["testvol-9000", "another.snapshot", "testvol-616", "some-testvol-12"],
- }
- vols = {
- False: ["something-else", "testvol-42", "testvol-451"],
- True: [
- "another.snapshot",
- "testvol-616",
- "testvol-9000",
- "some-testvol-12",
- "yet.another.thing",
- ],
- }
- mock_api = MockApi(attached=attached, vols=vols)
+ mock_api = MockApi(
+ attached=T_ATTACHED, exported=T_EXPORTED, targets=T_TARGETS, vols=T_VOLS
+ )
r_cleanup.remove_volumes(cfg, mock_api, "testvol-")
- assert mock_api.attached == attached
- assert mock_api.vols == vols
+ assert mock_api.attached == T_ATTACHED
+ assert mock_api.exported == T_EXPORTED
+ assert mock_api.targets == T_TARGETS
+ assert mock_api.vols == T_VOLS
assert mock_api.invoked == {
"attachmentsList": 1,
+ "iSCSIConfig": 1,
"volumesList": 1,
"snapshotsList": 1,
}