Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 1 | """Test the sp_rand_cleanup tool.""" |
| 2 | |
| 3 | import collections |
| 4 | import errno |
| 5 | import pathlib |
| 6 | import tempfile |
| 7 | |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 8 | from typing import DefaultDict, Dict, List, NamedTuple # noqa: H301 |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 9 | |
| 10 | import pytest |
| 11 | |
| 12 | from storpool import sptypes # type: ignore |
| 13 | |
| 14 | from sp_rand import cleanup as r_cleanup |
| 15 | from sp_rand import defs as r_defs |
| 16 | |
| 17 | |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 18 | class VolSnapData(NamedTuple): |
| 19 | """Test data: what volumes and snapshots should be shown as attached.""" |
| 20 | |
| 21 | volumes: List[str] |
| 22 | snapshots: List[str] |
| 23 | |
| 24 | |
| 25 | class ExportsData(NamedTuple): |
| 26 | """Test data: which volumes are exported to an initiator.""" |
| 27 | |
| 28 | portal_group: str |
| 29 | volume_name: str |
| 30 | |
| 31 | |
| 32 | class TargetData(NamedTuple): |
| 33 | """Test data: an iSCSI target record for a StorPool volume.""" |
| 34 | |
| 35 | name: str |
| 36 | volume: str |
| 37 | |
| 38 | |
| 39 | T_ATTACHED = VolSnapData( |
| 40 | volumes=["something-else", "testvol-42"], |
| 41 | snapshots=["testvol-9000", "another.snapshot", "testvol-616", "some-testvol-12"], |
| 42 | ) |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 43 | |
| 44 | T_EXPORTED = { |
| 45 | "beleriand": [ |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 46 | ExportsData(portal_group="neighbors", volume_name="testvol-451"), |
| 47 | ExportsData(portal_group="ours", volume_name="something-else"), |
| 48 | ExportsData(portal_group="ours", volume_name="testvol-616"), |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 49 | ], |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 50 | "gondor": [ExportsData(portal_group="ours", volume_name="testvol-12")], |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 51 | } |
| 52 | |
| 53 | T_TARGETS = { |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 54 | 1: TargetData(name="ouriqn:target-something-else", volume="something-else"), |
| 55 | 2: TargetData(name="ouriqn:target-testvol-12", volume="testvol-12"), |
| 56 | 3: TargetData(name="ouriqn:target-testvol-616", volume="testvol-616"), |
| 57 | 4: TargetData(name="neiqn:whee-testvol-451", volume="testvol-451"), |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 58 | } |
| 59 | |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 60 | T_VOLS = VolSnapData( |
| 61 | volumes=["something-else", "testvol-42", "testvol-451"], |
| 62 | snapshots=[ |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 63 | "another.snapshot", |
| 64 | "testvol-616", |
| 65 | "testvol-9000", |
| 66 | "some-testvol-12", |
| 67 | "yet.another.thing", |
| 68 | ], |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 69 | ) |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 70 | |
| 71 | |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 72 | def test_parse_file() -> None: |
| 73 | """Make sure the name prefix file will be parsed correctly.""" |
| 74 | with tempfile.TemporaryDirectory() as tempd_obj: |
| 75 | tempd = pathlib.Path(tempd_obj) |
| 76 | cfg = r_cleanup.Config.default()._replace(confdir=tempd) |
| 77 | |
| 78 | conffile = tempd / r_defs.FILENAME |
| 79 | conffile.write_text(f"{r_defs.PREFIX_VAR}=testvol-\n", encoding="UTF-8") |
| 80 | |
| 81 | parsed_conffile, prefix = r_cleanup.parse_file(cfg) |
| 82 | assert (parsed_conffile, prefix) == (conffile, "testvol-") |
| 83 | |
| 84 | |
| 85 | def do_test_remove_file(noop: bool) -> None: |
| 86 | """Make sure the name prefix file will be removed... or not.""" |
| 87 | with tempfile.TemporaryDirectory() as tempd_obj: |
| 88 | tempd = pathlib.Path(tempd_obj) |
| 89 | cfg = r_cleanup.Config.default()._replace(confdir=tempd, noop=noop) |
| 90 | |
| 91 | conffile = tempd / r_defs.FILENAME |
| 92 | otherfile = tempd / "something.conf" |
| 93 | |
| 94 | otherfile.write_text("Can't touch this!\n", encoding="UTF-8") |
| 95 | assert sorted(tempd.iterdir()) == [otherfile] |
| 96 | |
| 97 | # Make sure it does not complain if the file does not exist. |
| 98 | r_cleanup.remove_file(cfg, conffile) |
| 99 | assert sorted(tempd.iterdir()) == [otherfile] |
| 100 | |
| 101 | conffile.write_text("hello\n", encoding="UTF-8") |
| 102 | assert sorted(tempd.iterdir()) == sorted([conffile, otherfile]) |
| 103 | |
| 104 | # Make sure it raises an error if it cannot remove the file. |
| 105 | tempd.chmod((tempd.stat().st_mode & 0o7777) & 0o7577) |
| 106 | try: |
| 107 | if noop: |
| 108 | r_cleanup.remove_file(cfg, conffile) |
| 109 | else: |
| 110 | with pytest.raises(OSError) as exc_info: |
| 111 | r_cleanup.remove_file(cfg, conffile) |
| 112 | assert exc_info.value.errno == errno.EACCES |
| 113 | finally: |
| 114 | tempd.chmod((tempd.stat().st_mode & 0o7777) | 0o0200) |
| 115 | assert sorted(tempd.iterdir()) == sorted([conffile, otherfile]) |
| 116 | |
| 117 | # Make sure it removes the file. |
| 118 | r_cleanup.remove_file(cfg, conffile) |
| 119 | if noop: |
| 120 | assert sorted(tempd.iterdir()) == sorted([conffile, otherfile]) |
| 121 | else: |
| 122 | assert sorted(tempd.iterdir()) == [otherfile] |
| 123 | |
| 124 | |
| 125 | def test_remove_file() -> None: |
| 126 | """Make sure the name prefix file will be removed.""" |
| 127 | do_test_remove_file(False) |
| 128 | |
| 129 | |
| 130 | def test_remove_file_noop() -> None: |
| 131 | """Make sure the name prefix file will not be removed.""" |
| 132 | do_test_remove_file(True) |
| 133 | |
| 134 | |
| 135 | class MockVolumeSummary(NamedTuple): |
| 136 | """Mock a volume or snapshot description.""" |
| 137 | |
| 138 | name: str |
| 139 | |
| 140 | |
| 141 | class MockApi: |
| 142 | """Mock the StorPool API.""" |
| 143 | |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 144 | attached: VolSnapData |
| 145 | exported: Dict[str, List[ExportsData]] |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 146 | invoked: DefaultDict[str, int] |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 147 | targets: Dict[int, TargetData] |
| 148 | vols: VolSnapData |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 149 | |
| 150 | def __init__( |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 151 | self, |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 152 | attached: VolSnapData, |
| 153 | exported: Dict[str, List[ExportsData]], |
| 154 | targets: Dict[int, TargetData], |
| 155 | vols: VolSnapData, |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 156 | ) -> None: |
| 157 | """Store deep copies of the passed definitions.""" |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 158 | self.attached = VolSnapData( |
| 159 | volumes=list(attached.volumes), snapshots=list(attached.snapshots) |
| 160 | ) |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 161 | self.exported = {key: list(value) for key, value in exported.items()} |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 162 | self.targets = dict(targets) |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 163 | self.vols = VolSnapData(volumes=list(vols.volumes), snapshots=list(vols.snapshots)) |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 164 | self.invoked = collections.defaultdict(lambda: 0) |
| 165 | |
| 166 | # pylint: disable=invalid-name |
| 167 | |
| 168 | def attachmentsList(self) -> List[sptypes.AttachmentDesc]: |
| 169 | """Return the current view of what is attached.""" |
| 170 | self.invoked["attachmentsList"] += 1 |
| 171 | return [ |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 172 | sptypes.AttachmentDesc(volume=name, snapshot=False, client=11, rights="rw", pos=0) |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 173 | for name in self.attached.volumes |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 174 | ] + [ |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 175 | sptypes.AttachmentDesc(volume=name, snapshot=True, client=11, rights="ro", pos=0) |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 176 | for name in self.attached.snapshots |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 177 | ] |
| 178 | |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 179 | def iSCSIConfig(self) -> sptypes.iSCSIConfig: |
| 180 | """Return the current view of the iSCSI configuration.""" |
| 181 | self.invoked["iSCSIConfig"] += 1 |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 182 | target_dict = {tgt.volume: tgt.name for tgt in self.targets.values()} |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 183 | |
| 184 | return sptypes.iSCSIConfig( |
| 185 | iscsi=sptypes.iSCSIConfigData( |
| 186 | baseName="just-us-here", |
| 187 | initiators={ |
| 188 | idx: sptypes.iSCSIInitiator( |
| 189 | name=name, |
| 190 | username="", |
| 191 | secret="", |
| 192 | nets=[], |
| 193 | exports=[ |
| 194 | sptypes.iSCSIExport( |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 195 | portalGroup=exp.portal_group, |
| 196 | target=target_dict[exp.volume_name], |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 197 | ) |
| 198 | for exp in explist |
| 199 | ], |
| 200 | ) |
| 201 | for idx, (name, explist) in enumerate(sorted(self.exported.items())) |
| 202 | }, |
| 203 | portalGroups={}, |
| 204 | targets={ |
| 205 | tid: sptypes.iSCSITarget( |
| 206 | currentControllerId=65535, |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 207 | name=tgt.name, |
| 208 | volume=tgt.volume, |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 209 | ) |
| 210 | for tid, tgt in self.targets.items() |
| 211 | }, |
| 212 | ) |
| 213 | ) |
| 214 | |
| 215 | def iSCSIConfigChange(self, change: sptypes.iSCSIConfigChange) -> None: |
| 216 | """Apply the "delete export" and "remove target" commands.""" |
| 217 | self.invoked["iSCSIConfigChange"] += 1 |
| 218 | |
| 219 | for cmd in change.commands: |
| 220 | expdel = cmd.exportDelete |
| 221 | tgtdel = cmd.deleteTarget |
| 222 | if expdel is not None: |
| 223 | initiator = self.exported[expdel.initiator] |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 224 | data = ExportsData(portal_group=expdel.portalGroup, volume_name=expdel.volumeName) |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 225 | assert data in initiator |
| 226 | initiator.remove(data) |
| 227 | elif tgtdel is not None: |
| 228 | found = [ |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 229 | tid for tid, tgt in self.targets.items() if tgt.volume == tgtdel.volumeName |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 230 | ] |
| 231 | assert len(found) == 1 |
| 232 | del self.targets[found[0]] |
| 233 | else: |
| 234 | raise ValueError(repr(cmd)) |
| 235 | |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 236 | def volumesReassignWait(self, req: sptypes.VolumesReassignWaitDesc) -> None: |
| 237 | """Update the current view of what is attached.""" |
| 238 | self.invoked["volumesReassignWait"] += 1 |
| 239 | |
| 240 | for item in req.reassign: |
| 241 | if isinstance(item, sptypes.VolumeReassignDesc): |
| 242 | assert item.volume.startswith("testvol-") |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 243 | self.attached.volumes.remove(item.volume) |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 244 | else: |
| 245 | assert isinstance(item, sptypes.SnapshotReassignDesc) |
| 246 | assert item.snapshot.startswith("testvol-") |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 247 | self.attached.snapshots.remove(item.snapshot) |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 248 | |
| 249 | assert item.detach == [11] |
| 250 | |
| 251 | def volumesList(self) -> List[MockVolumeSummary]: |
| 252 | """Return the current view of the available volumes.""" |
| 253 | self.invoked["volumesList"] += 1 |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 254 | return [MockVolumeSummary(name=name) for name in self.vols.volumes] |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 255 | |
| 256 | def snapshotsList(self) -> List[MockVolumeSummary]: |
| 257 | """Return the current view of the available snapshots.""" |
| 258 | self.invoked["snapshotsList"] += 1 |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 259 | return [MockVolumeSummary(name=name) for name in self.vols.snapshots] |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 260 | |
| 261 | def volumeDelete(self, name: str) -> None: |
| 262 | """Remove a volume from our view.""" |
| 263 | self.invoked["volumeDelete"] += 1 |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 264 | assert name not in self.attached.volumes |
| 265 | self.vols.volumes.remove(name) |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 266 | |
| 267 | def snapshotDelete(self, name: str) -> None: |
| 268 | """Remove a snapshot from our view.""" |
| 269 | self.invoked["snapshotDelete"] += 1 |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 270 | assert name not in self.attached.snapshots |
| 271 | self.vols.snapshots.remove(name) |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 272 | |
| 273 | |
| 274 | def test_remove_volumes() -> None: |
| 275 | """Make sure the StorPool volumes and snapshots will be removed.""" |
| 276 | cfg = r_cleanup.Config.default() |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 277 | mock_api = MockApi(attached=T_ATTACHED, exported=T_EXPORTED, targets=T_TARGETS, vols=T_VOLS) |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 278 | |
| 279 | r_cleanup.remove_volumes(cfg, mock_api, "testvol-") |
| 280 | |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 281 | assert mock_api.attached == VolSnapData( |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 282 | volumes=[name for name in T_ATTACHED.volumes if not name.startswith("testvol-")], |
| 283 | snapshots=[name for name in T_ATTACHED.snapshots if not name.startswith("testvol-")], |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 284 | ) |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 285 | assert mock_api.exported == { |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 286 | initiator: [item for item in exports if not item.volume_name.startswith("testvol-")] |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 287 | for initiator, exports in T_EXPORTED.items() |
| 288 | } |
| 289 | assert mock_api.targets == { |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 290 | tid: tgt for tid, tgt in T_TARGETS.items() if not tgt.volume.startswith("testvol-") |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 291 | } |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 292 | assert mock_api.vols == VolSnapData( |
| 293 | volumes=[name for name in T_VOLS.volumes if not name.startswith("testvol-")], |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 294 | snapshots=[name for name in T_VOLS.snapshots if not name.startswith("testvol-")], |
Peter Penchev | a8fc47e | 2022-06-24 12:14:41 +0300 | [diff] [blame] | 295 | ) |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 296 | |
| 297 | assert mock_api.invoked == { |
| 298 | "attachmentsList": 1, |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 299 | "iSCSIConfig": 1, |
| 300 | "iSCSIConfigChange": 2, |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 301 | "volumesReassignWait": 1, |
| 302 | "volumesList": 1, |
| 303 | "snapshotsList": 1, |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 304 | "volumeDelete": len([name for name in T_VOLS[False] if name.startswith("testvol-")]), |
| 305 | "snapshotDelete": len([name for name in T_VOLS[True] if name.startswith("testvol-")]), |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 306 | } |
| 307 | |
| 308 | |
| 309 | def test_remove_volumes_noop() -> None: |
| 310 | """Make sure the StorPool volumes and snapshots will be removed.""" |
| 311 | cfg = r_cleanup.Config.default()._replace(noop=True) |
Peter Penchev | 86d4760 | 2022-06-24 12:19:53 +0300 | [diff] [blame] | 312 | mock_api = MockApi(attached=T_ATTACHED, exported=T_EXPORTED, targets=T_TARGETS, vols=T_VOLS) |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 313 | |
| 314 | r_cleanup.remove_volumes(cfg, mock_api, "testvol-") |
| 315 | |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 316 | assert mock_api.attached == T_ATTACHED |
| 317 | assert mock_api.exported == T_EXPORTED |
| 318 | assert mock_api.targets == T_TARGETS |
| 319 | assert mock_api.vols == T_VOLS |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 320 | |
| 321 | assert mock_api.invoked == { |
| 322 | "attachmentsList": 1, |
Peter Pentchev | 2474e12 | 2022-06-23 20:34:43 +0300 | [diff] [blame] | 323 | "iSCSIConfig": 1, |
Peter Pentchev | d00519f | 2021-11-08 01:17:13 +0200 | [diff] [blame] | 324 | "volumesList": 1, |
| 325 | "snapshotsList": 1, |
| 326 | } |