blob: c54c53e17d4c6330cf7248d636b3be39fd1cef00 [file] [log] [blame]
Peter Pentchevd00519f2021-11-08 01:17:13 +02001"""Test the sp_rand_cleanup tool."""
2
3import collections
4import errno
5import pathlib
6import tempfile
7
Peter Pentchev2474e122022-06-23 20:34:43 +03008from typing import DefaultDict, Dict, List, NamedTuple, Tuple # noqa: H301
Peter Pentchevd00519f2021-11-08 01:17:13 +02009
10import pytest
11
12from storpool import sptypes # type: ignore
13
14from sp_rand import cleanup as r_cleanup
15from sp_rand import defs as r_defs
16
17
Peter Pentchev2474e122022-06-23 20:34:43 +030018T_ATTACHED = {
19 False: ["something-else", "testvol-42"],
20 True: ["testvol-9000", "another.snapshot", "testvol-616", "some-testvol-12"],
21}
22
23T_EXPORTED = {
24 "beleriand": [
25 ("neighbors", "testvol-451"),
26 ("ours", "something-else"),
27 ("ours", "testvol-616"),
28 ],
29 "gondor": [("ours", "testvol-12")],
30}
31
32T_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
39T_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 Pentchevd00519f2021-11-08 01:17:13 +020051def 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
64def 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
104def test_remove_file() -> None:
105 """Make sure the name prefix file will be removed."""
106 do_test_remove_file(False)
107
108
109def test_remove_file_noop() -> None:
110 """Make sure the name prefix file will not be removed."""
111 do_test_remove_file(True)
112
113
114class MockVolumeSummary(NamedTuple):
115 """Mock a volume or snapshot description."""
116
117 name: str
118
119
120class MockApi:
121 """Mock the StorPool API."""
122
123 attached: Dict[bool, List[str]]
Peter Pentchev2474e122022-06-23 20:34:43 +0300124 exported: Dict[str, List[Tuple[str, str]]]
Peter Pentchevd00519f2021-11-08 01:17:13 +0200125 invoked: DefaultDict[str, int]
Peter Pentchev2474e122022-06-23 20:34:43 +0300126 targets: Dict[int, Dict[str, str]]
Peter Pentchevd00519f2021-11-08 01:17:13 +0200127 vols: Dict[bool, List[str]]
128
129 def __init__(
Peter Pentchev2474e122022-06-23 20:34:43 +0300130 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 Pentchevd00519f2021-11-08 01:17:13 +0200135 ) -> None:
136 """Store deep copies of the passed definitions."""
137 self.attached = {key: list(value) for key, value in attached.items()}
Peter Pentchev2474e122022-06-23 20:34:43 +0300138 self.exported = {key: list(value) for key, value in exported.items()}
139 self.targets = {key: dict(value) for key, value in targets.items()}
Peter Pentchevd00519f2021-11-08 01:17:13 +0200140 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 Pentchev2474e122022-06-23 20:34:43 +0300160 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 Pentchevd00519f2021-11-08 01:17:13 +0200218 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
259def test_remove_volumes() -> None:
260 """Make sure the StorPool volumes and snapshots will be removed."""
261 cfg = r_cleanup.Config.default()
Peter Pentchev2474e122022-06-23 20:34:43 +0300262 mock_api = MockApi(
263 attached=T_ATTACHED, exported=T_EXPORTED, targets=T_TARGETS, vols=T_VOLS
264 )
Peter Pentchevd00519f2021-11-08 01:17:13 +0200265
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 Pentchev2474e122022-06-23 20:34:43 +0300270 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 Pentchevd00519f2021-11-08 01:17:13 +0200280 }
281 assert mock_api.vols == {
282 key: [name for name in value if not name.startswith("testvol-")]
Peter Pentchev2474e122022-06-23 20:34:43 +0300283 for key, value in T_VOLS.items()
Peter Pentchevd00519f2021-11-08 01:17:13 +0200284 }
285
286 assert mock_api.invoked == {
287 "attachmentsList": 1,
Peter Pentchev2474e122022-06-23 20:34:43 +0300288 "iSCSIConfig": 1,
289 "iSCSIConfigChange": 2,
Peter Pentchevd00519f2021-11-08 01:17:13 +0200290 "volumesReassignWait": 1,
291 "volumesList": 1,
292 "snapshotsList": 1,
293 "volumeDelete": len(
Peter Pentchev2474e122022-06-23 20:34:43 +0300294 [name for name in T_VOLS[False] if name.startswith("testvol-")]
Peter Pentchevd00519f2021-11-08 01:17:13 +0200295 ),
296 "snapshotDelete": len(
Peter Pentchev2474e122022-06-23 20:34:43 +0300297 [name for name in T_VOLS[True] if name.startswith("testvol-")]
Peter Pentchevd00519f2021-11-08 01:17:13 +0200298 ),
299 }
300
301
302def 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 Pentchev2474e122022-06-23 20:34:43 +0300305 mock_api = MockApi(
306 attached=T_ATTACHED, exported=T_EXPORTED, targets=T_TARGETS, vols=T_VOLS
307 )
Peter Pentchevd00519f2021-11-08 01:17:13 +0200308
309 r_cleanup.remove_volumes(cfg, mock_api, "testvol-")
310
Peter Pentchev2474e122022-06-23 20:34:43 +0300311 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 Pentchevd00519f2021-11-08 01:17:13 +0200315
316 assert mock_api.invoked == {
317 "attachmentsList": 1,
Peter Pentchev2474e122022-06-23 20:34:43 +0300318 "iSCSIConfig": 1,
Peter Pentchevd00519f2021-11-08 01:17:13 +0200319 "volumesList": 1,
320 "snapshotsList": 1,
321 }