Add the sp_rand_init and sp_rand_cleanup tools.
These will be used on the worker node to set up a random volume name
prefix for the StorPool volumes and snapshots created during
the OpenStack CI run, and detach and remove them all later.
Change-Id: Ic558e2183db8068545f7454f956dc8bc740959c6
diff --git a/tools/sp-rand/src/sp_rand/__init__.py b/tools/sp-rand/src/sp_rand/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/sp-rand/src/sp_rand/__init__.py
diff --git a/tools/sp-rand/src/sp_rand/cleanup.py b/tools/sp-rand/src/sp_rand/cleanup.py
new file mode 100644
index 0000000..3e1a464
--- /dev/null
+++ b/tools/sp-rand/src/sp_rand/cleanup.py
@@ -0,0 +1,147 @@
+"""Clean up: remove volumes, snapshots, and the file itself."""
+
+from __future__ import annotations
+
+import argparse
+import errno
+import pathlib
+
+from typing import NamedTuple, Tuple # noqa: H301
+
+import confget
+
+from storpool import spapi # type: ignore
+from storpool import sptypes
+
+from . import defs
+
+
+class Config(NamedTuple):
+ """Runtime configuration for the sp_rand_cleanup tool."""
+
+ confdir: pathlib.Path
+ noop: bool
+ skip_api: bool
+
+ @staticmethod
+ def default() -> Config:
+ """Build a configuration object with default values."""
+ return Config(confdir=defs.DEFAULT_CONFDIR, noop=False, skip_api=False)
+
+
+def parse_file(cfg: Config) -> Tuple[pathlib.Path, str]:
+ """Parse the StorPool configuration file."""
+ conffile = cfg.confdir / defs.FILENAME
+ print(f"Parsing {conffile}")
+ data = confget.read_ini_file(confget.Config([], filename=str(conffile)))
+
+ assert isinstance(data, dict)
+ assert sorted(data) == [""]
+ assert sorted(data[""]) == [defs.PREFIX_VAR]
+ prefix = data[""][defs.PREFIX_VAR]
+ print(f"- got prefix {prefix!r}")
+
+ return conffile, prefix
+
+
+def remove_volumes(cfg: Config, api: spapi.Api, prefix: str) -> None:
+ """Detach and remove any pertinent StorPool volumes and snapshots."""
+ print("Querying the StorPool API for attached volumes and snapshots")
+ to_detach = []
+ for desc in api.attachmentsList(): # pylint: disable=not-callable
+ if not desc.volume.startswith(prefix):
+ continue
+ if desc.snapshot:
+ print(f"Will detach snapshot {desc.volume} from client {desc.client}")
+ to_detach.append(
+ sptypes.SnapshotReassignDesc(
+ detach=[desc.client], force=True, snapshot=desc.volume
+ )
+ )
+ else:
+ print(f"Will detach volume {desc.volume} from client {desc.client}")
+ to_detach.append(
+ sptypes.VolumeReassignDesc(
+ detach=[desc.client], force=True, volume=desc.volume
+ )
+ )
+
+ if to_detach:
+ print(f"Detaching {len(to_detach)} volumes/snapshots")
+ if not cfg.noop:
+ api.volumesReassignWait( # pylint: disable=not-callable
+ sptypes.VolumesReassignWaitDesc(reassign=to_detach)
+ )
+
+ print("Querying the StorPool API for existing volumes")
+ for name in [vol.name for vol in api.volumesList()]:
+ if not name.startswith(prefix):
+ continue
+ print(f"Removing volume {name}")
+ if not cfg.noop:
+ api.volumeDelete(name)
+
+ print("Querying the StorPool API for existing snapshots")
+ for name in [snap.name for snap in api.snapshotsList()]:
+ if not name.startswith(prefix):
+ continue
+ print(f"Removing snapshot {name}")
+ if not cfg.noop:
+ api.snapshotDelete(name)
+
+
+def remove_file(cfg: Config, conffile: pathlib.Path) -> None:
+ """Remove the StorPool configuration file."""
+ print(f"Removing {conffile}")
+ if not cfg.noop:
+ try:
+ conffile.unlink()
+ except OSError as err:
+ if err.errno != errno.ENOENT:
+ raise
+
+
+def parse_args() -> Config:
+ """Parse the command-line arguments."""
+ parser = argparse.ArgumentParser(prog="sp_rand_cleanup")
+ parser.add_argument(
+ "-d",
+ "--confdir",
+ type=pathlib.Path,
+ default=defs.DEFAULT_CONFDIR,
+ help="The directory to look for the configuration file in",
+ )
+ parser.add_argument(
+ "-N",
+ "--noop",
+ action="store_true",
+ help="No-operation mode; display what would have been done",
+ )
+ parser.add_argument(
+ "--skip-api",
+ type=str,
+ help=(
+ "If explicitly given the 'yes' value, do not "
+ "delete any volumes or snapshots"
+ ),
+ )
+
+ args = parser.parse_args()
+
+ return Config(confdir=args.confdir, noop=args.noop, skip_api=args.skip_api == "yes")
+
+
+def main() -> None:
+ """Main program: parse command-line options, handle them."""
+ cfg = parse_args()
+ conffile, prefix = parse_file(cfg)
+
+ if not cfg.skip_api:
+ api = spapi.Api.fromConfig()
+ remove_volumes(cfg, api, prefix)
+
+ remove_file(cfg, conffile)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/sp-rand/src/sp_rand/defs.py b/tools/sp-rand/src/sp_rand/defs.py
new file mode 100644
index 0000000..bf2ad8a
--- /dev/null
+++ b/tools/sp-rand/src/sp_rand/defs.py
@@ -0,0 +1,11 @@
+"""Common definitions for the StorPool random prefix toolset."""
+
+import pathlib
+
+
+VERSION = "0.1.0"
+
+FILENAME = "sp-ostack-random.conf"
+PREFIX_VAR = "SP_OPENSTACK_VOLUME_PREFIX"
+
+DEFAULT_CONFDIR = pathlib.Path("/etc/storpool.conf.d")
diff --git a/tools/sp-rand/src/sp_rand/init.py b/tools/sp-rand/src/sp_rand/init.py
new file mode 100644
index 0000000..60baa88
--- /dev/null
+++ b/tools/sp-rand/src/sp_rand/init.py
@@ -0,0 +1,100 @@
+"""Set up the random volume name prefix."""
+
+from __future__ import annotations
+
+import argparse
+import datetime
+import pathlib
+import random
+
+from typing import NamedTuple, Tuple # noqa: H301
+
+from . import defs
+
+DEFAULT_TAG = "osci-1-date"
+
+
+class Config(NamedTuple):
+ """Runtime configuration for the sp_rand_init tool."""
+
+ confdir: pathlib.Path
+ noop: bool
+ prefix_var: str
+ tag: str
+
+ @staticmethod
+ def default() -> Config:
+ """Build a configuration object with the default values."""
+ return Config(
+ confdir=defs.DEFAULT_CONFDIR,
+ noop=False,
+ prefix_var=defs.PREFIX_VAR,
+ tag=DEFAULT_TAG,
+ )
+
+
+def generate_prefix(cfg: Config) -> str:
+ """Generate a random name prefix."""
+ now = datetime.datetime.now()
+ rval = random.randint(1000, 9999)
+ tdate = f"{now.year:04}{now.month:02}{now.day:02}"
+ ttime = f"{now.hour:02}{now.minute:02}"
+ return f"{cfg.tag}-{tdate}-{ttime}-{rval}-"
+
+
+def store_prefix(cfg: Config) -> Tuple[pathlib.Path, str]:
+ """Generate and store the random name prefix into a file."""
+ prefix = generate_prefix(cfg)
+ conffile = cfg.confdir / defs.FILENAME
+ if not cfg.noop:
+ conffile.write_text(f"{cfg.prefix_var}={prefix}\n", encoding="UTF-8")
+ return conffile, prefix
+
+
+def parse_args() -> Config:
+ """Parse the command-line arguments."""
+ parser = argparse.ArgumentParser(prog="sp_rand_init")
+ parser.add_argument(
+ "-d",
+ "--confdir",
+ type=pathlib.Path,
+ default=defs.DEFAULT_CONFDIR,
+ help="The directory to create the configuration file in",
+ )
+ parser.add_argument(
+ "-N",
+ "--noop",
+ action="store_true",
+ help="No-operation mode; display what would have been done",
+ )
+ parser.add_argument(
+ "-p",
+ "--prefix-var",
+ type=str,
+ default=defs.PREFIX_VAR,
+ help="The name of the variable to store into the configuration file",
+ )
+ parser.add_argument(
+ "-t",
+ "--tag",
+ type=str,
+ default=DEFAULT_TAG,
+ help="The tag that the prefix should start with",
+ )
+
+ args = parser.parse_args()
+
+ return Config(
+ confdir=args.confdir, noop=args.noop, prefix_var=args.prefix_var, tag=args.tag
+ )
+
+
+def main() -> None:
+ """Main program: parse options, generate a prefix, store it."""
+ cfg = parse_args()
+ conffile, prefix = store_prefix(cfg)
+ print(f"{prefix}\n{conffile}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/sp-rand/src/sp_rand/py.typed b/tools/sp-rand/src/sp_rand/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/sp-rand/src/sp_rand/py.typed