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