diff --git a/sidebarsUserDocs.js b/sidebarsUserDocs.js index c05de04316..527c226395 100644 --- a/sidebarsUserDocs.js +++ b/sidebarsUserDocs.js @@ -28,6 +28,15 @@ const sidebars = { ] } ] + }, + { + type: 'category', + label: 'Portability Hints', + link: { + type: 'doc', + id: 'usage-hints/index' + }, + items: ['usage-hints/find-image/index'] } ] } diff --git a/user-docs/usage-hints/find-image/.flake8 b/user-docs/usage-hints/find-image/.flake8 new file mode 100644 index 0000000000..e44b810841 --- /dev/null +++ b/user-docs/usage-hints/find-image/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E501 diff --git a/user-docs/usage-hints/find-image/find_img.py b/user-docs/usage-hints/find-image/find_img.py new file mode 100755 index 0000000000..d6d3227f13 --- /dev/null +++ b/user-docs/usage-hints/find-image/find_img.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# +# find_img.py +# +# Searches for an image with distribution and version and purpose +# +# (c) Kurt Garloff , 7/2025 +# SPDX-License-Identifier: MIT +"This module finds the a distribution image with a given purpose" + +import os +import sys +import openstack +# import logging + + +def warn(log, msg): + "warn output" + if log: + log.warn(msg) + else: + print(f"WARN: {msg}", file=sys.stderr) + + +def debug(log, msg): + "debug output" + if log: + log.debug(msg) + else: + print(f"DEBUG: {msg}", file=sys.stderr) + + +def img_sort_heuristic(images, distro, version, purpose): + """Sort list to prefer old names""" + # Do sorting magic (could be omitted) + newlist = [] + distro = distro.lower() + version = version.lower() + purpose = purpose.lower() + # 0th: Exact match old SCS naming scheme ("Ubuntu 24.04 Minimal") + for img in images: + newel = (img.id, img.name) + if img.name.lower() == f"{distro} {version} {purpose}": + newlist.append(newel) + elif img.name.lower() == f"{distro} {purpose} {version}": + newlist.append(newel) + # 1st: Exact match old SCS naming scheme ("Ubuntu 24.04") + for img in images: + newel = (img.id, img.name) + if img.name.lower() == f"{distro} {version}": + newlist.append(newel) + # 2nd: Fuzzy match old SCS naming scheme ("Ubuntu 24.04*") + for img in images: + newel = (img.id, img.name) + if img.name.lower().startswith(f"{distro} {version}") and newel not in newlist: + newlist.append(newel) + # 3rd: Even more fuzzy match old SCS naming scheme ("Ubuntu*24.04") + for img in images: + newel = (img.id, img.name) + if img.name.lower().startswith(f"{distro}") and img.name.lower().endswith(f"{version}") \ + and newel not in newlist: + newlist.append(newel) + # 4th: Rest + for img in images: + newel = (img.id, img.name) + if newel not in newlist: + newlist.append(newel) + return newlist + + +def find_image(conn, distro, version, purpose="generic", strict=False, log=None): + """Return a sorted list of ID,Name pairs that contain the wanted image. + Empty list indicates no image has been found. The list is sorted such + that (on SCS-compliant clouds), it will very likely contain the best + matching, most recent image as first element. + If strict is set, multiple matches are not allowed. + """ + ldistro = distro.lower() + # FIXME: The image.images() method only passes selected filters + purpose_out = purpose + images = [x for x in conn.image.images(os_distro=ldistro, os_version=version, + sort="name:desc,created_at:desc", visibility="public") + if x.properties.get("os_purpose") == purpose] + if len(images) == 0: + warn(log, f"No image found with os_distro={ldistro} os_version={version} os_purpose={purpose}") + purpose_out = "" + # images = list(conn.image.images(os_distro=ldistro, os_version=version, + # sort="name:desc,created_at:desc")) + images = [x for x in conn.image.images(os_distro=ldistro, os_version=version, + sort="name:desc,created_at:desc") + if "os_purpose" not in x.properties] + if len(images) == 0: + warn(log, f"No image found with os_distro={ldistro} os_version={version} without os_purpose") + return [] + # Now comes sorting magic for best backwards compatibility + if len(images) > 1: + debug(log, f"Several {purpose_out} images found with os_distro={ldistro} os_version={version}") + if strict: + return [] + return img_sort_heuristic(images, distro, version, purpose) + return [(img.id, img.name) for img in images] + + +def usage(): + "Usage hints (CLI)" + print("Usage: find-img.py [-s] DISTRO VERSION [PURPOSE]", file=sys.stderr) + print("Returns all images matching, latest first, purpose defaulting to generic", file=sys.stderr) + print("[-s] sets strict mode where only one match is allowed.", file=sys.stderr) + print("You need to have OS_CLOUD set when running this", file=sys.stderr) + sys.exit(1) + + +def main(argv): + "Main entry for CLI" + if len(argv) < 3: + usage() + try: + conn = openstack.connect(cloud=os.environ["OS_CLOUD"]) + except openstack.exceptions.ConfigException: + print(f"No valid entry for cloud {os.environ['OS_CLOUD']}", file=sys.stderr) + usage() + except KeyError: + print("OS_CLOUD environment not configured", file=sys.stderr) + usage() + conn.authorize() + purpose = "generic" + strict = False + if argv[1] == "-s": + argv = argv[1:] + strict = True + if len(argv) > 3: + purpose = argv[3] + images = find_image(conn, argv[1], argv[2], purpose, strict) + for img in images: + print(f"{img[0]} {img[1]}") + return len(images) == 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/user-docs/usage-hints/find-image/find_img.sh b/user-docs/usage-hints/find-image/find_img.sh new file mode 100755 index 0000000000..fd379588e2 --- /dev/null +++ b/user-docs/usage-hints/find-image/find_img.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# +# Find Image by properties +# +# (c) Kurt Garloff , 7/2025 +# SPDX-License-Identifier: MIT + +usage() +{ + echo "Usage: find-img [-s] distro version [purpose]" + echo "Returns all images matching, latest first, purpose defaults to generic" + echo "If some images have the wanted purpose, only those will be shown" +} + +get_images_raw() +{ + # global OS_RESP + DIST=$(echo "$1" | tr A-Z a-z) + VERS="$2" + #VERS=$(echo "$2" | tr A-Z a-z) + shift; shift + #echo "DEBUG: openstack image list --property os_distro=$DIST --property os_version=$VERS $@ -f value -c ID -c Name --sort created_at:desc" + 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) +} + + +img_sort_heuristic() +{ + # Acts on global OS_RESP + # FIXME: We could do all sorts of advanced heuristics here, looking at the name etc. + # We only do a few pattern matches here + # distro version purpose + local NEW_RESP0=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $2 $3\$") + # distro version purpose with extras appended + local NEW_RESP1=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $2 $3" | grep -iv "^[0-9a-f\-]* $1 $2 $3\$") + # distro purpose version + local NEW_RESP2=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $3 $2\$") + # distro purpose version with extras appended + local NEW_RESP3=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $3 $2" | grep -iv "^[0-9a-f\-]* $1 $3 $2\$") + # distro version + local NEW_RESP4=$(echo "$OS_RESP" | grep -i "^[0-9a-f\-]* $1 $2\$") + # distro version with extras (but not purpose) + 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") + # distro extra version (but extra != purpose) + 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\$") + 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') +} + +get_images() +{ + PURPOSE="${3:-generic}" + PURP="$PURPOSE" + get_images_raw "$1" "$2" --property os_purpose=$PURPOSE + if test -z "$OS_RESP"; then + echo "WARN: No image found with os_distro=$1 os_version=$2 os_purpose=$PURPOSE" 1>&2 + PURP="" + # We're screwed as we can not filter for the absence of os_purpose with CLI + # We could loop and do an image show and then flter out, but that's very slow + get_images_raw "$1" "$2" # --property os_purpose= + # FIXME: We need to filter out images with os_purpose property set + NEW_RESP="" + while read ID Name; do + PROPS=$(openstack image show $ID -f value -c properties) + if test $? != 0; then continue; fi + if echo "$PROPS" | grep os_purpose >/dev/null 2>&1; then continue; fi + NEW_RESP=$(echo -en "$NEW_RESP\n$ID $Name") + done < <(echo "$OS_RESP") + OS_RESP=$(echo "$NEW_RESP" | sed '/^$/d') + fi + NR_IMG=$(echo "$OS_RESP" | sed '/^$/d' | wc -l) + if test "$NR_IMG" = "0"; then echo "ERROR: No image found with os_distro=$1 os_version=$2" 1>&2; return 1 + elif test "$NR_IMG" = "1"; then return 0 + else + echo "DEBUG: Several $PURP images matching os_distro=$1 os_version=$2" 1>&2; + if test -n "$STRICT"; then return 1; fi + img_sort_heuristic "$1" "$2" "$PURPOSE" + return 0 + fi +} + +if test -z "$OS_CLOUD" -a -z "$OS_AUTH_URL"; then + echo "You need to configure clouds.yaml/secure.yaml and set OS_CLOUD" 1>&2 + exit 2 +fi +if test "$1" = "-s"; then STRICT=1; shift; fi +if test -z "$1"; then usage; exit 1; fi + +get_images "$@" +RC=$? +echo "$OS_RESP" +(exit $RC) diff --git a/user-docs/usage-hints/find-image/find_img.tf b/user-docs/usage-hints/find-image/find_img.tf new file mode 100644 index 0000000000..d3625178db --- /dev/null +++ b/user-docs/usage-hints/find-image/find_img.tf @@ -0,0 +1,71 @@ +#!/usr/bin/env tofu apply -auto-approve +# Pass variables with -var os_purpose=minimal +# (c) Kurt Garloff +# SPDX-License-Identifier: MIT + +variable "os_distro" { + type = string + default = "ubuntu" +} + +variable "os_version" { + type = string + default = "24.04" +} + +variable "os_purpose" { + type = string + default = "generic" +} + +terraform { + required_providers { + openstack = { + source = "terraform-provider-openstack/openstack" + version = "~> 1.54.0" + } + } +} + +provider "openstack" { + # Your cloud config (or use environment variable OS_CLOUD) +} + +data "openstack_images_image_v2" "my_image" { + most_recent = true + + properties = { + os_distro = "${var.os_distro}" + os_version = "${var.os_version}" + os_purpose = "${var.os_purpose}" + } + #sort = "name:desc,created_at:desc" + #sort_key = "name" + #sort_direction = "desc" +} + +# Output the results for inspection +output "selected_image_id" { + value = data.openstack_images_image_v2.my_image.id +} + +output "selected_image_name" { + value = data.openstack_images_image_v2.my_image.name +} + +output "selected_image_created_at" { + value = data.openstack_images_image_v2.my_image.created_at +} + +output "selected_image_properties" { + value = { + os_distro = data.openstack_images_image_v2.my_image.properties.os_distro + os_version = data.openstack_images_image_v2.my_image.properties.os_version + os_purpose = data.openstack_images_image_v2.my_image.properties.os_purpose + } +} + +output "selected_image_tags" { + value = data.openstack_images_image_v2.my_image.tags +} + diff --git a/user-docs/usage-hints/find-image/find_img.yaml b/user-docs/usage-hints/find-image/find_img.yaml new file mode 100644 index 0000000000..074add292e --- /dev/null +++ b/user-docs/usage-hints/find-image/find_img.yaml @@ -0,0 +1,161 @@ +--- +# Logic to identify image with given os_distro, os_version, os_purpose +# Simply find the image for new SCS clouds that have os_purpose set. +# Fall back to name matching otherwise. +# (c) Kurt Garloff , 11/2025 +# SPDX-License-Identifier: MIT +# Created with help from Claude AI + +- name: Select Image with purpose and fall back to name matching + hosts: localhost + gather_facts: false + vars: + # Primary selection criteria + os_version: '24.04' + os_distro: 'ubuntu' + os_purpose: 'generic' + cloud_name: "{{ lookup('env', 'OS_CLOUD') | default('openstack') }}" + + tasks: + - name: Get available images matching os_distro and os_version + openstack.cloud.image_info: + cloud: '{{ cloud_name }}' + properties: + os_distro: '{{ os_distro }}' + os_version: '{{ os_version }}' + register: _distro_images + + - name: 'Show images that match {{os_distro}} {{os_version}}' + debug: + msg: '{{ _distro_images.images | to_nice_json }}' + + - name: 'First choice: Match os_purpose' + set_fact: + _primary_images: >- + {{ + _distro_images.images + | selectattr('properties.os_purpose', 'defined') + | selectattr('properties.os_purpose', 'equalto', os_purpose) + | list + }} + + - name: 'Select primary image if found' + set_fact: + selected_image: >- + {{ + _primary_images + | sort(attribute='created_at', reverse=true) + | sort(attribute='name', reverse=true) + | first + }} + match_type: 'primary' + when: _primary_images | length > 0 + + # Fallback logic - only executed if no primary match + - block: + - name: 'Fallback 1 pattern' + set_fact: + _pattern1: "(?i){{ os_distro | regex_escape }}\\s+{{ os_version | regex_escape }}\\s+{{ os_purpose | regex_escape }}.*" + - name: "Fallback 1: Filter images without os_purpose, matching name pattern '{{ os_distro }} {{ os_version }} {{ os_purpose }}'" + set_fact: + _fallback1_images: >- + {{ + _distro_images.images + | rejectattr('properties.os_purpose', 'defined') + | selectattr('name', 'search', _pattern1) + | list + }} + - name: Select fallback 1 match if found + set_fact: + selected_image: >- + {{ + _fallback1_images + | sort(attribute='created_at', reverse=true) + | sort(attribute='name', reverse=true) + | first + }} + match_type: 'fallback_pattern1' + when: _fallback1_images | length > 0 + when: _primary_images | length == 0 + + - block: + - name: 'Fallback 2 pattern' + set_fact: + _pattern2: "(?i){{ os_distro | regex_escape }}\\s+{{ os_purpose | regex_escape }}\\s+{{ os_version | regex_escape }}.*" + - name: "Fallback 2: Filter images without os_purpose, matching name pattern '{{ os_distro }} {{ os_purpose }} {{ os_version }}'" + set_fact: + _fallback2_images: >- + {{ + _distro_images.images + | rejectattr('properties.os_purpose', 'defined') + | selectattr('name', 'search', _pattern2) + | list + }} + - name: Select fallback 2 match if found + set_fact: + selected_image: >- + {{ + _fallback2_images + | sort(attribute='created_at', reverse=true) + | sort(attribute='name', reverse=true) + | first + }} + match_type: 'fallback_pattern2' + when: _fallback2_images | length > 0 + when: + - _primary_images | length == 0 + - _fallback1_images | default([]) | length == 0 + + - block: + - name: 'Fallback 3 pattern' + set_fact: + _pattern3: "(?i){{ os_distro | regex_escape }}\\s+{{ os_version | regex_escape }}.*" + - name: "Fallback 3: Filter images without os_purpose, matching name pattern '{{ os_distro }} {{ os_version }}'" + set_fact: + _fallback3_images: >- + {{ + _distro_images.images + | rejectattr('properties.os_purpose', 'defined') + | selectattr('name', 'search', _pattern3) + | list + }} + - name: Select fallback 3 match if found + set_fact: + selected_image: >- + {{ + _fallback3_images + | sort(attribute='created_at', reverse=true) + | sort(attribute='name', reverse=true) + | first + }} + match_type: 'fallback_pattern3' + when: _fallback3_images | length > 0 + when: + - _primary_images | length == 0 + - _fallback1_images | default([]) | length == 0 + - _fallback2_images | default([]) | length == 0 + + - name: Display selected image + debug: + msg: + - 'Match Type: {{ match_type }}' + - 'Image Name: {{ selected_image.name }}' + - 'Image ID: {{ selected_image.id }}' + - 'Created: {{ selected_image.created_at }}' + - 'Properties: {{ selected_image.properties }}' + when: selected_image is defined + + - name: Fail if no suitable image found + fail: + msg: | + No suitable image found matching criteria: + - os_distro: {{ os_distro }} + - os_version: {{ os_version }} + - os_purpose: {{ os_purpose }} + + Tried: + 1. Images with os_purpose property = '{{ os_purpose }}' + 2. Images matching name pattern '{{ os_distro }} {{ os_version }} {{ os_purpose }}' + 3. Images matching name pattern '{{ os_distro }} {{ os_purpose }} {{ os_version }}' + 4. Images matching name pattern '{{ os_distro }} {{ os_version }}' + when: selected_image is not defined diff --git a/user-docs/usage-hints/find-image/find_img2.py b/user-docs/usage-hints/find-image/find_img2.py new file mode 100755 index 0000000000..26bc0f771a --- /dev/null +++ b/user-docs/usage-hints/find-image/find_img2.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# +# find_img2.py +# +# Find images for opentofu/terraform +# +# (c) Kurt Garloff , 11/2025 +# SPDX-License-Identifier: MIT +"This module finds the a distribution image with a given purpose" + +import sys +import json +import find_img + +try: + in_data = json.loads(sys.stdin.read()) + conn = find_img.openstack.connect() # cloud=os.environ["OS_CLOUD"]) + images = find_img.find_image(conn, in_data["os_distro"], in_data["os_version"], in_data["os_purpose"]) + for img in images: + print(f"DEBUG: {img[0]} {img[1]}", file=sys.stderr) + if not (len(images)): + print(f"No image found for {in_data}", file=sys.stderr) + sys.exit(1) + output = { "image_id": images[0][0], + "image_name": images[0][1] } + print(json.dumps(output)) + +except Exception as e: + print(f"Error: {str(e)}", file=sys.stderr) + sys.exit(2) diff --git a/user-docs/usage-hints/find-image/find_img2.tf b/user-docs/usage-hints/find-image/find_img2.tf new file mode 100644 index 0000000000..25b02f9005 --- /dev/null +++ b/user-docs/usage-hints/find-image/find_img2.tf @@ -0,0 +1,52 @@ +#!/usr/bin/env tofu apply -auto-approve +# Pass variables with -var os_purpose=minimal +# (c) Kurt Garloff +# SPDX-License-Identifier: MIT + +variable "os_distro2" { + type = string + default = "ubuntu" +} + +variable "os_version2" { + type = string + default = "24.04" +} + +variable "os_purpose2" { + type = string + default = "generic" +} + +#terraform { +# required_providers { +# openstack = { +# source = "terraform-provider-openstack/openstack" +# version = "~> 1.54.0" +# } +# } +#} + +#provider "openstack" { +# # Your cloud config (or use environment variables OS_CLOUD) +#} + +# Call python find_img.py to find best image +data "external" "my_image2" { + program = ["python3", "${path.module}/find_img2.py"] + + query = { + os_distro = "${var.os_distro2}" + os_version = "${var.os_version2}" + os_purpose = "${var.os_purpose2}" + } +} + +# Output the results for inspection +output "selected_image2" { + value = { + id = data.external.my_image2.result.image_id + name = data.external.my_image2.result.image_name + } +} + diff --git a/user-docs/usage-hints/find-image/horizon.png b/user-docs/usage-hints/find-image/horizon.png new file mode 100644 index 0000000000..5296f078d8 Binary files /dev/null and b/user-docs/usage-hints/find-image/horizon.png differ diff --git a/user-docs/usage-hints/find-image/index.md b/user-docs/usage-hints/find-image/index.md new file mode 100644 index 0000000000..aabb9ca2aa --- /dev/null +++ b/user-docs/usage-hints/find-image/index.md @@ -0,0 +1,267 @@ +--- +layout: post +title: 'Locating provider-managed images' +author: + - 'Kurt Garloff' +avatar: + - 'kgarloff.jpg' +--- + +## Purpose + +Many providers provide public images that they maintain for user convenience. +Maintenance means that they regularly update it to include the latest bug- and +security fixes. The exact policy is transparent from the image metadata as +specified in SCS standard [scs-0102](https://docs.scs.community/standards/iaas/scs-0102). +A few images have to be managed this way by the provider according to +[scs-0104](https://docs.scs.community/standards/iaas/scs-0104). +Previously (with [scs-0102-v1](https://docs.scs.community/standards/scs-0102-v1-image-metadata)) +the image could be referenced by a standard name to always get the current +image whereas a reference by UUID would result in an unchanged image (until +it is removed according to the provider's policy that is transparent from +the metadata). + +Some providers prefer to use different image names. We intend to allow this with +scs-0102-v2. +This however means that identifying the most recent "Ubuntu 24.04" image on +an SCS-compatible IaaS cloud becomes a bit harder in a portable way. +This article describes how to do this. + +## The new `os_purpose` property + +While we suggest to rename or better to hide old images, there can still legitimately +be several variants of images, e.g. minimal variants or Kubernetes node images etc. +These must not be confused with the standard general purpose images. To avoid +confusion, we have introduced a new `os_purpose` (recommended in v1.1 of scs-0102 +and mandatory in v2) field, that can be set to `generic`, `minimal`, `k8snode`, +`gpu`, `network`, or `custom` according to scs-0102-v2. +To now find the latest general purpose Ubuntu Noble Numbat 24.04 image, one can search the +image catalog for `os_distro=ubuntu`, `os_version=24.04`, and `os_purpose=generic`. +This is straightforward if all SCS clouds already comply to the new metadata standard +and only have one matching image. +It's a bit more complex in case we have to deal with a mixture of old and new ... + +## Identifying the right image using python (openstack-SDK) + +To find the Ubuntu 24.04 generic image, we would just do + +```python + images = [x for x in conn.image.images(os_distro=distro, os_version=version, + sort="name:desc,created_at:desc") + if x.properties.get("os_purpose") == purpose] +``` + +where `conn` is a connection to your OpenStack project and `distro`, `version` and +`purpose` have been set to the lowercase strings you are looking for. + +Three notes: + +- We use a list comprehension to filter for `os_purpose` because `os_purpose` + is not one of the hardcoded properties that the SDK knows unlike `os_distro` + and `os_version`. +- We can add additional filtering such as `visibility="public"` if we just want + to look for public images. +- We sort the list, so in case we have several matches, we want the images grouped + by image name and within the same name have the latest images first. This would + typically put the latest image first in the case where a provider renames old + images "Ubuntu 24.04" to "Ubuntu 24.04 timestamp" or fails to rename them. + (The latter would not be compliant with scs-0102.) + +It gets a bit harder when you want SCS clouds that comply to the old v1 standard +and do not yet have the `os_purpose` field set. Above call then returns an empty +list. We then would fall back to look for images that match `os_distro` and +`os_version`, but have no `os_purpose` property. + +```python + images = [x for x in conn.image.images(os_distro=distro, os_version=version, + sort="name:desc,created_at:desc") + if "os_purpose" not in x.properties] +``` + +We have to expect several matches here and need some heuristic to find the +right image, preferrably the one matching the old naming convention. + +Full code that does this is available in [find_img.py](find_img.py). +The script assume that you have set your `OS_CLOUD` environment variable +and have configured working `clouds.yaml` and `secure.yaml`. +Feel free to copy, I deliberately put this under MIT license. + +## Identifying the image with OpenStack CLI + +Unlike with Python, we can pass the `os_purpose` field just like the other +properties. + +```bash +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 +``` + +where `OS_CLOUD` environment has been configured to access your cloud project and +`DIST`, `VERS` and `PURP` are set to the lowercased image properties you +are looking for. An additional filter `--public` parameter could be passed to only +list public images. See above python comment for the sorting rationale. + +Dealing with old SCS clouds (not yet implementing v2 of scs-0102) is harder +with shell code. The reason is that we can not pass a flag to `openstack +image list` that would tell it to restrict results to records without an +`os_purpose` property. So this requires looping over the images and filtering +out all images with `os_purpose` (but not matching our request). We would +have to expect several matches now again and sort them by a heuristic, +somewhat similar (but not identical) to the python code. + +Full code that does this is available in [find_img.sh](find_img.sh). + +## opentofu / terraform + +With opentofu (or Hashicorp's terraform if you still use it), identifying +the image in an HCL recipe looks like this: + +```hcl +# Find the image +data "openstack_images_image_v2" "my_image" { + most_recent = true + + properties = { + os_distro = "ubuntu" + os_version = "24.04" + os_purpose = "generic" + } + # sort = "name:desc,created_at:desc" + # sort_key = "name" + # sort_direction = "desc" +} + +# Use the selected image +resource "openstack_compute_instance_v2" "instance" { + image_id = data.openstack_images_image_v2.my_image.id + ... +} +``` + +This will find the most recent image wtih the `os_` variables set to `ubuntu`, `24.04`, `generic`. +Note that unlike the python and shell examples, we can not easily sort for name and creation +date at the same time; the only option to deal with several matches is to tell opentofu's +openstack provider to return the latest (the one with the newest `created_at` date). + +An example can be found in [find_img.tf](find_img.tf). Call it with `tofu apply -auto-approve` +(after you ran `tofu init` in this directory once). + +The fallback to name matching for clouds that don't have `os_purpose` yet is harder. + +We use an external program, the python script from before to select the right image and just create +a little wrapper around it: [find_img2.py](find_img2.py). The HCL then looks like this: + +```hcl +# Call python find_img.py to find best image +data "external" "my_image2" { + program = ["python3", "${path.module}/find_img2.py"] + + query = { + os_distro = "${var.os_distro2}" + os_version = "${var.os_version2}" + os_purpose = "${var.os_purpose2}" + } +} + +# Output the results for inspection +output "selected_image2" { + value = { + id = data.external.my_image2.result.image_id + name = data.external.my_image2.result.image_name + } +} +``` + +The HCL is in [find_img2.tf](find_img2.tf). +Note that I have appended a `2` to the variable names, so they don't clash in case you have the +original example in the same directory. + +## heat + +I did not find a good way to select an image based on its properties in heat. +Obviously, you can use the python (or shell) script above and pass the image name +or ID as a parameter when invoking heat. + +```yaml +heat_template_version: 2018-08-31 + +parameters: + image: + type: string + description: Image ID or name + constraints: + - custom_constraint: glance.image + +resources: + my_instance: + type: OS::Nova::Server + properties: + image: { get_param: image } + # ... other properties +``` + +and call `openstack stack create --parameter image=$IMAGE_ID $TEMPLATE $STACKNAME`. + +## ansible + +Finding the right image in ansible can be done with a task that matches the properties +in a straight forward way. + +```yaml +--- +- name: Select Image with purpose + hosts: localhost + gather_facts: false + vars: + # Primary selection criteria + os_version: '24.04' + os_distro: 'ubuntu' + os_purpose: 'generic' + cloud_name: "{{ lookup('env', 'OS_CLOUD') | default('openstack') }}" + + tasks: + - name: Get available images matching os_distro and os_version and os_purpose + openstack.cloud.image_info: + cloud: '{{ cloud_name }}' + properties: + os_distro: '{{ os_distro }}' + os_version: '{{ os_version }}' + register: _distro_images + - name: Select image with proper multi-key sort (single task) + set_fact: + selected_image: >- + {{ + _distro_images.images + | selectattr('properties.os_purpose', 'defined') + | selectattr('properties.os_purpose', 'equalto', os_purpose) + | list + | sort(attribute='created_at', reverse=true) + | sort(attribute='name', reverse=true) + | first + }} +``` + +The fallback to name matching (for older clouds not yet complying to scs-0102-v2) +can be done in ansible, but gets a bit complex. Find the ansible tasks in +[find_img.yaml](find_img.yaml). +So, while ansible YAML proves to be more expressive than HCL here, the by far +simplest code is the python implementation. + +Transparency hint: The ansible YAML has been produced with the help of Claude AI. +I tested (and fixed) it. + +## Horizon Web Interface (GUI) + +If you are using the Horizon GUI, you may be able to manually determin which +image is the one you want to use. To make sure and systematically analyze it by +looking at `os_distro`, `os_version` and `os_purpose`, you need to go to +Compute, Images, click on the Image. You can see all metadata, see screenshot. + +![Horizon Image Details](horizon.png) + +## Skyline Web Interface (GUI) + +This is similar to Horizon, you go to Compute, Images, Public Images tab, +click on image ID. See screenshot. + +![Skyline Image Details](skyline.png) diff --git a/user-docs/usage-hints/find-image/skyline.png b/user-docs/usage-hints/find-image/skyline.png new file mode 100644 index 0000000000..d8cc8d45d1 Binary files /dev/null and b/user-docs/usage-hints/find-image/skyline.png differ diff --git a/user-docs/usage-hints/index.md b/user-docs/usage-hints/index.md new file mode 100644 index 0000000000..5b7daa087b --- /dev/null +++ b/user-docs/usage-hints/index.md @@ -0,0 +1,7 @@ +--- +title: Overview +--- + +We collect and document best practices that allow developers +to benefit from the SCS standardization and create automation +that works across SCS clouds.