Skip to content

Commit 0ffff94

Browse files
garlofftoothstone
andauthored
Start writing user documentation for Image finding. (#326)
* 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]> * Wording improvements Signed-off-by: Kurt Garloff <[email protected]> * Tiny code improvements. Signed-off-by: Kurt Garloff <[email protected]> * Comment out link to not yet existing standard version. scs-0102-v2 is not there yet ... Signed-off-by: Kurt Garloff <[email protected]> * Try to avoid matching double space in tables. This fixes the markdownlint rule. Signed-off-by: Kurt Garloff <[email protected]> * Add to sidebar. (1st attempt.) Signed-off-by: Kurt Garloff <[email protected]> * Link index file explicitly. Signed-off-by: Kurt Garloff <[email protected]> * opentofu/HCL, first part. Signed-off-by: Kurt Garloff <[email protected]> * Revert "Try to avoid matching double space in tables." This reverts commit b634819. Signed-off-by: Kurt Garloff <[email protected]> * OK, make opentofu work with external helper. This was more complicated than I hoped for. We could have done some HCL magic IF the openstack provider would have allowed to query image properties by IDs which it does not seem to. So the external python3 program is easiest and we reuse what we already have. Also comment on heat. Signed-off-by: Kurt Garloff <[email protected]> * Add ansible approach. Signed-off-by: Kurt Garloff <[email protected]> * Ansible tested now. Signed-off-by: Kurt Garloff <[email protected]> * Break long line in shell example. Signed-off-by: Kurt Garloff <[email protected]> * Add link to second terraform bits. Fix one typo. Signed-off-by: Kurt Garloff <[email protected]> * Update usage output in find_img.py Thanks, @toothstone Co-authored-by: toothstone <[email protected]> Signed-off-by: Kurt Garloff <[email protected]> * Add screenshots from horizon and skyline. Thanks to @toothstone for the idea! Signed-off-by: Kurt Garloff <[email protected]> --------- Signed-off-by: Kurt Garloff <[email protected]> Co-authored-by: toothstone <[email protected]>
1 parent cee19a6 commit 0ffff94

File tree

12 files changed

+830
-0
lines changed

12 files changed

+830
-0
lines changed

sidebarsUserDocs.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ const sidebars = {
2828
]
2929
}
3030
]
31+
},
32+
{
33+
type: 'category',
34+
label: 'Portability Hints',
35+
link: {
36+
type: 'doc',
37+
id: 'usage-hints/index'
38+
},
39+
items: ['usage-hints/find-image/index']
3140
}
3241
]
3342
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
ignore = E501
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python3
2+
#
3+
# find_img.py
4+
#
5+
# Searches for an image with distribution and version and purpose
6+
#
7+
# (c) Kurt Garloff <[email protected]>, 7/2025
8+
# SPDX-License-Identifier: MIT
9+
"This module finds the a distribution image with a given purpose"
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 best
75+
matching, 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+
purpose_out = purpose
81+
images = [x for x in conn.image.images(os_distro=ldistro, os_version=version,
82+
sort="name:desc,created_at:desc", visibility="public")
83+
if x.properties.get("os_purpose") == purpose]
84+
if len(images) == 0:
85+
warn(log, f"No image found with os_distro={ldistro} os_version={version} os_purpose={purpose}")
86+
purpose_out = ""
87+
# images = list(conn.image.images(os_distro=ldistro, os_version=version,
88+
# sort="name:desc,created_at:desc"))
89+
images = [x for x in conn.image.images(os_distro=ldistro, os_version=version,
90+
sort="name:desc,created_at:desc")
91+
if "os_purpose" not in x.properties]
92+
if len(images) == 0:
93+
warn(log, f"No image found with os_distro={ldistro} os_version={version} without os_purpose")
94+
return []
95+
# Now comes sorting magic for best backwards compatibility
96+
if len(images) > 1:
97+
debug(log, f"Several {purpose_out} images found with os_distro={ldistro} os_version={version}")
98+
if strict:
99+
return []
100+
return img_sort_heuristic(images, distro, version, purpose)
101+
return [(img.id, img.name) for img in images]
102+
103+
104+
def usage():
105+
"Usage hints (CLI)"
106+
print("Usage: find-img.py [-s] DISTRO VERSION [PURPOSE]", file=sys.stderr)
107+
print("Returns all images matching, latest first, purpose defaulting to generic", file=sys.stderr)
108+
print("[-s] sets strict mode where only one match is allowed.", file=sys.stderr)
109+
print("You need to have OS_CLOUD set when running this", file=sys.stderr)
110+
sys.exit(1)
111+
112+
113+
def main(argv):
114+
"Main entry for CLI"
115+
if len(argv) < 3:
116+
usage()
117+
try:
118+
conn = openstack.connect(cloud=os.environ["OS_CLOUD"])
119+
except openstack.exceptions.ConfigException:
120+
print(f"No valid entry for cloud {os.environ['OS_CLOUD']}", file=sys.stderr)
121+
usage()
122+
except KeyError:
123+
print("OS_CLOUD environment not configured", file=sys.stderr)
124+
usage()
125+
conn.authorize()
126+
purpose = "generic"
127+
strict = False
128+
if argv[1] == "-s":
129+
argv = argv[1:]
130+
strict = True
131+
if len(argv) > 3:
132+
purpose = argv[3]
133+
images = find_image(conn, argv[1], argv[2], purpose, strict)
134+
for img in images:
135+
print(f"{img[0]} {img[1]}")
136+
return len(images) == 0
137+
138+
139+
if __name__ == "__main__":
140+
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 [-s] 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 a few pattern matches here
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+
PURP="$PURPOSE"
53+
get_images_raw "$1" "$2" --property os_purpose=$PURPOSE
54+
if test -z "$OS_RESP"; then
55+
echo "WARN: No image found with os_distro=$1 os_version=$2 os_purpose=$PURPOSE" 1>&2
56+
PURP=""
57+
# We're screwed as we can not filter for the absence of os_purpose with CLI
58+
# We could loop and do an image show and then flter out, but that's very slow
59+
get_images_raw "$1" "$2" # --property os_purpose=
60+
# FIXME: We need to filter out images with os_purpose property set
61+
NEW_RESP=""
62+
while read ID Name; do
63+
PROPS=$(openstack image show $ID -f value -c properties)
64+
if test $? != 0; then continue; fi
65+
if echo "$PROPS" | grep os_purpose >/dev/null 2>&1; then continue; fi
66+
NEW_RESP=$(echo -en "$NEW_RESP\n$ID $Name")
67+
done < <(echo "$OS_RESP")
68+
OS_RESP=$(echo "$NEW_RESP" | sed '/^$/d')
69+
fi
70+
NR_IMG=$(echo "$OS_RESP" | sed '/^$/d' | wc -l)
71+
if test "$NR_IMG" = "0"; then echo "ERROR: No image found with os_distro=$1 os_version=$2" 1>&2; return 1
72+
elif test "$NR_IMG" = "1"; then return 0
73+
else
74+
echo "DEBUG: Several $PURP images matching os_distro=$1 os_version=$2" 1>&2;
75+
if test -n "$STRICT"; then return 1; fi
76+
img_sort_heuristic "$1" "$2" "$PURPOSE"
77+
return 0
78+
fi
79+
}
80+
81+
if test -z "$OS_CLOUD" -a -z "$OS_AUTH_URL"; then
82+
echo "You need to configure clouds.yaml/secure.yaml and set OS_CLOUD" 1>&2
83+
exit 2
84+
fi
85+
if test "$1" = "-s"; then STRICT=1; shift; fi
86+
if test -z "$1"; then usage; exit 1; fi
87+
88+
get_images "$@"
89+
RC=$?
90+
echo "$OS_RESP"
91+
(exit $RC)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env tofu apply -auto-approve
2+
# Pass variables with -var os_purpose=minimal
3+
# (c) Kurt Garloff <[email protected]>
4+
# SPDX-License-Identifier: MIT
5+
6+
variable "os_distro" {
7+
type = string
8+
default = "ubuntu"
9+
}
10+
11+
variable "os_version" {
12+
type = string
13+
default = "24.04"
14+
}
15+
16+
variable "os_purpose" {
17+
type = string
18+
default = "generic"
19+
}
20+
21+
terraform {
22+
required_providers {
23+
openstack = {
24+
source = "terraform-provider-openstack/openstack"
25+
version = "~> 1.54.0"
26+
}
27+
}
28+
}
29+
30+
provider "openstack" {
31+
# Your cloud config (or use environment variable OS_CLOUD)
32+
}
33+
34+
data "openstack_images_image_v2" "my_image" {
35+
most_recent = true
36+
37+
properties = {
38+
os_distro = "${var.os_distro}"
39+
os_version = "${var.os_version}"
40+
os_purpose = "${var.os_purpose}"
41+
}
42+
#sort = "name:desc,created_at:desc"
43+
#sort_key = "name"
44+
#sort_direction = "desc"
45+
}
46+
47+
# Output the results for inspection
48+
output "selected_image_id" {
49+
value = data.openstack_images_image_v2.my_image.id
50+
}
51+
52+
output "selected_image_name" {
53+
value = data.openstack_images_image_v2.my_image.name
54+
}
55+
56+
output "selected_image_created_at" {
57+
value = data.openstack_images_image_v2.my_image.created_at
58+
}
59+
60+
output "selected_image_properties" {
61+
value = {
62+
os_distro = data.openstack_images_image_v2.my_image.properties.os_distro
63+
os_version = data.openstack_images_image_v2.my_image.properties.os_version
64+
os_purpose = data.openstack_images_image_v2.my_image.properties.os_purpose
65+
}
66+
}
67+
68+
output "selected_image_tags" {
69+
value = data.openstack_images_image_v2.my_image.tags
70+
}
71+

0 commit comments

Comments
 (0)