blob: 384f6aba84003c4cd68997e446f54e47ca49b721 [file] [log] [blame]
Peter Pentchevd00519f2021-11-08 01:17:13 +02001"""Clean up: remove volumes, snapshots, and the file itself."""
2
3from __future__ import annotations
4
5import argparse
6import errno
Peter Pentchev2474e122022-06-23 20:34:43 +03007import itertools
Peter Pentchevd00519f2021-11-08 01:17:13 +02008import pathlib
9
10from typing import NamedTuple, Tuple # noqa: H301
11
12import confget
13
14from storpool import spapi # type: ignore
15from storpool import sptypes
16
17from . import defs
18
19
20class Config(NamedTuple):
21 """Runtime configuration for the sp_rand_cleanup tool."""
22
23 confdir: pathlib.Path
24 noop: bool
25 skip_api: bool
26
27 @staticmethod
28 def default() -> Config:
29 """Build a configuration object with default values."""
30 return Config(confdir=defs.DEFAULT_CONFDIR, noop=False, skip_api=False)
31
32
33def parse_file(cfg: Config) -> Tuple[pathlib.Path, str]:
34 """Parse the StorPool configuration file."""
35 conffile = cfg.confdir / defs.FILENAME
36 print(f"Parsing {conffile}")
37 data = confget.read_ini_file(confget.Config([], filename=str(conffile)))
38
39 assert isinstance(data, dict)
40 assert sorted(data) == [""]
41 assert sorted(data[""]) == [defs.PREFIX_VAR]
42 prefix = data[""][defs.PREFIX_VAR]
43 print(f"- got prefix {prefix!r}")
44
45 return conffile, prefix
46
47
Peter Pentchev2474e122022-06-23 20:34:43 +030048def remove_iscsi_targets_and_exports(cfg: Config, api: spapi.Api, prefix: str) -> None:
49 """Unexport volumes, remove the iSCSI target records."""
50 print("Querying the StorPool API for the iSCSI configuration")
51 iscsi = api.iSCSIConfig().iscsi
52
53 print("Looking for target records for our volumes")
54 targets = {
Peter Penchev86d47602022-06-24 12:19:53 +030055 tgt.name: tgt.volume for tgt in iscsi.targets.values() if tgt.volume.startswith(prefix)
Peter Pentchev2474e122022-06-23 20:34:43 +030056 }
57
58 if targets:
59 print(f"Found {len(targets)} target records, looking for exports")
60 exports = list(
61 itertools.chain(
62 *(
63 [
64 (idata.name, exp.portalGroup, targets[exp.target])
65 for exp in idata.exports
66 if exp.target in targets
67 ]
68 for idata in iscsi.initiators.values()
69 )
70 )
71 )
72
73 if exports:
74 to_remove = []
75 for exp_name, exp_pg, exp_vol in exports:
76 print(f"Will remove the export for {exp_vol} to {exp_name} in {exp_pg}")
77 to_remove.append(
78 sptypes.iSCSIConfigCommand(
79 exportDelete=sptypes.iSCSICommandExportDelete(
80 initiator=exp_name, portalGroup=exp_pg, volumeName=exp_vol
81 )
82 )
83 )
84
85 print(f"Prepared for removing {len(to_remove)} exports")
86 if not cfg.noop:
87 api.iSCSIConfigChange(sptypes.iSCSIConfigChange(commands=to_remove))
88 else:
89 print("No iSCSI exports to remove")
90
91 to_remove = []
92 for vol in sorted(targets.values()):
93 print(f"Will remove the target record for volume {vol}")
94 to_remove.append(
95 sptypes.iSCSIConfigCommand(
96 deleteTarget=sptypes.iSCSICommandDeleteTarget(volumeName=vol)
97 )
98 )
99
100 print(f"Prepared for removing {len(to_remove)} targets")
101 if not cfg.noop:
102 api.iSCSIConfigChange(sptypes.iSCSIConfigChange(commands=to_remove))
103 else:
104 print("No iSCSI targets to remove, not looking for exports at all")
105
106
Peter Penchev6a29fa02022-06-24 12:18:37 +0300107def remove_attachments(cfg: Config, api: spapi.Api, prefix: str) -> None:
108 """Detach StorPool volumes and snapshots from the test hosts."""
Peter Pentchevd00519f2021-11-08 01:17:13 +0200109 print("Querying the StorPool API for attached volumes and snapshots")
110 to_detach = []
111 for desc in api.attachmentsList(): # pylint: disable=not-callable
112 if not desc.volume.startswith(prefix):
113 continue
114 if desc.snapshot:
115 print(f"Will detach snapshot {desc.volume} from client {desc.client}")
116 to_detach.append(
Peter Penchev86d47602022-06-24 12:19:53 +0300117 sptypes.SnapshotReassignDesc(detach=[desc.client], force=True, snapshot=desc.volume)
Peter Pentchevd00519f2021-11-08 01:17:13 +0200118 )
119 else:
120 print(f"Will detach volume {desc.volume} from client {desc.client}")
121 to_detach.append(
Peter Penchev86d47602022-06-24 12:19:53 +0300122 sptypes.VolumeReassignDesc(detach=[desc.client], force=True, volume=desc.volume)
Peter Pentchevd00519f2021-11-08 01:17:13 +0200123 )
124
125 if to_detach:
126 print(f"Detaching {len(to_detach)} volumes/snapshots")
127 if not cfg.noop:
128 api.volumesReassignWait( # pylint: disable=not-callable
129 sptypes.VolumesReassignWaitDesc(reassign=to_detach)
130 )
131
Peter Pentchev2474e122022-06-23 20:34:43 +0300132
Peter Penchev6a29fa02022-06-24 12:18:37 +0300133def remove_volumes_and_snapshots(cfg: Config, api: spapi.Api, prefix: str) -> None:
134 """Actually delete the StorPool volumes and snapshots."""
Peter Pentchevd00519f2021-11-08 01:17:13 +0200135 print("Querying the StorPool API for existing volumes")
136 for name in [vol.name for vol in api.volumesList()]:
137 if not name.startswith(prefix):
138 continue
139 print(f"Removing volume {name}")
140 if not cfg.noop:
141 api.volumeDelete(name)
142
143 print("Querying the StorPool API for existing snapshots")
144 for name in [snap.name for snap in api.snapshotsList()]:
145 if not name.startswith(prefix):
146 continue
147 print(f"Removing snapshot {name}")
148 if not cfg.noop:
149 api.snapshotDelete(name)
150
151
Peter Penchev6a29fa02022-06-24 12:18:37 +0300152def remove_volumes(cfg: Config, api: spapi.Api, prefix: str) -> None:
153 """Detach and remove any pertinent StorPool volumes and snapshots."""
154 remove_attachments(cfg, api, prefix)
155 remove_iscsi_targets_and_exports(cfg, api, prefix)
156 remove_volumes_and_snapshots(cfg, api, prefix)
157
158
Peter Pentchevd00519f2021-11-08 01:17:13 +0200159def remove_file(cfg: Config, conffile: pathlib.Path) -> None:
160 """Remove the StorPool configuration file."""
161 print(f"Removing {conffile}")
162 if not cfg.noop:
163 try:
164 conffile.unlink()
165 except OSError as err:
166 if err.errno != errno.ENOENT:
167 raise
168
169
170def parse_args() -> Config:
171 """Parse the command-line arguments."""
172 parser = argparse.ArgumentParser(prog="sp_rand_cleanup")
173 parser.add_argument(
174 "-d",
175 "--confdir",
176 type=pathlib.Path,
177 default=defs.DEFAULT_CONFDIR,
178 help="The directory to look for the configuration file in",
179 )
180 parser.add_argument(
181 "-N",
182 "--noop",
183 action="store_true",
184 help="No-operation mode; display what would have been done",
185 )
186 parser.add_argument(
187 "--skip-api",
188 type=str,
Peter Penchev86d47602022-06-24 12:19:53 +0300189 help=("If explicitly given the 'yes' value, do not " "delete any volumes or snapshots"),
Peter Pentchevd00519f2021-11-08 01:17:13 +0200190 )
191
192 args = parser.parse_args()
193
194 return Config(confdir=args.confdir, noop=args.noop, skip_api=args.skip_api == "yes")
195
196
197def main() -> None:
198 """Main program: parse command-line options, handle them."""
199 cfg = parse_args()
200 conffile, prefix = parse_file(cfg)
201
202 if not cfg.skip_api:
203 api = spapi.Api.fromConfig()
204 remove_volumes(cfg, api, prefix)
205
206 remove_file(cfg, conffile)
207
208
209if __name__ == "__main__":
210 main()