Skip to content

Commit 5c9ae2d

Browse files
feat(images): OpenStack image upload playbook
1 parent a15fb27 commit 5c9ae2d

5 files changed

Lines changed: 379 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ docs/workflows/
3131

3232
# mkdocs site output
3333
/site/
34+
35+
# miscellaneous
36+
.local/

ansible/README.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Running Ansible locally
2+
3+
The Ansible container is built by [`.github/workflows/containers.yaml`](../.github/workflows/containers.yaml) from [`containers/ansible/Dockerfile`](../containers/ansible/Dockerfile). That image copies this directory into `/runner/project` and the Argo workflow in [`workflows/argo-events/workflowtemplates/ansible-run.yaml`](../workflows/argo-events/workflowtemplates/ansible-run.yaml) expects inventory at `/runner/inventory/hosts.yaml`.
4+
5+
## Build the container
6+
7+
From the repository root:
8+
9+
```bash
10+
docker build -f containers/ansible/Dockerfile -t understack-ansible .
11+
```
12+
13+
## Run locally with a Python virtualenv
14+
15+
This uses the same Python packages and Ansible collections installed by the container build.
16+
17+
From the repository root:
18+
19+
```bash
20+
python3 -m venv .venv
21+
source .venv/bin/activate
22+
python -m pip install --upgrade pip
23+
pip install -r ansible/requirements.txt
24+
ansible-galaxy collection install -r ansible/requirements.yml
25+
```
26+
27+
Prepare inventory:
28+
29+
```bash
30+
mkdir -p .local/ansible/inventory
31+
cp /path/to/your/inventory.yaml .local/ansible/inventory/hosts.yaml
32+
```
33+
34+
If the playbook talks to OpenStack, put `clouds.yaml` in a standard OpenStack location:
35+
36+
```bash
37+
mkdir -p ~/.config/openstack
38+
cp /path/to/clouds.yaml ~/.config/openstack/clouds.yaml
39+
```
40+
41+
Run playbooks from the `ansible/` directory so the local roles resolve from `./roles`:
42+
43+
```bash
44+
cd ansible
45+
ansible-playbook -i ../.local/ansible/inventory/hosts.yaml debug.yaml -vvv
46+
```
47+
48+
Examples:
49+
50+
```bash
51+
cd ansible
52+
ansible-playbook -i ../.local/ansible/inventory/hosts.yaml image-upload.yaml -vvv
53+
ansible-playbook -i ../.local/ansible/inventory/hosts.yaml keystone-post-deploy.yaml -vvv
54+
```
55+
56+
Notes:
57+
58+
- activate the virtualenv with `source .venv/bin/activate` before running playbooks in a new shell
59+
- `image-upload.yaml` needs a `glance` group in inventory and valid OpenStack credentials
60+
- `nova-post-deploy.yaml` also needs flavor and device-type data; override the role paths or create local symlinks to match the defaults under `/runner/data`
61+
- Nautobot playbooks may also need `NAUTOBOT_TOKEN` exported in your shell
62+
63+
## Prepare local input files
64+
65+
Create a local inventory file at the same path the workflow uses in-cluster:
66+
67+
```bash
68+
mkdir -p .local/ansible/inventory
69+
cp /path/to/your/inventory.yaml .local/ansible/inventory/hosts.yaml
70+
```
71+
72+
If the playbook talks to OpenStack, mount a `clouds.yaml` file into a standard OpenStack location inside the container:
73+
74+
```bash
75+
mkdir -p .local/openstack
76+
cp ~/.config/openstack/clouds.yaml .local/openstack/clouds.yaml
77+
```
78+
79+
If the playbook needs extra mounted data, keep the same paths it expects in-cluster:
80+
81+
- `nova-post-deploy.yaml`: mount flavors at `/runner/data/flavors/` and device types at `/runner/data/device-types/`
82+
- playbooks using Nautobot: set `NAUTOBOT_TOKEN`
83+
- commands matching the Argo workflow: set `UNDERSTACK_ENV`
84+
85+
## Run a playbook with ansible-runner
86+
87+
This matches the Argo workflow shape closely. The repo copy is mounted over `/runner/project` so local edits are used without rebuilding the image.
88+
89+
```bash
90+
docker run --rm -it \
91+
-v "$PWD/ansible:/runner/project" \
92+
-v "$PWD/.local/ansible/inventory:/runner/inventory" \
93+
-v "$PWD/.local/openstack:/etc/openstack:ro" \
94+
-e UNDERSTACK_ENV=dev \
95+
-e NAUTOBOT_TOKEN="$NAUTOBOT_TOKEN" \
96+
understack-ansible \
97+
ansible-runner run /tmp/runner \
98+
--project-dir /runner/project \
99+
--playbook debug.yaml \
100+
--cmdline "-i /runner/inventory/hosts.yaml --extra-vars 'env=dev' -vvv"
101+
```
102+
103+
Notes:
104+
105+
- `ansible-runner` must be passed explicitly because the image entrypoint is `dumb-init`
106+
- replace `debug.yaml` with the playbook you want to run
107+
- if a playbook does not use Nautobot, omit `NAUTOBOT_TOKEN`
108+
- if a playbook does not use OpenStack, omit the `/etc/openstack` mount
109+
110+
## Example: run the image upload playbook
111+
112+
[`image-upload.yaml`](./image-upload.yaml) targets the `glance` group and uses OpenStack auth, so the inventory needs a `glance` host or group and the container needs `clouds.yaml`:
113+
114+
```bash
115+
docker run --rm -it \
116+
-v "$PWD/ansible:/runner/project" \
117+
-v "$PWD/.local/ansible/inventory:/runner/inventory" \
118+
-v "$PWD/.local/openstack:/etc/openstack:ro" \
119+
understack-ansible \
120+
ansible-runner run /tmp/runner \
121+
--project-dir /runner/project \
122+
--playbook image-upload.yaml \
123+
--cmdline "-i /runner/inventory/hosts.yaml -vvv"
124+
```
125+
126+
## Example: run the Nova flavor playbook
127+
128+
[`nova-post-deploy.yaml`](./nova-post-deploy.yaml) also expects ConfigMap-style data directories. Mount local directories to the same paths:
129+
130+
```bash
131+
docker run --rm -it \
132+
-v "$PWD/ansible:/runner/project" \
133+
-v "$PWD/.local/ansible/inventory:/runner/inventory" \
134+
-v "$PWD/.local/openstack:/etc/openstack:ro" \
135+
-v "$PWD/hardware/flavors:/runner/data/flavors:ro" \
136+
-v "$PWD/hardware/device-types:/runner/data/device-types:ro" \
137+
understack-ansible \
138+
ansible-runner run /tmp/runner \
139+
--project-dir /runner/project \
140+
--playbook nova-post-deploy.yaml \
141+
--cmdline "-i /runner/inventory/hosts.yaml -vvv"
142+
```
143+
144+
## Run with ansible-playbook instead
145+
146+
If you want a simpler interactive container shell:
147+
148+
```bash
149+
docker run --rm -it \
150+
-v "$PWD/ansible:/runner/project" \
151+
-v "$PWD/.local/ansible/inventory:/runner/inventory" \
152+
-v "$PWD/.local/openstack:/etc/openstack:ro" \
153+
--workdir /runner/project \
154+
--entrypoint /bin/bash \
155+
understack-ansible
156+
```
157+
158+
Then run:
159+
160+
```bash
161+
ansible-playbook -i /runner/inventory/hosts.yaml debug.yaml -vvv
162+
```

ansible/image-upload.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
# Copyright (c) 2025 Rackspace Technology, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
16+
- name: Understack Image Uploader
17+
hosts: glance
18+
connection: local
19+
20+
pre_tasks:
21+
- name: Check OpenStack connectivity
22+
ansible.builtin.import_tasks: tasks/check_openstack_auth.yml
23+
24+
roles:
25+
- role: openstack_glance_image_upload
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
3+
# Temporary directory for downloading images
4+
image_download_dir: "/tmp/glance_images"
5+
6+
# List of images to upload to Glance
7+
understack_images:
8+
- uuid: 00000000-0000-0000-1111-000000000000
9+
name: esp.img
10+
description: EFI system partition image from understack-images-20251016184704
11+
external_url: https://github.com/rackerlabs/understack/releases/download/understack-images-20251016184704/esp.img
12+
is_public: false
13+
is_protected: false
14+
disk_format: raw
15+
container_format: bare
16+
min_disk: 0
17+
min_ram: 0
18+
properties: {}
19+
- uuid: 00000000-0000-0000-2222-000000000000
20+
name: ipa-debian-bookworm.kernel
21+
description: IPA Debian Bookworm kernel image from understack-images-20251016184704
22+
external_url: https://github.com/rackerlabs/understack/releases/download/understack-images-20251016184704/ipa-debian-bookworm.kernel
23+
is_public: false
24+
is_protected: false
25+
disk_format: raw
26+
container_format: bare
27+
min_disk: 0
28+
min_ram: 0
29+
properties: {}
30+
- uuid: 00000000-0000-0000-3333-000000000000
31+
name: ipa-debian-bookworm.initramfs
32+
description: IPA Debian Bookworm initramfs image from understack-images-20251016184704
33+
external_url: https://github.com/rackerlabs/understack/releases/download/understack-images-20251016184704/ipa-debian-bookworm.initramfs
34+
is_public: false
35+
is_protected: false
36+
disk_format: raw
37+
container_format: bare
38+
min_disk: 0
39+
min_ram: 0
40+
properties: {}
41+
- uuid: 00000000-0000-0000-4444-000000000000
42+
name: Ubuntu 24.04
43+
description: Ubuntu Noble qcow2 image from understack-images-20251016184704
44+
external_url: https://github.com/rackerlabs/understack/releases/download/understack-images-20251016184704/ubuntu-noble.qcow2
45+
is_public: true
46+
is_protected: false
47+
disk_format: qcow2
48+
container_format: bare
49+
min_disk: 0
50+
min_ram: 0
51+
properties:
52+
os_distro: ubuntu
53+
os_version: "24.04"
54+
img_config_drive: mandatory
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
---
2+
3+
- name: Ensure download directory exists
4+
ansible.builtin.file:
5+
path: "{{ image_download_dir }}"
6+
state: directory
7+
mode: '0755'
8+
9+
- name: Look up existing Glance images
10+
openstack.cloud.image_info:
11+
cloud: "{{ openstack_cloud | default(omit) }}"
12+
image: "{{ item.uuid }}"
13+
register: existing_image_info
14+
loop: "{{ understack_images }}"
15+
loop_control:
16+
label: "{{ item.name }}"
17+
when: understack_images | length > 0
18+
19+
- name: Reserve missing Glance image IDs before upload
20+
openstack.cloud.image:
21+
cloud: "{{ openstack_cloud | default(omit) }}"
22+
name: "{{ item.item.name }}"
23+
id: "{{ item.item.uuid }}"
24+
disk_format: "{{ item.item.disk_format }}"
25+
container_format: "{{ item.item.container_format | default('bare') }}"
26+
is_public: "{{ item.item.is_public | default(true) }}"
27+
is_protected: "{{ item.item.is_protected | default(false) }}"
28+
min_disk: "{{ item.item.min_disk | default(0) }}"
29+
min_ram: "{{ item.item.min_ram | default(0) }}"
30+
properties: "{{ item.item.properties | default({}) }}"
31+
tags: "{{ item.item.tags | default([]) }}"
32+
state: present
33+
wait: false
34+
loop: "{{ existing_image_info.results }}"
35+
loop_control:
36+
label: "{{ item.item.name }}"
37+
when:
38+
- understack_images | length > 0
39+
- item.images | length == 0
40+
41+
- name: Download images that still need data upload
42+
ansible.builtin.get_url:
43+
url: "{{ item.item.external_url }}"
44+
dest: "{{ image_download_dir }}/{{ item.item.uuid }}.{{ item.item.disk_format }}"
45+
checksum: "{{ item.item.checksum | default(omit) }}"
46+
mode: '0644'
47+
loop: "{{ existing_image_info.results }}"
48+
loop_control:
49+
label: "{{ item.item.name }}"
50+
when:
51+
- understack_images | length > 0
52+
- item.images | length == 0 or item.images[0].status == 'queued'
53+
54+
- name: Look up target Glance images after reservation
55+
openstack.cloud.image_info:
56+
cloud: "{{ openstack_cloud | default(omit) }}"
57+
image: "{{ item.uuid }}"
58+
register: target_image_info
59+
loop: "{{ understack_images }}"
60+
loop_control:
61+
label: "{{ item.name }}"
62+
when: understack_images | length > 0
63+
64+
- name: Upload image data to queued Glance images
65+
openstack.cloud.image:
66+
cloud: "{{ openstack_cloud | default(omit) }}"
67+
name: "{{ item.images[0].name }}"
68+
id: "{{ item.item.uuid }}"
69+
filename: "{{ image_download_dir }}/{{ item.item.uuid }}.{{ item.item.disk_format }}"
70+
disk_format: "{{ item.images[0].disk_format }}"
71+
container_format: "{{ item.images[0].container_format }}"
72+
state: present
73+
wait: true
74+
loop: "{{ target_image_info.results }}"
75+
loop_control:
76+
label: "{{ item.item.name }}"
77+
when:
78+
- understack_images | length > 0
79+
- item.images | length > 0
80+
- item.images[0].status == 'queued'
81+
82+
- name: Tell Glance to import the image data (using use_import and glance-direct)
83+
openstack.cloud.image:
84+
cloud: "{{ openstack_cloud | default(omit) }}"
85+
id: "{{ item.item.uuid }}"
86+
name: "{{ item.images[0].name }}"
87+
use_import: true
88+
state: present
89+
wait: true
90+
loop: "{{ target_image_info.results }}"
91+
loop_control:
92+
label: "{{ item.item.name }}"
93+
when:
94+
- understack_images | length > 0
95+
- item.images | length > 0
96+
- item.images[0].status == 'uploading'
97+
98+
- name: Wait for Glance images to become active
99+
openstack.cloud.image_info:
100+
cloud: "{{ openstack_cloud | default(omit) }}"
101+
image: "{{ item.uuid }}"
102+
register: final_image_info
103+
loop: "{{ understack_images }}"
104+
loop_control:
105+
label: "{{ item.name }}"
106+
until:
107+
- final_image_info.images | length > 0
108+
- final_image_info.images[0].status == 'active'
109+
retries: 60
110+
delay: 10
111+
when: understack_images | length > 0
112+
113+
- name: Verify all Glance images are active
114+
ansible.builtin.assert:
115+
that:
116+
- item.images | length > 0
117+
- item.images[0].status == 'active'
118+
fail_msg: >-
119+
Glance image {{ item.item.name }} ({{ item.item.uuid }}) did not become active.
120+
Current status: {{ item.images[0].status if (item.images | length > 0) else 'missing' }}
121+
success_msg: >-
122+
Glance image {{ item.item.name }} ({{ item.item.uuid }}) is active.
123+
loop: "{{ final_image_info.results }}"
124+
loop_control:
125+
label: "{{ item.item.name }}"
126+
when: understack_images | length > 0
127+
128+
# - name: Clean up downloaded images
129+
# ansible.builtin.file:
130+
# path: "{{ image_download_dir }}/{{ item.uuid }}.{{ item.disk_format }}"
131+
# state: absent
132+
# loop: "{{ understack_images }}"
133+
# loop_control:
134+
# label: "{{ item.name }}"
135+
# when: understack_images | length > 0

0 commit comments

Comments
 (0)