Skip to content

Commit fafc68a

Browse files
committed
Add kolla-images.py
1 parent 0cfa548 commit fafc68a

File tree

1 file changed

+365
-0
lines changed

1 file changed

+365
-0
lines changed

tools/kolla-images.py

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Script to manage Kolla container image tags.
5+
6+
Background:
7+
In Kolla Ansible each container is deployed using a specific image.
8+
Typically the image is named the same as the container, with underscores
9+
replaced by dashes, however there are some exceptions. Sometimes multiple
10+
containers use the same image.
11+
12+
The image tag deployed by each container is defined by a Kolla Ansible variable
13+
named <container>_tag. There are also intermediate tag variables to make it
14+
easier to set the tag for all containers in a service, e.g. nova_tag is the
15+
default for nova_api_tag, nova_compute, etc. There is a global default tag
16+
defined by openstack_tag. This setup forms a hierarchy of tag variables.
17+
18+
This script captures this logic, as well as exceptions to these rules.
19+
"""
20+
21+
import argparse
22+
import inspect
23+
import json
24+
import os
25+
import pathlib
26+
import re
27+
import subprocess
28+
import sys
29+
from typing import Dict, List, Optional
30+
31+
import yaml
32+
33+
34+
# Dict of Kolla image tags to deploy for each service.
35+
# Each key is the tag variable prefix name, and the value is another dict,
36+
# where the key is the OS distro and the value is the tag to deploy.
37+
# This is the content of etc/kayobe/kolla-image-tags.yml.
38+
KollaImageTags = Dict[str, Dict[str, str]]
39+
40+
# Maps a Kolla image to a list of containers that use the image.
41+
IMAGE_TO_CONTAINERS_EXCEPTIONS: Dict[str, List[str]] = {
42+
"haproxy": [
43+
"glance_tls_proxy",
44+
"neutron_tls_proxy",
45+
],
46+
"mariadb-server": [
47+
"mariadb",
48+
"mariabackup",
49+
],
50+
"neutron-eswitchd": [
51+
"neutron_mlnx_agent",
52+
],
53+
"neutron-metadata-agent": [
54+
"neutron_metadata_agent",
55+
"neutron_ovn_metadata_agent",
56+
],
57+
"nova-conductor": [
58+
"nova_super_conductor",
59+
"nova_conductor",
60+
],
61+
"prometheus-v2-server": [
62+
"prometheus_server",
63+
],
64+
}
65+
66+
# Maps a container to the parent tag variable in the hierarchy.
67+
CONTAINER_TO_PREFIX_VAR_EXCEPTIONS: Dict[str, str] = {
68+
"cron": "common",
69+
"fluentd": "common",
70+
"glance_tls_proxy": "haproxy",
71+
"hacluster_corosync": "openstack",
72+
"hacluster_pacemaker": "openstack",
73+
"hacluster_pacemaker_remote": "openstack",
74+
"heat_api_cfn": "heat",
75+
"ironic_neutron_agent": "neutron",
76+
"kolla_toolbox": "common",
77+
"mariabackup": "mariadb",
78+
"neutron_eswitchd": "neutron_mlnx_agent",
79+
"neutron_tls_proxy": "haproxy",
80+
"nova_compute_ironic": "nova",
81+
"redis_sentinel": "openstack",
82+
"swift_object_expirer": "swift",
83+
"tgtd": "iscsi",
84+
}
85+
86+
# List of supported base distributions and versions.
87+
SUPPORTED_BASE_DISTROS = [
88+
"rocky-9",
89+
"ubuntu-jammy",
90+
]
91+
92+
93+
def parse_args() -> argparse.Namespace:
94+
"""Parse command line arguments."""
95+
parser = argparse.ArgumentParser()
96+
parser.add_argument("--base-distros", default=",".join(SUPPORTED_BASE_DISTROS), choices=SUPPORTED_BASE_DISTROS)
97+
subparsers = parser.add_subparsers(dest="command", required=True)
98+
99+
subparser = subparsers.add_parser("check-hierarchy", help="Check tag variable hierarchy against kolla-ansible")
100+
subparser.add_argument("--kolla-ansible-path", required=True, help="Path to kolla-ansible repostory checked out to correct branch")
101+
102+
subparser = subparsers.add_parser("check-tags", help="Check specified tags for each image exist in the Ark registry")
103+
subparser.add_argument("--registry", required=True, help="Hostname of container image registry")
104+
subparser.add_argument("--namespace", required=True, help="Namespace in container image registry")
105+
106+
subparsers.add_parser("list-containers", help="List supported containers based on pulp.yml")
107+
108+
subparsers.add_parser("list-images", help="List supported images based on pulp.yml")
109+
110+
subparsers.add_parser("list-tags", help="List tags for each image based on kolla-image-tags.yml")
111+
112+
subparsers.add_parser("list-tag-vars", help="List Kolla Ansible tag variables")
113+
114+
return parser.parse_args()
115+
116+
117+
def get_abs_path(relative_path: str) -> str:
118+
"""Return the absolute path of a file in SKC."""
119+
script_path = pathlib.Path(inspect.getfile(inspect.currentframe()))
120+
return script_path.parent.parent / relative_path
121+
122+
123+
def read_images(images_file: str) -> List[str]:
124+
"""Read image list from pulp.yml config file."""
125+
with open(get_abs_path(images_file), "r") as f:
126+
variables = yaml.safe_load(f)
127+
return variables["stackhpc_pulp_images_kolla"]
128+
129+
130+
def read_unbuildable_images(images_file: str) -> Dict[str, List[str]]:
131+
"""Read unbuildable image list from pulp.yml config file."""
132+
with open(get_abs_path(images_file), "r") as f:
133+
variables = yaml.safe_load(f)
134+
return variables["stackhpc_kolla_unbuildable_images"]
135+
136+
137+
def read_kolla_image_tags(tags_file: str) -> KollaImageTags:
138+
"""Read kolla image tags kolla-image-tags.yml config file."""
139+
with open(get_abs_path(tags_file), "r") as f:
140+
variables = yaml.safe_load(f)
141+
return variables["kolla_image_tags"]
142+
143+
144+
def get_containers(image):
145+
"""Return a list of containers that use the specified image."""
146+
default_container = image.replace('-', '_')
147+
return IMAGE_TO_CONTAINERS_EXCEPTIONS.get(image, [default_container])
148+
149+
150+
def get_parent_tag_name(kolla_image_tags: KollaImageTags, base_distro: Optional[str], container: str) -> str:
151+
"""Return the parent tag variable for a container in the tag variable hierarchy."""
152+
153+
if container in CONTAINER_TO_PREFIX_VAR_EXCEPTIONS:
154+
parent = CONTAINER_TO_PREFIX_VAR_EXCEPTIONS[container]
155+
if parent in kolla_image_tags:
156+
return parent
157+
158+
def tag_key(tag):
159+
"""Return a sort key to order the tags."""
160+
if tag == "openstack":
161+
# This is the default tag.
162+
return 0
163+
elif tag != container and container.startswith(tag) and (base_distro is None or base_distro in kolla_image_tags[tag]):
164+
# Prefix match - sort by the longest match.
165+
return -len(tag)
166+
else:
167+
# No match.
168+
return 1
169+
170+
return sorted(kolla_image_tags.keys(), key=tag_key)[0]
171+
172+
173+
def get_parent_tag(kolla_image_tags: KollaImageTags, base_distro: str, container: str) -> str:
174+
"""Return the tag used by the parent in the hierarchy."""
175+
parent_tag_name = get_parent_tag_name(kolla_image_tags, base_distro, container)
176+
return kolla_image_tags[parent_tag_name][base_distro]
177+
178+
179+
def get_tag(kolla_image_tags: KollaImageTags, base_distro: str, container: str) -> str:
180+
"""Return the tag for a container."""
181+
container_tag = kolla_image_tags.get(container, {}).get(base_distro)
182+
if container_tag:
183+
return container_tag
184+
185+
return get_parent_tag(kolla_image_tags, base_distro, container)
186+
187+
188+
def get_tags(base_distros: List[str], kolla_image_tags: KollaImageTags) -> Dict[str, List[str]]:
189+
"""Return a list of tags used for each image."""
190+
images = read_images("etc/kayobe/pulp.yml")
191+
unbuildable_images = read_unbuildable_images("etc/kayobe/pulp.yml")
192+
image_tags: Dict[str, List[str]] = {}
193+
for base_distro in base_distros:
194+
for image in images:
195+
if image not in unbuildable_images[base_distro]:
196+
for container in get_containers(image):
197+
tag = get_tag(kolla_image_tags, base_distro, container)
198+
tags = image_tags.setdefault(image, [])
199+
if tag not in tags:
200+
tags.append(tag)
201+
return image_tags
202+
203+
204+
def get_openstack_release() -> str:
205+
"""Return the OpenStack release."""
206+
with open(get_abs_path(".gitreview"), "r") as f:
207+
gitreview = f.readlines()
208+
for line in gitreview:
209+
if "=" not in line:
210+
continue
211+
key, value = line.split("=")
212+
if key.strip().strip() == "defaultbranch":
213+
value = value.strip().rstrip()
214+
prefix = "stable/"
215+
assert value.startswith(prefix)
216+
return value[len(prefix):]
217+
raise Exception("Failed to determine OpenStack release")
218+
219+
220+
def validate(kolla_image_tags: KollaImageTags):
221+
"""Validate the kolla_image_tags variable."""
222+
tag_var_re = re.compile(r"^[a-z0-9_]+$")
223+
openstack_release = get_openstack_release()
224+
tag_res = {
225+
base_distro: re.compile(f"^{openstack_release}-{base_distro}-[\d]{{8}}T[\d]{{6}}$")
226+
for base_distro in SUPPORTED_BASE_DISTROS
227+
}
228+
errors = []
229+
if "openstack" not in kolla_image_tags:
230+
errors.append("Missing default openstack tag")
231+
for tag_var, base_distros in kolla_image_tags.items():
232+
if not tag_var_re.match(tag_var):
233+
errors.append(f"Key {tag_var} does not match expected pattern. It should match {tag_var_re.pattern}")
234+
for base_distro, tag in base_distros.items():
235+
if base_distro not in SUPPORTED_BASE_DISTROS:
236+
errors.append(f"{tag_var}: base distro {base_distro} not supported. Options: {SUPPORTED_BASE_DISTROS}")
237+
continue
238+
if not tag_res[base_distro].match(tag):
239+
errors.append(f"{tag_var}: {base_distro}: tag {tag} does not match expected pattern. It should match {tag_res[base_distro].pattern}")
240+
if errors:
241+
print("Errors in kolla_image_tags variable:")
242+
for error in errors:
243+
print(error)
244+
sys.exit(1)
245+
246+
247+
def check_tags(base_distros: List[str], kolla_image_tags: KollaImageTags, registry: str, namespace: str):
248+
"""Check whether expected tags are present in container image registry."""
249+
image_tags = get_tags(base_distros, kolla_image_tags)
250+
251+
missing = {}
252+
for image, tags in image_tags.items():
253+
for _ in range(3):
254+
try:
255+
output = subprocess.check_output(f"skopeo list-tags docker://{registry}/{namespace}/{image}", shell=True)
256+
except Exception as e:
257+
exc = e
258+
else:
259+
break
260+
else:
261+
raise exc
262+
ark_tags = json.loads(output)["Tags"]
263+
missing_tags = set(tags) - set(ark_tags)
264+
if missing_tags:
265+
missing[image] = list(missing_tags)
266+
267+
if missing:
268+
print(f"ERROR: Some expected tags not found in {namespace} namespace")
269+
print(yaml.dump(missing, indent=2))
270+
sys.exit(1)
271+
272+
273+
def check_hierarchy(kolla_ansible_path: str):
274+
"""Check the tag variable hierarchy against Kolla Ansible variables."""
275+
cmd = """git grep -h '^[a-z0-9_]*_tag:' ansible/roles/*/defaults/main.yml"""
276+
hierarchy_str = subprocess.check_output(cmd, shell=True, cwd=os.path.realpath(kolla_ansible_path))
277+
hierarchy = yaml.safe_load(hierarchy_str)
278+
# This one is not a container:
279+
hierarchy.pop("octavia_amp_image_tag")
280+
tag_var_re = re.compile(r"^([a-z0-9_]+)_tag$")
281+
parent_re = re.compile(r"{{[\s]*([a-z0-9_]+)_tag[\s]*}}")
282+
hierarchy = {
283+
tag_var_re.match(tag_var).group(1): parent_re.match(parent).group(1)
284+
for tag_var, parent in hierarchy.items()
285+
}
286+
kolla_image_tags: KollaImageTags = {image: {} for image in hierarchy}
287+
kolla_image_tags["openstack"] = {}
288+
errors = []
289+
for tag_var, expected in hierarchy.items():
290+
parent = get_parent_tag_name(kolla_image_tags, None, tag_var)
291+
if parent != expected:
292+
errors.append((tag_var, parent, expected))
293+
if errors:
294+
print("Errors:")
295+
for tag_var, parent, expected in errors:
296+
print(f"{tag_var} -> {parent} != {expected}")
297+
if errors:
298+
sys.exit(1)
299+
300+
301+
def list_containers(base_distros: List[str]):
302+
"""List supported containers."""
303+
images = read_images("etc/kayobe/pulp.yml")
304+
unbuildable_images = read_unbuildable_images("etc/kayobe/pulp.yml")
305+
containers = set()
306+
for base_distro in base_distros:
307+
for image in images:
308+
if image not in unbuildable_images[base_distro]:
309+
containers |= set(get_containers(image))
310+
print(yaml.dump(sorted(containers)))
311+
312+
313+
def list_images(base_distros: List[str]):
314+
"""List supported images."""
315+
images = read_images("etc/kayobe/pulp.yml")
316+
print(yaml.dump(images))
317+
318+
319+
def list_tags(base_distros: List[str], kolla_image_tags: KollaImageTags):
320+
"""List tags used by each image."""
321+
image_tags = get_tags(base_distros, kolla_image_tags)
322+
323+
print(yaml.dump(image_tags))
324+
325+
326+
def list_tag_vars(kolla_image_tags: KollaImageTags):
327+
"""List tag variables."""
328+
tag_vars = []
329+
for tag_var in kolla_image_tags:
330+
if tag_var == "openstack":
331+
default = ""
332+
else:
333+
parent_tag_name = get_parent_tag_name(kolla_image_tags, None, tag_var)
334+
default = f" | default({parent_tag_name}_tag)"
335+
tag_vars.append(f"{tag_var}_tag: \"{{{{ kolla_image_tags['{tag_var}'][kolla_base_distro_and_version]{default} }}}}\"")
336+
337+
for tag_var in tag_vars:
338+
print(tag_var)
339+
340+
341+
def main():
342+
args = parse_args()
343+
kolla_image_tags = read_kolla_image_tags("etc/kayobe/kolla-image-tags.yml")
344+
base_distros = args.base_distros.split(",")
345+
346+
validate(kolla_image_tags)
347+
348+
if args.command == "check-hierarchy":
349+
check_hierarchy(args.kolla_ansible_path)
350+
elif args.command == "check-tags":
351+
check_tags(base_distros, kolla_image_tags, args.registry, args.namespace)
352+
elif args.command == "list-containers":
353+
list_containers(base_distros)
354+
elif args.command == "list-images":
355+
list_images(base_distros)
356+
elif args.command == "list-tags":
357+
list_tags(base_distros, kolla_image_tags)
358+
elif args.command == "list-tag-vars":
359+
list_tag_vars(kolla_image_tags)
360+
else:
361+
sys.exit(1)
362+
363+
364+
if __name__ == "__main__":
365+
main()

0 commit comments

Comments
 (0)