blob: 80ff0523e66434d6483f4960433e6c8c36550c8a [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 = {
55 tgt.name: tgt.volume
56 for tgt in iscsi.targets.values()
57 if tgt.volume.startswith(prefix)
58 }
59
60 if targets:
61 print(f"Found {len(targets)} target records, looking for exports")
62 exports = list(
63 itertools.chain(
64 *(
65 [
66 (idata.name, exp.portalGroup, targets[exp.target])
67 for exp in idata.exports
68 if exp.target in targets
69 ]
70 for idata in iscsi.initiators.values()
71 )
72 )
73 )
74
75 if exports:
76 to_remove = []
77 for exp_name, exp_pg, exp_vol in exports:
78 print(f"Will remove the export for {exp_vol} to {exp_name} in {exp_pg}")
79 to_remove.append(
80 sptypes.iSCSIConfigCommand(
81 exportDelete=sptypes.iSCSICommandExportDelete(
82 initiator=exp_name, portalGroup=exp_pg, volumeName=exp_vol
83 )
84 )
85 )
86
87 print(f"Prepared for removing {len(to_remove)} exports")
88 if not cfg.noop:
89 api.iSCSIConfigChange(sptypes.iSCSIConfigChange(commands=to_remove))
90 else:
91 print("No iSCSI exports to remove")
92
93 to_remove = []
94 for vol in sorted(targets.values()):
95 print(f"Will remove the target record for volume {vol}")
96 to_remove.append(
97 sptypes.iSCSIConfigCommand(
98 deleteTarget=sptypes.iSCSICommandDeleteTarget(volumeName=vol)
99 )
100 )
101
102 print(f"Prepared for removing {len(to_remove)} targets")
103 if not cfg.noop:
104 api.iSCSIConfigChange(sptypes.iSCSIConfigChange(commands=to_remove))
105 else:
106 print("No iSCSI targets to remove, not looking for exports at all")
107
108
Peter Pentchevd00519f2021-11-08 01:17:13 +0200109def remove_volumes(cfg: Config, api: spapi.Api, prefix: str) -> None:
110 """Detach and remove any pertinent StorPool volumes and snapshots."""
111 print("Querying the StorPool API for attached volumes and snapshots")
112 to_detach = []
113 for desc in api.attachmentsList(): # pylint: disable=not-callable
114 if not desc.volume.startswith(prefix):
115 continue
116 if desc.snapshot:
117 print(f"Will detach snapshot {desc.volume} from client {desc.client}")
118 to_detach.append(
119 sptypes.SnapshotReassignDesc(
120 detach=[desc.client], force=True, snapshot=desc.volume
121 )
122 )
123 else:
124 print(f"Will detach volume {desc.volume} from client {desc.client}")
125 to_detach.append(
126 sptypes.VolumeReassignDesc(
127 detach=[desc.client], force=True, volume=desc.volume
128 )
129 )
130
131 if to_detach:
132 print(f"Detaching {len(to_detach)} volumes/snapshots")
133 if not cfg.noop:
134 api.volumesReassignWait( # pylint: disable=not-callable
135 sptypes.VolumesReassignWaitDesc(reassign=to_detach)
136 )
137
Peter Pentchev2474e122022-06-23 20:34:43 +0300138 remove_iscsi_targets_and_exports(cfg, api, prefix)
139
Peter Pentchevd00519f2021-11-08 01:17:13 +0200140 print("Querying the StorPool API for existing volumes")
141 for name in [vol.name for vol in api.volumesList()]:
142 if not name.startswith(prefix):
143 continue
144 print(f"Removing volume {name}")
145 if not cfg.noop:
146 api.volumeDelete(name)
147
148 print("Querying the StorPool API for existing snapshots")
149 for name in [snap.name for snap in api.snapshotsList()]:
150 if not name.startswith(prefix):
151 continue
152 print(f"Removing snapshot {name}")
153 if not cfg.noop:
154 api.snapshotDelete(name)
155
156
157def remove_file(cfg: Config, conffile: pathlib.Path) -> None:
158 """Remove the StorPool configuration file."""
159 print(f"Removing {conffile}")
160 if not cfg.noop:
161 try:
162 conffile.unlink()
163 except OSError as err:
164 if err.errno != errno.ENOENT:
165 raise
166
167
168def parse_args() -> Config:
169 """Parse the command-line arguments."""
170 parser = argparse.ArgumentParser(prog="sp_rand_cleanup")
171 parser.add_argument(
172 "-d",
173 "--confdir",
174 type=pathlib.Path,
175 default=defs.DEFAULT_CONFDIR,
176 help="The directory to look for the configuration file in",
177 )
178 parser.add_argument(
179 "-N",
180 "--noop",
181 action="store_true",
182 help="No-operation mode; display what would have been done",
183 )
184 parser.add_argument(
185 "--skip-api",
186 type=str,
187 help=(
188 "If explicitly given the 'yes' value, do not "
189 "delete any volumes or snapshots"
190 ),
191 )
192
193 args = parser.parse_args()
194
195 return Config(confdir=args.confdir, noop=args.noop, skip_api=args.skip_api == "yes")
196
197
198def main() -> None:
199 """Main program: parse command-line options, handle them."""
200 cfg = parse_args()
201 conffile, prefix = parse_file(cfg)
202
203 if not cfg.skip_api:
204 api = spapi.Api.fromConfig()
205 remove_volumes(cfg, api, prefix)
206
207 remove_file(cfg, conffile)
208
209
210if __name__ == "__main__":
211 main()