Skip to content

Commit e40df25

Browse files
committed
Start writing user documentation for Image finding.
Just to show what to do if we don't have standardized image names any longer. Python and Shell (CLI) covered, opentofu is TBD. Signed-off-by: Kurt Garloff <[email protected]>
1 parent 5666a0c commit e40df25

File tree

3 files changed

+340
-0
lines changed

3 files changed

+340
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python3
2+
#
3+
# find-img.py
4+
#
5+
# Searches for an image with distribution and version
6+
#
7+
# (c) Kurt Garloff <[email protected]>, 7/2025
8+
# SPDX-License-Identifier: MIT
9+
"This module finds the a vanilla distribution image"
10+
11+
import os
12+
import sys
13+
import openstack
14+
# import logging
15+
16+
17+
def warn(log, msg):
18+
"warn output"
19+
if log:
20+
log.warn(msg)
21+
else:
22+
print(f"WARN: {msg}", file=sys.stderr)
23+
24+
25+
def debug(log, msg):
26+
"debug output"
27+
if log:
28+
log.debug(msg)
29+
else:
30+
print(f"DEBUG: {msg}", file=sys.stderr)
31+
32+
33+
def img_sort_heuristic(images, distro, version, purpose):
34+
"""Sort list to prefer old names"""
35+
# Do sorting magic (could be omitted)
36+
newlist = []
37+
distro = distro.lower()
38+
version = version.lower()
39+
purpose = purpose.lower()
40+
# 0th: Exact match old SCS naming scheme ("Ubuntu 24.04 Minimal")
41+
for img in images:
42+
newel = (img.id, img.name)
43+
if img.name.lower() == f"{distro} {version} {purpose}":
44+
newlist.append(newel)
45+
elif img.name.lower() == f"{distro} {purpose} {version}":
46+
newlist.append(newel)
47+
# 1st: Exact match old SCS naming scheme ("Ubuntu 24.04")
48+
for img in images:
49+
newel = (img.id, img.name)
50+
if img.name.lower() == f"{distro} {version}":
51+
newlist.append(newel)
52+
# 2nd: Fuzzy match old SCS naming scheme ("Ubuntu 24.04*")
53+
for img in images:
54+
newel = (img.id, img.name)
55+
if img.name.lower().startswith(f"{distro} {version}") and newel not in newlist:
56+
newlist.append(newel)
57+
# 3rd: Even more fuzzy match old SCS naming scheme ("Ubuntu*24.04")
58+
for img in images:
59+
newel = (img.id, img.name)
60+
if img.name.lower().startswith(f"{distro}") and img.name.lower().endswith(f"{version}") \
61+
and newel not in newlist:
62+
newlist.append(newel)
63+
# 4th: Rest
64+
for img in images:
65+
newel = (img.id, img.name)
66+
if newel not in newlist:
67+
newlist.append(newel)
68+
return newlist
69+
70+
71+
def find_image(conn, distro, version, purpose="generic", strict=False, log=None):
72+
"""Return a sorted list of ID,Name pairs that contain the wanted image.
73+
Empty list indicates no image has been found. The list is sorted such
74+
that (on SCS-compliant clouds), it will very likely contain the most
75+
vanilla, most recent image as first element.
76+
If strict is set, multiple matches are not allowed.
77+
"""
78+
ldistro = distro.lower()
79+
# FIXME: The image.images() method only passes selected filters
80+
images = [x for x in conn.image.images(os_distro=ldistro, os_version=version,
81+
sort="name:desc,created_at:desc", visibility="public")
82+
if x.properties.get("os_purpose") == purpose]
83+
if len(images) == 0:
84+
warn(log, f"No image found with os_distro={ldistro} os_version={version} os_purpose={purpose}")
85+
# images = list(conn.image.images(os_distro=ldistro, os_version=version,
86+
# sort="name:desc,created_at:desc"))
87+
images = [x for x in conn.image.images(os_distro=ldistro, os_version=version,
88+
sort="name:desc,created_at:desc")
89+
if "os_purpose" not in x.properties]
90+
if len(images) == 0:
91+
warn(log, f"No image found with os_distro={ldistro} os_version={version} without os_purpose")
92+
return []
93+
# Now comes sorting magic for best backwards compatibility
94+
if len(images) > 1:
95+
debug(log, f"Several {purpose} images found with os_distro={ldistro} os_version={version}")
96+
if (strict):
97+
return []
98+
return img_sort_heuristic(images, distro, version, purpose)
99+
return [(img.id, img.name) for img in images]
100+
101+
102+
def usage():
103+
"Usage hints (CLI)"
104+
print("Usage: find-img.sh [-s] DISTRO VERSION [PURPOSE]", file=sys.stderr)
105+
print("Returns all images matching, latest first, purpose defaulting to generic", file=sys.stderr)
106+
print("[-s] sets strict mode where only one match is allowed.", file=sys.stderr)
107+
print("You need to have OS_CLOUD set when running this", file=sys.stderr)
108+
sys.exit(1)
109+
110+
111+
def main(argv):
112+
"Main entry for CLI"
113+
if len(argv) < 3:
114+
usage()
115+
try:
116+
conn = openstack.connect(cloud=os.environ["OS_CLOUD"])
117+
except openstack.exceptions.ConfigException:
118+
print(f"No valid entry for cloud {os.environ['OS_CLOUD']}", file=sys.stderr)
119+
usage()
120+
except KeyError:
121+
print("OS_CLOUD environment not configured", file=sys.stderr)
122+
usage()
123+
conn.authorize()
124+
purpose = "generic"
125+
strict = False
126+
if argv[1] == "-s":
127+
argv = argv[1:]
128+
strict = True
129+
if len(argv) > 3:
130+
purpose = argv[3]
131+
images = find_image(conn, argv[1], argv[2], purpose, strict)
132+
for img in images:
133+
print(f"{img[0]} {img[1]}")
134+
return len(images) == 0
135+
136+
137+
if __name__ == "__main__":
138+
sys.exit(main(sys.argv))
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/bin/bash
2+
#
3+
# Find Image by properties
4+
#
5+
# (c) Kurt Garloff <[email protected]>, 7/2025
6+
# SPDX-License-Identifier: MIT
7+
8+
usage()
9+
{
10+
echo "Usage: find-img distro version [purpose]"
11+
echo "Returns all images matching, latest first, purpose defaults to generic"
12+
echo "If some images have the wanted purpose, only those will be shown"
13+
}
14+
15+
get_images_raw()
16+
{
17+
# global OS_RESP
18+
DIST=$(echo "$1" | tr A-Z a-z)
19+
VERS="$2"
20+
#VERS=$(echo "$2" | tr A-Z a-z)
21+
shift; shift
22+
#echo "DEBUG: openstack image list --property os_distro=$DIST --property os_version=$VERS $@ -f value -c ID -c Name --sort created_at:desc"
23+
OS_RESP=$(openstack image list --property os_distro="$DIST" --property os_version="$VERS" $@ -f value -c ID -c Name --sort name:desc,created_at:desc)
24+
}
25+
26+
27+
img_sort_heuristic()
28+
{
29+
# Acts on global OS_RESP
30+
# FIXME: We could do all sorts of advanced heuristics here, looking at the name etc.
31+
# We only do one thing here: Sort the image that matches the old naming scheme first.
32+
# distro version purpose
33+
local NEW_RESP0=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $2 $3\$")
34+
# distro version purpose with extras appended
35+
local NEW_RESP1=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $2 $3" | grep -iv "^[0-9a-f\-]* $1 $2 $3\$")
36+
# distro purpose version
37+
local NEW_RESP2=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $3 $2\$")
38+
# distro purpose version with extras appended
39+
local NEW_RESP3=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $3 $2" | grep -iv "^[0-9a-f\-]* $1 $3 $2\$")
40+
# distro version
41+
local NEW_RESP4=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $2\$")
42+
# distro version with extras (but not purpose)
43+
local NEW_RESP5=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $2" | grep -iv "^[0-9a-f\-]* $1 $2\$" | grep -iv "^[0-9a-f\-]* $1 $2 $3")
44+
# distro extra version (but extra != purpose)
45+
local NEW_RESP6=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 .*$2\$" | grep -iv "^[0-9a-f\-]* $1 $3 $2\$" | grep -iv "$1 $2\$")
46+
OS_RESP=$(echo -e "$NEW_RESP0\n$NEW_RESP1\n$NEW_RESP2\n$NEW_RESP3\n$NEW_RESP4\n$NEW_RESP5\n$NEW_RESP6" | sed '/^$/d')
47+
}
48+
49+
get_images()
50+
{
51+
PURPOSE=${3:-generic}
52+
get_images_raw "$1" "$2" --property os_purpose=$PURPOSE
53+
if test -z "$OS_RESP"; then
54+
echo "WARN: No image found with os_distro=$1 os_version=$2 os_purpose=$PURPOSE" 1>&2
55+
# We're screwed as we can not filter for the absence of os_purpose with CLI
56+
# We could loop and do an image show and then flter out, but that's very slow
57+
get_images_raw "$1" "$2" # --property os_purpose=
58+
# FIXME: We need to filter out images with os_purpose property set
59+
NEW_RESP=""
60+
while read ID Name; do
61+
PROPS=$(openstack image show $ID -f value -c properties)
62+
if test $? != 0; then continue; fi
63+
if echo "$PROPS" | grep os_purpose >/dev/null 2>&1; then continue; fi
64+
NEW_RESP=$(echo -en "$NEW_RESP\n$ID $Name")
65+
done < <(echo "$OS_RESP")
66+
OS_RESP=$(echo "$NEW_RESP" | sed '/^$/d')
67+
fi
68+
NR_IMG=$(echo "$OS_RESP" | sed '/^$/d' | wc -l)
69+
if test "$NR_IMG" = "0"; then echo "ERROR: No image found with os_distro=$1 os_version=$2" 1>&2; return 1
70+
elif test "$NR_IMG" = "1"; then return 0
71+
else
72+
echo "DEBUG: Several $PURPOSE images matching os_distro=$1 os_version=$2" 1>&2;
73+
if test -n "$STRICT"; then return 1; fi
74+
img_sort_heuristic "$1" "$2" "$PURPOSE"
75+
return 0
76+
fi
77+
}
78+
79+
if test -z "$OS_CLOUD" -a -z "$OS_AUTH_URL"; then
80+
echo "You need to configure clouds.yaml/secure.yaml and set OS_CLOUD" 1>&2
81+
exit 2
82+
fi
83+
if test -z "$1"; then
84+
usage
85+
exit 1
86+
fi
87+
88+
get_images "$@"
89+
RC=$?
90+
echo "$OS_RESP"
91+
(exit $RC)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
layout: post
3+
title: 'Locating provider-managed images'
4+
author:
5+
- 'Kurt Garloff'
6+
avatar:
7+
- 'kgarloff.jpg'
8+
---
9+
10+
## Purpose
11+
12+
Many providers provide public images that they maintain for user convenience.
13+
Maintenance means that they regularly update it to include the latest bug- and
14+
security fixes. The exact policy is transparent from the image metadata as
15+
specified in SCS standard [scs-0102](https://docs.scs.community/standards/iaas/scs-0102).
16+
A few images have to be managed this way by the provider according to
17+
[scs-0104](https://docs.scs.community/standards/iaas/scs-0104).
18+
Previously (with [scs-0102-v1](https://docs.scs.community/standards/scs-0102-v1-image-metadata))
19+
the image could be referenced by a standard name to always get the current
20+
image whereas a reference by UUID would result in an unchanged image (until
21+
it is removed according to the provider's policy that is transparent from
22+
the metadata).
23+
24+
Some providers prefer to use different image names. We intend to allow this with
25+
[scs-0102-v2](https://docs.scs.community/standards/scs-0102-v2-image-metadata).
26+
This however means that identifying the most recent "Ubuntu 24.04" image on
27+
an SCS-compatible IaaS cloud becomes a bit harder in a portable way.
28+
This article describes how to do this.
29+
30+
## The new `os_purpose` property
31+
32+
While we suggest to rename or better to hide old images, there can still legitimately
33+
be several variants of images, e.g. minimal variants or Kubernetes node images etc.
34+
These must not be confused with the standard general purpose images. To avoid
35+
confusion, we have introduce a new `os_purpose` (recommended in v1.1 of scs-0102
36+
and mandatory in v2) field, that can be set to `generic`, `minimal`, `k8snode`,
37+
`gpu`, `network`, or `custom` in v2.
38+
To now find the latest general purpose Ubuntu Noble Numbat 24.04 image, one can search the
39+
image catalog for `os_distro=ubuntu`, `os_version=24.04`, and `os_purpose=generic`.
40+
This is straightforward if all SCS clouds already comply to the new metadata standard
41+
and only have one matching image.
42+
It's a bit more complex in case we have to deal with a mixture of old and new ...
43+
44+
## Identifying the right image using python (openstack-SDK)
45+
46+
To find the Ubuntu 24.04 generic image, we would just do
47+
48+
```python
49+
images = [x for x in conn.image.images(os_distro=distro, os_version=version,
50+
sort="name:desc,created_at:desc")
51+
if x.properties.get("os_purpose") == purpose]
52+
```
53+
54+
where `conn` is a connection to your OpenStack project and `distro`, `version` and
55+
`purpose` have been set to the lowercase strings you are looking for.
56+
57+
Three notes:
58+
59+
- We use a list comprehension to filter for `os_purpose` because `os_purpose`
60+
is not one of the hardcoded properties that the SDK knows unlike `os_distro`
61+
and `os_version`.
62+
- We can add additional filtering such as `visibility="public"` if we just want
63+
to look for public images.
64+
- We sort the list, so in case we have several matches, we want the images grouped
65+
by image name and within the same name have the latest images first. This would
66+
typically find the latest image both in the case where a provider renames old
67+
images "Ubuntu 24.04" to "Ubuntu 24.04 timestamp" or fails to rename them.
68+
(The latter would not be compliant with scs-0102.)
69+
70+
It gets a bit harder when you want SCS clouds that comply to the old v1 standard
71+
and do not yet have the `os_purpose` field set. Above call then returns an empty
72+
list. We then would fall back to look for images that match `os_distro` and
73+
`os_version`, but have no `os_purpose` property.
74+
75+
```python
76+
images = [x for x in conn.image.images(os_distro=distro, os_version=version,
77+
sort="name:desc,created_at:desc")
78+
if "os_purpose" not in x.properties]
79+
```
80+
81+
We have to expect several matches here and need some heuristic to find the
82+
right image, preferrably the one matching the old naming convention.
83+
84+
Full code that does this is available in [find_img.py](find_img.py).
85+
Feel free to copy, I deliberately put this under MIT license.
86+
87+
## Identifying the image with OpenStack CLI
88+
89+
Unlike with Python, we can pass the `os_purpose` field just like the other
90+
properties.
91+
92+
```bash
93+
openstack image list --property os_distro="$DIST" --property os_version="$VERS" --property os_purpose="$PURP" -f value -c ID -c Name --sort name:desc,created_at:desc
94+
```
95+
96+
where `OS_CLOUD` environment has been configured to access your cloud project and
97+
`DIST`, `VERS` and `PURP` are set to the lowercased image properties you
98+
are looking for. An additional filter `--public` parameter could be passed to only
99+
list public images. See above python comment for the sorting rationale.
100+
101+
Dealing with old SCS clouds (not yet implementing v2 of scs-0102) is harder
102+
with shell code. The reason is that we can not pass a flag to `openstack
103+
image list` that would tell it to restrict results to records without an
104+
`os_purpose` property. So this requires looping over the images and filtering
105+
out all images with `os_purpose` (but not matching our request).
106+
107+
Full code that does this is available in [find_img.sh](find_img.sh).
108+
109+
## Terraform / opentofu
110+
111+
TBW

0 commit comments

Comments
 (0)