Calendar: remove past event
[tails/test.git] / bin / rm-config
blob06464140cd8d1b1ccf45795a970abb701d34d42d
1 #! /usr/bin/python3
3 import argparse
4 import hashlib
5 import io
6 import logging
7 from pathlib import Path
8 import re
9 import shlex
10 import subprocess
11 import sys
12 import tempfile
13 from xdg.BaseDirectory import xdg_config_home  # type: ignore
14 from voluptuous import Any, Schema  # type: ignore
15 from voluptuous.validators import (  # type: ignore
16     And, Date, IsDir, IsFile, Match, NotIn
18 import yaml
20 LOG_FORMAT = "%(levelname)s %(message)s"
21 log = logging.getLogger()
23 STAGES = [
24     "base",
25     "built-almost-final",
26     "reproduced-images",
27     "built-iuks",
30 # pylint: disable=E1120
31 InputStr = And(str, NotIn(["FIXME"]))
32 IsBuildManifest = And(IsFile(), Match(re.compile(r".*[.]build-manifest$")))
33 IsIsoFile = And(IsFile(), Match(re.compile(r".*[.]iso$")))
34 IsImgFile = And(IsFile(), Match(re.compile(r".*[.]img$")))
36 STAGE_SCHEMA = {
37     "base": {
38         "tails_signature_key": InputStr,
39         "isos": IsDir(),
40         "artifacts": IsDir(),
41         "master_checkout": IsDir(),
42         "release_checkout": IsDir(),
43         "version": InputStr,
44         "previous_version": InputStr,
45         "previous_stable_version": InputStr,
46         "next_planned_major_version": InputStr,
47         "second_next_planned_major_version": InputStr,
48         "next_planned_bugfix_version": InputStr,
49         "next_planned_version": InputStr,
50         "next_potential_emergency_version": InputStr,
51         "next_stable_changelog_version": InputStr,
52         "release_date": Date(),
53         "major_release": Any(0, 1),
54         "dist": Any("stable", "alpha"),
55         "release_branch": InputStr,
56         "tag": InputStr,
57         "previous_tag": InputStr,
58         "website_release_branch": InputStr,
59         "iuks_dir": IsDir(),
60         "iuks_hashes": InputStr,
61         "milestone": InputStr,
62         "tails_signature_key_long_id": InputStr,
63         "iuk_source_versions": InputStr,
64     },
65     "built-almost-final": {
66         "almost_final_build_manifest": IsBuildManifest,
67     },
68     "reproduced-images": {
69         "matching_jenkins_images_build_id": int,
70     },
71     "built-iuks": {
72         "iso_path": IsIsoFile,
73         "img_path": IsImgFile,
74         "iso_sha256sum": str,
75         "img_sha256sum": str,
76         "iso_size_in_bytes": int,
77         "img_size_in_bytes": int,
78         "candidate_jenkins_iuks_build_id": int,
79         "iuks_hashes": IsFile(),
80     }
82 # pylint: enable=E1120
85 def git_repo_root():
86     """Returns the root of the current Git repository as a Path object"""
87     return Path(
88         subprocess.check_output(["git", "rev-parse", "--show-toplevel"],
89                                 encoding="utf8").rstrip("\n"))
92 def sha256_file(filename):
93     """Returns the hex-encoded SHA256 hash of FILENAME"""
94     sha256 = hashlib.sha256()
95     with io.open(filename, mode="rb") as input_fd:
96         content = input_fd.read()
97         sha256.update(content)
98     return sha256.hexdigest()
101 class Config():
102     """Load, validate, generate, and output Release Management configuration"""
103     def __init__(self, stage: str):
104         self.stage = stage
105         self.config_files = [
106             git_repo_root() / "config/release_management/defaults.yml"
107         ] + list(
108             (Path(xdg_config_home) / "tails/release_management").glob("*.yml"))
109         self.data = self.load_config_files()
110         self.data.update(self.generate_config())
111         log.debug("Configuration:\n%s", self.data)
112         self.validate()
114     def load_config_files(self):
115         """
116         Load all relevant configuration files and return the resulting
117         configuration dict
118         """
119         data = {}
120         for config_file in self.config_files:
121             log.debug("Loading %s", config_file)
122             data.update(yaml.safe_load(open(config_file, 'r')))
123         return data
125     def generate_config(self):
126         """
127         Returns a dict of supplemental, programmatically-generated,
128         configuration.
129         """
130         version = self.data["version"]
131         tails_signature_key = self.data["tails_signature_key"]
132         tag = version.replace("~", "-")
133         release_branch = "testing" \
134             if self.data["major_release"] == 1 \
135             else "stable"
136         iuks_dir = Path(self.data["isos"]) / "iuks/v2"
137         iuk_hashes = Path(iuks_dir) / ("to_%s.sha256sum" % version)
138         iuk_source_versions = subprocess.check_output(
139             [git_repo_root() / "bin/iuk-source-versions", version],
140             encoding="utf8").rstrip("\n")
141         generated_config = {
142             "release_branch": release_branch,
143             "tag": tag,
144             "previous_tag": self.data["previous_version"].replace("~", "-"),
145             "website_release_branch": "web/release-%s" % tag,
146             "iuk_source_versions": iuk_source_versions,
147             "iuks_dir": str(iuks_dir),
148             "iuks_hashes": str(iuk_hashes),
149             "milestone": re.sub('~.*', '', self.data["version"]),
150             "tails_signature_key_long_id": tails_signature_key[24:],
151         }
152         if self.stage == 'built-iuks':
153             iso_path = Path(self.data["isos"]) \
154                 / ("tails-amd64-%s/tails-amd64-%s.iso" % (version, version))
155             img_path = Path(self.data["isos"]) \
156                 / ("tails-amd64-%s/tails-amd64-%s.img" % (version, version))
157             generated_config.update({
158                 "iso_path": str(iso_path),
159                 "img_path": str(img_path),
160                 "iso_sha256sum": sha256_file(iso_path),
161                 "img_sha256sum": sha256_file(img_path),
162                 "iso_size_in_bytes": iso_path.stat().st_size,
163                 "img_size_in_bytes": img_path.stat().st_size,
164             })
165         return generated_config
167     def schema(self):
168         """
169         Returns a configuration validation schema function for
170         the current stage
171         """
172         schema = {}
173         for stage in STAGES:
174             schema.update(STAGE_SCHEMA[stage])
175             if stage == self.stage:
176                 break
177         log.debug("Schema:\n%s", schema)
178         return Schema(schema, required=True)
180     def validate(self):
181         """Checks that the configuration is valid, else raise exception"""
182         schema = self.schema()
183         schema(self.data)
185     def to_shell(self):
186         """
187         Returns shell commands that, if executed, would export the
188         configuration into the environment.
189         """
190         return "\n".join([
191             "export %(key)s=%(val)s" % {
192                 "key": k.upper(),
193                 "val": shlex.quote(str(v))
194             } for (k, v) in self.data.items()
195         ]) + "\n"
198 def generate_boilerplate(stage: str):
199     """Generate boilerplate for STAGE"""
200     log.debug("Generating boilerplate for stage '%s'", stage)
201     with open(git_repo_root() /
202               ("config/release_management/templates/%s.yml" % stage)) as src:
203         with open(
204                 Path(xdg_config_home) / "tails/release_management/current.yml",
205                 'a') as dst:
206             dst.write(src.read())
209 def generate_environment(stage: str):
210     """
211     Prints to stdout the path to a file that contains commands
212     that export the configuration for STAGE to the environment.
213     """
214     log.debug("Generating environment for stage '%s'", stage)
215     config = Config(stage=stage)
216     shell_snippet = tempfile.NamedTemporaryFile(delete=False)
217     with open(shell_snippet.name, 'w') as shell_snippet_fd:
218         shell_snippet_fd.write(config.to_shell())
219     print(shell_snippet.name)
222 def validate_configuration(stage: str):
223     """Validate configuration for STAGE, raise exception if invalid"""
224     log.debug("Validating configuration for stage '%s'", stage)
225     Config(stage=stage)
226     log.info("Configuration is valid")
229 def main():
230     """Command-line entry point"""
231     parser = argparse.ArgumentParser(
232         description="Query and manage Release Management configuration")
233     parser.add_argument("--debug", action="store_true", help="debug output")
234     subparsers = parser.add_subparsers(help="sub-command help", dest="command")
236     parser_generate_boilerplate = subparsers.add_parser(
237         "generate-boilerplate",
238         help="Creates a configuration file template that you will fill")
239     parser_generate_boilerplate.add_argument("--stage",
240                                              type=str,
241                                              action="store",
242                                              default="base",
243                                              help="")
244     parser_generate_boilerplate.set_defaults(func=generate_boilerplate)
246     parser_validate_configuration = subparsers.add_parser(
247         "validate-configuration", help="Validate configuration files")
248     parser_validate_configuration.add_argument("--stage",
249                                                type=str,
250                                                action="store",
251                                                default="base",
252                                                help="")
253     parser_validate_configuration.set_defaults(func=validate_configuration)
255     parser_generate_environment = subparsers.add_parser(
256         "generate-environment",
257         help="Creates a shell sourceable file with resulting environment")
258     parser_generate_environment.add_argument("--stage",
259                                              type=str,
260                                              action="store",
261                                              default="base",
262                                              help="")
263     parser_generate_environment.set_defaults(func=generate_environment)
265     args = parser.parse_args()
267     if args.debug:
268         logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
269     else:
270         logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
272     if args.command is None:
273         parser.print_help()
274     else:
275         args.func(stage=args.stage)
278 if __name__ == '__main__':
279     sys.exit(main())