3 # Copyright (C) 2021 Red Hat, Inc.
4 # SPDX-License-Identifier: LGPL-2.1-or-later
13 from pathlib import Path
14 from tempfile import TemporaryDirectory
19 def required_deps(*deps):
24 def inner_decorator(func):
25 def wrapped(*args, **kwargs):
26 cmd = func.__name__[len('_action_'):]
30 importlib.import_module(dep)
33 msg = f"'{pkg}' not found (required by the '{cmd}' command)"
34 print(msg, file=sys.stderr)
38 return inner_decorator
43 # Options that are common to all actions that use containers
44 containerparser = argparse.ArgumentParser(add_help=False)
45 containerparser.add_argument(
47 help="perform action on target OS",
49 containerparser.add_argument(
51 choices=["auto", "podman", "docker"],
53 help="container engine to use",
55 containerparser.add_argument(
57 default=os.getlogin(), # exempt from syntax-check
58 help="login to use inside the container",
60 containerparser.add_argument(
62 default="registry.gitlab.com/libvirt/libvirt/ci-",
63 help="use container images from non-default location",
65 containerparser.add_argument(
68 help="use container images with non-default tags",
70 containerparser.add_argument(
74 help="path to lcitool (default: $PATH)",
77 # Options that are common to actions communicating with a GitLab
79 gitlabparser = argparse.ArgumentParser(add_help=False)
80 gitlabparser.add_argument(
82 default="libvirt/libvirt",
83 help="GitLab project namespace"
85 gitlabparser.add_argument(
87 default="https://gitlab.com",
88 help="base GitLab URI"
92 self._parser = argparse.ArgumentParser()
93 subparsers = self._parser.add_subparsers(
97 subparsers.required = True
99 jobparser = subparsers.add_parser(
101 help="Run a GitLab CI job or 'shell' in a local environment",
102 parents=[containerparser],
103 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
105 jobparser.add_argument(
107 choices=["build", "codestyle", "potfile", "rpmbuild",
108 "shell", "test", "website"],
110 help="Run a GitLab CI job or 'shell' in a local environment",
112 jobparser.set_defaults(func=Application._action_run)
115 listimagesparser = subparsers.add_parser(
117 help="list known container images",
118 parents=[gitlabparser],
119 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
121 listimagesparser.set_defaults(func=Application._action_list_images)
124 check_staleparser = subparsers.add_parser(
126 help="check for existence of stale images on the GitLab instance",
127 parents=[gitlabparser],
128 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
130 check_staleparser.set_defaults(func=Application._action_check_stale)
133 return self._parser.parse_args()
139 if self._repo is None:
142 self._repo = Repo(search_parent_directories=True)
146 self._basedir = pathlib.Path(__file__).resolve().parent
147 self._args = Parser().parse()
151 def _prepare_repo_copy(repo, dest):
152 return repo.clone(dest, local=True)
154 def _lcitool_run(self, args):
155 positional_args = ["container"]
156 opts = ["--user", self._args.login]
157 tmpdir = TemporaryDirectory(prefix="scratch",
158 dir=Path(self.repo.working_dir, "ci"))
160 repo_dest_path = Path(tmpdir.name, "libvirt.git").as_posix()
161 repo_clone = self._prepare_repo_copy(self.repo, repo_dest_path)
162 opts.extend(["--workload-dir", repo_clone.working_dir])
164 if self._args.job == "shell":
165 positional_args.append("shell")
169 "build": "run_build",
170 "codestyle": "run_codestyle",
171 "potfile": "run_potfile",
172 "rpmbuild": "run_rpmbuild",
173 "website": "run_website_build",
176 if self._args.engine != "auto":
177 positional_args.extend(["--engine", self._args.engine])
179 with open(Path(tmpdir.name, "script"), "w") as f:
181 contents = textwrap.dedent(f"""\
187 {job2func[self._args.job]}
192 positional_args.append("run")
193 opts.extend(["--script", script_path])
195 opts.append(f"{self._args.image_prefix}{self._args.target}:{self._args.image_tag}")
198 proc = subprocess.run([self._args.lcitool] + positional_args + opts)
199 except KeyboardInterrupt:
202 # this will take care of the generated script file above as well
204 return proc.returncode
206 def _check_stale_images(self):
207 namespace = self._args.namespace
208 gitlab_uri = self._args.gitlab_uri
209 registry_uri = util.get_registry_uri(namespace, gitlab_uri)
211 stale_images = util.get_registry_stale_images(registry_uri, self._basedir)
213 spacing = "\n" + 4 * " "
214 stale_fmt = [f"{k} (ID: {v})" for k, v in stale_images.items()]
215 stale_details = spacing.join(stale_fmt)
216 stale_ids = ' '.join([str(id) for id in stale_images.values()])
217 registry_uri = util.get_registry_uri(namespace, gitlab_uri)
219 msg = textwrap.dedent(f"""
220 The following images are stale and can be purged from the registry:
224 You can delete the images listed above using this shell snippet:
226 $ for image_id in {stale_ids}; do
227 curl --request DELETE --header "PRIVATE-TOKEN: <access_token>" \\
228 {registry_uri}/$image_id;
231 You can generate a personal access token here:
233 {gitlab_uri}/-/profile/personal_access_tokens
235 print(msg.replace("STALE_DETAILS", stale_details))
237 @required_deps("git")
238 def _action_run(self):
239 return self._lcitool_run(self._args.job)
241 def _action_list_images(self):
242 registry_uri = util.get_registry_uri(self._args.namespace,
243 self._args.gitlab_uri)
244 images = util.get_registry_images(registry_uri)
246 # skip the "ci-" prefix each of our container images' name has
248 names = [i["name"][len(name_prefix):] for i in images]
251 native = [name for name in names if "-cross-" not in name]
252 cross = [name for name in names if "-cross-" in name]
255 print("Available x86 container images:\n")
256 print(spacing + ("\n" + spacing).join(native))
260 print("Available cross-compiler container images:\n")
261 print(spacing + ("\n" + spacing).join(cross))
263 def _action_check_stale(self):
264 self._check_stale_images()
267 self._args.func(self)
270 if __name__ == "__main__":