blob: 101cd62f077e39ddbc77134d7d29b556f63df1f7 [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 Pencheva8fc47e2022-06-24 12:14:41 +03008from typing import DefaultDict, Dict, List, NamedTuple # 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 Pencheva8fc47e2022-06-24 12:14:41 +030018class 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
25class ExportsData(NamedTuple):
26 """Test data: which volumes are exported to an initiator."""
27
28 portal_group: str
29 volume_name: str
30
31
32class TargetData(NamedTuple):
33 """Test data: an iSCSI target record for a StorPool volume."""
34
35 name: str
36 volume: str
37
38
39T_ATTACHED = VolSnapData(
40 volumes=["something-else", "testvol-42"],
41 snapshots=["testvol-9000", "another.snapshot", "testvol-616", "some-testvol-12"],
42)
Peter Pentchev2474e122022-06-23 20:34:43 +030043
44T_EXPORTED = {
45 "beleriand": [
Peter Pencheva8fc47e2022-06-24 12:14:41 +030046 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 Pentchev2474e122022-06-23 20:34:43 +030049 ],
Peter Pencheva8fc47e2022-06-24 12:14:41 +030050 "gondor": [ExportsData(portal_group="ours", volume_name="testvol-12")],
Peter Pentchev2474e122022-06-23 20:34:43 +030051}
52
53T_TARGETS = {
Peter Pencheva8fc47e2022-06-24 12:14:41 +030054 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 Pentchev2474e122022-06-23 20:34:43 +030058}
59
Peter Pencheva8fc47e2022-06-24 12:14:41 +030060T_VOLS = VolSnapData(
61 volumes=["something-else", "testvol-42", "testvol-451"],
62 snapshots=[
Peter Pentchev2474e122022-06-23 20:34:43 +030063 "another.snapshot",
64 "testvol-616",
65 "testvol-9000",
66 "some-testvol-12",
67 "yet.another.thing",
68 ],
Peter Pencheva8fc47e2022-06-24 12:14:41 +030069)
Peter Pentchev2474e122022-06-23 20:34:43 +030070
71
Peter Pentchevd00519f2021-11-08 01:17:13 +020072def 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
85def 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
125def test_remove_file() -> None:
126 """Make sure the name prefix file will be removed."""
127 do_test_remove_file(False)
128
129
130def test_remove_file_noop() -> None:
131 """Make sure the name prefix file will not be removed."""
132 do_test_remove_file(True)
133
134
135class MockVolumeSummary(NamedTuple):
136 """Mock a volume or snapshot description."""
137
138 name: str
139
140
141class MockApi:
142 """Mock the StorPool API."""
143
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300144 attached: VolSnapData
145 exported: Dict[str, List[ExportsData]]
Peter Pentchevd00519f2021-11-08 01:17:13 +0200146 invoked: DefaultDict[str, int]
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300147 targets: Dict[int, TargetData]
148 vols: VolSnapData
Peter Pentchevd00519f2021-11-08 01:17:13 +0200149
150 def __init__(
Peter Pentchev2474e122022-06-23 20:34:43 +0300151 self,
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300152 attached: VolSnapData,
153 exported: Dict[str, List[ExportsData]],
154 targets: Dict[int, TargetData],
155 vols: VolSnapData,
Peter Pentchevd00519f2021-11-08 01:17:13 +0200156 ) -> None:
157 """Store deep copies of the passed definitions."""
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300158 self.attached = VolSnapData(
159 volumes=list(attached.volumes), snapshots=list(attached.snapshots)
160 )
Peter Pentchev2474e122022-06-23 20:34:43 +0300161 self.exported = {key: list(value) for key, value in exported.items()}
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300162 self.targets = dict(targets)
Peter Penchev86d47602022-06-24 12:19:53 +0300163 self.vols = VolSnapData(volumes=list(vols.volumes), snapshots=list(vols.snapshots))
Peter Pentchevd00519f2021-11-08 01:17:13 +0200164 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 Penchev86d47602022-06-24 12:19:53 +0300172 sptypes.AttachmentDesc(volume=name, snapshot=False, client=11, rights="rw", pos=0)
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300173 for name in self.attached.volumes
Peter Pentchevd00519f2021-11-08 01:17:13 +0200174 ] + [
Peter Penchev86d47602022-06-24 12:19:53 +0300175 sptypes.AttachmentDesc(volume=name, snapshot=True, client=11, rights="ro", pos=0)
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300176 for name in self.attached.snapshots
Peter Pentchevd00519f2021-11-08 01:17:13 +0200177 ]
178
Peter Pentchev2474e122022-06-23 20:34:43 +0300179 def iSCSIConfig(self) -> sptypes.iSCSIConfig:
180 """Return the current view of the iSCSI configuration."""
181 self.invoked["iSCSIConfig"] += 1
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300182 target_dict = {tgt.volume: tgt.name for tgt in self.targets.values()}
Peter Pentchev2474e122022-06-23 20:34:43 +0300183
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 Pencheva8fc47e2022-06-24 12:14:41 +0300195 portalGroup=exp.portal_group,
196 target=target_dict[exp.volume_name],
Peter Pentchev2474e122022-06-23 20:34:43 +0300197 )
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 Pencheva8fc47e2022-06-24 12:14:41 +0300207 name=tgt.name,
208 volume=tgt.volume,
Peter Pentchev2474e122022-06-23 20:34:43 +0300209 )
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 Penchev86d47602022-06-24 12:19:53 +0300224 data = ExportsData(portal_group=expdel.portalGroup, volume_name=expdel.volumeName)
Peter Pentchev2474e122022-06-23 20:34:43 +0300225 assert data in initiator
226 initiator.remove(data)
227 elif tgtdel is not None:
228 found = [
Peter Penchev86d47602022-06-24 12:19:53 +0300229 tid for tid, tgt in self.targets.items() if tgt.volume == tgtdel.volumeName
Peter Pentchev2474e122022-06-23 20:34:43 +0300230 ]
231 assert len(found) == 1
232 del self.targets[found[0]]
233 else:
234 raise ValueError(repr(cmd))
235
Peter Pentchevd00519f2021-11-08 01:17:13 +0200236 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 Pencheva8fc47e2022-06-24 12:14:41 +0300243 self.attached.volumes.remove(item.volume)
Peter Pentchevd00519f2021-11-08 01:17:13 +0200244 else:
245 assert isinstance(item, sptypes.SnapshotReassignDesc)
246 assert item.snapshot.startswith("testvol-")
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300247 self.attached.snapshots.remove(item.snapshot)
Peter Pentchevd00519f2021-11-08 01:17:13 +0200248
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 Pencheva8fc47e2022-06-24 12:14:41 +0300254 return [MockVolumeSummary(name=name) for name in self.vols.volumes]
Peter Pentchevd00519f2021-11-08 01:17:13 +0200255
256 def snapshotsList(self) -> List[MockVolumeSummary]:
257 """Return the current view of the available snapshots."""
258 self.invoked["snapshotsList"] += 1
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300259 return [MockVolumeSummary(name=name) for name in self.vols.snapshots]
Peter Pentchevd00519f2021-11-08 01:17:13 +0200260
261 def volumeDelete(self, name: str) -> None:
262 """Remove a volume from our view."""
263 self.invoked["volumeDelete"] += 1
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300264 assert name not in self.attached.volumes
265 self.vols.volumes.remove(name)
Peter Pentchevd00519f2021-11-08 01:17:13 +0200266
267 def snapshotDelete(self, name: str) -> None:
268 """Remove a snapshot from our view."""
269 self.invoked["snapshotDelete"] += 1
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300270 assert name not in self.attached.snapshots
271 self.vols.snapshots.remove(name)
Peter Pentchevd00519f2021-11-08 01:17:13 +0200272
273
274def test_remove_volumes() -> None:
275 """Make sure the StorPool volumes and snapshots will be removed."""
276 cfg = r_cleanup.Config.default()
Peter Penchev86d47602022-06-24 12:19:53 +0300277 mock_api = MockApi(attached=T_ATTACHED, exported=T_EXPORTED, targets=T_TARGETS, vols=T_VOLS)
Peter Pentchevd00519f2021-11-08 01:17:13 +0200278
279 r_cleanup.remove_volumes(cfg, mock_api, "testvol-")
280
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300281 assert mock_api.attached == VolSnapData(
Peter Penchev86d47602022-06-24 12:19:53 +0300282 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 Pencheva8fc47e2022-06-24 12:14:41 +0300284 )
Peter Pentchev2474e122022-06-23 20:34:43 +0300285 assert mock_api.exported == {
Peter Penchev86d47602022-06-24 12:19:53 +0300286 initiator: [item for item in exports if not item.volume_name.startswith("testvol-")]
Peter Pentchev2474e122022-06-23 20:34:43 +0300287 for initiator, exports in T_EXPORTED.items()
288 }
289 assert mock_api.targets == {
Peter Penchev86d47602022-06-24 12:19:53 +0300290 tid: tgt for tid, tgt in T_TARGETS.items() if not tgt.volume.startswith("testvol-")
Peter Pentchevd00519f2021-11-08 01:17:13 +0200291 }
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300292 assert mock_api.vols == VolSnapData(
293 volumes=[name for name in T_VOLS.volumes if not name.startswith("testvol-")],
Peter Penchev86d47602022-06-24 12:19:53 +0300294 snapshots=[name for name in T_VOLS.snapshots if not name.startswith("testvol-")],
Peter Pencheva8fc47e2022-06-24 12:14:41 +0300295 )
Peter Pentchevd00519f2021-11-08 01:17:13 +0200296
297 assert mock_api.invoked == {
298 "attachmentsList": 1,
Peter Pentchev2474e122022-06-23 20:34:43 +0300299 "iSCSIConfig": 1,
300 "iSCSIConfigChange": 2,
Peter Pentchevd00519f2021-11-08 01:17:13 +0200301 "volumesReassignWait": 1,
302 "volumesList": 1,
303 "snapshotsList": 1,
Peter Penchev86d47602022-06-24 12:19:53 +0300304 "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 Pentchevd00519f2021-11-08 01:17:13 +0200306 }
307
308
309def 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 Penchev86d47602022-06-24 12:19:53 +0300312 mock_api = MockApi(attached=T_ATTACHED, exported=T_EXPORTED, targets=T_TARGETS, vols=T_VOLS)
Peter Pentchevd00519f2021-11-08 01:17:13 +0200313
314 r_cleanup.remove_volumes(cfg, mock_api, "testvol-")
315
Peter Pentchev2474e122022-06-23 20:34:43 +0300316 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 Pentchevd00519f2021-11-08 01:17:13 +0200320
321 assert mock_api.invoked == {
322 "attachmentsList": 1,
Peter Pentchev2474e122022-06-23 20:34:43 +0300323 "iSCSIConfig": 1,
Peter Pentchevd00519f2021-11-08 01:17:13 +0200324 "volumesList": 1,
325 "snapshotsList": 1,
326 }