fix: add image conversion
diff --git a/molecule/default/group_vars/all/molecule.yml b/molecule/default/group_vars/all/molecule.yml
index 7b122e0..db8ff71 100644
--- a/molecule/default/group_vars/all/molecule.yml
+++ b/molecule/default/group_vars/all/molecule.yml
@@ -2,8 +2,7 @@
 

 glance_images:

   - name: cirros

-    source_url:  https://object-storage.public.mtl1.vexxhost.net/swift/v1/a91f106f55e64246babde7402c21b87a/cirros/

-    image_file: cirros-0.6.0-x86_64-disk.raw

+    url: http://download.cirros-cloud.net/0.6.1/cirros-0.6.1-x86_64-disk.img

     min_disk: 1

     disk_format: raw

     container_format: bare

diff --git a/roles/glance/tasks/main.yml b/roles/glance/tasks/main.yml
index 92b9f14..f3ff7d5 100644
--- a/roles/glance/tasks/main.yml
+++ b/roles/glance/tasks/main.yml
@@ -54,46 +54,17 @@
     openstack_helm_ingress_annotations: "{{ _glance_ingress_annotations | combine(glance_ingress_annotations) }}"
 
 - name: Create images
-  when: glance_images | length > 0
-  block:
-    - name: Wait until image service ready
-      kubernetes.core.k8s_info:
-        api_version: apps/v1
-        kind: Deployment
-        name: glance-api
-        namespace: openstack
-        wait_sleep: 10
-        wait_timeout: 600
-        wait: true
-        wait_condition:
-          type: Available
-          status: true
-
-    - name: Download images
-      ansible.builtin.get_url:
-        url: "{{ item.source_url | regex_replace('\\/$', '') }}/{{ item.image_file }}"
-        dest: "/tmp/{{ item.image_file }}"
-        mode: "0600"
-      loop: "{{ glance_images }}"
-
-    - name: Upload images
-      openstack.cloud.image:
-        cloud: atmosphere
-        name: "{{ item.name }}"
-        state: present
-        filename: "/tmp/{{ item.image_file }}"
-        min_disk: "{{ item.min_disk | default(omit) }}"
-        min_ram: "{{ item.min_ram | default(omit) }}"
-        container_format: "{{ item.container_format | default(omit) }}"
-        disk_format: "{{ item.disk_format | default(omit) }}"
-        properties: "{{ item.properties | default(omit) }}"
-        kernel: "{{ item.kernel | default(omit) }}"
-        ramdisk: "{{ item.ramdisk | default(omit) }}"
-        is_public: "{{ item.is_public | default(omit) }}"
-      loop: "{{ glance_images }}"
-      # NOTE(mnaser): This often fails since the SSL certificates are not
-      #               ready yet. We need to wait for them to be ready.
-      retries: 60
-      delay: 5
-      register: _result
-      until: _result is not failed
+  ansible.builtin.include_role:
+    name: glance_image
+  loop: "{{ glance_images }}"
+  vars:
+    glance_image_name: "{{ item.name }}"
+    glance_image_url: "{{ item.url }}"
+    glance_image_min_disk: "{{ item.min_disk | default(omit) }}"
+    glance_image_min_ram: "{{ item.min_ram | default(omit) }}"
+    glance_image_container_format: "{{ item.container_format | default(omit) }}"
+    glance_image_disk_format: "{{ item.disk_format | default(omit) }}"
+    glance_image_properties: "{{ item.properties | default(omit) }}"
+    glance_image_kernel: "{{ item.kernel | default(omit) }}"
+    glance_image_ramdisk: "{{ item.ramdisk | default(omit) }}"
+    glance_image_is_public: "{{ item.is_public | default(omit) }}"
diff --git a/roles/glance_image/README.md b/roles/glance_image/README.md
new file mode 100644
index 0000000..f014107
--- /dev/null
+++ b/roles/glance_image/README.md
@@ -0,0 +1 @@
+# `glance_image`
diff --git a/roles/glance_image/meta/main.yml b/roles/glance_image/meta/main.yml
new file mode 100644
index 0000000..1fbcf19
--- /dev/null
+++ b/roles/glance_image/meta/main.yml
@@ -0,0 +1,29 @@
+# Copyright (c) 2022 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+galaxy_info:
+  author: VEXXHOST, Inc.
+  description: Ansible role for managing OpenStack Glance images
+  license: Apache-2.0
+  min_ansible_version: 5.5.0
+  standalone: false
+  platforms:
+    - name: Ubuntu
+      versions:
+        - focal
+
+dependencies:
+  - role: defaults
+  - role: qemu_utils
+  - role: openstacksdk
diff --git a/roles/glance_image/tasks/main.yml b/roles/glance_image/tasks/main.yml
new file mode 100644
index 0000000..d1c0486
--- /dev/null
+++ b/roles/glance_image/tasks/main.yml
@@ -0,0 +1,91 @@
+# Copyright (c) 2023 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+- name: Check if image exists
+  run_once: true
+  openstack.cloud.image_info:
+    cloud: atmosphere
+    image: "{{ glance_image_name }}"
+  register: _image_info
+
+- name: Download image and upload into Glance
+  run_once: true
+  when: _image_info.openstack_image == None
+  block:
+    - name: Generate temporary work directory
+      ansible.builtin.tempfile:
+        state: directory
+      register: _workdir
+
+    - name: Download image
+      ansible.builtin.get_url:
+        url: "{{ glance_image_url }}"
+        dest: "{{ _workdir.path }}/{{ glance_image_url | basename }}"
+        mode: 0600
+      register: _get_url
+
+    - name: Get image format
+      changed_when: false
+      ansible.builtin.shell: |
+        set -o pipefail
+        qemu-img info {{ _get_url.dest }} | grep -i "file format" | awk '{ print $3 }'
+      args:
+        executable: /bin/bash
+      register: _image_format
+
+    - name: Convert file to target disk format
+      when: _image_format.stdout != glance_image_disk_format
+      ansible.builtin.command:
+        qemu-img convert -O {{ glance_image_disk_format }} {{ _get_url.dest }} {{ _get_url.dest }}.converted
+
+    - name: Wait until image service ready
+      kubernetes.core.k8s_info:
+        api_version: apps/v1
+        kind: Deployment
+        name: glance-api
+        namespace: openstack
+        wait_sleep: 1
+        wait_timeout: 600
+        wait: true
+        wait_condition:
+          type: Available
+          status: true
+
+    - name: Upload image into Glance
+      openstack.cloud.image:
+        cloud: atmosphere
+        name: "{{ glance_image_name }}"
+        filename: "{{ _get_url.dest }}{% if _image_format.stdout != glance_image_disk_format %}.converted{% endif %}"
+        min_disk: "{{ glance_image_min_disk | default(omit) }}"
+        min_ram: "{{ glance_image_min_ram | default(omit) }}"
+        container_format: "{{ glance_image_container_format | default(omit) }}"
+        disk_format: "{{ glance_image_disk_format | default(omit) }}"
+        properties: "{{ glance_image_properties | default(omit) }}"
+        kernel: "{{ glance_image_kernel | default(omit) }}"
+        ramdisk: "{{ glance_image_ramdisk | default(omit) }}"
+        is_public: "{{ glance_image_is_public | default(omit) }}"
+        tags: "{{ glance_image_tags | default(omit) }}"
+        wait: true
+        timeout: 600
+      # NOTE(mnaser): This often fails since the SSL certificates are not
+      #               ready yet. We need to wait for them to be ready.
+      retries: 60
+      delay: 5
+      register: _result
+      until: _result is not failed
+  always:
+    - name: Remove work directory
+      ansible.builtin.file:
+        path: "{{ _workdir.path }}"
+        state: absent
diff --git a/roles/magnum/defaults/main.yml b/roles/magnum/defaults/main.yml
index f4cc1d4..35ba3ae 100644
--- a/roles/magnum/defaults/main.yml
+++ b/roles/magnum/defaults/main.yml
@@ -24,19 +24,12 @@
 magnum_registry_ingress_annotations: {}
 
 # List of images to load into OpenStack for Magnum
+magnum_image_container_format: bare
+magnum_image_disk_format: raw
 magnum_images:
   - name: ubuntu-2004-v1.23.13
-    source_url: https://object-storage.public.mtl1.vexxhost.net/swift/v1/a91f106f55e64246babde7402c21b87a/magnum-capi/
-    image_file: ubuntu-2004-v1.23.13.qcow2
-    disk_format: qcow2
-    container_format: bare
+    url: https://object-storage.public.mtl1.vexxhost.net/swift/v1/a91f106f55e64246babde7402c21b87a/magnum-capi/ubuntu-2004-v1.23.13.qcow2
   - name: ubuntu-2004-v1.24.7
-    source_url: https://object-storage.public.mtl1.vexxhost.net/swift/v1/a91f106f55e64246babde7402c21b87a/magnum-capi/
-    image_file: ubuntu-2004-v1.24.7.qcow2
-    disk_format: qcow2
-    container_format: bare
+    url: https://object-storage.public.mtl1.vexxhost.net/swift/v1/a91f106f55e64246babde7402c21b87a/magnum-capi/ubuntu-2004-v1.24.7.qcow2
   - name: ubuntu-2004-v1.25.3
-    source_url: https://object-storage.public.mtl1.vexxhost.net/swift/v1/a91f106f55e64246babde7402c21b87a/magnum-capi/
-    image_file: ubuntu-2004-v1.25.3.qcow2
-    disk_format: qcow2
-    container_format: bare
+    url: https://object-storage.public.mtl1.vexxhost.net/swift/v1/a91f106f55e64246babde7402c21b87a/magnum-capi/ubuntu-2004-v1.25.3.qcow2
diff --git a/roles/magnum/tasks/main.yml b/roles/magnum/tasks/main.yml
index 201aca5..ea6b181 100644
--- a/roles/magnum/tasks/main.yml
+++ b/roles/magnum/tasks/main.yml
@@ -197,43 +197,12 @@
     openstack_helm_ingress_service_port: 5000
     openstack_helm_ingress_annotations: "{{ _magnum_registry_ingress_annotations | combine(magnum_registry_ingress_annotations) }}"
 
-- name: Create k8s images
-  when: magnum_images | length > 0
-  block:
-    - name: Wait until image service ready
-      kubernetes.core.k8s_info:
-        api_version: apps/v1
-        kind: Deployment
-        name: glance-api
-        namespace: openstack
-        wait_sleep: 10
-        wait_timeout: 600
-        wait: true
-        wait_condition:
-          type: Available
-          status: true
-
-    - name: Download images
-      ansible.builtin.get_url:
-        url: "{{ item.source_url | regex_replace('\\/$', '') }}/{{ item.image_file }}"
-        dest: "/tmp/{{ item.image_file }}"
-        mode: "0600"
-      loop: "{{ magnum_images }}"
-
-    - name: Upload images
-      openstack.cloud.image:
-        cloud: atmosphere
-        name: "{{ item.name }}"
-        state: present
-        filename: "/tmp/{{ item.image_file }}"
-        container_format: "{{ item.container_format | default(omit) }}"
-        disk_format: "{{ item.disk_format | default(omit) }}"
-        properties:
-          os_distro: ubuntu-focal
-      loop: "{{ magnum_images }}"
-      # NOTE(mnaser): This often fails since the SSL certificates are not
-      #               ready yet. We need to wait for them to be ready.
-      retries: 60
-      delay: 5
-      register: _result
-      until: _result is not failed
+- name: Upload images
+  ansible.builtin.include_role:
+    name: glance_image
+  loop: "{{ magnum_images }}"
+  vars:
+    glance_image_name: "{{ item.name }}"
+    glance_image_url: "{{ item.url }}"
+    glance_image_container_format: "{{ magnum_image_container_format }}"
+    glance_image_disk_format: "{{ magnum_image_disk_format }}"
diff --git a/roles/octavia/defaults/main.yml b/roles/octavia/defaults/main.yml
index 7aa9366..b3e588b 100644
--- a/roles/octavia/defaults/main.yml
+++ b/roles/octavia/defaults/main.yml
@@ -29,4 +29,9 @@
 octavia_management_subnet_cidr: "172.24.0.0/22"
 
 # Octavia amphora image url
-octavia_amphora_image_url: "https://tarballs.opendev.org/openstack/octavia/test-images/test-only-amphora-x64-haproxy-ubuntu-focal.qcow2"
+octavia_amphora_image_name: amphora-x64-haproxy
+octavia_amphora_image_url: https://tarballs.opendev.org/openstack/octavia/test-images/test-only-amphora-x64-haproxy-ubuntu-focal.qcow2
+octavia_amphora_image_container_format: bare
+octavia_amphora_image_disk_format: raw
+octavia_amphora_image_tags:
+  - amphora
diff --git a/roles/octavia/tasks/main.yml b/roles/octavia/tasks/main.yml
index 7ccbd0a..a28dede 100644
--- a/roles/octavia/tasks/main.yml
+++ b/roles/octavia/tasks/main.yml
@@ -133,21 +133,20 @@
     is_public: false
   register: _octavia_amphora_flavor
 
-- name: Download amphora image
-  ansible.builtin.get_url:
-    url: "{{ octavia_amphora_image_url }}"
-    dest: "/tmp/{{ octavia_amphora_image_url | basename }}"
-    mode: 0644
+- name: Upload Amphora image
+  ansible.builtin.include_role:
+    name: glance_image
+  vars:
+    glance_image_name: "{{ octavia_amphora_image_name }}"
+    glance_image_url: "{{ octavia_amphora_image_url }}"
+    glance_image_container_format: "{{ octavia_amphora_image_container_format }}"
+    glance_image_disk_format: "{{ octavia_amphora_image_disk_format }}"
+    glance_image_tags: "{{ octavia_amphora_image_tags }}"
 
-- name: Upload images
-  openstack.cloud.image:
+- name: Get Amphora image information
+  openstack.cloud.image_info:
     cloud: atmosphere
-    name: "amphora-x64-haproxy"
-    filename: "/tmp/{{ octavia_amphora_image_url | basename }}"
-    container_format: "bare"
-    disk_format: "qcow2"
-    tags:
-      - "amphora"
+    image: "{{ octavia_amphora_image_name }}"
   register: _octavia_amphora_image
 
 - name: Create CAs & Issuers
diff --git a/roles/octavia/vars/main.yml b/roles/octavia/vars/main.yml
index af3c061..be4639f 100644
--- a/roles/octavia/vars/main.yml
+++ b/roles/octavia/vars/main.yml
@@ -92,7 +92,7 @@
       controller_worker:
         amp_boot_network_list: "{{ _octavia_management_network.id }}"
         amp_flavor_id: "{{ _octavia_amphora_flavor.id }}"
-        amp_image_owner_id: "{{ _octavia_amphora_image.image.owner }}"
+        amp_image_owner_id: "{{ _octavia_amphora_image.openstack_image.owner }}"
         amp_secgroup_list: "{{ _octavia_amphora_sg.id }}"
         amp_ssh_key_name: null
         client_ca: /etc/octavia/certs/client/ca.crt
diff --git a/roles/qemu_utils/README.md b/roles/qemu_utils/README.md
new file mode 100644
index 0000000..f78bb7a
--- /dev/null
+++ b/roles/qemu_utils/README.md
@@ -0,0 +1 @@
+# `qemu-utils`
diff --git a/roles/qemu_utils/meta/main.yml b/roles/qemu_utils/meta/main.yml
new file mode 100644
index 0000000..3558d4e
--- /dev/null
+++ b/roles/qemu_utils/meta/main.yml
@@ -0,0 +1,24 @@
+# Copyright (c) 2022 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+galaxy_info:
+  author: VEXXHOST, Inc.
+  description: Install QEMU utilities
+  license: Apache-2.0
+  min_ansible_version: 5.5.0
+  standalone: false
+  platforms:
+    - name: Ubuntu
+      versions:
+        - focal
diff --git a/roles/qemu_utils/tasks/main.yml b/roles/qemu_utils/tasks/main.yml
new file mode 100644
index 0000000..ac3b65f
--- /dev/null
+++ b/roles/qemu_utils/tasks/main.yml
@@ -0,0 +1,18 @@
+# Copyright (c) 2023 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+- name: Install packages
+  ansible.builtin.apt:
+    name: qemu-utils
+    state: present