From 12f01fae7c6e2a230d78e80cb16d5b4285da4f1b Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Wed, 3 Jun 2026 14:30:45 +0100 Subject: [PATCH 01/11] Prototype libvirt-backed ARD deployments This change introduces the first provider workflow for creating ARD DevStack test deployments without Molecule or Vagrant owning the VM lifecycle. The new render, apply, deploy, destroy, and cleanup playbooks dispatch to provider roles and add an initial libvirt implementation based on rendered network and domain XML. The libvirt provider uses Debian 13 genericcloud images, XDG cache and state paths, cloud-init config-drive data, deterministic VM names and addresses, and per-deployment inventory and state files. It creates a two-node devstack-1 deployment with a controller and compute node using the stack user, SSH key injection, password fallback, passwordless sudo, serial console configuration, and persistent console logs. The provider design is updated to describe the deployment workspace, portable image, flavor, and preference model, KubeVirt instancetype defaults, XDG storage layout, and refreshed upstream DevStack and Zuul submodules. Signed-off-by: Sean Mooney --- .gitignore | 7 +- ARD_PROVIDER_DESIGN.md | 1632 +++++++++++++++++ ansible.cfg | 5 + .../devstack-instancetype-preference.yaml | 43 + ansible/playbooks/ard-apply.yaml | 11 + ansible/playbooks/ard-cleanup.yaml | 7 + ansible/playbooks/ard-deploy-devstack.yaml | 16 + ansible/playbooks/ard-destroy.yaml | 7 + ansible/playbooks/ard-render.yaml | 7 + .../roles/ard_devstack_config/tasks/main.yml | 16 + .../roles/ard_libvirt_destroy/tasks/main.yml | 64 + .../roles/ard_libvirt_image/tasks/main.yml | 68 + .../ard_libvirt_inventory/tasks/main.yml | 65 + .../roles/ard_libvirt_network/tasks/main.yml | 43 + ansible/roles/ard_libvirt_node/tasks/main.yml | 12 + ansible/roles/ard_libvirt_node/tasks/node.yml | 207 +++ .../ard_libvirt_node/templates/domain.xml.j2 | 54 + .../ard_libvirt_preflight/tasks/main.yml | 41 + .../roles/ard_provider_cleanup/tasks/main.yml | 12 + .../roles/ard_provider_destroy/tasks/main.yml | 13 + .../roles/ard_provider_image/tasks/main.yml | 12 + .../ard_provider_inventory/tasks/main.yml | 12 + .../roles/ard_provider_network/tasks/main.yml | 12 + .../roles/ard_provider_node/tasks/main.yml | 12 + .../ard_provider_preflight/tasks/main.yml | 8 + .../roles/ard_provider_render/tasks/main.yml | 185 ++ deployments/.keep | 0 pyproject.toml | 11 + submodules/devstack | 2 +- submodules/openstack-zuul-jobs | 2 +- submodules/zuul-jobs | 2 +- uv.lock | 255 +++ 32 files changed, 2838 insertions(+), 5 deletions(-) create mode 100644 ARD_PROVIDER_DESIGN.md create mode 100644 ansible.cfg create mode 100644 ansible/files/kubevirt/devstack-instancetype-preference.yaml create mode 100644 ansible/playbooks/ard-apply.yaml create mode 100644 ansible/playbooks/ard-cleanup.yaml create mode 100644 ansible/playbooks/ard-deploy-devstack.yaml create mode 100644 ansible/playbooks/ard-destroy.yaml create mode 100644 ansible/playbooks/ard-render.yaml create mode 100644 ansible/roles/ard_devstack_config/tasks/main.yml create mode 100644 ansible/roles/ard_libvirt_destroy/tasks/main.yml create mode 100644 ansible/roles/ard_libvirt_image/tasks/main.yml create mode 100644 ansible/roles/ard_libvirt_inventory/tasks/main.yml create mode 100644 ansible/roles/ard_libvirt_network/tasks/main.yml create mode 100644 ansible/roles/ard_libvirt_node/tasks/main.yml create mode 100644 ansible/roles/ard_libvirt_node/tasks/node.yml create mode 100644 ansible/roles/ard_libvirt_node/templates/domain.xml.j2 create mode 100644 ansible/roles/ard_libvirt_preflight/tasks/main.yml create mode 100644 ansible/roles/ard_provider_cleanup/tasks/main.yml create mode 100644 ansible/roles/ard_provider_destroy/tasks/main.yml create mode 100644 ansible/roles/ard_provider_image/tasks/main.yml create mode 100644 ansible/roles/ard_provider_inventory/tasks/main.yml create mode 100644 ansible/roles/ard_provider_network/tasks/main.yml create mode 100644 ansible/roles/ard_provider_node/tasks/main.yml create mode 100644 ansible/roles/ard_provider_preflight/tasks/main.yml create mode 100644 ansible/roles/ard_provider_render/tasks/main.yml create mode 100644 deployments/.keep create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 5fc83c8..afb409a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -.venv +.venv/ +.tmp/ clouds.yaml okd/build/ okd/output/ -pull-secret \ No newline at end of file +pull-secret +deployments/* +!deployments/.keep diff --git a/ARD_PROVIDER_DESIGN.md b/ARD_PROVIDER_DESIGN.md new file mode 100644 index 0000000..09b50fb --- /dev/null +++ b/ARD_PROVIDER_DESIGN.md @@ -0,0 +1,1632 @@ +# ARD Provider Design Plan + +## 1. Purpose + +This document defines a short-term provider framework for ARD focused on VM-backed DevStack environments. It supersedes the short-term direction of `ARD_OCI_DESIGN.md`, which should be treated as deferred container-provider work. + +The immediate goal is to replace the current Molecule/Vagrant/libvirt provisioning dependency with provider roles that can create DevStack-capable nodes using either: + +1. local libvirt; or +2. KubeVirt / OpenShift Virtualization on a remote OpenShift cluster. + +Docker, Podman, OCI system containers, and systemd-nspawn remain possible future providers, but they are not the initial focus. + +## 2. Goals + +1. Reproduce the current ARD Vagrant/libvirt workflow without Vagrant. +2. Add OpenShift Virtualization as an additional VM hosting option. +3. Preserve existing ARD DevStack deployment roles and playbooks. +4. Keep Make and Molecule using the same Ansible provider roles. +5. Keep provider-specific logic isolated to provider roles. +6. Make provider-created VMs look like the current ARD/Zuul multinode inventory contract. +7. Support both all-in-one and multinode DevStack topologies. +8. Use KubeVirt pod-network/masquerade mode first, without requiring Multus. +9. For KubeVirt multinode, rely on DevStack/Linux bridge/OVS/OVN overlay networking with VXLAN tunnels when needed. + +## 3. Non-goals + +- Do not implement Docker/Podman/nspawn providers in the first phase. +- Do not require Vagrant for the new provider flows. +- Do not require Multus or secondary L2 networks for initial KubeVirt support. +- Do not replace DevStack with Kolla, Kolla-Ansible, or OpenStack-Helm. +- Do not split OpenStack services into individual containers. +- Do not modify DevStack roles to know whether nodes came from libvirt or KubeVirt. + +## 4. Existing ARD Contract to Preserve + +Current ARD Molecule/Vagrant scenarios create machines with stable names and groups, then ARD deploys DevStack through existing roles. + +The important inventory shape is: + +```yaml +controller: + groups: + - controller + - switch + +compute1: + groups: + - compute + - peers + - subnode + +compute2: + groups: + - compute + - peers + - subnode +``` + +Existing roles and playbooks to preserve: + +- `ansible/deploy_multinode_devstack.yaml` +- `ansible/devstack_common.yaml` +- `ansible/roles/devstack_common/` +- `ansible/roles/devstack_controller/` +- `ansible/roles/devstack_compute/` +- upstream/openstack roles such as `write-devstack-local-conf` and `run-devstack` + +The provider framework should replace only the provisioning layer. + +## 5. High-level Architecture + +```text +make / molecule / zuul + | + v +ARD provider playbooks + | + v +provider dispatcher roles + | + v +libvirt provider OR kubevirt provider + | + v +VMs with SSH access + | + v +dynamic Ansible inventory + | + v +existing ARD DevStack deployment roles + | + v +DevStack inside VMs +``` + +The provider's job ends when: + +1. VMs exist. +2. SSH works. +3. Ansible inventory has the expected names, groups, and facts. + +After that, the existing ARD DevStack roles take over. + +## 6. Proposed Repository Layout + +```text +ARD_PROVIDER_DESIGN.md +ARD_OCI_DESIGN.md # deferred/future container-provider design +Makefile + +ansible/ + playbooks/ + ard-render.yaml + ard-apply.yaml + ard-create.yaml # compatibility wrapper for ard-apply.yaml + ard-deploy-devstack.yaml + ard-verify.yaml + ard-destroy.yaml + ard-cleanup.yaml + ard-collect-logs.yaml + ard-site.yaml + ard-kubevirt-ensure-resources.yaml + + files/ + kubevirt/ + devstack-instancetype-preference.yaml + + roles/ + ard_provider_render/ + ard_provider_preflight/ + ard_provider_image/ + ard_provider_network/ + ard_provider_node/ + ard_provider_inventory/ + ard_provider_state/ + ard_provider_destroy/ + ard_provider_cleanup/ + ard_provider_collect_logs/ + + ard_libvirt_preflight/ + ard_libvirt_image/ + ard_libvirt_network/ + ard_libvirt_node/ + ard_libvirt_inventory/ + ard_libvirt_destroy/ + ard_libvirt_collect_logs/ + + ard_kubevirt_preflight/ + ard_kubevirt_image/ + ard_kubevirt_network/ + ard_kubevirt_node/ + ard_kubevirt_inventory/ + ard_kubevirt_destroy/ + ard_kubevirt_collect_logs/ + +deployments/ + / + deployment.yaml # provider, topology, image/flavor defaults + nodes.yaml # rendered node list; user-tweakable + devstack/ + common.yaml # upstream/Zuul-style Ansible vars shared by all nodes + group_vars/ + controller.yaml # controller group vars using existing controller_* names + compute.yaml # compute group vars using existing compute_* names + host_vars/ + controller.yaml # optional per-node Ansible vars + compute1.yaml + inventory.yaml # generated provider inventory + provider-state.yaml # provider resource names/ids for destroy + rendered/ + kubevirt/ + libvirt/ + logs/ + +molecule/ + libvirt-multinode/ + molecule.yml + create.yml + converge.yml + verify.yml + destroy.yml + + kubevirt-multinode/ + molecule.yml + create.yml + converge.yml + verify.yml + destroy.yml +``` + +The generic `ard_provider_*` roles dispatch to provider-specific roles according to `ard_provider`. + +Example dispatcher pattern: + +```yaml +- name: Run provider-specific node creation + include_role: + name: "ard_{{ ard_provider }}_node" +``` + +## 7. Provider-neutral Variables + +### 7.1 Provider selection + +```yaml +ard_provider: libvirt +``` + +Supported initial values: + +```yaml +ard_provider: libvirt +ard_provider: kubevirt +``` + +Deferred/future values: + +```yaml +ard_provider: podman +ard_provider: docker +ard_provider: nspawn +``` + +### 7.2 Portable image, flavor, and preference model + +ARD nodes should describe the desired guest in provider-neutral terms, then let each provider translate those terms into its native implementation. + +The portable model has three layers: + +1. `ard_images`: boot image definitions, always assumed to be cloud images that support cloud-init. +2. `ard_flavors`: sizing and CPU capability definitions. +3. `ard_vm_preferences`: machine/device preferences that are meaningful across providers where possible. + +Default image and local cache: + +```yaml +ard_default_image: debian-13 +ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default('~/.cache', true) }}/ard/images" +ard_image_download: true +ard_image_checksum_required: false + +ard_images: + debian-13: + os_family: debian + version: "13" + cloud_init: true + provider: + kubevirt: + source_kind: DataVolume + name: debian-13-2026-05-20 + namespace: "{{ ard_kubevirt_image_namespace | default(ard_kubevirt_namespace) }}" + libvirt: + url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 + alternate_urls: + - https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2 + name: debian-13 + format: qcow2 + cloud_init_datasource: NoCloud + + ubuntu-24.04: + os_family: ubuntu + version: "24.04" + cloud_init: true + provider: + libvirt: + url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img + name: ubuntu-24.04 + format: qcow2 + cloud_init_datasource: NoCloud + + ubuntu-26.04: + os_family: ubuntu + version: "26.04" + cloud_init: true + provider: + libvirt: + # Keep configurable until the Ubuntu 26.04 codename and final cloud + # image URL are available. + url: "{{ ard_libvirt_ubuntu_26_04_cloud_image_url }}" + name: ubuntu-26.04 + format: qcow2 + cloud_init_datasource: NoCloud +``` + +Both initial VM providers rely on cloud images plus cloud-init for hostname, the `stack` user, sudo, Python bootstrap, and SSH key injection. Provider image roles should download configured libvirt cloud images into `ard_image_cache_dir` when missing, optionally validate checksums when configured, and never mutate cached base images. The default cache path follows XDG cache conventions via `$XDG_CACHE_HOME`, falling back to `~/.cache`. Per-deployment disks are overlays or copies derived from the cached base image. + +Default flavors: + +```yaml +ard_default_controller_flavor: devstack-control +ard_default_compute_flavor: devstack-compute + +ard_flavors: + devstack-control: + description: All-in-one or controller node flavor. + vcpus: 8 + memory: 16Gi + nested_virt: true + provider: + kubevirt: + instancetype: devstack-8c16g + libvirt: + vcpus: 8 + memory_mb: 16384 + cpu_mode: host-passthrough + + devstack-compute: + description: Compute node flavor for multinode topologies. + vcpus: 8 + memory: 8Gi + nested_virt: true + provider: + kubevirt: + instancetype: devstack-8c8g + libvirt: + vcpus: 8 + memory_mb: 8192 + cpu_mode: host-passthrough +``` + +Default VM preference: + +```yaml +ard_default_vm_preference: devstack + +ard_vm_preferences: + devstack: + provider: + kubevirt: + preference: devstack + libvirt: + machine_type: q35 + disk_bus: virtio + interface_model: virtio + rng: true + efi_secure_boot: false +``` + +For KubeVirt, `devstack-control` maps to `VirtualMachineInstancetype/devstack-8c16g`, `devstack-compute` maps to `VirtualMachineInstancetype/devstack-8c8g`, and all DevStack VMs use `VirtualMachinePreference/devstack` by default. + +For libvirt, the same flavor and preference names are translated into domain CPU, memory, machine, disk, interface, and RNG settings. + +### 7.3 Node topology + +```yaml +ard_nodes: + - name: controller + hostname: controller + groups: + - controller + - switch + image: debian-13 + flavor: devstack-control + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.96.2 + profiles: + - ssh + - nested_virt + - ovn + + - name: compute1 + hostname: compute1 + groups: + - compute + - peers + - subnode + image: debian-13 + flavor: devstack-compute + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.96.3 + profiles: + - ssh + - nested_virt + - ovn + + - name: compute2 + hostname: compute2 + groups: + - compute + - peers + - subnode + image: debian-13 + flavor: devstack-compute + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.96.4 + profiles: + - ssh + - nested_virt + - ovn +``` + +Provider roles translate this specification into libvirt domains or KubeVirt VirtualMachines. + +### 7.4 Deployment workspace model + +ARD should support a Molecule-like workflow where the basic shape of a deployment is rendered once to disk, edited by the user, then applied to either provider. The on-disk unit of state is a deployment subdirectory: + +```text +deployments// + deployment.yaml + nodes.yaml + devstack/ + common.yaml + group_vars/ + controller.yaml + compute.yaml + host_vars/ + controller.yaml + compute1.yaml + inventory.yaml + provider-state.yaml + rendered/ + kubevirt/ + libvirt/ + logs/ +``` + +The deployment name is derived from the subfolder name by default. For example, `deployments/devstack-a/` implies: + +```yaml +ard_deployments_dir: deployments +ard_deployment_dir: deployments/devstack-a +ard_deployment_name: devstack-a +ard_resource_name_prefix: ard-devstack-a +``` + +If `ard_deployment_name` is passed explicitly, it must match the basename of `ard_deployment_dir` unless the user also sets an explicit override for advanced workflows. + +The workflow is: + +1. **Render**: create `deployments//` from provider-neutral defaults and templates. This does not contact libvirt or OpenShift. +2. **Modify**: edit `deployment.yaml`, `nodes.yaml`, and the layered files under `devstack/` on disk. This is where scenario-specific DevStack `local.conf` inputs are changed per deployment, per group, or per node. +3. **Apply**: read the deployment folder, create or update provider resources, wait for SSH, generate `inventory.yaml`, and write `provider-state.yaml`. +4. **Destroy**: use `provider-state.yaml`, the deployment name, and provider labels/name prefixes to destroy provider resources. Keep the deployment folder and logs. +5. **Cleanup**: remove the local deployment folder after destroy when the user no longer needs the rendered scenario inputs or state. + +Example deployment inputs: + +```yaml +# deployments/devstack-a/deployment.yaml +ard_provider: kubevirt +ard_default_image: debian-13 +ard_default_controller_flavor: devstack-control +ard_default_compute_flavor: devstack-compute +ard_default_vm_preference: devstack +ard_kubevirt_namespace: ard-devstack +ard_kubevirt_storage_class: null +``` + +```yaml +# deployments/devstack-a/nodes.yaml +ard_nodes: + - name: controller + groups: [controller, switch] + image: debian-13 + flavor: devstack-control + preference: devstack + - name: compute1 + groups: [compute, peers, subnode] + image: debian-13 + flavor: devstack-compute + preference: devstack +``` + +```yaml +# deployments/devstack-a/devstack/common.yaml +# Keep this close to Zuul job vars and the existing ARD/devstack role inputs. +devstack_branch: master +run_devstack: true +enable_ceph: false +configure_vdpa: false +``` + +```yaml +# deployments/devstack-a/devstack/group_vars/controller.yaml +controller_localrc_extra: + ENABLE_TENANT_TUNNELS: true + ENABLE_TENANT_VLANS: false +controller_local_conf_extra: {} +controller_services_extra: {} +``` + +```yaml +# deployments/devstack-a/devstack/group_vars/compute.yaml +compute_localrc_extra: + ENABLE_TENANT_TUNNELS: true + ENABLE_TENANT_VLANS: false +compute_local_conf_extra: {} +compute_services_extra: {} +``` + +```yaml +# deployments/devstack-a/devstack/host_vars/compute1.yaml +compute_localrc_extra: + LIBVIRT_TYPE: qemu +``` + +The deployment name must be affixed to every created provider resource so multiple copies of the same rendered scenario can coexist. Provider resource names should use `ard--`, for example `ard-devstack-1-controller`, while Ansible inventory hostnames remain the logical ARD names. + +Required resource identity contract: + +```yaml +metadata_or_tags: + app.kubernetes.io/part-of: ard + app.kubernetes.io/instance: devstack-a + ard.openstack.org/deployment: devstack-a + ard.openstack.org/provider: kubevirt + ard.openstack.org/node: controller +``` + +For KubeVirt, apply these labels to deployment-scoped `VirtualMachine`, per-node `DataVolume`/PVC, cloud-init `Secret` if used, SSH `Service`, and any generated support resources. Shared setup resources such as `VirtualMachineInstancetype/devstack-8c16g`, `VirtualMachineInstancetype/devstack-8c8g`, and `VirtualMachinePreference/devstack` are not deployment-scoped unless explicitly rendered into the deployment namespace as part of setup. + +For libvirt, include the deployment name in domains, volumes, cloud-init seed ISOs, and generated network names where applicable. Example storage layout: + +```text +$XDG_STATE_HOME/ard/libvirt/images// # or ~/.local/state/ard/libvirt/images when XDG_STATE_HOME is unset + controller.qcow2 + controller-seed.iso + compute1.qcow2 + compute1-seed.iso +``` + +`provider-state.yaml` should record native resource names and identifiers, but destroy must also support a label/name-prefix fallback so cleanup is possible if state is partially missing. + +### 7.5 DevStack local.conf input layering + +A deployment must not assume one `local.conf` for all nodes. It also should not invent a parallel local.conf input schema as the primary interface. Because ARD reuses the upstream DevStack Ansible roles used by Zuul CI jobs, the deployment workspace should preserve the same Ansible variable contract wherever possible. + +The alignment target is: relevant Zuul job vars should be copyable or lightly adapted into the deployment workspace, while provider provisioning inputs remain separate in `deployment.yaml` and `nodes.yaml`. + +The existing integration point is already present: + +- `ansible/roles/devstack_controller/tasks/main.yml` calls `write-devstack-local-conf` using `controller_localrc`, `controller_localrc_extra`, `controller_local_conf`, `controller_local_conf_extra`, `controller_services`, and `controller_services_extra`. +- `ansible/roles/devstack_compute/tasks/main.yml` calls `write-devstack-local-conf` using `compute_localrc`, `compute_localrc_extra`, `compute_local_conf`, `compute_local_conf_extra`, `compute_services`, and `compute_services_extra`. + +The provider workflow should add a provider-neutral `ard_devstack_config` loader before `devstack_controller.yaml` and `devstack_compute.yaml`. That loader reads normal Ansible-shaped var files from the deployment workspace and exposes them using the same variable names consumed by the existing roles. It does not render `local.conf` itself and does not replace `write-devstack-local-conf`. + +Recommended workspace shape: + +```text +deployments//devstack/ + common.yaml + group_vars/ + controller.yaml + compute.yaml + host_vars/ + controller.yaml + compute1.yaml +``` + +Merge order for each host: + +```text +existing role defaults + -> deployments//devstack/common.yaml + -> deployments//devstack/group_vars/.yaml + -> deployments//devstack/host_vars/.yaml + -> inventory vars / CLI extra-vars / Zuul job vars +``` + +The merged values should use the existing ARD/upstream-compatible variables directly: + +```yaml +# controller hosts +controller_localrc_extra +controller_local_conf_extra +controller_services_extra +controller_devstack_plugins + +# compute hosts +compute_localrc_extra +compute_local_conf_extra +compute_services_extra +compute_devstack_plugins +``` + +Later layers override or deep-merge according to the same `combine` strategy already used by `devstack_controller` and `devstack_compute`. This keeps DevStack rendering provider-neutral and node-aware while preserving the current upstream `write-devstack-local-conf` integration and making local CI reproduction the default design target. + +## 8. Inventory Contract + +Every provider must add nodes to active Ansible inventory with the same effective facts. + +For `controller`: + +```yaml +inventory_hostname: controller +ansible_host: 192.168.96.2 +ansible_user: stack +ansible_private_key_file: ~/.ssh/id_ed25519_stack +ard_deployment_name: devstack-a +ard_provider_resource_name: ard-devstack-a-controller +nodepool: + private_ipv4: 192.168.96.2 + public_ipv4: 192.168.96.2 +zuul: + executor: + log_root: /tmp/zuul_logs + work_root: /tmp/work_root +``` + +For `compute1`: + +```yaml +inventory_hostname: compute1 +ansible_host: 192.168.96.3 +ansible_user: stack +ansible_private_key_file: ~/.ssh/id_ed25519_stack +nodepool: + private_ipv4: 192.168.96.3 + public_ipv4: 192.168.96.3 +``` + +Groups come from `ard_nodes[*].groups`. + +For deployment workspaces, `inventory.yaml` is generated inside `deployments//` and can be re-read by later apply, deploy, verify, collect-log, and destroy phases. Inventory hostnames remain stable logical names such as `controller` and `compute1`; provider resource names are tracked separately through `ard_provider_resource_name` and `provider-state.yaml`. + +This lets existing ARD defaults continue to evaluate expressions such as: + +```yaml +SERVICE_HOST: "{{ hostvars[groups['controller'][0]]['nodepool']['private_ipv4'] }}" +HOST_IP: "{{ hostvars[inventory_hostname]['nodepool']['private_ipv4'] }}" +``` + +## 9. Common Provider Playbooks + +### 9.1 `ard-render.yaml` + +Creates or refreshes a deployment workspace from defaults. It writes `deployment.yaml`, `nodes.yaml`, and the layered files under `devstack/` without creating provider resources. + +```yaml +- name: Render ARD deployment workspace + hosts: localhost + gather_facts: false + roles: + - ard_provider_render +``` + +### 9.2 `ard-apply.yaml` / `ard-create.yaml` + +Reads a deployment workspace, creates provider resources, and writes dynamic inventory/state. `ard-create.yaml` can remain as a compatibility wrapper for users and Molecule scenarios that already expect a create phase. + +```yaml +- name: Apply ARD provider deployment + hosts: localhost + gather_facts: true + roles: + - ard_provider_preflight + - ard_provider_image + - ard_provider_network + - ard_provider_node + - ard_provider_inventory + - ard_provider_state +``` + +### 9.3 `ard-deploy-devstack.yaml` + +Re-discovers provider inventory and deploys DevStack. + +```yaml +- name: Discover ARD provider nodes + hosts: localhost + gather_facts: false + roles: + - ard_provider_inventory + +- name: Load deployment DevStack config + hosts: all + gather_facts: false + roles: + - ard_devstack_config + +- name: Deploy ARD multinode DevStack + import_playbook: ../deploy_multinode_devstack.yaml +``` + +### 9.4 `ard-site.yaml` + +Full local flow. It can render a deployment workspace if needed, apply it, deploy DevStack, and verify. + +```yaml +- import_playbook: ard-render.yaml +- import_playbook: ard-apply.yaml +- import_playbook: ard-deploy-devstack.yaml + +- name: Verify ARD deployment + import_playbook: ard-verify.yaml +``` + +### 9.5 `ard-destroy.yaml` + +Collects logs and destroys provider resources for a deployment workspace. It keeps the deployment folder so inputs, generated inventory, provider state, and logs remain available for inspection. + +```yaml +- name: Destroy ARD provider deployment + hosts: localhost + gather_facts: false + roles: + - ard_provider_inventory + - ard_provider_collect_logs + - ard_provider_destroy +``` + +### 9.6 `ard-cleanup.yaml` + +Removes local deployment workspace state after destroy. + +```yaml +- name: Cleanup ARD deployment workspace + hosts: localhost + gather_facts: false + roles: + - ard_provider_cleanup +``` + +Provider inventory should be rediscovered at the start of every playbook that needs it. Do not rely on `add_host` from an earlier execution persisting across Make, Molecule, or Zuul phases. + +## 10. Libvirt Provider Design + +The libvirt provider is the first target because it most directly replaces the current Vagrant/libvirt workflow. + +### 10.1 Provider variables + +```yaml +ard_provider: libvirt + +ard_libvirt_uri: qemu:///system +ard_libvirt_pool: ard +ard_libvirt_network_name: ard-mgmt +ard_libvirt_network_cidr: 192.168.96.0/24 +ard_libvirt_network_gateway: 192.168.96.1 +ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default('~/.cache', true) }}/ard/images" +ard_image_download: true +ard_image_checksum_required: false +ard_libvirt_image_dir: "{{ lookup('env', 'XDG_STATE_HOME') | default('~/.local/state', true) }}/ard/libvirt/images" +ard_libvirt_debian_13_cloud_image_url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 +ard_libvirt_debian_13_alternate_cloud_image_url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2 +ard_libvirt_ubuntu_24_04_cloud_image_url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img +ard_libvirt_ubuntu_26_04_cloud_image_url: null +ard_default_image: debian-13 +ard_default_controller_flavor: devstack-control +ard_default_compute_flavor: devstack-compute +ard_default_vm_preference: devstack +``` + +### 10.2 Roles + +```text +ard_libvirt_preflight +ard_libvirt_image +ard_libvirt_network +ard_libvirt_node +ard_libvirt_inventory +ard_libvirt_destroy +ard_libvirt_collect_logs +``` + +### 10.3 Preflight + +Check: + +- libvirt daemon is available +- Ansible can connect to `ard_libvirt_uri` +- `qemu-img` is installed +- cloud-init tooling is available, e.g. `cloud-localds` or equivalent +- selected network CIDR does not collide with obvious host networks +- enough disk exists for overlays +- enough memory/CPU exists for requested nodes +- nested virtualization is enabled if `nested_virt` is requested + +### 10.4 Image handling + +Preferred image model: + +1. Resolve `ard_nodes[*].image` through `ard_images`. +2. Download or reuse the base cloud image from `ard_image_cache_dir`. +3. Optionally validate the cached image checksum when configured. +4. Create a qcow2 overlay per node under `ard_libvirt_image_dir//`. +5. Create a cloud-init NoCloud/config-drive seed ISO per node. +6. Boot each VM from its overlay plus seed. + +The cache is shared across deployments and must not be mutated. Destroy removes deployment overlays and seed ISOs by default, but leaves cached base images intact. + +Example: + +```text +$XDG_CACHE_HOME/ard/images/ # or ~/.cache/ard/images when XDG_CACHE_HOME is unset + debian-13-genericcloud-amd64.qcow2 + noble-server-cloudimg-amd64.img + +$XDG_STATE_HOME/ard/libvirt/images/devstack-a/ # or ~/.local/state/ard/libvirt/images/devstack-a when XDG_STATE_HOME is unset + controller.qcow2 + controller-seed.iso + compute1.qcow2 + compute1-seed.iso +``` + +### 10.5 Cloud-init + +Cloud-init should provide: + +- hostname +- stack user +- sudo permissions +- SSH authorized key +- static network config or DHCP client config +- optional package bootstrap if needed + +Example user-data intent: + +```yaml +#cloud-config +hostname: controller +users: + - name: stack + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + ssh_authorized_keys: + - ssh-ed25519 ... +packages: + - python3 + - sudo + - git + - rsync +``` + +### 10.6 Networking + +The libvirt provider should create a NATed management network by default. + +Example: + +```yaml +ard_libvirt_network_name: ard-mgmt +ard_libvirt_network_cidr: 192.168.96.0/24 +ard_libvirt_network_gateway: 192.168.96.1 +``` + +Static IPs are preferred for reproducibility. DHCP reservations are also acceptable if inventory discovery is reliable. + +For multinode DevStack, the libvirt management network can carry service traffic and overlay tunnel traffic. DevStack can configure Linux bridge, OVS, or OVN inside the VMs as needed. + +### 10.7 Node creation + +Preferred implementation: + +- render libvirt network and domain XML from Jinja templates +- define/start resources with `virsh` +- keep rendered XML under `deployments//rendered/libvirt/` for review and debugging + +Possible future alternatives: + +- `community.libvirt.virt` +- `community.libvirt.virt_net` +- `community.libvirt.virt_pool` + +`virt-install` should not be used by default. It is mostly a convenience wrapper around libvirt XML creation, and templated XML is more reviewable and reproducible for ARD provider work. + +Required domain properties: + +- memory from the resolved portable flavor, e.g. `ard_flavors[flavor].provider.libvirt.memory_mb` +- vCPUs from the resolved portable flavor, e.g. `ard_flavors[flavor].provider.libvirt.vcpus` +- CPU mode from the resolved portable flavor, e.g. `ard_flavors[flavor].provider.libvirt.cpu_mode` +- machine/device defaults from the resolved portable preference, e.g. q35, virtio disk/interface, RNG, and secure-boot behavior +- disk overlay per node +- cloud-init seed disk +- network interface attached to `ard_libvirt_network_name` + +### 10.8 Console logging + +Libvirt domains should keep an interactive PTY serial console for `virsh console` / virt-manager and also log serial output to the deployment's XDG state directory: + +```text +$XDG_STATE_HOME/ard/libvirt/images//-console.log +``` + +The domain XML should use a Jinja-rendered serial device with a libvirt `` element so early boot, kernel, cloud-init, and getty output are available even if virt-manager is opened after the output was produced. + +### 10.9 Inventory + +Inventory can be built from: + +1. static IPs from `ard_nodes`; or +2. libvirt DHCP leases; or +3. guest agent data later. + +Initial implementation should prefer static IPs because ARD already assumes deterministic addresses in several places. + +## 11. KubeVirt / OpenShift Virtualization Provider Design + +The KubeVirt provider allows ARD to create DevStack-capable VMs on a remote OpenShift cluster. + +The initial networking mode is **pod network / masquerade**. Do not require Multus or a secondary L2 network at first. + +For multinode DevStack, rely on the VM management/pod-network IP path for node-to-node communication. DevStack's internal Linux bridge, OVS, or OVN configuration can create tenant networking and VXLAN tunnels inside the VMs when needed. + +### 11.1 Provider variables + +```yaml +ard_provider: kubevirt + +ard_kubevirt_namespace: ard-devstack +# Omit by default so PVC/DataVolume creation can inherit the namespace, +# boot source, CDI, or cluster default StorageClass. Set only when a scenario +# needs to force a specific class. +ard_kubevirt_storage_class: null +ard_kubevirt_network_mode: masquerade +ard_kubevirt_ssh_access: nodeport +ard_kubevirt_image_source: datavolume +ard_kubevirt_default_image: debian-13 +ard_kubevirt_default_controller_flavor: devstack-control +ard_kubevirt_default_compute_flavor: devstack-compute +ard_kubevirt_default_preference: devstack +ard_kubevirt_ensure_instancetype_resources: false +ard_kubevirt_delete_namespace: false +``` + +Allowed initial SSH access modes: + +```yaml +ard_kubevirt_ssh_access: nodeport # expose each VM SSH via a NodePort Service +ard_kubevirt_ssh_access: loadbalancer # expose each VM SSH via LoadBalancer Service, if available +ard_kubevirt_ssh_access: port_forward # local/dev only; less suitable for long-running automation +ard_kubevirt_ssh_access: bastion # future mode using a bastion pod/VM +``` + +Initial recommendation: + +```yaml +ard_kubevirt_network_mode: masquerade +ard_kubevirt_ssh_access: nodeport +``` + +### 11.2 Roles + +```text +ard_kubevirt_preflight +ard_kubevirt_image +ard_kubevirt_network +ard_kubevirt_node +ard_kubevirt_inventory +ard_kubevirt_destroy +ard_kubevirt_collect_logs +``` + +### 11.3 Bundled instancetype and preference resources + +ARD should carry the default KubeVirt instancetype and preference definitions in: + +```text +ansible/files/kubevirt/devstack-instancetype-preference.yaml +``` + +These definitions are intentionally namespaced `VirtualMachineInstancetype` and `VirtualMachinePreference` resources so they can be applied to the target ARD namespace. The provider should use existing resources when they are already present. It should not create or update them implicitly unless `ard_kubevirt_ensure_instancetype_resources` is enabled or an explicit setup target/playbook is run. + +Resource defaults: + +- `VirtualMachineInstancetype/devstack-8c16g`: 8 guest CPUs, host-passthrough CPU model, automatic IOThreads, 16Gi memory. +- `VirtualMachineInstancetype/devstack-8c8g`: 8 guest CPUs, host-passthrough CPU model, automatic IOThreads, 8Gi memory. +- `VirtualMachinePreference/devstack`: q35, virtio disk/interface, RNG, KVM preference, EFI with secure boot disabled, and core CPU topology. + +### 11.4 Preflight + +Check: + +- kubeconfig is available +- selected namespace exists or can be created +- OpenShift Virtualization/KubeVirt APIs are available +- CDI APIs are available if using DataVolumes +- selected StorageClass exists, if `ard_kubevirt_storage_class` is set +- caller has RBAC for required resources +- VM feature gates are adequate for requested profiles +- SSH exposure mode is possible +- nested virtualization is available if `nested_virt` is requested and Nova should use KVM + +Relevant Kubernetes/OpenShift resource kinds: + +- `VirtualMachine` +- `VirtualMachineInstance` +- `DataVolume` +- `PersistentVolumeClaim` +- `Secret` +- `Service` +- possibly `Route` for web endpoints later, but not required for SSH + +### 11.5 Image handling + +Initial image flow: + +1. Resolve `ard_nodes[*].image` through `ard_images`. +2. Prefer an existing KubeVirt/CDI boot source, DataVolume, or PVC when present. +3. For the default `debian-13` image, assume an existing `DataVolume`/PVC named `debian-13-2026-05-20` unless the scenario overrides it. +4. Clone or create one PVC per VM root disk from that source. +5. Attach cloud-init data via `cloudInitNoCloud`. + +Example existing image reference: + +```yaml +ard_images: + debian-13: + os_family: debian + version: "13" + cloud_init: true + provider: + kubevirt: + source_kind: DataVolume + name: debian-13-2026-05-20 + namespace: "{{ ard_kubevirt_image_namespace | default(ard_kubevirt_namespace) }}" +``` + +When creating PVCs or DataVolumes, the provider should omit `storageClassName` by default. This lets the namespace, CDI boot source, or cluster default choose the StorageClass. `ard_kubevirt_storage_class` is an explicit override for environments that need one. + +The provider should avoid re-importing the base image unless requested. + +### 11.6 VM creation + +Each ARD node maps to one KubeVirt `VirtualMachine`. + +Example shape: + +```yaml +apiVersion: kubevirt.io/v1 +kind: VirtualMachine +metadata: + name: controller + namespace: ard-devstack + labels: + app.kubernetes.io/part-of: ard + app.kubernetes.io/instance: devstack-a + ard.openstack.org/deployment: devstack-a + ard.openstack.org/provider: kubevirt + ard.openstack.org/node: controller + ard.node/name: controller +spec: + running: true + instancetype: + kind: VirtualMachineInstancetype + name: devstack-8c16g + preference: + kind: VirtualMachinePreference + name: devstack + template: + metadata: + labels: + app.kubernetes.io/part-of: ard + app.kubernetes.io/instance: devstack-a + ard.openstack.org/deployment: devstack-a + ard.openstack.org/provider: kubevirt + ard.openstack.org/node: controller + ard.node/name: controller + spec: + domain: + devices: + interfaces: + - name: default + masquerade: {} + networks: + - name: default + pod: {} + volumes: + - name: rootdisk + persistentVolumeClaim: + claimName: controller-rootdisk + - name: cloudinitdisk + cloudInitNoCloud: + userData: | + #cloud-config + hostname: controller + users: + - name: stack + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + ssh_authorized_keys: + - ssh-ed25519 ... +``` + +All-in-one and controller nodes use `devstack-8c16g`; compute nodes in a multinode topology use `devstack-8c8g`; all DevStack VMs use `VirtualMachinePreference/devstack` unless explicitly overridden. + +### 11.7 Initial KubeVirt networking decision: pod network / masquerade + +The first KubeVirt implementation should use the default pod network with masquerade binding. + +Benefits: + +- no Multus prerequisite +- works on more OpenShift Virtualization clusters +- simpler RBAC and cluster setup +- no dependency on provider-specific L2 network attachments +- sufficient for initial single-node and many multinode control-plane tests + +Tradeoffs: + +- VM IPs may not be stable in the same way as libvirt static IPs +- direct SSH needs an exposure mechanism +- traffic between VMs traverses the pod network path +- not a true shared L2 segment between VMs +- floating IP/provider network testing is limited unless additional routing is configured + +### 11.8 SSH exposure for masquerade mode + +With masquerade networking, the provider must expose SSH to Ansible. + +Initial modes: + +#### NodePort + +Create one Service per VM: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: controller-ssh + namespace: ard-devstack +spec: + type: NodePort + selector: + ard.node/name: controller + ports: + - name: ssh + protocol: TCP + port: 22 + targetPort: 22 +``` + +Inventory then uses: + +```yaml +ansible_host: +ansible_port: +``` + +#### LoadBalancer + +If the cluster supports LoadBalancer Services, expose each VM SSH through a LoadBalancer. + +Inventory then uses: + +```yaml +ansible_host: +ansible_port: 22 +``` + +#### Port-forward + +Useful for local development, but less suitable for long-running unattended automation. + +Inventory uses local forwarded ports: + +```yaml +ansible_host: 127.0.0.1 +ansible_port: +``` + +#### Bastion + +Future mode. A bastion pod or VM can provide a stable SSH jump host into the namespace. + +Inventory uses: + +```yaml +ansible_ssh_common_args: "-o ProxyJump=..." +``` + +### 11.9 KubeVirt inventory model + +In masquerade mode, distinguish between: + +1. the address Ansible uses to SSH into the VM; and +2. the address DevStack should use inside the VM for service and tunnel traffic. + +Provider inventory should support both. + +Example: + +```yaml +controller: + ansible_host: 192.0.2.25 + ansible_port: 32022 + ansible_user: stack + ard_node_internal_ip: 10.0.2.2 + nodepool: + private_ipv4: 10.0.2.2 + public_ipv4: 10.0.2.2 +``` + +However, `10.0.2.2` is illustrative only. The provider must discover or configure the VM-internal IP used for node-to-node communication. + +Potential approaches: + +1. Query VM status interfaces from KubeVirt. +2. Use QEMU guest agent if available. +3. Use cloud-init to configure a known internal address where supported. +4. Use Ansible fact gathering after SSH connects and set `nodepool.private_ipv4` from the VM's default interface. + +Initial recommendation: + +- Use SSH exposure only for Ansible connectivity. +- After SSH works, gather facts inside the VM. +- Set `nodepool.private_ipv4` to the VM's default IPv4 address as seen inside the guest. +- Use those `nodepool` addresses for DevStack `HOST_IP`, `SERVICE_HOST`, and tunnel endpoints. + +### 11.10 Multinode DevStack on KubeVirt with masquerade + +The initial KubeVirt multinode design assumes the VM management/default IP path is sufficient for control-plane and overlay traffic. + +For DevStack multinode: + +- controller and computes communicate over their guest default interfaces +- DevStack config sets `HOST_IP` and `SERVICE_HOST` from `nodepool.private_ipv4` +- Neutron/OVN/OVS tenant networking uses tunnels where needed +- VXLAN tunnel endpoints use the VM management/default IPs + +This avoids requiring Multus or a shared L2 provider network in the cluster. + +Expected DevStack implications: + +```yaml +HOST_IP: "{{ hostvars[inventory_hostname]['nodepool']['private_ipv4'] }}" +SERVICE_HOST: "{{ hostvars[groups['controller'][0]]['nodepool']['private_ipv4'] }}" +TUNNEL_ENDPOINT_IP: "{{ hostvars[inventory_hostname]['nodepool']['private_ipv4'] }}" +``` + +If using ML2/OVS with Linux bridge or VXLAN tunnels, ARD should ensure local.conf selects a tunnel-based tenant network mode rather than relying on external L2 adjacency. + +Example intent: + +```ini +ENABLE_TENANT_TUNNELS=True +ENABLE_TENANT_VLANS=False +Q_ML2_TENANT_NETWORK_TYPE=vxlan +``` + +The exact options depend on the selected Neutron backend and DevStack branch. + +### 11.11 Deferred KubeVirt networking modes + +Do not require these initially: + +- Multus bridge networks +- SR-IOV networks +- dedicated L2 provider networks +- direct floating-IP reachability from outside the cluster + +These can be added later as optional modes: + +```yaml +ard_kubevirt_network_mode: multus +ard_kubevirt_network_attachment: ard-l2 +``` + +## 12. Provider-neutral Profiles + +Profiles are interpreted differently by libvirt and KubeVirt. + +### 12.1 `ssh` + +Provider must ensure: + +- stack user exists +- SSH key is installed +- Ansible can connect +- Python is available for Ansible + +### 12.2 `nested_virt` + +Libvirt: + +- check host nested virtualization +- set CPU mode, usually host-passthrough + +KubeVirt: + +- check cluster supports nested virtualization for VMs +- set required VM CPU/model/features if needed +- if unavailable, allow scenario to fall back to QEMU by setting DevStack `LIBVIRT_TYPE=qemu` + +### 12.3 `ovn` / `ovs` + +Provider does not configure Neutron directly. It only ensures the VM can support the requested DevStack configuration. + +Libvirt: + +- normal VM kernel/module behavior + +KubeVirt: + +- verify guest OS image can load/use required userspace packages +- avoid relying on external L2 adjacency in initial masquerade mode + +### 12.4 `storage_lvm` + +VM providers are safer for Cinder LVM than privileged containers because LVM/device state is isolated inside the guest. + +Provider may add an extra disk per node: + +```yaml +extra_disks: + - name: cinder + size_gb: 20 +``` + +### 12.5 `ceph` + +Ceph can be added as a higher-level DevStack plugin profile. Provider-specific work is mostly CPU/memory/disk sizing and optional extra disks. + +## 13. Make Targets + +Make should call provider-neutral playbooks. + +Example: + +```make +ARD_PROVIDER ?= libvirt +ARD_INVENTORY ?= localhost, +ARD_DEPLOYMENT ?= default +ARD_DEPLOYMENTS_DIR ?= deployments +ARD_DEPLOYMENT_DIR ?= $(ARD_DEPLOYMENTS_DIR)/$(ARD_DEPLOYMENT) +ARD_KUBEVIRT_NAMESPACE ?= ard-devstack +ARD_EXTRA_VARS ?= ard_provider=$(ARD_PROVIDER) ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) ard_kubevirt_namespace=$(ARD_KUBEVIRT_NAMESPACE) + +.PHONY: render +render: + ansible-playbook -i $(ARD_INVENTORY) ansible/playbooks/ard-render.yaml \ + -e $(ARD_EXTRA_VARS) + +.PHONY: apply +apply: + ansible-playbook -i $(ARD_INVENTORY) ansible/playbooks/ard-apply.yaml \ + -e ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) + +.PHONY: create +create: apply + +.PHONY: deploy +deploy: + ansible-playbook -i $(ARD_INVENTORY) ansible/playbooks/ard-deploy-devstack.yaml \ + -e ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) + +.PHONY: verify +verify: + ansible-playbook -i $(ARD_INVENTORY) ansible/playbooks/ard-verify.yaml \ + -e ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) + +.PHONY: destroy +destroy: + ansible-playbook -i $(ARD_INVENTORY) ansible/playbooks/ard-destroy.yaml \ + -e ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) + +.PHONY: cleanup +cleanup: + ansible-playbook -i $(ARD_INVENTORY) ansible/playbooks/ard-cleanup.yaml \ + -e ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) + +.PHONY: site +site: + ansible-playbook -i $(ARD_INVENTORY) ansible/playbooks/ard-site.yaml \ + -e $(ARD_EXTRA_VARS) + +.PHONY: kubevirt-resources +kubevirt-resources: + oc apply -n $(ARD_KUBEVIRT_NAMESPACE) \ + -f ansible/files/kubevirt/devstack-instancetype-preference.yaml +``` + +Usage: + +```bash +make render ARD_PROVIDER=kubevirt ARD_DEPLOYMENT=devstack-a +vi deployments/devstack-a/devstack/controller.yaml +vi deployments/devstack-a/devstack/nodes/compute1.yaml +make apply ARD_DEPLOYMENT=devstack-a +make deploy ARD_DEPLOYMENT=devstack-a +make destroy ARD_DEPLOYMENT=devstack-a +make cleanup ARD_DEPLOYMENT=devstack-a + +make site ARD_PROVIDER=libvirt ARD_DEPLOYMENT=devstack-libvirt-a +make site ARD_PROVIDER=kubevirt ARD_DEPLOYMENT=devstack-kubevirt-a +make kubevirt-resources ARD_KUBEVIRT_NAMESPACE=ard-devstack +``` + +The KubeVirt provider should use `VirtualMachineInstancetype` and `VirtualMachinePreference` resources if they already exist. It should not create them by default. `make kubevirt-resources` is the explicit setup path to create or update the repo-carried defaults in the target namespace. An Ansible equivalent, `ard-kubevirt-ensure-resources.yaml`, can provide the same behavior for non-Make workflows. + +## 14. Molecule Integration + +Molecule should use ansible-native or delegated mode and call the same provider-neutral playbooks. + +Example scenario structure: + +```text +molecule/libvirt-multinode/ + molecule.yml + create.yml + converge.yml + verify.yml + destroy.yml +``` + +`create.yml`: + +```yaml +- import_playbook: ../../ansible/playbooks/ard-create.yaml +``` + +`converge.yml`: + +```yaml +- import_playbook: ../../ansible/playbooks/ard-deploy-devstack.yaml +``` + +`destroy.yml`: + +```yaml +- import_playbook: ../../ansible/playbooks/ard-destroy.yaml +``` + +The same pattern applies to KubeVirt. + +Molecule should not own provider-specific VM creation logic. It should be a workflow runner and verifier. + +## 15. Zuul Integration + +Zuul jobs should be able to call the same playbooks. + +Important rule: do not assume dynamic inventory created in one Zuul phase persists into another phase. + +Preferred patterns: + +### 15.1 Create and deploy in one run playbook + +```yaml +- name: Create provider VMs + hosts: localhost + roles: + - ard_provider_preflight + - ard_provider_image + - ard_provider_network + - ard_provider_node + - ard_provider_inventory + +- name: Deploy DevStack + import_playbook: ansible/deploy_multinode_devstack.yaml +``` + +### 15.2 Pre-run creates, run re-discovers + +- pre-run creates VMs +- run re-discovers VMs and calls `ard_provider_inventory` +- run deploys DevStack +- post-run re-discovers or collects logs via provider APIs + +For KubeVirt, post-run log collection should work from the OpenShift API even if SSH fails. + +## 16. Phased Implementation Plan + +### Phase 0: Rename the design focus + +- Treat `ARD_OCI_DESIGN.md` as deferred container-provider design. +- Add this provider-focused design. +- Decide provider-neutral variable names. + +### Phase 1: Provider-neutral playbooks, dispatch roles, and upstream role refresh + +- Refresh git submodules to current upstream master before validating the new provider flow: + - `submodules/devstack` + - `submodules/zuul-jobs` + - `submodules/openstack-zuul-jobs` +- Re-check that ARD's `devstack_controller` and `devstack_compute` roles still call the current upstream `write-devstack-local-conf` and `run-devstack` roles with compatible variable names. +- Add `ard-create.yaml`. +- Add `ard-deploy-devstack.yaml`. +- Add `ard-destroy.yaml`. +- Add dispatcher roles: + - `ard_provider_preflight` + - `ard_provider_image` + - `ard_provider_network` + - `ard_provider_node` + - `ard_provider_inventory` + - `ard_provider_destroy` + +### Phase 2: Libvirt single-node + +- Create one controller VM. +- Bootstrap stack user via cloud-init. +- Verify SSH and Ansible fact gathering. +- Add inventory facts matching current ARD expectations. + +### Phase 3: Libvirt multinode + +- Create controller + compute1. +- Then controller + compute1 + compute2. +- Match current Vagrant scenario groups. +- Run existing `deploy_multinode_devstack.yaml`. + +### Phase 4: Molecule libvirt scenario without Vagrant + +- Add `molecule/libvirt-multinode`. +- Use provider playbooks for create/converge/destroy. +- Remove Vagrant from that path. + +### Phase 5: KubeVirt single-node with masquerade + +- Preflight OpenShift Virtualization access. +- Import or reference cloud image. +- Create one controller VM. +- Expose SSH through NodePort or chosen access mode. +- Gather facts inside the VM. +- Run a minimal DevStack deployment. + +### Phase 6: KubeVirt multinode with masquerade + +- Create controller + compute VM. +- Use guest default IPs as `nodepool.private_ipv4`. +- Configure DevStack for tunnel-based tenant networking when needed. +- Validate controller/compute communication over the pod-network path. + +### Phase 7: Zuul provider jobs + +- Add experimental jobs for libvirt and/or KubeVirt provider flows. +- Ensure post-run log collection works after partial failure. + +### Phase 8: Advanced storage and networking profiles + +- Extra disks for Cinder LVM. +- Ceph-capable sizing profiles. +- Optional KubeVirt Multus mode if needed later. + +## 17. Risks and Open Questions + +### 17.1 KubeVirt masquerade IP discovery + +The provider must distinguish SSH endpoint from guest internal IP. Initial implementation should gather facts over SSH and set `nodepool.private_ipv4` from inside the guest. + +### 17.2 KubeVirt SSH exposure + +NodePort may not be available or allowed on all clusters. The provider should support multiple access modes, but start with one working mode. + +### 17.3 Nested virtualization in KubeVirt + +Nova KVM inside OpenShift Virtualization VMs depends on cluster support. Scenarios should be able to fall back to QEMU when nested virtualization is unavailable. + +### 17.4 Multinode overlay over pod network + +VXLAN or other tunnel traffic between VMs must work over the pod-network path. This should be validated early. + +### 17.5 Provider-neutral static addressing + +Libvirt can easily provide static management IPs. KubeVirt masquerade mode may require discovery rather than static assignment. + +### 17.6 Log collection + +Provider log collection should work without relying on a successful DevStack deployment or even working SSH. + +### 17.7 Existing ARD assumptions + +Some ARD roles may assume VM-like network behavior from Vagrant/libvirt. These should be adjusted only where necessary and kept provider-neutral. + +### 17.8 Stale submodules and upstream role drift + +ARD depends on git submodules for DevStack and Zuul/OpenStack Zuul job roles. These submodules may lag current upstream master. Refreshing them can change role defaults, job vars, or assumptions around `write-devstack-local-conf`, `run-devstack`, inventory, and Zuul-style variables. The provider prototype should update the submodules and validate that ARD's local DevStack wrapper roles still align with the current upstream role interfaces. + +### 17.9 Libvirt prototype decisions + +These decisions should be resolved before or during the first libvirt prototype: + +- Debian 13 image variant: default to `debian-13-genericcloud-amd64.qcow2`; the `debian-13-nocloud-amd64.qcow2` image prompts for first-boot installation configuration in this workflow and is kept only as an alternate for future investigation. +- Image cache location: follow XDG cache conventions with `$XDG_CACHE_HOME/ard/images`, falling back to `~/.cache/ard/images`. +- Checksum policy: support checksums when configured, but do not require them for the first prototype unless reproducibility/security requirements demand it. +- Libvirt URI: prototype against `qemu:///system`; defer `qemu:///session` support. The first prototype assumes the invoking user has sufficient `libvirt`/`qemu` group access and should not use Ansible `become` by default. +- VM naming: use `ard--` as the provider resource name, e.g. `ard-devstack-1-controller`, while keeping inventory hostnames as `controller`, `compute1`, etc. +- Destroy semantics: delete per-deployment overlays, seed ISOs, domains, and generated networks by default, but keep cached base images. +- Filesystem paths: store downloaded base images in XDG cache and libvirt per-deployment base copies, overlays, and seed ISOs in XDG state. The provider should not write to `/var/lib/libvirt/images` by default and should not silently sudo provider operations. +- Network default: use NAT `192.168.96.0/24` for the libvirt prototype. + +## 18. Recommended Initial Defaults + +For local work: + +```yaml +ard_provider: libvirt +ard_default_image: debian-13 +ard_default_controller_flavor: devstack-control +ard_default_compute_flavor: devstack-compute +ard_default_vm_preference: devstack +ard_default_topology: one-controller-one-compute +``` + +For remote OpenShift work: + +```yaml +ard_provider: kubevirt +ard_kubevirt_network_mode: masquerade +ard_kubevirt_ssh_access: nodeport +ard_kubevirt_storage_class: null +ard_kubevirt_default_image: debian-13 +ard_kubevirt_default_controller_flavor: devstack-control +ard_kubevirt_default_compute_flavor: devstack-compute +ard_kubevirt_default_preference: devstack +ard_kubevirt_ensure_instancetype_resources: false +ard_default_topology: one-controller-one-compute +``` + +For KubeVirt multinode DevStack, express local.conf inputs through the existing controller/compute variables, for example in `deployments//devstack/group_vars/controller.yaml` and `deployments//devstack/group_vars/compute.yaml`: + +```yaml +controller_localrc_extra: + ENABLE_TENANT_TUNNELS: true + ENABLE_TENANT_VLANS: false + +compute_localrc_extra: + ENABLE_TENANT_TUNNELS: true + ENABLE_TENANT_VLANS: false +``` + +The exact DevStack networking keys should be validated against the selected Neutron backend and branch. + +## 19. Summary + +Short-term ARD provider work should focus on VM providers: + +1. libvirt to replace the current Molecule/Vagrant/libvirt workflow; and +2. KubeVirt/OpenShift Virtualization to use a remote OpenShift cluster as an additional VM host. + +The first KubeVirt implementation should use pod-network/masquerade mode and avoid requiring Multus. For multinode DevStack, ARD should rely on the guest default network path and DevStack-managed tunnel overlays such as VXLAN when tenant or inter-node overlay networking is required. + +The provider layer creates VMs and inventory. Existing ARD DevStack roles continue to deploy OpenStack. This keeps the provisioning model replaceable without making DevStack deployment provider-specific. diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..6f28eb9 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +roles_path = ansible/roles:submodules/devstack/roles:submodules/zuul-jobs/roles:submodules/openstack-zuul-jobs/roles +host_key_checking = False +retry_files_enabled = False +stdout_callback = default diff --git a/ansible/files/kubevirt/devstack-instancetype-preference.yaml b/ansible/files/kubevirt/devstack-instancetype-preference.yaml new file mode 100644 index 0000000..19295d8 --- /dev/null +++ b/ansible/files/kubevirt/devstack-instancetype-preference.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: instancetype.kubevirt.io/v1beta1 +kind: VirtualMachineInstancetype +metadata: + name: devstack-8c16g +spec: + cpu: + guest: 8 + model: host-passthrough + ioThreadsPolicy: auto + memory: + guest: 16Gi +--- +apiVersion: instancetype.kubevirt.io/v1beta1 +kind: VirtualMachineInstancetype +metadata: + name: devstack-8c8g +spec: + cpu: + guest: 8 + model: host-passthrough + ioThreadsPolicy: auto + memory: + guest: 8Gi +--- +apiVersion: instancetype.kubevirt.io/v1beta1 +kind: VirtualMachinePreference +metadata: + name: devstack +spec: + cpu: + preferredCPUTopology: cores + devices: + preferredDiskBus: virtio + preferredInterfaceModel: virtio + preferredRng: {} + features: + preferredKvm: {} + firmware: + preferredEfi: + secureBoot: false + machine: + preferredMachineType: q35 diff --git a/ansible/playbooks/ard-apply.yaml b/ansible/playbooks/ard-apply.yaml new file mode 100644 index 0000000..8540ec7 --- /dev/null +++ b/ansible/playbooks/ard-apply.yaml @@ -0,0 +1,11 @@ +--- +- name: Apply ARD provider deployment + hosts: localhost + connection: local + gather_facts: true + roles: + - ard_provider_preflight + - ard_provider_image + - ard_provider_network + - ard_provider_node + - ard_provider_inventory diff --git a/ansible/playbooks/ard-cleanup.yaml b/ansible/playbooks/ard-cleanup.yaml new file mode 100644 index 0000000..d410d36 --- /dev/null +++ b/ansible/playbooks/ard-cleanup.yaml @@ -0,0 +1,7 @@ +--- +- name: Cleanup ARD deployment workspace + hosts: localhost + connection: local + gather_facts: false + roles: + - ard_provider_cleanup diff --git a/ansible/playbooks/ard-deploy-devstack.yaml b/ansible/playbooks/ard-deploy-devstack.yaml new file mode 100644 index 0000000..b7a99c3 --- /dev/null +++ b/ansible/playbooks/ard-deploy-devstack.yaml @@ -0,0 +1,16 @@ +--- +- name: Discover ARD provider nodes + hosts: localhost + connection: local + gather_facts: false + roles: + - ard_provider_inventory + +- name: Load ARD deployment DevStack variables + hosts: all + gather_facts: false + roles: + - ard_devstack_config + +- name: Deploy ARD multinode DevStack + import_playbook: ../deploy_multinode_devstack.yaml diff --git a/ansible/playbooks/ard-destroy.yaml b/ansible/playbooks/ard-destroy.yaml new file mode 100644 index 0000000..74290e8 --- /dev/null +++ b/ansible/playbooks/ard-destroy.yaml @@ -0,0 +1,7 @@ +--- +- name: Destroy ARD provider deployment + hosts: localhost + connection: local + gather_facts: false + roles: + - ard_provider_destroy diff --git a/ansible/playbooks/ard-render.yaml b/ansible/playbooks/ard-render.yaml new file mode 100644 index 0000000..d6d4697 --- /dev/null +++ b/ansible/playbooks/ard-render.yaml @@ -0,0 +1,7 @@ +--- +- name: Render ARD deployment workspace + hosts: localhost + connection: local + gather_facts: false + roles: + - ard_provider_render diff --git a/ansible/roles/ard_devstack_config/tasks/main.yml b/ansible/roles/ard_devstack_config/tasks/main.yml new file mode 100644 index 0000000..5c74a94 --- /dev/null +++ b/ansible/roles/ard_devstack_config/tasks/main.yml @@ -0,0 +1,16 @@ +--- +- name: Load deployment common DevStack vars + include_vars: + file: "{{ ard_deployment_dir }}/devstack/common.yaml" + when: lookup('ansible.builtin.file', ard_deployment_dir + '/devstack/common.yaml', errors='ignore') | length > 0 + +- name: Load deployment group DevStack vars + include_vars: + file: "{{ ard_deployment_dir }}/devstack/group_vars/{{ item }}.yaml" + loop: "{{ group_names }}" + when: lookup('ansible.builtin.file', ard_deployment_dir + '/devstack/group_vars/' + item + '.yaml', errors='ignore') | length > 0 + +- name: Load deployment host DevStack vars + include_vars: + file: "{{ ard_deployment_dir }}/devstack/host_vars/{{ inventory_hostname }}.yaml" + when: lookup('ansible.builtin.file', ard_deployment_dir + '/devstack/host_vars/' + inventory_hostname + '.yaml', errors='ignore') | length > 0 diff --git a/ansible/roles/ard_libvirt_destroy/tasks/main.yml b/ansible/roles/ard_libvirt_destroy/tasks/main.yml new file mode 100644 index 0000000..1d94365 --- /dev/null +++ b/ansible/roles/ard_libvirt_destroy/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: Check for provider state file + stat: + path: "{{ ard_deployment_dir }}/provider-state.yaml" + register: ard_provider_state_stat + +- name: Load provider state + include_vars: + file: "{{ ard_deployment_dir }}/provider-state.yaml" + name: ard_state + when: ard_provider_state_stat.stat.exists + +- name: Build destroy domain list from state + set_fact: + ard_destroy_domains: "{{ ard_state.domains | default([]) }}" + when: ard_provider_state_stat.stat.exists + +- name: Build destroy domain list from prefix fallback + shell: "virsh --connect {{ ard_libvirt_uri }} list --all --name | awk '/^ard-{{ ard_deployment_name }}-/ { print }'" + register: ard_destroy_domain_names + changed_when: false + when: not ard_provider_state_stat.stat.exists + +- name: Initialize fallback destroy domain list + set_fact: + ard_destroy_domains: [] + when: not ard_provider_state_stat.stat.exists + +- name: Convert prefix fallback domains to state-like objects + set_fact: + ard_destroy_domains: "{{ ard_destroy_domains + [{'name': item}] }}" + loop: "{{ ard_destroy_domain_names.stdout_lines | default([]) }}" + when: not ard_provider_state_stat.stat.exists + +- name: Destroy libvirt domains + command: "virsh --connect {{ ard_libvirt_uri }} destroy {{ item.name }}" + loop: "{{ ard_destroy_domains | default([]) }}" + register: ard_destroy_domain + changed_when: ard_destroy_domain.rc == 0 + failed_when: false + +- name: Undefine libvirt domains and remove storage references + command: "virsh --connect {{ ard_libvirt_uri }} undefine {{ item.name }} --nvram --remove-all-storage" + loop: "{{ ard_destroy_domains | default([]) }}" + register: ard_undefine_domain + changed_when: ard_undefine_domain.rc == 0 + failed_when: false + +- name: Destroy libvirt network + command: "virsh --connect {{ ard_libvirt_uri }} net-destroy {{ ard_libvirt_network_name }}" + register: ard_net_destroy + changed_when: ard_net_destroy.rc == 0 + failed_when: false + +- name: Undefine libvirt network + command: "virsh --connect {{ ard_libvirt_uri }} net-undefine {{ ard_libvirt_network_name }}" + register: ard_net_undefine + changed_when: ard_net_undefine.rc == 0 + failed_when: false + +- name: Remove libvirt deployment image directory + file: + path: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}" + state: absent diff --git a/ansible/roles/ard_libvirt_image/tasks/main.yml b/ansible/roles/ard_libvirt_image/tasks/main.yml new file mode 100644 index 0000000..a643973 --- /dev/null +++ b/ansible/roles/ard_libvirt_image/tasks/main.yml @@ -0,0 +1,68 @@ +--- +- name: Resolve deployment image + set_fact: + ard_selected_image_name: "{{ ard_default_image }}" + ard_selected_image: "{{ ard_images[ard_default_image] }}" + ard_selected_libvirt_image: "{{ ard_images[ard_default_image].provider.libvirt }}" + +- name: Validate libvirt image URL is configured + assert: + that: + - ard_selected_libvirt_image.url is defined + - ard_selected_libvirt_image.url | length > 0 + - ard_selected_libvirt_image.url != None + fail_msg: "No libvirt image URL configured for {{ ard_selected_image_name }}" + +- name: Resolve ARD image cache directory + set_fact: + ard_image_cache_dir_resolved: "{{ ard_image_cache_dir | regex_replace('^~', lookup('env', 'HOME')) }}" + +- name: Ensure ARD image cache exists + file: + path: "{{ ard_image_cache_dir_resolved }}" + state: directory + mode: "0755" + +- name: Set cached base image path + set_fact: + ard_cached_base_image: "{{ ard_image_cache_dir_resolved + '/' + (ard_selected_libvirt_image.cache_filename | default(ard_selected_libvirt_image.url | basename)) }}" + +- name: Download base cloud image when missing + get_url: + url: "{{ ard_selected_libvirt_image.url }}" + dest: "{{ ard_cached_base_image }}" + mode: "0644" + checksum: "{{ ard_selected_libvirt_image.checksum | default(omit) }}" + when: ard_image_download | default(true) | bool + +- name: Ensure cached base image exists + stat: + path: "{{ ard_cached_base_image }}" + register: ard_cached_base_image_stat + +- name: Fail when cached base image is unavailable + assert: + that: + - ard_cached_base_image_stat.stat.exists + fail_msg: "Cached image {{ ard_cached_base_image }} is missing and ard_image_download is disabled or failed." + +- name: Ensure libvirt image directories exist + file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}" + +- name: Grant libvirt-qemu traverse access to XDG state parents + command: "setfacl -m u:libvirt-qemu:x {{ item }}" + changed_when: false + loop: + - "{{ lookup('env', 'HOME') }}" + - "{{ lookup('env', 'HOME') }}/.local" + - "{{ lookup('env', 'XDG_STATE_HOME') | default(lookup('env', 'HOME') + '/.local/state', true) }}" + when: ard_libvirt_image_dir is search(lookup('env', 'HOME')) + +- name: Grant libvirt-qemu access to ARD libvirt image tree + command: "setfacl -R -m u:libvirt-qemu:rx {{ ard_libvirt_image_dir }}" + changed_when: false diff --git a/ansible/roles/ard_libvirt_inventory/tasks/main.yml b/ansible/roles/ard_libvirt_inventory/tasks/main.yml new file mode 100644 index 0000000..dbd9386 --- /dev/null +++ b/ansible/roles/ard_libvirt_inventory/tasks/main.yml @@ -0,0 +1,65 @@ +--- +- name: Write ARD deployment inventory + copy: + dest: "{{ ard_deployment_dir }}/inventory.yaml" + mode: "0644" + content: | + --- + all: + hosts: + {% for node in ard_nodes %} + {{ node.name }}: + ansible_host: {{ node.networks[0].ip }} + ansible_user: stack + ansible_private_key_file: {{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + ard_deployment_name: {{ ard_deployment_name }} + ard_provider_resource_name: {{ node.provider_resource_name | default('ard-' + ard_deployment_name + '-' + node.name) }} + nodepool: + private_ipv4: {{ node.networks[0].ip }} + public_ipv4: {{ node.networks[0].ip }} + zuul: + executor: + log_root: /tmp/zuul_logs + work_root: /tmp/work_root + {% endfor %} + children: + {% set group_names = [] %} + {% for node in ard_nodes %} + {% for group in node.groups %} + {% if group not in group_names %} + {% set _ = group_names.append(group) %} + {% endif %} + {% endfor %} + {% endfor %} + {% for group in group_names %} + {{ group }}: + hosts: + {% for node in ard_nodes if group in node.groups %} + {{ node.name }}: + {% endfor %} + {% endfor %} + +- name: Write ARD provider state + copy: + dest: "{{ ard_deployment_dir }}/provider-state.yaml" + mode: "0644" + content: | + --- + ard_provider: libvirt + ard_deployment_name: {{ ard_deployment_name }} + ard_libvirt_uri: {{ ard_libvirt_uri }} + ard_libvirt_network_name: {{ ard_libvirt_network_name }} + ard_libvirt_image_dir: {{ ard_libvirt_image_dir }}/{{ ard_deployment_name }} + ard_cached_base_image: {{ ard_cached_base_image | default('') }} + ard_libvirt_base_image: {{ ard_libvirt_base_image | default('') }} + domains: + {% for node in ard_nodes %} + - inventory_name: {{ node.name }} + name: {{ node.provider_resource_name | default('ard-' + ard_deployment_name + '-' + node.name) }} + ip: {{ node.networks[0].ip }} + mac: {{ node.networks[0].mac }} + overlay: {{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ node.name }}.qcow2 + seed: {{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ node.name }}-seed.iso + console_log: {{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ node.name }}-console.log + {% endfor %} diff --git a/ansible/roles/ard_libvirt_network/tasks/main.yml b/ansible/roles/ard_libvirt_network/tasks/main.yml new file mode 100644 index 0000000..4e2f3fc --- /dev/null +++ b/ansible/roles/ard_libvirt_network/tasks/main.yml @@ -0,0 +1,43 @@ +--- +- name: Create rendered libvirt directory + file: + path: "{{ ard_deployment_dir }}/rendered/libvirt" + state: directory + mode: "0755" + +- name: Render libvirt network XML + copy: + dest: "{{ ard_deployment_dir }}/rendered/libvirt/network.xml" + mode: "0644" + content: | + + {{ ard_libvirt_network_name }} + + + + {% for node in ard_nodes %} + + {% endfor %} + + + + +- name: Check whether libvirt network exists + command: "virsh --connect {{ ard_libvirt_uri }} net-info {{ ard_libvirt_network_name }}" + register: ard_libvirt_network_info + changed_when: false + failed_when: false + +- name: Define libvirt network + command: "virsh --connect {{ ard_libvirt_uri }} net-define {{ ard_deployment_dir }}/rendered/libvirt/network.xml" + when: ard_libvirt_network_info.rc != 0 + +- name: Start libvirt network + command: "virsh --connect {{ ard_libvirt_uri }} net-start {{ ard_libvirt_network_name }}" + register: ard_libvirt_net_start + changed_when: ard_libvirt_net_start.rc == 0 + failed_when: ard_libvirt_net_start.rc != 0 and ('already active' not in (ard_libvirt_net_start.stderr | default(''))) + +- name: Mark libvirt network autostart + command: "virsh --connect {{ ard_libvirt_uri }} net-autostart {{ ard_libvirt_network_name }}" + changed_when: false diff --git a/ansible/roles/ard_libvirt_node/tasks/main.yml b/ansible/roles/ard_libvirt_node/tasks/main.yml new file mode 100644 index 0000000..8d54ca6 --- /dev/null +++ b/ansible/roles/ard_libvirt_node/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Ensure libvirt deployment directory exists + file: + path: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}" + state: directory + mode: "0755" + +- name: Create libvirt node resources + include_tasks: node.yml + loop: "{{ ard_nodes }}" + loop_control: + loop_var: ard_node diff --git a/ansible/roles/ard_libvirt_node/tasks/node.yml b/ansible/roles/ard_libvirt_node/tasks/node.yml new file mode 100644 index 0000000..1c9c949 --- /dev/null +++ b/ansible/roles/ard_libvirt_node/tasks/node.yml @@ -0,0 +1,207 @@ +--- +- name: Resolve node provider facts + set_fact: + ard_node_name: "{{ ard_node.name }}" + ard_node_resource_name: "{{ ard_node.provider_resource_name | default('ard-' + ard_deployment_name + '-' + ard_node.name) }}" + ard_node_flavor: "{{ ard_flavors[ard_node.flavor].provider.libvirt }}" + ard_node_root_disk_gb: "{{ ard_flavors[ard_node.flavor].root_disk_gb | default(80) }}" + ard_node_ip: "{{ ard_node.networks[0].ip }}" + ard_node_mac: "{{ ard_node.networks[0].mac }}" + ard_node_render_dir: "{{ ard_deployment_dir }}/rendered/libvirt/{{ ard_node.name }}" + ard_node_overlay: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ ard_node.name }}.qcow2" + ard_node_seed_local: "{{ ard_deployment_dir }}/rendered/libvirt/{{ ard_node.name }}/{{ ard_node.name }}-seed.iso" + ard_node_seed: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ ard_node.name }}-seed.iso" + ard_node_console_log: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ ard_node.name }}-console.log" + ard_node_nvram: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ ard_node.name }}-vars.fd" + +- name: Ensure rendered node directory exists + file: + path: "{{ ard_node_render_dir }}" + state: directory + mode: "0755" + +- name: Render cloud-init user-data + copy: + dest: "{{ ard_node_render_dir }}/user-data" + mode: "0644" + content: | + #cloud-config + hostname: {{ ard_node.hostname | default(ard_node.name) }} + manage_etc_hosts: true + ssh_authorized_keys: + - {{ ard_stack_public_key }} + users: + - default + - name: stack + gecos: ARD Stack User + shell: /bin/bash + sudo: ALL=(ALL) NOPASSWD:ALL + groups: sudo + lock_passwd: false + ssh_authorized_keys: + - {{ ard_stack_public_key }} + chpasswd: + expire: false + users: + - name: stack + password: {{ stack_user_password | default('tester') }} + type: text + package_update: true + packages: + - python3 + - sudo + - openssh-server + - qemu-guest-agent + - git + - rsync + - curl + - ca-certificates + write_files: + - path: /etc/default/grub.d/99-ard-serial-console.cfg + owner: root:root + permissions: '0644' + content: | + GRUB_TERMINAL="serial console" + GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1" + GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT console=tty0 console=ttyS0,115200n8" + - path: /etc/systemd/system/serial-getty@ttyS0.service.d/ard-autolog.conf + owner: root:root + permissions: '0644' + content: | + [Service] + TTYVTDisallocate=no + - path: /etc/ssh/sshd_config.d/90-ard-password-auth.conf + owner: root:root + permissions: '0644' + content: | + PasswordAuthentication yes + KbdInteractiveAuthentication yes + Banner /etc/issue.net + - path: /etc/issue + owner: root:root + permissions: '0644' + content: | + ARD DevStack VM + Login user: stack + Password fallback: tester + SSH key auth is preferred; the ARD stack key is installed for the default cloud user and stack. + + - path: /etc/issue.net + owner: root:root + permissions: '0644' + content: | + ARD DevStack VM + Login user: stack + Password fallback: tester + SSH key auth is preferred. + + - path: /etc/motd + owner: root:root + permissions: '0644' + content: | + ARD DevStack VM + Login user: stack + Password fallback: tester + SSH key auth is preferred; the ARD stack key is installed for the default cloud user and stack. + bootcmd: + - [ sh, -c, 'echo "ARD cloud-init bootcmd on $(hostname)" > /dev/ttyS0 || true' ] + runcmd: + - [ systemctl, daemon-reload ] + - [ systemctl, enable, --now, serial-getty@ttyS0.service ] + - [ systemctl, restart, ssh.service ] + - [ update-grub ] + - [ sh, -c, 'echo "ARD cloud-init runcmd complete on $(hostname)" > /dev/ttyS0 || true' ] + ssh_pwauth: true + +- name: Render cloud-init meta-data + copy: + dest: "{{ ard_node_render_dir }}/meta-data" + mode: "0644" + content: | + instance-id: {{ ard_node_resource_name }} + local-hostname: {{ ard_node.hostname | default(ard_node.name) }} + +- name: Render cloud-init network-config + copy: + dest: "{{ ard_node_render_dir }}/network-config" + mode: "0644" + content: | + version: 2 + ethernets: + ard-mgmt: + match: + macaddress: "{{ ard_node_mac }}" + set-name: eth0 + addresses: + - {{ ard_node_ip }}/24 + routes: + - to: default + via: {{ ard_libvirt_network_gateway }} + nameservers: + addresses: + - {{ ard_libvirt_network_gateway }} + - 1.1.1.1 + +- name: Create cloud-init seed ISO locally + command: "cloud-localds --network-config={{ ard_node_render_dir }}/network-config {{ ard_node_seed_local }} {{ ard_node_render_dir }}/user-data {{ ard_node_render_dir }}/meta-data" + changed_when: true + +- name: Copy cloud-init seed ISO into libvirt image directory + copy: + src: "{{ ard_node_seed_local }}" + dest: "{{ ard_node_seed }}" + mode: "0644" + remote_src: true + +- name: Create node qcow2 disk from cached base image + command: >- + qemu-img convert -O qcow2 + {{ ard_cached_base_image }} + {{ ard_node_overlay }} + args: + creates: "{{ ard_node_overlay }}" + +- name: Resize node qcow2 disk + command: "qemu-img resize {{ ard_node_overlay }} {{ ard_node_root_disk_gb }}G" + register: ard_node_disk_resize + changed_when: ard_node_disk_resize.rc == 0 + failed_when: ard_node_disk_resize.rc != 0 and ('Image is already larger' not in (ard_node_disk_resize.stderr | default(''))) + +- name: Ensure node console log exists + file: + path: "{{ ard_node_console_log }}" + state: touch + mode: "0664" + +- name: Grant libvirt-qemu access to node storage + command: "setfacl -m u:libvirt-qemu:rx {{ item }}" + changed_when: false + loop: + - "{{ ard_node_overlay }}" + - "{{ ard_node_seed }}" + +- name: Grant libvirt-qemu write access to node console log + command: "setfacl -m u:libvirt-qemu:rw {{ ard_node_console_log }}" + changed_when: false + +- name: Check whether libvirt domain exists + command: "virsh --connect {{ ard_libvirt_uri }} dominfo {{ ard_node_resource_name }}" + register: ard_libvirt_domain_info + changed_when: false + failed_when: false + +- name: Render libvirt domain XML + template: + src: domain.xml.j2 + dest: "{{ ard_node_render_dir }}/domain.xml" + mode: "0644" + +- name: Define libvirt domain from rendered XML + command: "virsh --connect {{ ard_libvirt_uri }} define {{ ard_node_render_dir }}/domain.xml" + when: ard_libvirt_domain_info.rc != 0 + +- name: Start libvirt domain + command: "virsh --connect {{ ard_libvirt_uri }} start {{ ard_node_resource_name }}" + register: ard_libvirt_domain_start + changed_when: ard_libvirt_domain_start.rc == 0 + failed_when: ard_libvirt_domain_start.rc != 0 and ('already active' not in (ard_libvirt_domain_start.stderr | default(''))) diff --git a/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 b/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 new file mode 100644 index 0000000..712b0c3 --- /dev/null +++ b/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 @@ -0,0 +1,54 @@ + + {{ ard_node_resource_name }} + {{ ard_node_flavor.memory_mb | int * 1024 }} + {{ ard_node_flavor.memory_mb | int * 1024 }} + {{ ard_node_flavor.vcpus }} + + hvm + /usr/share/OVMF/OVMF_CODE_4M.fd + {{ ard_node_nvram }} + + + + + + + + + destroy + restart + destroy + + /usr/bin/qemu-system-x86_64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /dev/urandom + + + diff --git a/ansible/roles/ard_libvirt_preflight/tasks/main.yml b/ansible/roles/ard_libvirt_preflight/tasks/main.yml new file mode 100644 index 0000000..78c6c84 --- /dev/null +++ b/ansible/roles/ard_libvirt_preflight/tasks/main.yml @@ -0,0 +1,41 @@ +--- +- name: Check required libvirt provider commands + shell: "command -v {{ item }}" + changed_when: false + loop: + - virsh + - qemu-img + - cloud-localds + - setfacl + - ssh-keygen + register: ard_libvirt_command_checks + failed_when: ard_libvirt_command_checks.rc != 0 + +- name: Check OVMF firmware exists + stat: + path: /usr/share/OVMF/OVMF_CODE_4M.fd + register: ard_ovmf_code + +- name: Require OVMF firmware for Debian cloud image boot + assert: + that: + - ard_ovmf_code.stat.exists + fail_msg: "OVMF firmware not found at /usr/share/OVMF/OVMF_CODE_4M.fd" + +- name: Check libvirt connection + command: "virsh --connect {{ ard_libvirt_uri }} uri" + changed_when: false + +- name: Ensure stack SSH key exists + command: ssh-keygen -t ed25519 -N '' -f {{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack + args: + creates: "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack" + +- name: Read stack public SSH key + slurp: + src: "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack.pub" + register: ard_stack_public_key_slurp + +- name: Set stack public SSH key fact + set_fact: + ard_stack_public_key: "{{ ard_stack_public_key_slurp.content | b64decode | trim }}" diff --git a/ansible/roles/ard_provider_cleanup/tasks/main.yml b/ansible/roles/ard_provider_cleanup/tasks/main.yml new file mode 100644 index 0000000..92a590a --- /dev/null +++ b/ansible/roles/ard_provider_cleanup/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Refuse to cleanup without ard_deployment_dir + assert: + that: + - ard_deployment_dir is defined + - ard_deployment_dir | length > 0 + - ard_deployment_dir != '/' + +- name: Remove ARD deployment workspace + file: + path: "{{ ard_deployment_dir }}" + state: absent diff --git a/ansible/roles/ard_provider_destroy/tasks/main.yml b/ansible/roles/ard_provider_destroy/tasks/main.yml new file mode 100644 index 0000000..b63904f --- /dev/null +++ b/ansible/roles/ard_provider_destroy/tasks/main.yml @@ -0,0 +1,13 @@ +--- +- name: Load ARD deployment variables + include_vars: + file: "{{ ard_deployment_dir }}/deployment.yaml" + +- name: Load ARD node variables if present + include_vars: + file: "{{ ard_deployment_dir }}/nodes.yaml" + when: lookup('ansible.builtin.file', ard_deployment_dir + '/nodes.yaml', errors='ignore') | length > 0 + +- name: Run provider destroy handling + include_role: + name: "ard_{{ ard_provider }}_destroy" diff --git a/ansible/roles/ard_provider_image/tasks/main.yml b/ansible/roles/ard_provider_image/tasks/main.yml new file mode 100644 index 0000000..25451b6 --- /dev/null +++ b/ansible/roles/ard_provider_image/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Load ARD deployment variables + include_vars: + file: "{{ ard_deployment_dir }}/deployment.yaml" + +- name: Load ARD node variables + include_vars: + file: "{{ ard_deployment_dir }}/nodes.yaml" + +- name: Run provider image handling + include_role: + name: "ard_{{ ard_provider }}_image" diff --git a/ansible/roles/ard_provider_inventory/tasks/main.yml b/ansible/roles/ard_provider_inventory/tasks/main.yml new file mode 100644 index 0000000..5bad470 --- /dev/null +++ b/ansible/roles/ard_provider_inventory/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Load ARD deployment variables + include_vars: + file: "{{ ard_deployment_dir }}/deployment.yaml" + +- name: Load ARD node variables + include_vars: + file: "{{ ard_deployment_dir }}/nodes.yaml" + +- name: Run provider inventory handling + include_role: + name: "ard_{{ ard_provider }}_inventory" diff --git a/ansible/roles/ard_provider_network/tasks/main.yml b/ansible/roles/ard_provider_network/tasks/main.yml new file mode 100644 index 0000000..514cdc6 --- /dev/null +++ b/ansible/roles/ard_provider_network/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Load ARD deployment variables + include_vars: + file: "{{ ard_deployment_dir }}/deployment.yaml" + +- name: Load ARD node variables + include_vars: + file: "{{ ard_deployment_dir }}/nodes.yaml" + +- name: Run provider network handling + include_role: + name: "ard_{{ ard_provider }}_network" diff --git a/ansible/roles/ard_provider_node/tasks/main.yml b/ansible/roles/ard_provider_node/tasks/main.yml new file mode 100644 index 0000000..a8752be --- /dev/null +++ b/ansible/roles/ard_provider_node/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Load ARD deployment variables + include_vars: + file: "{{ ard_deployment_dir }}/deployment.yaml" + +- name: Load ARD node variables + include_vars: + file: "{{ ard_deployment_dir }}/nodes.yaml" + +- name: Run provider node handling + include_role: + name: "ard_{{ ard_provider }}_node" diff --git a/ansible/roles/ard_provider_preflight/tasks/main.yml b/ansible/roles/ard_provider_preflight/tasks/main.yml new file mode 100644 index 0000000..635040e --- /dev/null +++ b/ansible/roles/ard_provider_preflight/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Load ARD deployment variables + include_vars: + file: "{{ ard_deployment_dir }}/deployment.yaml" + +- name: Run provider preflight + include_role: + name: "ard_{{ ard_provider }}_preflight" diff --git a/ansible/roles/ard_provider_render/tasks/main.yml b/ansible/roles/ard_provider_render/tasks/main.yml new file mode 100644 index 0000000..29ed1c0 --- /dev/null +++ b/ansible/roles/ard_provider_render/tasks/main.yml @@ -0,0 +1,185 @@ +--- +- name: Set ARD deployment render facts + set_fact: + ard_deployment_dir: "{{ ard_deployment_dir | default('deployments/devstack-1') }}" + ard_deployment_name: "{{ ard_deployment_name | default((ard_deployment_dir | default('deployments/devstack-1')) | basename) }}" + ard_provider: "{{ ard_provider | default('libvirt') }}" + +- name: Ensure ARD deployment directories exist + file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ ard_deployment_dir }}" + - "{{ ard_deployment_dir }}/devstack/group_vars" + - "{{ ard_deployment_dir }}/devstack/host_vars" + - "{{ ard_deployment_dir }}/rendered/libvirt" + - "{{ ard_deployment_dir }}/logs" + +- name: Render deployment provider inputs + copy: + dest: "{{ ard_deployment_dir }}/deployment.yaml" + force: false + mode: "0644" + content: | + --- + ard_provider: {{ ard_provider }} + ard_deployment_name: {{ ard_deployment_name }} + ard_resource_name_prefix: ard-{{ ard_deployment_name }} + + ard_default_image: debian-13 + ard_default_controller_flavor: devstack-control + ard_default_compute_flavor: devstack-compute + ard_default_vm_preference: devstack + + ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default(lookup('env', 'HOME') + '/.cache', true) }}/ard/images" + ard_image_download: true + ard_image_checksum_required: false + + ard_libvirt_uri: qemu:///system + ard_libvirt_pool: ard + ard_libvirt_network_name: ard-{{ ard_deployment_name }} + ard_libvirt_network_cidr: 192.168.96.0/24 + ard_libvirt_network_gateway: 192.168.96.1 + ard_libvirt_image_dir: "{{ lookup('env', 'XDG_STATE_HOME') | default(lookup('env', 'HOME') + '/.local/state', true) }}/ard/libvirt/images" + + ard_images: + debian-13: + os_family: debian + version: "13" + cloud_init: true + provider: + libvirt: + url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 + alternate_urls: + - https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2 + name: debian-13 + cache_filename: debian-13-genericcloud-amd64.qcow2 + format: qcow2 + cloud_init_datasource: NoCloud + ubuntu-24.04: + os_family: ubuntu + version: "24.04" + cloud_init: true + provider: + libvirt: + url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img + name: ubuntu-24.04 + cache_filename: noble-server-cloudimg-amd64.img + format: qcow2 + cloud_init_datasource: NoCloud + ubuntu-26.04: + os_family: ubuntu + version: "26.04" + cloud_init: true + provider: + libvirt: + url: null + name: ubuntu-26.04 + cache_filename: ubuntu-26.04-cloudimg-amd64.img + format: qcow2 + cloud_init_datasource: NoCloud + + ard_flavors: + devstack-control: + vcpus: 8 + memory: 16Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 16384 + cpu_mode: host-passthrough + devstack-compute: + vcpus: 8 + memory: 8Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 8192 + cpu_mode: host-passthrough + +- name: Render two-node topology inputs + copy: + dest: "{{ ard_deployment_dir }}/nodes.yaml" + force: false + mode: "0644" + content: | + --- + ard_nodes: + - name: controller + hostname: controller + provider_resource_name: ard-{{ ard_deployment_name }}-controller + groups: + - controller + - switch + image: debian-13 + flavor: devstack-control + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.96.2 + mac: "52:54:00:96:00:02" + profiles: + - ssh + - nested_virt + - ovn + - name: compute1 + hostname: compute1 + provider_resource_name: ard-{{ ard_deployment_name }}-compute1 + groups: + - compute + - peers + - subnode + image: debian-13 + flavor: devstack-compute + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.96.3 + mac: "52:54:00:96:00:03" + profiles: + - ssh + - nested_virt + - ovn + +- name: Render common DevStack vars + copy: + dest: "{{ ard_deployment_dir }}/devstack/common.yaml" + force: false + mode: "0644" + content: | + --- + devstack_branch: master + run_devstack: true + enable_ceph: false + configure_vdpa: false + external_bridge_mtu: 1450 + devstack_libvirt_type: kvm + stack_user_password: tester + +- name: Render controller DevStack vars + copy: + dest: "{{ ard_deployment_dir }}/devstack/group_vars/controller.yaml" + force: false + mode: "0644" + content: | + --- + controller_localrc_extra: {} + controller_local_conf_extra: {} + controller_services_extra: {} + +- name: Render compute DevStack vars + copy: + dest: "{{ ard_deployment_dir }}/devstack/group_vars/compute.yaml" + force: false + mode: "0644" + content: | + --- + compute_localrc_extra: {} + compute_local_conf_extra: {} + compute_services_extra: {} diff --git a/deployments/.keep b/deployments/.keep new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0c8bf7a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "ard" +version = "0.1.0" +description = "Ansible Role DevStack local provider tooling" +requires-python = ">=3.13" +dependencies = [ + "ansible-core==2.18.7", +] + +[tool.uv] +package = false diff --git a/submodules/devstack b/submodules/devstack index aac6b6c..d69725d 160000 --- a/submodules/devstack +++ b/submodules/devstack @@ -1 +1 @@ -Subproject commit aac6b6c7912b3feae4b68789508bee4bf1544731 +Subproject commit d69725d9d48c11491f06185d708bb2a1ebacd544 diff --git a/submodules/openstack-zuul-jobs b/submodules/openstack-zuul-jobs index 310132d..aebda82 160000 --- a/submodules/openstack-zuul-jobs +++ b/submodules/openstack-zuul-jobs @@ -1 +1 @@ -Subproject commit 310132db5afdc30c5bd0b1150f620f43e016a9cf +Subproject commit aebda82f8822e38db5bbd25ab31ea110792e8c2b diff --git a/submodules/zuul-jobs b/submodules/zuul-jobs index 57df8b9..e1387d2 160000 --- a/submodules/zuul-jobs +++ b/submodules/zuul-jobs @@ -1 +1 @@ -Subproject commit 57df8b9d6d38bbc581e4543ddb59cab77082e4a9 +Subproject commit e1387d2973e9745660e32631bf41a3d4dff70bbb diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..1fe3b2e --- /dev/null +++ b/uv.lock @@ -0,0 +1,255 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "ansible-core" +version = "2.18.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "resolvelib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/cd25e1af669941fbb5c3d7ac4494cf4a288cb11f53225648d552f8bd8e54/ansible_core-2.18.7.tar.gz", hash = "sha256:1a129bf9fcd5dca2b17e83ce77147ee2fbc3c51a4958970152897cc5b6d0aae7", size = 3090256, upload-time = "2025-07-15T17:49:24.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/a7/568e51c20f49c9e76a555a876ed641ecc59df29e93868f24cdf8c3289f6a/ansible_core-2.18.7-py3-none-any.whl", hash = "sha256:ac42ecb480fb98890d338072f7298cd462fb2117da6700d989c7ae688962ba69", size = 2209456, upload-time = "2025-07-15T17:49:22.549Z" }, +] + +[[package]] +name = "ard" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "ansible-core" }, +] + +[package.metadata] +requires-dist = [{ name = "ansible-core", specifier = "==2.18.7" }] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "resolvelib" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/10/f699366ce577423cbc3df3280063099054c23df70856465080798c6ebad6/resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", size = 21065, upload-time = "2023-03-09T05:10:38.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" }, +] From 409994101b1f3e8e30d6c3372cab767181e1e7da Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Wed, 3 Jun 2026 21:33:27 +0100 Subject: [PATCH 02/11] Fix DevStack provider end-to-end deployment The libvirt provider workflow can now drive a full two-node DevStack deployment using the current upstream DevStack roles. The deployment loader now uses stat checks for optional workspace variables, waits for cloud-init bootstrap to complete before running DevStack, and keeps the Ansible runtime dependencies needed by the existing roles. This also refreshes the DevStack service defaults to match current OVN naming by using q-ovn-agent instead of the old metadata-agent service. The controller keeps Horizon enabled for local development while compute nodes only run the services required for the subnode role. Repo cache synchronization no longer preserves host ownership and now excludes generated tox, build, egg, pycache, and pyc artifacts so local caches remain reusable source caches rather than runtime snapshots. Signed-off-by: Sean Mooney --- ansible/deploy_multinode_devstack.yaml | 28 ++++++++++--- ansible/playbooks/ard-deploy-devstack.yaml | 21 ++++++++++ .../roles/ard_devstack_config/tasks/main.yml | 39 ++++++++++++++----- .../roles/devstack_compute/defaults/main.yml | 12 ++++-- .../devstack_controller/defaults/main.yml | 6 ++- .../roles/ensure_stack_user/tasks/main.yml | 6 +-- pyproject.toml | 2 + uv.lock | 26 ++++++++++++- 8 files changed, 117 insertions(+), 23 deletions(-) diff --git a/ansible/deploy_multinode_devstack.yaml b/ansible/deploy_multinode_devstack.yaml index 61c4b82..0f5dd1d 100644 --- a/ansible/deploy_multinode_devstack.yaml +++ b/ansible/deploy_multinode_devstack.yaml @@ -104,6 +104,16 @@ synchronize: dest: '/opt/stack/' src: '~/.cache/ard/repos/{{item}}' + rsync_opts: + - "--no-owner" + - "--no-group" + - "--exclude=.tox/" + - "--exclude=.eggs/" + - "--exclude=*.egg-info/" + - "--exclude=build/" + - "--exclude=dist/" + - "--exclude=__pycache__/" + - "--exclude=*.pyc" with_items: "{{repo_names.stdout_lines}}" when: "use_local_cache | default(true) | bool" @@ -125,12 +135,10 @@ work_root: /tmp/work_root - name: sync controller data to subnodes - hosts: subnode + hosts: all roles: - - { - role: sync-devstack-data, - when: "(run_devstack | default(true) | bool) and (controller_services_extra is defined and controller_services_extra['tls-proxy'] is defined and controller_services_extra['tls-proxy'] | bool)" - } + - role: sync-devstack-data + when: run_devstack | default(true) | bool - name: sync ceph configs to subnodes hosts: subnode @@ -190,6 +198,16 @@ mode: pull src: '/opt/stack/{{ item }}' dest: '~/.cache/ard/repos/' + rsync_opts: + - "--no-owner" + - "--no-group" + - "--exclude=.tox/" + - "--exclude=.eggs/" + - "--exclude=*.egg-info/" + - "--exclude=build/" + - "--exclude=dist/" + - "--exclude=__pycache__/" + - "--exclude=*.pyc" with_items: "{{repo_names.stdout_lines}}" when: "use_local_cache | default(true) | bool" diff --git a/ansible/playbooks/ard-deploy-devstack.yaml b/ansible/playbooks/ard-deploy-devstack.yaml index b7a99c3..c714e97 100644 --- a/ansible/playbooks/ard-deploy-devstack.yaml +++ b/ansible/playbooks/ard-deploy-devstack.yaml @@ -12,5 +12,26 @@ roles: - ard_devstack_config +- name: Wait for provider node bootstrap to finish + hosts: all + gather_facts: false + tasks: + - name: Wait for cloud-init completion + become: true + ansible.builtin.shell: | + if command -v cloud-init >/dev/null 2>&1; then + cloud-init status --wait + else + echo 'cloud-init not installed; assuming bootstrap already consumed it' + fi + register: ard_cloud_init_status + changed_when: false + failed_when: >- + ard_cloud_init_status.rc not in [0, 2] + or ( + 'status: done' not in ard_cloud_init_status.stdout + and 'cloud-init not installed' not in ard_cloud_init_status.stdout + ) + - name: Deploy ARD multinode DevStack import_playbook: ../deploy_multinode_devstack.yaml diff --git a/ansible/roles/ard_devstack_config/tasks/main.yml b/ansible/roles/ard_devstack_config/tasks/main.yml index 5c74a94..2afcc88 100644 --- a/ansible/roles/ard_devstack_config/tasks/main.yml +++ b/ansible/roles/ard_devstack_config/tasks/main.yml @@ -1,16 +1,35 @@ --- +- name: Check deployment common DevStack vars + ansible.builtin.stat: + path: "{{ ard_deployment_dir }}/devstack/common.yaml" + register: ard_devstack_common_vars_file + delegate_to: localhost + - name: Load deployment common DevStack vars - include_vars: - file: "{{ ard_deployment_dir }}/devstack/common.yaml" - when: lookup('ansible.builtin.file', ard_deployment_dir + '/devstack/common.yaml', errors='ignore') | length > 0 + ansible.builtin.include_vars: + file: "{{ ard_devstack_common_vars_file.stat.path }}" + when: ard_devstack_common_vars_file.stat.exists -- name: Load deployment group DevStack vars - include_vars: - file: "{{ ard_deployment_dir }}/devstack/group_vars/{{ item }}.yaml" +- name: Check deployment group DevStack var files + ansible.builtin.stat: + path: "{{ ard_deployment_dir }}/devstack/group_vars/{{ item }}.yaml" loop: "{{ group_names }}" - when: lookup('ansible.builtin.file', ard_deployment_dir + '/devstack/group_vars/' + item + '.yaml', errors='ignore') | length > 0 + register: ard_devstack_group_var_files + delegate_to: localhost + +- name: Load deployment group DevStack vars + ansible.builtin.include_vars: + file: "{{ item.stat.path }}" + loop: "{{ ard_devstack_group_var_files.results }}" + when: item.stat.exists + +- name: Check deployment host DevStack vars + ansible.builtin.stat: + path: "{{ ard_deployment_dir }}/devstack/host_vars/{{ inventory_hostname }}.yaml" + register: ard_devstack_host_vars_file + delegate_to: localhost - name: Load deployment host DevStack vars - include_vars: - file: "{{ ard_deployment_dir }}/devstack/host_vars/{{ inventory_hostname }}.yaml" - when: lookup('ansible.builtin.file', ard_deployment_dir + '/devstack/host_vars/' + inventory_hostname + '.yaml', errors='ignore') | length > 0 + ansible.builtin.include_vars: + file: "{{ ard_devstack_host_vars_file.stat.path }}" + when: ard_devstack_host_vars_file.stat.exists diff --git a/ansible/roles/devstack_compute/defaults/main.yml b/ansible/roles/devstack_compute/defaults/main.yml index dbb2fa1..4da043d 100644 --- a/ansible/roles/devstack_compute/defaults/main.yml +++ b/ansible/roles/devstack_compute/defaults/main.yml @@ -53,20 +53,26 @@ compute_services: # Ignore any default set by devstack. Emit a "disable_all_services". base: false # Shared services + dstat: false + file_tracker: true + memory_tracker: true tls-proxy: true # Nova services n-cpu: true n-novnc: true # Placement service + openstack-cli-server: true placement-client: true # OVN services ovn-controller: true - ovn-northd: true ovs-vswitchd: true ovsdb-server: true - q-ovn-metadata-agent: true + q-ovn-agent: true # Cinder - c-vol: false + c-bak: true + c-vol: true + horizon: false + tempest: false compute_local_conf_extra: {} compute_local_conf: post-config: diff --git a/ansible/roles/devstack_controller/defaults/main.yml b/ansible/roles/devstack_controller/defaults/main.yml index db27080..03a6b31 100644 --- a/ansible/roles/devstack_controller/defaults/main.yml +++ b/ansible/roles/devstack_controller/defaults/main.yml @@ -48,7 +48,10 @@ controller_services: # Ignore any default set by devstack. Emit a "disable_all_services". base: false # Shared services + dstat: false etcd3: true + file_tracker: true + memory_tracker: true mysql: true rabbit: true tls-proxy: true @@ -64,6 +67,7 @@ controller_services: n-novnc: true n-sch: true # Placement service + openstack-cli-server: true placement-api: true # OVN services ovn-controller: true @@ -72,7 +76,7 @@ controller_services: ovsdb-server: true # Neutron services q-svc: true - q-ovn-metadata-agent: true + q-ovn-agent: true # Swift services s-account: true s-container: true diff --git a/ansible/roles/ensure_stack_user/tasks/main.yml b/ansible/roles/ensure_stack_user/tasks/main.yml index 7ed3bbe..a676c29 100644 --- a/ansible/roles/ensure_stack_user/tasks/main.yml +++ b/ansible/roles/ensure_stack_user/tasks/main.yml @@ -65,7 +65,7 @@ ansible.posix.authorized_key: user: stack state: present - key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/{{ssh_key_filename}}.pub') }}" + key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/' + ssh_key_filename + '.pub') }}" - name: setup root user ssh keys become: yes @@ -88,7 +88,7 @@ ansible.posix.authorized_key: user: root state: present - key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/{{ssh_key_filename}}.pub') }}" + key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/' + ssh_key_filename + '.pub') }}" - name: setup ansible user ssh keys tags: ssh @@ -110,7 +110,7 @@ ansible.posix.authorized_key: user: "{{ ansible_user }}" state: present - key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/{{ssh_key_filename}}.pub') }}" + key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/' + ssh_key_filename + '.pub') }}" - name: update ansible_user diff --git a/pyproject.toml b/pyproject.toml index 0c8bf7a..6537026 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,8 @@ description = "Ansible Role DevStack local provider tooling" requires-python = ">=3.13" dependencies = [ "ansible-core==2.18.7", + "passlib==1.7.4", + "netaddr==1.3.0", ] [tool.uv] diff --git a/uv.lock b/uv.lock index 1fe3b2e..7171d6f 100644 --- a/uv.lock +++ b/uv.lock @@ -24,10 +24,16 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "ansible-core" }, + { name = "netaddr" }, + { name = "passlib" }, ] [package.metadata] -requires-dist = [{ name = "ansible-core", specifier = "==2.18.7" }] +requires-dist = [ + { name = "ansible-core", specifier = "==2.18.7" }, + { name = "netaddr", specifier = "==1.3.0" }, + { name = "passlib", specifier = "==1.7.4" }, +] [[package]] name = "cffi" @@ -191,6 +197,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "netaddr" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/90/188b2a69654f27b221fba92fda7217778208532c962509e959a9cee5229d/netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a", size = 2260504, upload-time = "2024-05-28T21:30:37.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023, upload-time = "2024-05-28T21:30:34.191Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -200,6 +215,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, +] + [[package]] name = "pycparser" version = "3.0" From 343d0ab4dd67a221e561d691ce8eb2f13c190249 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Thu, 4 Jun 2026 21:20:37 +0100 Subject: [PATCH 03/11] Add podman molecule coverage for role tests Replace incomplete delegated Molecule skeletons for role-level tests with real Podman-backed scenarios. The converted scenarios cover the existing lightweight roles without introducing Vagrant or libvirt requirements, add role-specific verification, and keep top-level deployment Molecule scenarios out of scope. Add Molecule and the Podman plugin to the uv-managed environment, plus root and role-local make targets so role scenarios can be run from either the repository root or individual role directories. Document the role-level Molecule workflow in the README. Signed-off-by: Sean Mooney --- Makefile | 17 + README.md | 36 ++ ansible/roles/deploy_install_yamls/Makefile | 4 + .../molecule/default/Dockerfile.j2 | 11 + .../molecule/default/converge.yml | 41 ++- .../molecule/default/create.yml | 35 -- .../molecule/default/destroy.yml | 24 -- .../molecule/default/molecule.yml | 30 +- .../molecule/default/verify.yml | 20 +- ansible/roles/ensure_kustomize/Makefile | 4 + .../molecule/default/Dockerfile.j2 | 12 + .../molecule/default/converge.yml | 13 +- .../molecule/default/create.yml | 35 -- .../molecule/default/destroy.yml | 24 -- .../molecule/default/molecule.yml | 30 +- .../molecule/default/verify.yml | 15 +- ansible/roles/prepare_dev_tools/Makefile | 4 + .../molecule/default/Dockerfile.j2 | 11 + .../molecule/default/converge.yml | 13 +- .../molecule/default/create.yml | 35 -- .../molecule/default/destroy.yml | 24 -- .../molecule/default/molecule.yml | 30 +- .../molecule/default/verify.yml | 19 +- ansible/roles/print_debug_ip/Makefile | 4 + .../molecule/default/Dockerfile.j2 | 9 + .../molecule/default/converge.yml | 6 +- .../molecule/default/create.yml | 35 -- .../molecule/default/destroy.yml | 24 -- .../molecule/default/molecule.yml | 30 +- .../molecule/default/verify.yml | 10 +- pyproject.toml | 2 + uv.lock | 313 ++++++++++++++++++ 32 files changed, 644 insertions(+), 276 deletions(-) create mode 100644 Makefile create mode 100644 ansible/roles/deploy_install_yamls/Makefile create mode 100644 ansible/roles/deploy_install_yamls/molecule/default/Dockerfile.j2 delete mode 100644 ansible/roles/deploy_install_yamls/molecule/default/create.yml delete mode 100644 ansible/roles/deploy_install_yamls/molecule/default/destroy.yml create mode 100644 ansible/roles/ensure_kustomize/Makefile create mode 100644 ansible/roles/ensure_kustomize/molecule/default/Dockerfile.j2 delete mode 100644 ansible/roles/ensure_kustomize/molecule/default/create.yml delete mode 100644 ansible/roles/ensure_kustomize/molecule/default/destroy.yml create mode 100644 ansible/roles/prepare_dev_tools/Makefile create mode 100644 ansible/roles/prepare_dev_tools/molecule/default/Dockerfile.j2 delete mode 100644 ansible/roles/prepare_dev_tools/molecule/default/create.yml delete mode 100644 ansible/roles/prepare_dev_tools/molecule/default/destroy.yml create mode 100644 ansible/roles/print_debug_ip/Makefile create mode 100644 ansible/roles/print_debug_ip/molecule/default/Dockerfile.j2 delete mode 100644 ansible/roles/print_debug_ip/molecule/default/create.yml delete mode 100644 ansible/roles/print_debug_ip/molecule/default/destroy.yml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e74979a --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: molecule-test molecule-role-tests molecule-role-% + +MOLECULE_ROLE_DIRS := $(sort $(dir $(wildcard ansible/roles/*/molecule/*/molecule.yml))) + +molecule-test: molecule-role-tests + +molecule-role-tests: + @set -e; \ + for scenario_dir in $(MOLECULE_ROLE_DIRS); do \ + role_dir=$${scenario_dir%/molecule/*/}; \ + scenario=$${scenario_dir%/}; scenario=$${scenario##*/}; \ + echo "==> $$role_dir :: $$scenario"; \ + (cd $$role_dir && uv run --project ../../.. molecule test -s $$scenario); \ + done + +molecule-role-%: + cd ansible/roles/$* && uv run --project ../../.. molecule test diff --git a/README.md b/README.md index 99e23f1..a83cfcf 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,42 @@ vagrant ssh crc [stack@crc ~]$ ssh -i ~/.crc/machines/crc/id_ecdsa core@`crc ip` ``` +Role molecule tests +------------------- + +The uv-managed development environment includes Molecule and the Podman +Molecule plugin. Role-level Molecule scenarios live under +``ansible/roles/*/molecule`` and are intended to avoid Vagrant for role unit +coverage. Use containers for roles that do not need full VM semantics; reserve +ARD libvirt-backed scenarios for roles that need a real VM, systemd, cloud-init, +libvirt, or DevStack node behavior. + +Run all role-level Molecule scenarios from the repository root: + +``` +make molecule-test +``` + +Run one role-level scenario from the repository root: + +``` +make molecule-role-ensure_kustomize +``` + +You can also run a role scenario from the role directory. Activate the uv venv +first, then run Molecule directly or via the role-local make target: + +``` +source ../../../.venv/bin/activate +cd ansible/roles/ensure_kustomize +molecule test +# or +make molecule-test +``` + +Top-level ``molecule/`` scenarios are larger deployment scenarios and are not +part of this role-level test workflow. + Configure localhost to access deployed OpenShift crc ---------------------------------------------------- > **NOTE**: This overwrites ``~/.kube/config`` ! diff --git a/ansible/roles/deploy_install_yamls/Makefile b/ansible/roles/deploy_install_yamls/Makefile new file mode 100644 index 0000000..76a48d8 --- /dev/null +++ b/ansible/roles/deploy_install_yamls/Makefile @@ -0,0 +1,4 @@ +.PHONY: molecule-test + +molecule-test: + molecule test diff --git a/ansible/roles/deploy_install_yamls/molecule/default/Dockerfile.j2 b/ansible/roles/deploy_install_yamls/molecule/default/Dockerfile.j2 new file mode 100644 index 0000000..d4fb2e5 --- /dev/null +++ b/ansible/roles/deploy_install_yamls/molecule/default/Dockerfile.j2 @@ -0,0 +1,11 @@ +FROM docker.io/library/debian:13 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + git \ + make \ + python3 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/ansible/roles/deploy_install_yamls/molecule/default/converge.yml b/ansible/roles/deploy_install_yamls/molecule/default/converge.yml index 76c7b48..b6a7f7f 100644 --- a/ansible/roles/deploy_install_yamls/molecule/default/converge.yml +++ b/ansible/roles/deploy_install_yamls/molecule/default/converge.yml @@ -1,7 +1,42 @@ --- - name: Converge hosts: all + vars: + repo_dir: /tmp/ard-molecule/repos + install_yamls_repo_url: file:///tmp/install_yamls-source + install_yamls_branch: master + pre_tasks: + - name: Install test fixture dependencies + ansible.builtin.shell: | + apt-get update + apt-get install -y --no-install-recommends git make + changed_when: false + + - name: Create fake install_yamls source repository + ansible.builtin.file: + path: /tmp/install_yamls-source + state: directory + mode: "0755" + + - name: Write fake crc_storage target + ansible.builtin.copy: + dest: /tmp/install_yamls-source/Makefile + mode: "0644" + content: | + crc_storage: + @touch crc_storage_ran + + - name: Initialize fake install_yamls git repository + ansible.builtin.shell: | + git init --initial-branch=master + git config user.email molecule@example.invalid + git config user.name Molecule + git add Makefile + git commit -m 'Initial fake install_yamls repository' + args: + chdir: /tmp/install_yamls-source + creates: /tmp/install_yamls-source/.git/HEAD tasks: - - name: "Include deploy_install_yamls" - include_role: - name: "deploy_install_yamls" + - name: Include deploy_install_yamls + ansible.builtin.include_role: + name: deploy_install_yamls diff --git a/ansible/roles/deploy_install_yamls/molecule/default/create.yml b/ansible/roles/deploy_install_yamls/molecule/default/create.yml deleted file mode 100644 index a5ffae3..0000000 --- a/ansible/roles/deploy_install_yamls/molecule/default/create.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -- name: Create - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - tasks: - - # TODO: Developer must implement and populate 'server' variable - - - when: server.changed | default(false) | bool - block: - - name: Populate instance config dict - set_fact: - instance_conf_dict: { - 'instance': "{{ }}", - 'address': "{{ }}", - 'user': "{{ }}", - 'port': "{{ }}", - 'identity_file': "{{ }}", } - with_items: "{{ server.results }}" - register: instance_config_dict - - - name: Convert instance config dict to a list - set_fact: - instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" - - - name: Dump instance config - copy: - content: | - # Molecule managed - - {{ instance_conf | to_json | from_json | to_yaml }} - dest: "{{ molecule_instance_config }}" - mode: 0600 diff --git a/ansible/roles/deploy_install_yamls/molecule/default/destroy.yml b/ansible/roles/deploy_install_yamls/molecule/default/destroy.yml deleted file mode 100644 index 49047f0..0000000 --- a/ansible/roles/deploy_install_yamls/molecule/default/destroy.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -- name: Destroy - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - tasks: - # Developer must implement. - - # Mandatory configuration for Molecule to function. - - - name: Populate instance config - set_fact: - instance_conf: {} - - - name: Dump instance config - copy: - content: | - # Molecule managed - - {{ instance_conf | to_json | from_json | to_yaml }} - dest: "{{ molecule_instance_config }}" - mode: 0600 - when: server.changed | default(false) | bool diff --git a/ansible/roles/deploy_install_yamls/molecule/default/molecule.yml b/ansible/roles/deploy_install_yamls/molecule/default/molecule.yml index 74c8557..3698c6f 100644 --- a/ansible/roles/deploy_install_yamls/molecule/default/molecule.yml +++ b/ansible/roles/deploy_install_yamls/molecule/default/molecule.yml @@ -1,11 +1,37 @@ --- +role_name_check: 2 dependency: name: galaxy + enabled: false driver: - name: delegated + name: podman platforms: - - name: instance + - name: deploy-install-yamls-default + image: docker.io/library/debian:13 + dockerfile: Dockerfile.j2 + pre_build_image: false + pull: true + command: sleep infinity + tty: true provisioner: name: ansible + env: + ANSIBLE_REMOTE_TMP: /tmp/.ansible/tmp verifier: name: ansible +scenario: + test_sequence: + - dependency + - destroy + - syntax + - create + - converge + - idempotence + - verify + - destroy + create_sequence: + - dependency + - create + destroy_sequence: + - dependency + - destroy diff --git a/ansible/roles/deploy_install_yamls/molecule/default/verify.yml b/ansible/roles/deploy_install_yamls/molecule/default/verify.yml index 79044cd..7c71575 100644 --- a/ansible/roles/deploy_install_yamls/molecule/default/verify.yml +++ b/ansible/roles/deploy_install_yamls/molecule/default/verify.yml @@ -1,10 +1,20 @@ --- -# This is an example playbook to execute Ansible tests. - - name: Verify hosts: all gather_facts: false tasks: - - name: Example assertion - assert: - that: true + - name: Check install_yamls was cloned + ansible.builtin.stat: + path: /tmp/ard-molecule/repos/install_yamls/.git + register: install_yamls_git + + - name: Check crc_storage target was run + ansible.builtin.stat: + path: /tmp/ard-molecule/repos/install_yamls/.storage_provisioned + register: install_yamls_storage + + - name: Assert deploy_install_yamls effects + ansible.builtin.assert: + that: + - install_yamls_git.stat.isdir | default(false) + - install_yamls_storage.stat.exists diff --git a/ansible/roles/ensure_kustomize/Makefile b/ansible/roles/ensure_kustomize/Makefile new file mode 100644 index 0000000..76a48d8 --- /dev/null +++ b/ansible/roles/ensure_kustomize/Makefile @@ -0,0 +1,4 @@ +.PHONY: molecule-test + +molecule-test: + molecule test diff --git a/ansible/roles/ensure_kustomize/molecule/default/Dockerfile.j2 b/ansible/roles/ensure_kustomize/molecule/default/Dockerfile.j2 new file mode 100644 index 0000000..e9d75b1 --- /dev/null +++ b/ansible/roles/ensure_kustomize/molecule/default/Dockerfile.j2 @@ -0,0 +1,12 @@ +FROM docker.io/library/debian:13 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + python3 \ + sudo \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/ansible/roles/ensure_kustomize/molecule/default/converge.yml b/ansible/roles/ensure_kustomize/molecule/default/converge.yml index 855550c..f58723b 100644 --- a/ansible/roles/ensure_kustomize/molecule/default/converge.yml +++ b/ansible/roles/ensure_kustomize/molecule/default/converge.yml @@ -1,7 +1,14 @@ --- - name: Converge hosts: all + pre_tasks: + - name: Install role dependencies + ansible.builtin.shell: | + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl sudo + changed_when: false + tasks: - - name: "Include ensure_kustomize" - include_role: - name: "ensure_kustomize" + - name: Include ensure_kustomize + ansible.builtin.include_role: + name: ensure_kustomize diff --git a/ansible/roles/ensure_kustomize/molecule/default/create.yml b/ansible/roles/ensure_kustomize/molecule/default/create.yml deleted file mode 100644 index a5ffae3..0000000 --- a/ansible/roles/ensure_kustomize/molecule/default/create.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -- name: Create - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - tasks: - - # TODO: Developer must implement and populate 'server' variable - - - when: server.changed | default(false) | bool - block: - - name: Populate instance config dict - set_fact: - instance_conf_dict: { - 'instance': "{{ }}", - 'address': "{{ }}", - 'user': "{{ }}", - 'port': "{{ }}", - 'identity_file': "{{ }}", } - with_items: "{{ server.results }}" - register: instance_config_dict - - - name: Convert instance config dict to a list - set_fact: - instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" - - - name: Dump instance config - copy: - content: | - # Molecule managed - - {{ instance_conf | to_json | from_json | to_yaml }} - dest: "{{ molecule_instance_config }}" - mode: 0600 diff --git a/ansible/roles/ensure_kustomize/molecule/default/destroy.yml b/ansible/roles/ensure_kustomize/molecule/default/destroy.yml deleted file mode 100644 index 49047f0..0000000 --- a/ansible/roles/ensure_kustomize/molecule/default/destroy.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -- name: Destroy - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - tasks: - # Developer must implement. - - # Mandatory configuration for Molecule to function. - - - name: Populate instance config - set_fact: - instance_conf: {} - - - name: Dump instance config - copy: - content: | - # Molecule managed - - {{ instance_conf | to_json | from_json | to_yaml }} - dest: "{{ molecule_instance_config }}" - mode: 0600 - when: server.changed | default(false) | bool diff --git a/ansible/roles/ensure_kustomize/molecule/default/molecule.yml b/ansible/roles/ensure_kustomize/molecule/default/molecule.yml index 74c8557..c1dd762 100644 --- a/ansible/roles/ensure_kustomize/molecule/default/molecule.yml +++ b/ansible/roles/ensure_kustomize/molecule/default/molecule.yml @@ -1,11 +1,37 @@ --- +role_name_check: 2 dependency: name: galaxy + enabled: false driver: - name: delegated + name: podman platforms: - - name: instance + - name: ensure-kustomize-default + image: docker.io/library/debian:13 + dockerfile: Dockerfile.j2 + pre_build_image: false + pull: true + command: sleep infinity + tty: true provisioner: name: ansible + env: + ANSIBLE_REMOTE_TMP: /tmp/.ansible/tmp verifier: name: ansible +scenario: + test_sequence: + - dependency + - destroy + - syntax + - create + - converge + - idempotence + - verify + - destroy + create_sequence: + - dependency + - create + destroy_sequence: + - dependency + - destroy diff --git a/ansible/roles/ensure_kustomize/molecule/default/verify.yml b/ansible/roles/ensure_kustomize/molecule/default/verify.yml index 79044cd..8755dbf 100644 --- a/ansible/roles/ensure_kustomize/molecule/default/verify.yml +++ b/ansible/roles/ensure_kustomize/molecule/default/verify.yml @@ -1,10 +1,15 @@ --- -# This is an example playbook to execute Ansible tests. - - name: Verify hosts: all gather_facts: false tasks: - - name: Example assertion - assert: - that: true + - name: Run kustomize + ansible.builtin.command: /usr/bin/kustomize version + register: kustomize_version + changed_when: false + + - name: Assert kustomize reports a version + ansible.builtin.assert: + that: + - kustomize_version.rc == 0 + - kustomize_version.stdout | length > 0 diff --git a/ansible/roles/prepare_dev_tools/Makefile b/ansible/roles/prepare_dev_tools/Makefile new file mode 100644 index 0000000..76a48d8 --- /dev/null +++ b/ansible/roles/prepare_dev_tools/Makefile @@ -0,0 +1,4 @@ +.PHONY: molecule-test + +molecule-test: + molecule test diff --git a/ansible/roles/prepare_dev_tools/molecule/default/Dockerfile.j2 b/ansible/roles/prepare_dev_tools/molecule/default/Dockerfile.j2 new file mode 100644 index 0000000..c5d53bc --- /dev/null +++ b/ansible/roles/prepare_dev_tools/molecule/default/Dockerfile.j2 @@ -0,0 +1,11 @@ +FROM docker.io/library/debian:13 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + python3 \ + python3-apt \ + sudo \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/ansible/roles/prepare_dev_tools/molecule/default/converge.yml b/ansible/roles/prepare_dev_tools/molecule/default/converge.yml index c53c20c..10272e4 100644 --- a/ansible/roles/prepare_dev_tools/molecule/default/converge.yml +++ b/ansible/roles/prepare_dev_tools/molecule/default/converge.yml @@ -1,7 +1,14 @@ --- - name: Converge hosts: all + pre_tasks: + - name: Install sudo for become support + ansible.builtin.shell: | + apt-get update + apt-get install -y --no-install-recommends sudo + changed_when: false + tasks: - - name: "Include prepare_dev_tools" - include_role: - name: "prepare_dev_tools" + - name: Include prepare_dev_tools + ansible.builtin.include_role: + name: prepare_dev_tools diff --git a/ansible/roles/prepare_dev_tools/molecule/default/create.yml b/ansible/roles/prepare_dev_tools/molecule/default/create.yml deleted file mode 100644 index a5ffae3..0000000 --- a/ansible/roles/prepare_dev_tools/molecule/default/create.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -- name: Create - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - tasks: - - # TODO: Developer must implement and populate 'server' variable - - - when: server.changed | default(false) | bool - block: - - name: Populate instance config dict - set_fact: - instance_conf_dict: { - 'instance': "{{ }}", - 'address': "{{ }}", - 'user': "{{ }}", - 'port': "{{ }}", - 'identity_file': "{{ }}", } - with_items: "{{ server.results }}" - register: instance_config_dict - - - name: Convert instance config dict to a list - set_fact: - instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" - - - name: Dump instance config - copy: - content: | - # Molecule managed - - {{ instance_conf | to_json | from_json | to_yaml }} - dest: "{{ molecule_instance_config }}" - mode: 0600 diff --git a/ansible/roles/prepare_dev_tools/molecule/default/destroy.yml b/ansible/roles/prepare_dev_tools/molecule/default/destroy.yml deleted file mode 100644 index 49047f0..0000000 --- a/ansible/roles/prepare_dev_tools/molecule/default/destroy.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -- name: Destroy - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - tasks: - # Developer must implement. - - # Mandatory configuration for Molecule to function. - - - name: Populate instance config - set_fact: - instance_conf: {} - - - name: Dump instance config - copy: - content: | - # Molecule managed - - {{ instance_conf | to_json | from_json | to_yaml }} - dest: "{{ molecule_instance_config }}" - mode: 0600 - when: server.changed | default(false) | bool diff --git a/ansible/roles/prepare_dev_tools/molecule/default/molecule.yml b/ansible/roles/prepare_dev_tools/molecule/default/molecule.yml index 74c8557..625c837 100644 --- a/ansible/roles/prepare_dev_tools/molecule/default/molecule.yml +++ b/ansible/roles/prepare_dev_tools/molecule/default/molecule.yml @@ -1,11 +1,37 @@ --- +role_name_check: 2 dependency: name: galaxy + enabled: false driver: - name: delegated + name: podman platforms: - - name: instance + - name: prepare-dev-tools-default + image: docker.io/library/debian:13 + dockerfile: Dockerfile.j2 + pre_build_image: false + pull: true + command: sleep infinity + tty: true provisioner: name: ansible + env: + ANSIBLE_REMOTE_TMP: /tmp/.ansible/tmp verifier: name: ansible +scenario: + test_sequence: + - dependency + - destroy + - syntax + - create + - converge + - idempotence + - verify + - destroy + create_sequence: + - dependency + - create + destroy_sequence: + - dependency + - destroy diff --git a/ansible/roles/prepare_dev_tools/molecule/default/verify.yml b/ansible/roles/prepare_dev_tools/molecule/default/verify.yml index 79044cd..1e6c901 100644 --- a/ansible/roles/prepare_dev_tools/molecule/default/verify.yml +++ b/ansible/roles/prepare_dev_tools/molecule/default/verify.yml @@ -1,10 +1,19 @@ --- -# This is an example playbook to execute Ansible tests. - - name: Verify hosts: all gather_facts: false tasks: - - name: Example assertion - assert: - that: true + - name: Check expected tools are installed + ansible.builtin.shell: "command -v {{ item }}" + loop: + - git + - sudo + - wget + - rsync + changed_when: false + + - name: Check cloud-init was removed or absent + ansible.builtin.command: dpkg-query -W -f=${Status} cloud-init + register: cloud_init_status + changed_when: false + failed_when: cloud_init_status.rc == 0 diff --git a/ansible/roles/print_debug_ip/Makefile b/ansible/roles/print_debug_ip/Makefile new file mode 100644 index 0000000..76a48d8 --- /dev/null +++ b/ansible/roles/print_debug_ip/Makefile @@ -0,0 +1,4 @@ +.PHONY: molecule-test + +molecule-test: + molecule test diff --git a/ansible/roles/print_debug_ip/molecule/default/Dockerfile.j2 b/ansible/roles/print_debug_ip/molecule/default/Dockerfile.j2 new file mode 100644 index 0000000..9b64772 --- /dev/null +++ b/ansible/roles/print_debug_ip/molecule/default/Dockerfile.j2 @@ -0,0 +1,9 @@ +FROM docker.io/library/debian:13 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + python3 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/ansible/roles/print_debug_ip/molecule/default/converge.yml b/ansible/roles/print_debug_ip/molecule/default/converge.yml index a8521bb..5a66448 100644 --- a/ansible/roles/print_debug_ip/molecule/default/converge.yml +++ b/ansible/roles/print_debug_ip/molecule/default/converge.yml @@ -2,6 +2,6 @@ - name: Converge hosts: all tasks: - - name: "Include print_debug_ip" - include_role: - name: "print_debug_ip" + - name: Include print_debug_ip + ansible.builtin.include_role: + name: print_debug_ip diff --git a/ansible/roles/print_debug_ip/molecule/default/create.yml b/ansible/roles/print_debug_ip/molecule/default/create.yml deleted file mode 100644 index a5ffae3..0000000 --- a/ansible/roles/print_debug_ip/molecule/default/create.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -- name: Create - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - tasks: - - # TODO: Developer must implement and populate 'server' variable - - - when: server.changed | default(false) | bool - block: - - name: Populate instance config dict - set_fact: - instance_conf_dict: { - 'instance': "{{ }}", - 'address': "{{ }}", - 'user': "{{ }}", - 'port': "{{ }}", - 'identity_file': "{{ }}", } - with_items: "{{ server.results }}" - register: instance_config_dict - - - name: Convert instance config dict to a list - set_fact: - instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" - - - name: Dump instance config - copy: - content: | - # Molecule managed - - {{ instance_conf | to_json | from_json | to_yaml }} - dest: "{{ molecule_instance_config }}" - mode: 0600 diff --git a/ansible/roles/print_debug_ip/molecule/default/destroy.yml b/ansible/roles/print_debug_ip/molecule/default/destroy.yml deleted file mode 100644 index 49047f0..0000000 --- a/ansible/roles/print_debug_ip/molecule/default/destroy.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -- name: Destroy - hosts: localhost - connection: local - gather_facts: false - no_log: "{{ molecule_no_log }}" - tasks: - # Developer must implement. - - # Mandatory configuration for Molecule to function. - - - name: Populate instance config - set_fact: - instance_conf: {} - - - name: Dump instance config - copy: - content: | - # Molecule managed - - {{ instance_conf | to_json | from_json | to_yaml }} - dest: "{{ molecule_instance_config }}" - mode: 0600 - when: server.changed | default(false) | bool diff --git a/ansible/roles/print_debug_ip/molecule/default/molecule.yml b/ansible/roles/print_debug_ip/molecule/default/molecule.yml index 74c8557..1d1cf1b 100644 --- a/ansible/roles/print_debug_ip/molecule/default/molecule.yml +++ b/ansible/roles/print_debug_ip/molecule/default/molecule.yml @@ -1,11 +1,37 @@ --- +role_name_check: 2 dependency: name: galaxy + enabled: false driver: - name: delegated + name: podman platforms: - - name: instance + - name: print-debug-ip-default + image: docker.io/library/debian:13 + dockerfile: Dockerfile.j2 + pre_build_image: false + pull: true + command: sleep infinity + tty: true provisioner: name: ansible + env: + ANSIBLE_REMOTE_TMP: /tmp/.ansible/tmp verifier: name: ansible +scenario: + test_sequence: + - dependency + - destroy + - syntax + - create + - converge + - idempotence + - verify + - destroy + create_sequence: + - dependency + - create + destroy_sequence: + - dependency + - destroy diff --git a/ansible/roles/print_debug_ip/molecule/default/verify.yml b/ansible/roles/print_debug_ip/molecule/default/verify.yml index 79044cd..a0b6d0a 100644 --- a/ansible/roles/print_debug_ip/molecule/default/verify.yml +++ b/ansible/roles/print_debug_ip/molecule/default/verify.yml @@ -1,10 +1,10 @@ --- -# This is an example playbook to execute Ansible tests. - - name: Verify hosts: all gather_facts: false tasks: - - name: Example assertion - assert: - that: true + - name: Assert podman-backed host was provisioned + ansible.builtin.assert: + that: + - ansible_connection == 'podman' + - inventory_hostname == 'print-debug-ip-default' diff --git a/pyproject.toml b/pyproject.toml index 6537026..08a2d4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,8 @@ dependencies = [ "ansible-core==2.18.7", "passlib==1.7.4", "netaddr==1.3.0", + "molecule>=25.0.0", + "molecule-plugins[podman]>=23.7.0", ] [tool.uv] diff --git a/uv.lock b/uv.lock index 7171d6f..3a9f04b 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,22 @@ version = 1 revision = 3 requires-python = ">=3.13" +[[package]] +name = "ansible-compat" +version = "26.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansible-core" }, + { name = "jsonschema" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "subprocess-tee" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/42/53ba59f8116e716ec0af8dc47ef65b15f5e3bc28131e8e6cbe87e50203f5/ansible_compat-26.3.0.tar.gz", hash = "sha256:15f0ea5ff6fcc5587b6b11a4a436fdeefd0fd4a46afe92d4e483c28370082ae0", size = 216754, upload-time = "2026-03-31T15:47:58.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/a1/3cf5e81f192a2ae4086f80d37d80e257c7552cd7f319b0160c78251bbfea/ansible_compat-26.3.0-py3-none-any.whl", hash = "sha256:4fb48f324383033cf24df5ccb63bca4da766cb4ebbd4bc9dbad91b108f512a73", size = 27723, upload-time = "2026-03-31T15:47:56.534Z" }, +] + [[package]] name = "ansible-core" version = "2.18.7" @@ -24,6 +40,8 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "ansible-core" }, + { name = "molecule" }, + { name = "molecule-plugins" }, { name = "netaddr" }, { name = "passlib" }, ] @@ -31,10 +49,30 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "ansible-core", specifier = "==2.18.7" }, + { name = "molecule", specifier = ">=25.0.0" }, + { name = "molecule-plugins", extras = ["podman"], specifier = ">=23.7.0" }, { name = "netaddr", specifier = "==1.3.0" }, { name = "passlib", specifier = "==1.7.4" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + [[package]] name = "cffi" version = "2.0.0" @@ -80,6 +118,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "cryptography" version = "48.0.0" @@ -133,6 +192,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, ] +[[package]] +name = "enrich" +version = "1.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/77/cb9b3d6f2e2e5f8104e907ea4c4d575267238f52c51cf9f864b865a99710/enrich-1.2.7.tar.gz", hash = "sha256:0a2ab0d2931dff8947012602d1234d2a3ee002d9a355b5d70be6bf5466008893", size = 16918, upload-time = "2022-01-10T15:30:33.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/67/aecd1d435dbbdcea21a197d708e9ff0bcc7306c2847c6c87cc1a91e2cca4/enrich-1.2.7-py3-none-any.whl", hash = "sha256:f29b2c8c124b4dbd7c975ab5c3568f6c7a47938ea3b7d2106c8a3bd346545e4f", size = 8717, upload-time = "2022-01-10T15:30:32.723Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -145,6 +216,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -197,6 +307,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "molecule" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansible-compat" }, + { name = "ansible-core" }, + { name = "click" }, + { name = "enrich" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/e0/f671b5b0742a60b6067cbe9e57720b600ded5e0fcabb837663dd21b57568/molecule-26.4.0.tar.gz", hash = "sha256:12e4c905079f67628ae765506c697d2b8a744a65f2d4cbf5a3b22cb09d0dafc4", size = 4699013, upload-time = "2026-04-06T09:01:33.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/11/9f8322de3674b6ac12d0d203d18f81eb9262af19d2b8122c08b11821af97/molecule-26.4.0-py3-none-any.whl", hash = "sha256:b231f4955f95240a8e2b8194c086d0b5e8ab998032a9411eafe85429442a3bd5", size = 158898, upload-time = "2026-04-06T09:01:31.287Z" }, +] + +[[package]] +name = "molecule-plugins" +version = "25.8.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "molecule" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/11/b7316c96c9ecca901f3a7fd5927f3dd757e17e11a428fb83cc5349e9b063/molecule_plugins-25.8.12.tar.gz", hash = "sha256:75f32763e90275bfc24bcc0d27b9bb22ac973658bf902b2e3e8af8e2a1c32083", size = 106414, upload-time = "2025-08-12T19:57:36.628Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/ee/efdf33ffb32861d9b357a4e7cff018206729703440c028b92ed64fe834b1/molecule_plugins-25.8.12-py3-none-any.whl", hash = "sha256:c5b8ceb209a5ff87326631d60408f8d08b9431b2e339dad4c620130b50279ce0", size = 76357, upload-time = "2025-08-12T19:57:35.285Z" }, +] + [[package]] name = "netaddr" version = "1.3.0" @@ -224,6 +377,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -233,6 +395,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -269,6 +440,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "resolvelib" version = "1.0.1" @@ -277,3 +461,132 @@ sdist = { url = "https://files.pythonhosted.org/packages/ce/10/f699366ce577423cb wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" }, ] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, +] + +[[package]] +name = "subprocess-tee" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/22/991efbf35bc811dfe7edcd749253f0931d2d4838cf55176132633e1c82a7/subprocess_tee-0.4.2.tar.gz", hash = "sha256:91b2b4da3aae9a7088d84acaf2ea0abee3f4fd9c0d2eae69a9b9122a71476590", size = 14951, upload-time = "2024-06-17T19:51:51.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ab/e3a3be062cd544b2803760ff707dee38f0b1cb5685b2446de0ec19be28d9/subprocess_tee-0.4.2-py3-none-any.whl", hash = "sha256:21942e976715af4a19a526918adb03a8a27a8edab959f2d075b777e3d78f532d", size = 5249, upload-time = "2024-06-17T19:51:15.949Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] From fe5e84f2bf787bb80c4511d951da7930a78b9122 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Fri, 5 Jun 2026 01:56:46 +0100 Subject: [PATCH 04/11] Modernize top-level Molecule DevStack scenarios Replace the Vagrant-backed top-level Molecule scenarios with ARD libvirt-backed workflows that call the provider playbooks directly. The default scenario now deploys a two-node Debian 13 DevStack master topology, and the one-controller-two-compute scenario mirrors that topology with an additional compute node and nova-compute disabled on the controller. Remove obsolete OKD, MicroShift, shift-stack, Train, and Wallaby scenarios. Add a stable-2026.1 scenario using Ubuntu 24.04 cloud images. Document the updated top-level Molecule workflow and ignore generated scenario deployment artifacts. Validated with: - uv run molecule syntax -s default - uv run molecule syntax -s one-controller-two-compute - uv run molecule syntax -s stable-2026.1 - uv run molecule test -s default Signed-off-by: Sean Mooney --- .gitignore | 4 + README.md | 74 ++++++--------- molecule/default/INSTALL.rst | 31 ++++--- molecule/default/cleanup.yml | 9 ++ molecule/default/converge.yml | 32 +++++-- molecule/default/create.yml | 43 +++++++++ molecule/default/deployment/deployment.yaml | 67 ++++++++++++++ .../default/deployment/devstack/common.yaml | 8 ++ .../devstack/group_vars/compute.yaml | 4 + .../devstack/group_vars/controller.yaml | 4 + molecule/default/deployment/nodes.yaml | 37 ++++++++ molecule/default/destroy.yml | 25 +++++ molecule/default/molecule.yml | 57 ++++-------- molecule/default/verify.yml | 47 ++++++++-- molecule/microshift/INSTALL.rst | 23 ----- molecule/microshift/converge.yml | 11 --- molecule/microshift/molecule.yml | 37 -------- molecule/microshift/verify.yml | 10 -- molecule/multinode-stable-train/INSTALL.rst | 23 ----- molecule/multinode-stable-train/converge.yml | 29 ------ molecule/multinode-stable-train/molecule.yml | 53 ----------- molecule/multinode-stable-train/verify.yml | 10 -- molecule/multinode-stable-wallaby/INSTALL.rst | 23 ----- .../multinode-stable-wallaby/converge.yml | 28 ------ .../multinode-stable-wallaby/molecule.yml | 53 ----------- molecule/multinode-stable-wallaby/verify.yml | 10 -- molecule/okd/INSTALL.rst | 23 ----- molecule/okd/converge.yml | 7 -- molecule/okd/molecule.yml | 91 ------------------- molecule/okd/verify.yml | 10 -- .../one-controller-two-compute/INSTALL.rst | 30 +++--- .../one-controller-two-compute/cleanup.yml | 9 ++ .../one-controller-two-compute/converge.yml | 26 +++++- .../one-controller-two-compute/create.yml | 43 +++++++++ .../deployment/deployment.yaml | 67 ++++++++++++++ .../deployment/devstack/common.yaml | 8 ++ .../devstack/group_vars/compute.yaml | 4 + .../devstack/group_vars/controller.yaml | 5 + .../deployment/nodes.yaml | 56 ++++++++++++ .../one-controller-two-compute/destroy.yml | 25 +++++ .../one-controller-two-compute/molecule.yml | 73 ++++----------- .../one-controller-two-compute/verify.yml | 47 ++++++++-- molecule/shift-stack/INSTALL.rst | 23 ----- molecule/shift-stack/converge.yml | 5 - molecule/shift-stack/molecule.yml | 33 ------- molecule/shift-stack/verify.yml | 10 -- molecule/stable-2026.1/INSTALL.rst | 17 ++++ molecule/stable-2026.1/cleanup.yml | 9 ++ molecule/stable-2026.1/converge.yml | 25 +++++ molecule/stable-2026.1/create.yml | 43 +++++++++ .../stable-2026.1/deployment/deployment.yaml | 67 ++++++++++++++ .../deployment/devstack/common.yaml | 8 ++ .../devstack/group_vars/compute.yaml | 4 + .../devstack/group_vars/controller.yaml | 4 + molecule/stable-2026.1/deployment/nodes.yaml | 37 ++++++++ molecule/stable-2026.1/destroy.yml | 25 +++++ molecule/stable-2026.1/molecule.yml | 30 ++++++ molecule/stable-2026.1/verify.yml | 43 +++++++++ 58 files changed, 953 insertions(+), 706 deletions(-) create mode 100644 molecule/default/cleanup.yml create mode 100644 molecule/default/create.yml create mode 100644 molecule/default/deployment/deployment.yaml create mode 100644 molecule/default/deployment/devstack/common.yaml create mode 100644 molecule/default/deployment/devstack/group_vars/compute.yaml create mode 100644 molecule/default/deployment/devstack/group_vars/controller.yaml create mode 100644 molecule/default/deployment/nodes.yaml create mode 100644 molecule/default/destroy.yml delete mode 100644 molecule/microshift/INSTALL.rst delete mode 100644 molecule/microshift/converge.yml delete mode 100644 molecule/microshift/molecule.yml delete mode 100644 molecule/microshift/verify.yml delete mode 100644 molecule/multinode-stable-train/INSTALL.rst delete mode 100644 molecule/multinode-stable-train/converge.yml delete mode 100644 molecule/multinode-stable-train/molecule.yml delete mode 100644 molecule/multinode-stable-train/verify.yml delete mode 100644 molecule/multinode-stable-wallaby/INSTALL.rst delete mode 100644 molecule/multinode-stable-wallaby/converge.yml delete mode 100644 molecule/multinode-stable-wallaby/molecule.yml delete mode 100644 molecule/multinode-stable-wallaby/verify.yml delete mode 100644 molecule/okd/INSTALL.rst delete mode 100644 molecule/okd/converge.yml delete mode 100644 molecule/okd/molecule.yml delete mode 100644 molecule/okd/verify.yml create mode 100644 molecule/one-controller-two-compute/cleanup.yml create mode 100644 molecule/one-controller-two-compute/create.yml create mode 100644 molecule/one-controller-two-compute/deployment/deployment.yaml create mode 100644 molecule/one-controller-two-compute/deployment/devstack/common.yaml create mode 100644 molecule/one-controller-two-compute/deployment/devstack/group_vars/compute.yaml create mode 100644 molecule/one-controller-two-compute/deployment/devstack/group_vars/controller.yaml create mode 100644 molecule/one-controller-two-compute/deployment/nodes.yaml create mode 100644 molecule/one-controller-two-compute/destroy.yml delete mode 100644 molecule/shift-stack/INSTALL.rst delete mode 100644 molecule/shift-stack/converge.yml delete mode 100644 molecule/shift-stack/molecule.yml delete mode 100644 molecule/shift-stack/verify.yml create mode 100644 molecule/stable-2026.1/INSTALL.rst create mode 100644 molecule/stable-2026.1/cleanup.yml create mode 100644 molecule/stable-2026.1/converge.yml create mode 100644 molecule/stable-2026.1/create.yml create mode 100644 molecule/stable-2026.1/deployment/deployment.yaml create mode 100644 molecule/stable-2026.1/deployment/devstack/common.yaml create mode 100644 molecule/stable-2026.1/deployment/devstack/group_vars/compute.yaml create mode 100644 molecule/stable-2026.1/deployment/devstack/group_vars/controller.yaml create mode 100644 molecule/stable-2026.1/deployment/nodes.yaml create mode 100644 molecule/stable-2026.1/destroy.yml create mode 100644 molecule/stable-2026.1/molecule.yml create mode 100644 molecule/stable-2026.1/verify.yml diff --git a/.gitignore b/.gitignore index afb409a..6ac91f2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ okd/output/ pull-secret deployments/* !deployments/.keep +molecule/*/deployment/inventory.yaml +molecule/*/deployment/provider-state.yaml +molecule/*/deployment/rendered/ +molecule/*/deployment/logs/ diff --git a/README.md b/README.md index a83cfcf..1bc64af 100644 --- a/README.md +++ b/README.md @@ -27,39 +27,44 @@ Including an example of how to use your role (for instance, with variables passe roles: - { role: username.rolename, x: 42 } -Playbook to test Microshift deployment on Fedora CoreOS -------------------------------------------------------- +Top-level Molecule DevStack scenarios +------------------------------------- + +Top-level ``molecule/`` scenarios are full ARD/libvirt-backed DevStack +deployments. They use Molecule's default/delegated driver to call the ARD +provider playbooks directly; they do not use Vagrant. + +Available scenarios: + +* ``default``: two-node DevStack master on Debian 13 genericcloud + (``controller`` + ``compute1``). +* ``one-controller-two-compute``: same as ``default`` plus ``compute2``; + ``nova-compute`` is disabled on the controller. +* ``stable-2026.1``: two-node DevStack ``stable/2026.1`` on Ubuntu 24.04 cloud + images. + +Run a full scenario test from the repository root: ``` -- hosts: microshift - gather_facts: true - vars: - user_name: microshift - install_olm: false - manage_firewall: false - microshift_install_type: ostree - crio_install_type: ostree - crio_log_level: debug - roles: - - ensure_microshift +uv run molecule test -s default +uv run molecule test -s one-controller-two-compute +uv run molecule test -s stable-2026.1 ``` -Bootstrap OpenShift crc in a vm -------------------------------- +For an incremental and cheaper validation loop, create the VMs first and verify +SSH before converging DevStack: -This should create ``~/.ssh/id_ed25519_stack`` SSH key to login to VM as a stack user: -``` -molecule destroy -s shift-stack -molecule create -s shift-stack -molecule converge -s shift-stack ``` -Verify login to CRC VM and RHCOS k8s worker node. +uv run molecule create -s default +uv run ansible -i molecule/default/deployment/inventory.yaml all -m ping +uv run molecule converge -s default +uv run molecule verify -s default +uv run molecule destroy -s default ``` -cd ~/.cache/molecule/ansible_role_devstack/shift-stack -vagrant ssh crc -[stack@crc ~]$ ssh -i ~/.crc/machines/crc/id_ecdsa core@`crc ip` -``` +Scenario deployment workspaces live under ``molecule//deployment``. +Generated files such as ``inventory.yaml``, ``provider-state.yaml``, and +``rendered/`` are runtime artifacts and should not be committed. Role molecule tests ------------------- @@ -97,25 +102,6 @@ make molecule-test Top-level ``molecule/`` scenarios are larger deployment scenarios and are not part of this role-level test workflow. -Configure localhost to access deployed OpenShift crc ----------------------------------------------------- -> **NOTE**: This overwrites ``~/.kube/config`` ! - -Install shuttle first. - -``` -inv=~/.cache/molecule/ansible_role_devstack/shift-stack/inventory/ansible_inventory.yml -IP=$(ansible -i $inv -m debug -a 'var=hostvars["crc"].ansible_host' crc | sed -rn 's/.*ansible_host": "(.*)"/\1/p') -echo `ssh -i ~/.ssh/id_ed25519_stack stack@$IP tail -1 /etc/hosts | tail -1` | sudo tee -a /etc/host -ansible -b -i $inv -m slurp -a "src=/home/stack/.kube/config" crc | sed -r 's/crc \| SUCCESS => //' | jq -r '.content' | base64 -d > ~/.kube/config -ansible -b -i $inv -m shell -a "cd /home/stack/.crc/bin/oc; tar hcvf - oc | gzip -v4 > oc.tar.gz" crc -ansible -b -i $inv -m slurp -a "src=/home/stack/.crc/bin/oc/oc.tar.gz" crc | sed -r 's/crc \| SUCCESS => //' | jq -r '.content' | base64 -d > oc.tar.gz -tar xzf oc.tar.gz -sudo install -o root -g root -m 0755 oc /usr/local/bin/oc -sudo -E sshuttle -r stack@$IP -x $IP 0.0.0.0/0 -vv --ssh-cmd 'ssh -i $HOME/.ssh/id_ed25519_stack' -oc login -u kubeadmin -p `ssh -i ~/.ssh/id_ed25519_stack stack@$IP crc console --credentials | awk '/oc login -u kubeadmin/ {if ($0) print $12}'` https://api.crc.testing:6443 -``` - License ------- diff --git a/molecule/default/INSTALL.rst b/molecule/default/INSTALL.rst index 0c4bf5c..1a001ab 100644 --- a/molecule/default/INSTALL.rst +++ b/molecule/default/INSTALL.rst @@ -1,23 +1,24 @@ -********************************* -Vagrant driver installation guide -********************************* +************************************ +ARD libvirt scenario requirements +************************************ + +This scenario uses Molecule's default/delegated driver to call the ARD +libvirt provider playbooks. It does not require Vagrant. Requirements ============ -* Vagrant -* Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop - -Install -======= - -Please refer to the `Virtual environment`_ documentation for installation best -practices. If not using a virtual environment, please consider passing the -widely recommended `'--user' flag`_ when invoking ``pip``. +* the uv-managed project environment +* Molecule +* Ansible +* libvirt/qemu access to ``qemu:///system`` -.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ -.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site +Run +=== .. code-block:: bash - $ pip install 'molecule_vagrant' + uv run molecule create -s default + uv run molecule converge -s default + uv run molecule verify -s default + uv run molecule destroy -s default diff --git a/molecule/default/cleanup.yml b/molecule/default/cleanup.yml new file mode 100644 index 0000000..514394c --- /dev/null +++ b/molecule/default/cleanup.yml @@ -0,0 +1,9 @@ +--- +- name: Cleanup Molecule scenario + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: No cleanup required + ansible.builtin.debug: + msg: ARD workspace cleanup is explicit; destroy removes provider resources. diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index 4c282dd..0da1a5e 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -1,11 +1,25 @@ --- -- name: "execute deploy multi-node devstack" - import_playbook: ../../ansible/deploy_multinode_devstack.yaml +- name: Deploy DevStack into ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false vars: - devstack_branch: master - controller_services_extra: - # Shared services - tls-proxy: false - compute_services_extra: - # Shared services - tls-proxy: false + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Run ARD DevStack deployment playbook + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - ansible/playbooks/ard-deploy-devstack.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true diff --git a/molecule/default/create.yml b/molecule/default/create.yml new file mode 100644 index 0000000..f1bd5a2 --- /dev/null +++ b/molecule/default/create.yml @@ -0,0 +1,43 @@ +--- +- name: Create ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Apply ARD provider resources + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - localhost, + - ansible/playbooks/ard-apply.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true + + - name: Wait for ARD provider SSH access + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - all + - -m + - ansible.builtin.wait_for_connection + - -a + - timeout=600 sleep=5 + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false diff --git a/molecule/default/deployment/deployment.yaml b/molecule/default/deployment/deployment.yaml new file mode 100644 index 0000000..04030e0 --- /dev/null +++ b/molecule/default/deployment/deployment.yaml @@ -0,0 +1,67 @@ +--- +ard_provider: libvirt +ard_deployment_name: molecule-default +ard_resource_name_prefix: ard-molecule-default + +ard_default_image: debian-13 +ard_default_controller_flavor: devstack-control +ard_default_compute_flavor: devstack-compute +ard_default_vm_preference: devstack + +ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default(lookup('env', 'HOME') + '/.cache', true) }}/ard/images" +ard_image_download: true +ard_image_checksum_required: false + +ard_libvirt_uri: qemu:///system +ard_libvirt_pool: ard +ard_libvirt_network_name: ard-molecule-default +ard_libvirt_network_cidr: 192.168.96.0/24 +ard_libvirt_network_gateway: 192.168.96.1 +ard_libvirt_image_dir: "{{ lookup('env', 'XDG_STATE_HOME') | default(lookup('env', 'HOME') + '/.local/state', true) }}/ard/libvirt/images" + +ard_images: + debian-13: + os_family: debian + version: "13" + cloud_init: true + provider: + libvirt: + url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 + alternate_urls: + - https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2 + name: debian-13 + cache_filename: debian-13-genericcloud-amd64.qcow2 + format: qcow2 + cloud_init_datasource: NoCloud + ubuntu-24.04: + os_family: ubuntu + version: "24.04" + cloud_init: true + provider: + libvirt: + url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img + name: ubuntu-24.04 + cache_filename: noble-server-cloudimg-amd64.img + format: qcow2 + cloud_init_datasource: NoCloud +ard_flavors: + devstack-control: + vcpus: 8 + memory: 16Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 16384 + cpu_mode: host-passthrough + devstack-compute: + vcpus: 8 + memory: 8Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 8192 + cpu_mode: host-passthrough diff --git a/molecule/default/deployment/devstack/common.yaml b/molecule/default/deployment/devstack/common.yaml new file mode 100644 index 0000000..61f74c6 --- /dev/null +++ b/molecule/default/deployment/devstack/common.yaml @@ -0,0 +1,8 @@ +--- +devstack_branch: master +run_devstack: true +enable_ceph: false +configure_vdpa: false +external_bridge_mtu: 1450 +devstack_libvirt_type: kvm +stack_user_password: tester diff --git a/molecule/default/deployment/devstack/group_vars/compute.yaml b/molecule/default/deployment/devstack/group_vars/compute.yaml new file mode 100644 index 0000000..051f914 --- /dev/null +++ b/molecule/default/deployment/devstack/group_vars/compute.yaml @@ -0,0 +1,4 @@ +--- +compute_localrc_extra: {} +compute_local_conf_extra: {} +compute_services_extra: {} diff --git a/molecule/default/deployment/devstack/group_vars/controller.yaml b/molecule/default/deployment/devstack/group_vars/controller.yaml new file mode 100644 index 0000000..aa7587a --- /dev/null +++ b/molecule/default/deployment/devstack/group_vars/controller.yaml @@ -0,0 +1,4 @@ +--- +controller_localrc_extra: {} +controller_local_conf_extra: {} +controller_services_extra: {} diff --git a/molecule/default/deployment/nodes.yaml b/molecule/default/deployment/nodes.yaml new file mode 100644 index 0000000..8f5e6ae --- /dev/null +++ b/molecule/default/deployment/nodes.yaml @@ -0,0 +1,37 @@ +--- +ard_nodes: + - name: controller + hostname: controller + provider_resource_name: ard-molecule-default-controller + groups: + - controller + - switch + image: debian-13 + flavor: devstack-control + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.96.2 + mac: "52:54:00:96:00:02" + profiles: + - ssh + - nested_virt + - ovn + - name: compute1 + hostname: compute1 + provider_resource_name: ard-molecule-default-compute1 + groups: + - compute + - peers + - subnode + image: debian-13 + flavor: devstack-compute + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.96.3 + mac: "52:54:00:96:00:03" + profiles: + - ssh + - nested_virt + - ovn diff --git a/molecule/default/destroy.yml b/molecule/default/destroy.yml new file mode 100644 index 0000000..1284e84 --- /dev/null +++ b/molecule/default/destroy.yml @@ -0,0 +1,25 @@ +--- +- name: Destroy ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Destroy ARD provider resources + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - localhost, + - ansible/playbooks/ard-destroy.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml index 755505a..115dff4 100644 --- a/molecule/default/molecule.yml +++ b/molecule/default/molecule.yml @@ -1,49 +1,30 @@ --- dependency: name: galaxy -verifier: - name: ansible + enabled: false driver: - name: vagrant - provider: - name: libvirt - # Run vagrant up with --provision. - # Defaults to --no-provision) - provision: no - # vagrant-cachier configuration - # Defaults to 'machine' - # Any value different from 'machine' or 'box' will disable it - cachier: machine - # If set to false, set VAGRANT_NO_PARALLEL to '1' - # Defaults to true - parallel: true - # vagrant box to use by default - # Defaults to 'generic/alpine310' - default_box: 'generic/centos9s' + name: default platforms: - name: controller - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - machine_type: 'q35' - groups: - - controller - - switch - name: compute1 - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - machine_type: 'q35' - groups: - - compute - - peers - - subnode provisioner: name: ansible env: ANSIBLE_STDOUT_CALLBACK: yaml - ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH}:../../submodules/zuul-jobs/roles:../../submodules/devstack/roles:../../submodules/openstack-zuul-jobs/roles" +verifier: + name: ansible +scenario: + test_sequence: + - dependency + - destroy + - syntax + - create + - converge + - verify + - destroy + create_sequence: + - dependency + - create + destroy_sequence: + - dependency + - destroy diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml index 79044cd..29c6992 100644 --- a/molecule/default/verify.yml +++ b/molecule/default/verify.yml @@ -1,10 +1,43 @@ --- -# This is an example playbook to execute Ansible tests. - -- name: Verify - hosts: all +- name: Verify ARD DevStack deployment + hosts: localhost + connection: local gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" tasks: - - name: Example assertion - assert: - that: true + - name: Ping all generated inventory hosts + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - all + - -m + - ansible.builtin.ping + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false + + - name: Check DevStack directory on controller + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - controller + - -m + - ansible.builtin.command + - -a + - test -d /opt/repos/devstack + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false diff --git a/molecule/microshift/INSTALL.rst b/molecule/microshift/INSTALL.rst deleted file mode 100644 index 0c4bf5c..0000000 --- a/molecule/microshift/INSTALL.rst +++ /dev/null @@ -1,23 +0,0 @@ -********************************* -Vagrant driver installation guide -********************************* - -Requirements -============ - -* Vagrant -* Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop - -Install -======= - -Please refer to the `Virtual environment`_ documentation for installation best -practices. If not using a virtual environment, please consider passing the -widely recommended `'--user' flag`_ when invoking ``pip``. - -.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ -.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site - -.. code-block:: bash - - $ pip install 'molecule_vagrant' diff --git a/molecule/microshift/converge.yml b/molecule/microshift/converge.yml deleted file mode 100644 index b50ff09..0000000 --- a/molecule/microshift/converge.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- name: "execute deploy multi-node devstack" - import_playbook: ../../ansible/deploy_microshift.yaml - vars: - devstack_branch: master - controller_services_extra: - # Shared services - tls-proxy: false - compute_services_extra: - # Shared services - tls-proxy: false diff --git a/molecule/microshift/molecule.yml b/molecule/microshift/molecule.yml deleted file mode 100644 index af85fad..0000000 --- a/molecule/microshift/molecule.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -dependency: - name: galaxy -verifier: - name: ansible -driver: - name: vagrant - provider: - name: libvirt - # Run vagrant up with --provision. - # Defaults to --no-provision) - provision: no - # vagrant-cachier configuration - # Defaults to 'machine' - # Any value different from 'machine' or 'box' will disable it - cachier: machine - # If set to false, set VAGRANT_NO_PARALLEL to '1' - # Defaults to true - parallel: true - # vagrant box to use by default - # Defaults to 'generic/alpine310' - default_box: 'generic/centos9s' -platforms: - - name: microshift - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - machine_type: 'q35' - groups: - - openshift -provisioner: - name: ansible - env: - ANSIBLE_STDOUT_CALLBACK: yaml - ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH}:../../submodules/zuul-jobs/roles:../../submodules/devstack/roles:../../submodules/openstack-zuul-jobs/roles" diff --git a/molecule/microshift/verify.yml b/molecule/microshift/verify.yml deleted file mode 100644 index 79044cd..0000000 --- a/molecule/microshift/verify.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# This is an example playbook to execute Ansible tests. - -- name: Verify - hosts: all - gather_facts: false - tasks: - - name: Example assertion - assert: - that: true diff --git a/molecule/multinode-stable-train/INSTALL.rst b/molecule/multinode-stable-train/INSTALL.rst deleted file mode 100644 index 0c4bf5c..0000000 --- a/molecule/multinode-stable-train/INSTALL.rst +++ /dev/null @@ -1,23 +0,0 @@ -********************************* -Vagrant driver installation guide -********************************* - -Requirements -============ - -* Vagrant -* Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop - -Install -======= - -Please refer to the `Virtual environment`_ documentation for installation best -practices. If not using a virtual environment, please consider passing the -widely recommended `'--user' flag`_ when invoking ``pip``. - -.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ -.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site - -.. code-block:: bash - - $ pip install 'molecule_vagrant' diff --git a/molecule/multinode-stable-train/converge.yml b/molecule/multinode-stable-train/converge.yml deleted file mode 100644 index 4e317d9..0000000 --- a/molecule/multinode-stable-train/converge.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -- name: "execute deploy multi-node devstack" - import_playbook: ../../ansible/deploy_multinode_devstack.yaml - vars: - devstack_branch: stable/train - controller_services_extra: - g-reg: true - # on stable branches we default to ml2/ovs - # OVN services - ovn-controller: false - ovn-northd: false - ovs-vswitchd: false - ovsdb-server: false - # Neutron services - q-ovn-metadata-agent: false - q-agt: true - q-dhcp: true - q-l3: true - q-meta: true - q-metering: true - q-svc: true - compute_services_extra: - # OVN services - ovn-controller: false - ovn-northd: false - ovs-vswitchd: false - ovsdb-server: false - # Neutron services - q-agt: true diff --git a/molecule/multinode-stable-train/molecule.yml b/molecule/multinode-stable-train/molecule.yml deleted file mode 100644 index 76fd542..0000000 --- a/molecule/multinode-stable-train/molecule.yml +++ /dev/null @@ -1,53 +0,0 @@ ---- -dependency: - name: galaxy -verifier: - name: ansible -driver: - name: vagrant - provider: - name: libvirt - # Run vagrant up with --provision. - # Defaults to --no-provision) - provision: no - # vagrant-cachier configuration - # Defaults to 'machine' - # Any value different from 'machine' or 'box' will disable it - cachier: machine - # If set to false, set VAGRANT_NO_PARALLEL to '1' - # Defaults to true - parallel: true - # vagrant box to use by default - # Defaults to 'generic/alpine310' - default_box: 'generic/ubuntu1804' -platforms: - - name: controller - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - cputopology :sockets => '1', :cores => '4', :threads => '2' - - random :model => 'random' - groups: - - controller - - switch - - name: compute1 - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - cputopology :sockets => '1', :cores => '4', :threads => '2' - - random :model => 'random' - groups: - - compute - - peers - - subnode -provisioner: - name: ansible - env: - ANSIBLE_STDOUT_CALLBACK: yaml - ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH}:../../submodules/zuul-jobs/roles:../../submodules/devstack/roles:../../submodules/openstack-zuul-jobs/roles" diff --git a/molecule/multinode-stable-train/verify.yml b/molecule/multinode-stable-train/verify.yml deleted file mode 100644 index 79044cd..0000000 --- a/molecule/multinode-stable-train/verify.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# This is an example playbook to execute Ansible tests. - -- name: Verify - hosts: all - gather_facts: false - tasks: - - name: Example assertion - assert: - that: true diff --git a/molecule/multinode-stable-wallaby/INSTALL.rst b/molecule/multinode-stable-wallaby/INSTALL.rst deleted file mode 100644 index 0c4bf5c..0000000 --- a/molecule/multinode-stable-wallaby/INSTALL.rst +++ /dev/null @@ -1,23 +0,0 @@ -********************************* -Vagrant driver installation guide -********************************* - -Requirements -============ - -* Vagrant -* Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop - -Install -======= - -Please refer to the `Virtual environment`_ documentation for installation best -practices. If not using a virtual environment, please consider passing the -widely recommended `'--user' flag`_ when invoking ``pip``. - -.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ -.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site - -.. code-block:: bash - - $ pip install 'molecule_vagrant' diff --git a/molecule/multinode-stable-wallaby/converge.yml b/molecule/multinode-stable-wallaby/converge.yml deleted file mode 100644 index 5dec49c..0000000 --- a/molecule/multinode-stable-wallaby/converge.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -- name: "execute deploy multi-node devstack" - import_playbook: ../../ansible/deploy_multinode_devstack.yaml - vars: - devstack_branch: stable/wallaby - # on stable branches we default to ml2/ovs - controller_services_extra: - # OVN services - ovn-controller: false - ovn-northd: false - ovs-vswitchd: false - ovsdb-server: false - # Neutron services - q-ovn-metadata-agent: false - q-agt: true - q-dhcp: true - q-l3: true - q-meta: true - q-metering: true - q-svc: true - compute_services_extra: - # OVN services - ovn-controller: false - ovn-northd: false - ovs-vswitchd: false - ovsdb-server: false - # Neutron services - q-agt: true diff --git a/molecule/multinode-stable-wallaby/molecule.yml b/molecule/multinode-stable-wallaby/molecule.yml deleted file mode 100644 index f971369..0000000 --- a/molecule/multinode-stable-wallaby/molecule.yml +++ /dev/null @@ -1,53 +0,0 @@ ---- -dependency: - name: galaxy -verifier: - name: ansible -driver: - name: vagrant - provider: - name: libvirt - # Run vagrant up with --provision. - # Defaults to --no-provision) - provision: no - # vagrant-cachier configuration - # Defaults to 'machine' - # Any value different from 'machine' or 'box' will disable it - cachier: machine - # If set to false, set VAGRANT_NO_PARALLEL to '1' - # Defaults to true - parallel: true - # vagrant box to use by default - # Defaults to 'generic/alpine310' - default_box: 'generic/ubuntu2004' -platforms: - - name: controller - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - cputopology :sockets => '1', :cores => '4', :threads => '2' - - random :model => 'random' - groups: - - controller - - switch - - name: compute1 - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - cputopology :sockets => '1', :cores => '4', :threads => '2' - - random :model => 'random' - groups: - - compute - - peers - - subnode -provisioner: - name: ansible - env: - ANSIBLE_STDOUT_CALLBACK: yaml - ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH}:../../submodules/zuul-jobs/roles:../../submodules/devstack/roles:../../submodules/openstack-zuul-jobs/roles" diff --git a/molecule/multinode-stable-wallaby/verify.yml b/molecule/multinode-stable-wallaby/verify.yml deleted file mode 100644 index 79044cd..0000000 --- a/molecule/multinode-stable-wallaby/verify.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# This is an example playbook to execute Ansible tests. - -- name: Verify - hosts: all - gather_facts: false - tasks: - - name: Example assertion - assert: - that: true diff --git a/molecule/okd/INSTALL.rst b/molecule/okd/INSTALL.rst deleted file mode 100644 index 0c4bf5c..0000000 --- a/molecule/okd/INSTALL.rst +++ /dev/null @@ -1,23 +0,0 @@ -********************************* -Vagrant driver installation guide -********************************* - -Requirements -============ - -* Vagrant -* Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop - -Install -======= - -Please refer to the `Virtual environment`_ documentation for installation best -practices. If not using a virtual environment, please consider passing the -widely recommended `'--user' flag`_ when invoking ``pip``. - -.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ -.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site - -.. code-block:: bash - - $ pip install 'molecule_vagrant' diff --git a/molecule/okd/converge.yml b/molecule/okd/converge.yml deleted file mode 100644 index d367f46..0000000 --- a/molecule/okd/converge.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- name: Converge - hosts: all - tasks: - - name: "Include ansible_role_devstack" - include_role: - name: "ansible_role_devstack" diff --git a/molecule/okd/molecule.yml b/molecule/okd/molecule.yml deleted file mode 100644 index dd6eb00..0000000 --- a/molecule/okd/molecule.yml +++ /dev/null @@ -1,91 +0,0 @@ ---- -dependency: - name: galaxy -verifier: - name: ansible -driver: - name: vagrant - provider: - name: libvirt - # Run vagrant up with --provision. - # Defaults to --no-provision) - provision: no - # vagrant-cachier configuration - # Defaults to 'machine' - # Any value different from 'machine' or 'box' will disable it - cachier: machine - # If set to false, set VAGRANT_NO_PARALLEL to '1' - # Defaults to true - parallel: true - # vagrant box to use by default - # Defaults to 'generic/alpine310' - default_box: 'fedora-coreos' -platforms: - - name: controller-1 - memory: 6000 - cpus: 8 - interfaces: - - network_name: private_network - type: dhcp - network_address: '192.168.125.0' - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - random :model => 'random' - - qemuargs :value => "-fw_cfg" - - qemuargs :value => "name=opt/com.coreos/config,file=$MOLECULE_PROJECT_DIRECTORY/okd/vagrant.ign" - config_options: - ssh.keep_alive: yes - ssh.remote_user: 'core' - synced_folder: false - groups: - - okd-controllers - - openstack-controllers - - name: controller-2 - memory: 6000 - cpus: 8 - interfaces: - - network_name: private_network - type: dhcp - network_address: '192.168.125.0' - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - random :model => 'random' - - qemuargs :value => "-fw_cfg" - - qemuargs :value => "name=opt/com.coreos/config,file=$MOLECULE_PROJECT_DIRECTORY/okd/vagrant.ign" - config_options: - ssh.keep_alive: yes - ssh.remote_user: 'core' - synced_folder: false - groups: - - okd-controllers - - openstack-controllers - - name: controller-3 - memory: 6000 - cpus: 8 - interfaces: - - network_name: private_network - type: dhcp - network_address: '192.168.125.0' - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - random :model => 'random' - - qemuargs :value => "-fw_cfg" - - qemuargs :value => "name=opt/com.coreos/config,file=$MOLECULE_PROJECT_DIRECTORY/okd/vagrant.ign" - config_options: - ssh.keep_alive: yes - ssh.remote_user: 'core' - synced_folder: false - groups: - - okd-controllers - - openstack-controllers -provisioner: - name: ansible - env: - ANSIBLE_STDOUT_CALLBACK: yaml - ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH}:../../submodules/zuul-jobs/roles:../../submodules/devstack/roles:../../submodules/openstack-zuul-jobs/roles" diff --git a/molecule/okd/verify.yml b/molecule/okd/verify.yml deleted file mode 100644 index 79044cd..0000000 --- a/molecule/okd/verify.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# This is an example playbook to execute Ansible tests. - -- name: Verify - hosts: all - gather_facts: false - tasks: - - name: Example assertion - assert: - that: true diff --git a/molecule/one-controller-two-compute/INSTALL.rst b/molecule/one-controller-two-compute/INSTALL.rst index 0c4bf5c..2531219 100644 --- a/molecule/one-controller-two-compute/INSTALL.rst +++ b/molecule/one-controller-two-compute/INSTALL.rst @@ -1,23 +1,17 @@ -********************************* -Vagrant driver installation guide -********************************* +************************************ +ARD libvirt scenario requirements +************************************ -Requirements -============ +This scenario uses Molecule's default/delegated driver to call the ARD +libvirt provider playbooks. It creates one controller and two compute nodes. +It does not require Vagrant. -* Vagrant -* Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop - -Install -======= - -Please refer to the `Virtual environment`_ documentation for installation best -practices. If not using a virtual environment, please consider passing the -widely recommended `'--user' flag`_ when invoking ``pip``. - -.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ -.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site +Run +=== .. code-block:: bash - $ pip install 'molecule_vagrant' + uv run molecule create -s one-controller-two-compute + uv run molecule converge -s one-controller-two-compute + uv run molecule verify -s one-controller-two-compute + uv run molecule destroy -s one-controller-two-compute diff --git a/molecule/one-controller-two-compute/cleanup.yml b/molecule/one-controller-two-compute/cleanup.yml new file mode 100644 index 0000000..514394c --- /dev/null +++ b/molecule/one-controller-two-compute/cleanup.yml @@ -0,0 +1,9 @@ +--- +- name: Cleanup Molecule scenario + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: No cleanup required + ansible.builtin.debug: + msg: ARD workspace cleanup is explicit; destroy removes provider resources. diff --git a/molecule/one-controller-two-compute/converge.yml b/molecule/one-controller-two-compute/converge.yml index d72f59b..0da1a5e 100644 --- a/molecule/one-controller-two-compute/converge.yml +++ b/molecule/one-controller-two-compute/converge.yml @@ -1,3 +1,25 @@ --- -- name: "execute deploy multi-node devstack" - import_playbook: ../../ansible/deploy_multinode_devstack.yaml +- name: Deploy DevStack into ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Run ARD DevStack deployment playbook + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - ansible/playbooks/ard-deploy-devstack.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true diff --git a/molecule/one-controller-two-compute/create.yml b/molecule/one-controller-two-compute/create.yml new file mode 100644 index 0000000..f1bd5a2 --- /dev/null +++ b/molecule/one-controller-two-compute/create.yml @@ -0,0 +1,43 @@ +--- +- name: Create ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Apply ARD provider resources + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - localhost, + - ansible/playbooks/ard-apply.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true + + - name: Wait for ARD provider SSH access + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - all + - -m + - ansible.builtin.wait_for_connection + - -a + - timeout=600 sleep=5 + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false diff --git a/molecule/one-controller-two-compute/deployment/deployment.yaml b/molecule/one-controller-two-compute/deployment/deployment.yaml new file mode 100644 index 0000000..b90dd0c --- /dev/null +++ b/molecule/one-controller-two-compute/deployment/deployment.yaml @@ -0,0 +1,67 @@ +--- +ard_provider: libvirt +ard_deployment_name: molecule-octc +ard_resource_name_prefix: ard-molecule-octc + +ard_default_image: debian-13 +ard_default_controller_flavor: devstack-control +ard_default_compute_flavor: devstack-compute +ard_default_vm_preference: devstack + +ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default(lookup('env', 'HOME') + '/.cache', true) }}/ard/images" +ard_image_download: true +ard_image_checksum_required: false + +ard_libvirt_uri: qemu:///system +ard_libvirt_pool: ard +ard_libvirt_network_name: ard-molecule-octc +ard_libvirt_network_cidr: 192.168.97.0/24 +ard_libvirt_network_gateway: 192.168.97.1 +ard_libvirt_image_dir: "{{ lookup('env', 'XDG_STATE_HOME') | default(lookup('env', 'HOME') + '/.local/state', true) }}/ard/libvirt/images" + +ard_images: + debian-13: + os_family: debian + version: "13" + cloud_init: true + provider: + libvirt: + url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 + alternate_urls: + - https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2 + name: debian-13 + cache_filename: debian-13-genericcloud-amd64.qcow2 + format: qcow2 + cloud_init_datasource: NoCloud + ubuntu-24.04: + os_family: ubuntu + version: "24.04" + cloud_init: true + provider: + libvirt: + url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img + name: ubuntu-24.04 + cache_filename: noble-server-cloudimg-amd64.img + format: qcow2 + cloud_init_datasource: NoCloud +ard_flavors: + devstack-control: + vcpus: 8 + memory: 16Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 16384 + cpu_mode: host-passthrough + devstack-compute: + vcpus: 8 + memory: 8Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 8192 + cpu_mode: host-passthrough diff --git a/molecule/one-controller-two-compute/deployment/devstack/common.yaml b/molecule/one-controller-two-compute/deployment/devstack/common.yaml new file mode 100644 index 0000000..61f74c6 --- /dev/null +++ b/molecule/one-controller-two-compute/deployment/devstack/common.yaml @@ -0,0 +1,8 @@ +--- +devstack_branch: master +run_devstack: true +enable_ceph: false +configure_vdpa: false +external_bridge_mtu: 1450 +devstack_libvirt_type: kvm +stack_user_password: tester diff --git a/molecule/one-controller-two-compute/deployment/devstack/group_vars/compute.yaml b/molecule/one-controller-two-compute/deployment/devstack/group_vars/compute.yaml new file mode 100644 index 0000000..051f914 --- /dev/null +++ b/molecule/one-controller-two-compute/deployment/devstack/group_vars/compute.yaml @@ -0,0 +1,4 @@ +--- +compute_localrc_extra: {} +compute_local_conf_extra: {} +compute_services_extra: {} diff --git a/molecule/one-controller-two-compute/deployment/devstack/group_vars/controller.yaml b/molecule/one-controller-two-compute/deployment/devstack/group_vars/controller.yaml new file mode 100644 index 0000000..33e00a3 --- /dev/null +++ b/molecule/one-controller-two-compute/deployment/devstack/group_vars/controller.yaml @@ -0,0 +1,5 @@ +--- +controller_localrc_extra: {} +controller_local_conf_extra: {} +controller_services_extra: + n-cpu: false diff --git a/molecule/one-controller-two-compute/deployment/nodes.yaml b/molecule/one-controller-two-compute/deployment/nodes.yaml new file mode 100644 index 0000000..82d52eb --- /dev/null +++ b/molecule/one-controller-two-compute/deployment/nodes.yaml @@ -0,0 +1,56 @@ +--- +ard_nodes: + - name: controller + hostname: controller + provider_resource_name: ard-molecule-octc-controller + groups: + - controller + - switch + image: debian-13 + flavor: devstack-control + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.97.2 + mac: "52:54:00:97:00:02" + profiles: + - ssh + - nested_virt + - ovn + - name: compute1 + hostname: compute1 + provider_resource_name: ard-molecule-octc-compute1 + groups: + - compute + - peers + - subnode + image: debian-13 + flavor: devstack-compute + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.97.3 + mac: "52:54:00:97:00:03" + profiles: + - ssh + - nested_virt + - ovn + + - name: compute2 + hostname: compute2 + provider_resource_name: ard-molecule-octc-compute2 + groups: + - compute + - peers + - subnode + image: debian-13 + flavor: devstack-compute + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.97.4 + mac: "52:54:00:97:00:04" + profiles: + - ssh + - nested_virt + - ovn diff --git a/molecule/one-controller-two-compute/destroy.yml b/molecule/one-controller-two-compute/destroy.yml new file mode 100644 index 0000000..1284e84 --- /dev/null +++ b/molecule/one-controller-two-compute/destroy.yml @@ -0,0 +1,25 @@ +--- +- name: Destroy ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Destroy ARD provider resources + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - localhost, + - ansible/playbooks/ard-destroy.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true diff --git a/molecule/one-controller-two-compute/molecule.yml b/molecule/one-controller-two-compute/molecule.yml index d07468a..7ccaf1b 100644 --- a/molecule/one-controller-two-compute/molecule.yml +++ b/molecule/one-controller-two-compute/molecule.yml @@ -1,66 +1,31 @@ --- dependency: name: galaxy -verifier: - name: ansible + enabled: false driver: - name: vagrant - provider: - name: libvirt - # Run vagrant up with --provision. - # Defaults to --no-provision) - provision: no - # vagrant-cachier configuration - # Defaults to 'machine' - # Any value different from 'machine' or 'box' will disable it - cachier: machine - # If set to false, set VAGRANT_NO_PARALLEL to '1' - # Defaults to true - parallel: true - # vagrant box to use by default - # Defaults to 'generic/alpine310' - default_box: 'generic/ubuntu2004' + name: default platforms: - name: controller - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - cputopology :sockets => '1', :cores => '4', :threads => '2' - - random :model => 'random' - groups: - - controller - - switch - name: compute1 - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - cputopology :sockets => '1', :cores => '4', :threads => '2' - - random :model => 'random' - groups: - - compute - - peers - - subnode - name: compute2 - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - provider_raw_config_args: - - cputopology :sockets => '1', :cores => '4', :threads => '2' - - random :model => 'random' - groups: - - compute - - peers - - subnode provisioner: name: ansible env: ANSIBLE_STDOUT_CALLBACK: yaml - ANSIBLE_ROLES_PATH: "${ANSIBLE_ROLES_PATH}:../../submodules/zuul-jobs/roles:../../submodules/devstack/roles:../../submodules/openstack-zuul-jobs/roles" +verifier: + name: ansible +scenario: + test_sequence: + - dependency + - destroy + - syntax + - create + - converge + - verify + - destroy + create_sequence: + - dependency + - create + destroy_sequence: + - dependency + - destroy diff --git a/molecule/one-controller-two-compute/verify.yml b/molecule/one-controller-two-compute/verify.yml index 79044cd..29c6992 100644 --- a/molecule/one-controller-two-compute/verify.yml +++ b/molecule/one-controller-two-compute/verify.yml @@ -1,10 +1,43 @@ --- -# This is an example playbook to execute Ansible tests. - -- name: Verify - hosts: all +- name: Verify ARD DevStack deployment + hosts: localhost + connection: local gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" tasks: - - name: Example assertion - assert: - that: true + - name: Ping all generated inventory hosts + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - all + - -m + - ansible.builtin.ping + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false + + - name: Check DevStack directory on controller + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - controller + - -m + - ansible.builtin.command + - -a + - test -d /opt/repos/devstack + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false diff --git a/molecule/shift-stack/INSTALL.rst b/molecule/shift-stack/INSTALL.rst deleted file mode 100644 index 0c4bf5c..0000000 --- a/molecule/shift-stack/INSTALL.rst +++ /dev/null @@ -1,23 +0,0 @@ -********************************* -Vagrant driver installation guide -********************************* - -Requirements -============ - -* Vagrant -* Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop - -Install -======= - -Please refer to the `Virtual environment`_ documentation for installation best -practices. If not using a virtual environment, please consider passing the -widely recommended `'--user' flag`_ when invoking ``pip``. - -.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ -.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site - -.. code-block:: bash - - $ pip install 'molecule_vagrant' diff --git a/molecule/shift-stack/converge.yml b/molecule/shift-stack/converge.yml deleted file mode 100644 index 715dcd0..0000000 --- a/molecule/shift-stack/converge.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -- name: "execute deploy shift-stack" - import_playbook: ../../ansible/deploy_shift_stack.yaml - vars: - pull_secret_path: "../../../pull-secret" diff --git a/molecule/shift-stack/molecule.yml b/molecule/shift-stack/molecule.yml deleted file mode 100644 index 715c226..0000000 --- a/molecule/shift-stack/molecule.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -dependency: - name: galaxy -driver: - name: vagrant - provider: - name: libvirt - provision: no - parallel: true - default_box: 'generic/centos9s' -platforms: - - name: crc - memory: 20480 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - machine_type: 'q35' - groups: - - openshift - - name: compute - memory: 8192 - cpus: 8 - provider_options: - cpu_mode: 'host-passthrough' - nested: true - machine_type: 'q35' - groups: - - openstack -provisioner: - name: ansible -verifier: - name: ansible diff --git a/molecule/shift-stack/verify.yml b/molecule/shift-stack/verify.yml deleted file mode 100644 index 79044cd..0000000 --- a/molecule/shift-stack/verify.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# This is an example playbook to execute Ansible tests. - -- name: Verify - hosts: all - gather_facts: false - tasks: - - name: Example assertion - assert: - that: true diff --git a/molecule/stable-2026.1/INSTALL.rst b/molecule/stable-2026.1/INSTALL.rst new file mode 100644 index 0000000..7f4ada5 --- /dev/null +++ b/molecule/stable-2026.1/INSTALL.rst @@ -0,0 +1,17 @@ +************************************ +ARD libvirt scenario requirements +************************************ + +This scenario uses Molecule's default/delegated driver to call the ARD +libvirt provider playbooks. It deploys DevStack ``stable/2026.1`` on Ubuntu +24.04 cloud images. It does not require Vagrant. + +Run +=== + +.. code-block:: bash + + uv run molecule create -s stable-2026.1 + uv run molecule converge -s stable-2026.1 + uv run molecule verify -s stable-2026.1 + uv run molecule destroy -s stable-2026.1 diff --git a/molecule/stable-2026.1/cleanup.yml b/molecule/stable-2026.1/cleanup.yml new file mode 100644 index 0000000..514394c --- /dev/null +++ b/molecule/stable-2026.1/cleanup.yml @@ -0,0 +1,9 @@ +--- +- name: Cleanup Molecule scenario + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: No cleanup required + ansible.builtin.debug: + msg: ARD workspace cleanup is explicit; destroy removes provider resources. diff --git a/molecule/stable-2026.1/converge.yml b/molecule/stable-2026.1/converge.yml new file mode 100644 index 0000000..0da1a5e --- /dev/null +++ b/molecule/stable-2026.1/converge.yml @@ -0,0 +1,25 @@ +--- +- name: Deploy DevStack into ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Run ARD DevStack deployment playbook + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - ansible/playbooks/ard-deploy-devstack.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true diff --git a/molecule/stable-2026.1/create.yml b/molecule/stable-2026.1/create.yml new file mode 100644 index 0000000..f1bd5a2 --- /dev/null +++ b/molecule/stable-2026.1/create.yml @@ -0,0 +1,43 @@ +--- +- name: Create ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Apply ARD provider resources + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - localhost, + - ansible/playbooks/ard-apply.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true + + - name: Wait for ARD provider SSH access + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - all + - -m + - ansible.builtin.wait_for_connection + - -a + - timeout=600 sleep=5 + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false diff --git a/molecule/stable-2026.1/deployment/deployment.yaml b/molecule/stable-2026.1/deployment/deployment.yaml new file mode 100644 index 0000000..5d08752 --- /dev/null +++ b/molecule/stable-2026.1/deployment/deployment.yaml @@ -0,0 +1,67 @@ +--- +ard_provider: libvirt +ard_deployment_name: molecule-stable-2026-1 +ard_resource_name_prefix: ard-molecule-stable-2026-1 + +ard_default_image: ubuntu-24.04 +ard_default_controller_flavor: devstack-control +ard_default_compute_flavor: devstack-compute +ard_default_vm_preference: devstack + +ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default(lookup('env', 'HOME') + '/.cache', true) }}/ard/images" +ard_image_download: true +ard_image_checksum_required: false + +ard_libvirt_uri: qemu:///system +ard_libvirt_pool: ard +ard_libvirt_network_name: ard-molecule-stable-2026-1 +ard_libvirt_network_cidr: 192.168.98.0/24 +ard_libvirt_network_gateway: 192.168.98.1 +ard_libvirt_image_dir: "{{ lookup('env', 'XDG_STATE_HOME') | default(lookup('env', 'HOME') + '/.local/state', true) }}/ard/libvirt/images" + +ard_images: + debian-13: + os_family: debian + version: "13" + cloud_init: true + provider: + libvirt: + url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 + alternate_urls: + - https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2 + name: debian-13 + cache_filename: debian-13-genericcloud-amd64.qcow2 + format: qcow2 + cloud_init_datasource: NoCloud + ubuntu-24.04: + os_family: ubuntu + version: "24.04" + cloud_init: true + provider: + libvirt: + url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img + name: ubuntu-24.04 + cache_filename: noble-server-cloudimg-amd64.img + format: qcow2 + cloud_init_datasource: NoCloud +ard_flavors: + devstack-control: + vcpus: 8 + memory: 16Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 16384 + cpu_mode: host-passthrough + devstack-compute: + vcpus: 8 + memory: 8Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 8192 + cpu_mode: host-passthrough diff --git a/molecule/stable-2026.1/deployment/devstack/common.yaml b/molecule/stable-2026.1/deployment/devstack/common.yaml new file mode 100644 index 0000000..9196dc5 --- /dev/null +++ b/molecule/stable-2026.1/deployment/devstack/common.yaml @@ -0,0 +1,8 @@ +--- +devstack_branch: stable/2026.1 +run_devstack: true +enable_ceph: false +configure_vdpa: false +external_bridge_mtu: 1450 +devstack_libvirt_type: kvm +stack_user_password: tester diff --git a/molecule/stable-2026.1/deployment/devstack/group_vars/compute.yaml b/molecule/stable-2026.1/deployment/devstack/group_vars/compute.yaml new file mode 100644 index 0000000..051f914 --- /dev/null +++ b/molecule/stable-2026.1/deployment/devstack/group_vars/compute.yaml @@ -0,0 +1,4 @@ +--- +compute_localrc_extra: {} +compute_local_conf_extra: {} +compute_services_extra: {} diff --git a/molecule/stable-2026.1/deployment/devstack/group_vars/controller.yaml b/molecule/stable-2026.1/deployment/devstack/group_vars/controller.yaml new file mode 100644 index 0000000..aa7587a --- /dev/null +++ b/molecule/stable-2026.1/deployment/devstack/group_vars/controller.yaml @@ -0,0 +1,4 @@ +--- +controller_localrc_extra: {} +controller_local_conf_extra: {} +controller_services_extra: {} diff --git a/molecule/stable-2026.1/deployment/nodes.yaml b/molecule/stable-2026.1/deployment/nodes.yaml new file mode 100644 index 0000000..1f97063 --- /dev/null +++ b/molecule/stable-2026.1/deployment/nodes.yaml @@ -0,0 +1,37 @@ +--- +ard_nodes: + - name: controller + hostname: controller + provider_resource_name: ard-molecule-stable-2026-1-controller + groups: + - controller + - switch + image: ubuntu-24.04 + flavor: devstack-control + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.98.2 + mac: "52:54:00:98:00:02" + profiles: + - ssh + - nested_virt + - ovn + - name: compute1 + hostname: compute1 + provider_resource_name: ard-molecule-stable-2026-1-compute1 + groups: + - compute + - peers + - subnode + image: ubuntu-24.04 + flavor: devstack-compute + preference: devstack + networks: + - name: ard-mgmt + ip: 192.168.98.3 + mac: "52:54:00:98:00:03" + profiles: + - ssh + - nested_virt + - ovn diff --git a/molecule/stable-2026.1/destroy.yml b/molecule/stable-2026.1/destroy.yml new file mode 100644 index 0000000..1284e84 --- /dev/null +++ b/molecule/stable-2026.1/destroy.yml @@ -0,0 +1,25 @@ +--- +- name: Destroy ARD libvirt deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Destroy ARD provider resources + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - localhost, + - ansible/playbooks/ard-destroy.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true diff --git a/molecule/stable-2026.1/molecule.yml b/molecule/stable-2026.1/molecule.yml new file mode 100644 index 0000000..115dff4 --- /dev/null +++ b/molecule/stable-2026.1/molecule.yml @@ -0,0 +1,30 @@ +--- +dependency: + name: galaxy + enabled: false +driver: + name: default +platforms: + - name: controller + - name: compute1 +provisioner: + name: ansible + env: + ANSIBLE_STDOUT_CALLBACK: yaml +verifier: + name: ansible +scenario: + test_sequence: + - dependency + - destroy + - syntax + - create + - converge + - verify + - destroy + create_sequence: + - dependency + - create + destroy_sequence: + - dependency + - destroy diff --git a/molecule/stable-2026.1/verify.yml b/molecule/stable-2026.1/verify.yml new file mode 100644 index 0000000..29c6992 --- /dev/null +++ b/molecule/stable-2026.1/verify.yml @@ -0,0 +1,43 @@ +--- +- name: Verify ARD DevStack deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Ping all generated inventory hosts + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - all + - -m + - ansible.builtin.ping + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false + + - name: Check DevStack directory on controller + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - controller + - -m + - ansible.builtin.command + - -a + - test -d /opt/repos/devstack + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: false From 35873cbf09d4ec9c7c1672781ab2f024a521ca93 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Fri, 5 Jun 2026 02:46:26 +0100 Subject: [PATCH 05/11] Refine local ARD libvirt workflow Local ARD deployments need a reusable workflow that is not tied to Molecule and does not duplicate image and flavor definitions across every rendered workspace. This change adds shared provider defaults, topology rendering, Make targets, and bootstrap documentation for the local libvirt path. The provider defaults move the Debian and Ubuntu image registry, libvirt settings, XDG paths, DevStack flavors, and destroy cleanup knobs into a common role. Rendered deployment workspaces now keep only deployment-specific selections such as topology, image, resource prefix, and management network. The render workflow supports one-controller-one-compute and one-controller-two-compute presets while preserving editable workspace files. The root Makefile exposes render, apply, ping, deploy, verify, destroy, generated-artifact cleanup, and full site targets for named local deployments. The libvirt domain XML uses firmware auto-selection for UEFI boot with secure boot disabled, avoiding hard-coded OVMF and explicit NVRAM paths. Bootstrap now uses bindep plus uv and supports Debian/Ubuntu and Fedora/CentOS Stream style hosts without Vagrant dependencies. Validated with syntax checks for the ARD playbooks, Molecule syntax checks for the top-level scenarios, bootstrap dry-run validation, and local render/apply/ping/destroy smoke tests. Signed-off-by: Sean Mooney --- Makefile | 65 +++- README.md | 355 ++++++++++++++---- ansible/playbooks/ard-apply.yaml | 1 + ansible/playbooks/ard-cleanup.yaml | 1 + ansible/playbooks/ard-deploy-devstack.yaml | 2 + ansible/playbooks/ard-destroy.yaml | 1 + ansible/playbooks/ard-render.yaml | 1 + ansible/playbooks/ard-verify.yaml | 63 ++++ ansible/roles/ard_libvirt_node/tasks/node.yml | 1 - .../ard_libvirt_node/templates/domain.xml.j2 | 8 +- .../ard_libvirt_preflight/tasks/main.yml | 26 +- .../ard_provider_common/defaults/main.yml | 75 ++++ .../roles/ard_provider_common/tasks/main.yml | 6 + .../roles/ard_provider_destroy/tasks/main.yml | 16 + .../roles/ard_provider_render/tasks/main.yml | 169 ++++----- bindep.txt | 40 +- bootstrap-repo.sh | 214 +++++++++-- molecule/default/deployment/deployment.yaml | 55 +-- .../deployment/deployment.yaml | 55 +-- .../stable-2026.1/deployment/deployment.yaml | 55 +-- 20 files changed, 825 insertions(+), 384 deletions(-) create mode 100644 ansible/playbooks/ard-verify.yaml create mode 100644 ansible/roles/ard_provider_common/defaults/main.yml create mode 100644 ansible/roles/ard_provider_common/tasks/main.yml diff --git a/Makefile b/Makefile index e74979a..8e43ef5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,67 @@ -.PHONY: molecule-test molecule-role-tests molecule-role-% +.PHONY: \ + render apply ping deploy verify destroy destroy-clean-generated clean-generated cleanup site \ + molecule-test molecule-role-tests molecule-role-% + +ARD_PROVIDER ?= libvirt +ARD_DEPLOYMENT ?= devstack-1 +ARD_DEPLOYMENTS_DIR ?= $(CURDIR)/deployments +ARD_DEPLOYMENT_DIR ?= $(ARD_DEPLOYMENTS_DIR)/$(ARD_DEPLOYMENT) +ARD_TOPOLOGY ?= one-controller-one-compute +ARD_IMAGE ?= debian-13 +ARD_NETWORK_CIDR ?= 192.168.96.0/24 +ARD_EXTRA_VARS ?= + +ARD_RENDER_EXTRA_VARS = \ + ard_provider=$(ARD_PROVIDER) \ + ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) \ + ard_topology=$(ARD_TOPOLOGY) \ + ard_default_image=$(ARD_IMAGE) \ + ard_libvirt_network_cidr=$(ARD_NETWORK_CIDR) \ + $(ARD_EXTRA_VARS) + +ARD_DEPLOYMENT_EXTRA_VARS = \ + ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) \ + $(ARD_EXTRA_VARS) + +render: + uv run ansible-playbook -i localhost, ansible/playbooks/ard-render.yaml \ + -e "$(ARD_RENDER_EXTRA_VARS)" + +apply: + uv run ansible-playbook -i localhost, ansible/playbooks/ard-apply.yaml \ + -e "$(ARD_DEPLOYMENT_EXTRA_VARS)" + +ping: + uv run ansible -i $(ARD_DEPLOYMENT_DIR)/inventory.yaml all \ + -m ansible.builtin.ping + +deploy: + uv run ansible-playbook -i $(ARD_DEPLOYMENT_DIR)/inventory.yaml \ + ansible/playbooks/ard-deploy-devstack.yaml \ + -e "$(ARD_DEPLOYMENT_EXTRA_VARS)" + +verify: + uv run ansible-playbook -i localhost, ansible/playbooks/ard-verify.yaml \ + -e "$(ARD_DEPLOYMENT_EXTRA_VARS)" + +destroy: + uv run ansible-playbook -i localhost, ansible/playbooks/ard-destroy.yaml \ + -e "$(ARD_DEPLOYMENT_EXTRA_VARS)" + +destroy-clean-generated: + uv run ansible-playbook -i localhost, ansible/playbooks/ard-destroy.yaml \ + -e "$(ARD_DEPLOYMENT_EXTRA_VARS) ard_destroy_cleanup_generated=true" + +clean-generated: + rm -rf $(ARD_DEPLOYMENT_DIR)/inventory.yaml \ + $(ARD_DEPLOYMENT_DIR)/provider-state.yaml \ + $(ARD_DEPLOYMENT_DIR)/rendered + +cleanup: + uv run ansible-playbook -i localhost, ansible/playbooks/ard-cleanup.yaml \ + -e "$(ARD_DEPLOYMENT_EXTRA_VARS)" + +site: render apply deploy verify MOLECULE_ROLE_DIRS := $(sort $(dir $(wildcard ansible/roles/*/molecule/*/molecule.yml))) diff --git a/README.md b/README.md index 1bc64af..53b7608 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,252 @@ -Role Name -========= +# ARD -A brief description of the role goes here. +ARD is a local development and test harness for VM-backed DevStack +deployments. The primary local workflow uses Ansible provider playbooks to +provision libvirt VMs, generate an ARD/Zuul-like inventory, and then run the +existing DevStack deployment roles inside those VMs. -Requirements ------------- +The current local provider target is libvirt. KubeVirt/OpenShift +Virtualization is design work for a later phase. -Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. +## Quick start -Role Variables --------------- +Bootstrap the repository and host dependencies: -A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. +```bash +./bootstrap-repo.sh +``` -Dependencies ------------- +Render a named deployment workspace: -A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. +```bash +make render ARD_DEPLOYMENT=devstack-a +``` -Example Playbook ----------------- +Create the VMs and generated inventory: -Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: +```bash +make apply ARD_DEPLOYMENT=devstack-a +make ping ARD_DEPLOYMENT=devstack-a +``` - - hosts: servers - roles: - - { role: username.rolename, x: 42 } +Deploy DevStack: -Top-level Molecule DevStack scenarios -------------------------------------- +```bash +make deploy ARD_DEPLOYMENT=devstack-a +make verify ARD_DEPLOYMENT=devstack-a +``` -Top-level ``molecule/`` scenarios are full ARD/libvirt-backed DevStack -deployments. They use Molecule's default/delegated driver to call the ARD -provider playbooks directly; they do not use Vagrant. +Destroy provider resources when finished: -Available scenarios: +```bash +make destroy ARD_DEPLOYMENT=devstack-a +``` -* ``default``: two-node DevStack master on Debian 13 genericcloud - (``controller`` + ``compute1``). -* ``one-controller-two-compute``: same as ``default`` plus ``compute2``; - ``nova-compute`` is disabled on the controller. -* ``stable-2026.1``: two-node DevStack ``stable/2026.1`` on Ubuntu 24.04 cloud - images. +Remove the local deployment workspace only when you no longer need the rendered +inputs or generated state: -Run a full scenario test from the repository root: +```bash +make cleanup ARD_DEPLOYMENT=devstack-a +``` +## Local deployment workflow + +The normal local workflow is: + +1. `render`: create an editable deployment workspace. +2. `apply`: create libvirt network/domain/disk/seed resources and inventory. +3. `ping`: verify SSH/Ansible connectivity. +4. `deploy`: run the multinode DevStack playbook. +5. `verify`: run basic post-deploy checks. +6. `destroy`: remove libvirt resources but keep the workspace and generated artifacts for inspection. +7. `clean-generated`: remove generated inventory/state/rendered artifacts without touching provider resources. +8. `cleanup`: delete the workspace. + +A deployment workspace lives under: + +```text +deployments// + deployment.yaml + nodes.yaml + devstack/ + common.yaml + group_vars/ + controller.yaml + compute.yaml + host_vars/ + inventory.yaml # generated by apply + provider-state.yaml # generated by apply + rendered/ # generated provider artifacts + logs/ ``` -uv run molecule test -s default -uv run molecule test -s one-controller-two-compute -uv run molecule test -s stable-2026.1 + +`render` writes files with overwrite protection, so local edits are preserved. +After rendering, edit `nodes.yaml` or the files under `devstack/` to customize a +scenario before applying it. + +## Make targets + +The root `Makefile` wraps the provider playbooks: + +```bash +make render +make apply +make ping +make deploy +make verify +make destroy +make destroy-clean-generated +make clean-generated +make cleanup +make site +``` + +Useful variables: + +```text +ARD_DEPLOYMENT deployment name, default devstack-1 +ARD_DEPLOYMENTS_DIR deployment parent dir, default ./deployments +ARD_DEPLOYMENT_DIR full workspace path +ARD_PROVIDER provider, currently libvirt +ARD_TOPOLOGY topology preset +ARD_IMAGE image key, default debian-13 +ARD_NETWORK_CIDR libvirt management CIDR, default 192.168.96.0/24 +ARD_EXTRA_VARS extra Ansible vars appended to provider commands +``` + +Example: + +```bash +make render \ + ARD_DEPLOYMENT=devstack-a \ + ARD_TOPOLOGY=one-controller-two-compute \ + ARD_NETWORK_CIDR=192.168.99.0/24 +``` + +## Multiple local deployments + +Use a unique deployment name and management CIDR for each local deployment: + +```bash +make render ARD_DEPLOYMENT=devstack-a ARD_NETWORK_CIDR=192.168.99.0/24 +make render ARD_DEPLOYMENT=devstack-b ARD_NETWORK_CIDR=192.168.100.0/24 +``` + +Provider resources are named with the deployment name, for example: + +```text +ard-devstack-a-controller +ard-devstack-a-compute1 +``` + +Inventory hostnames remain logical names such as `controller`, `compute1`, and +`compute2`. + +## Topology presets + +Supported local render presets: + +```text +one-controller-one-compute +one-controller-two-compute +``` + +`one-controller-one-compute` renders: + +```text +controller +compute1 +``` + +`one-controller-two-compute` renders: + +```text +controller +compute1 +compute2 +``` + +The two-compute topology disables `nova-compute` on the controller through the +rendered controller group vars. + +## Images and flavors + +Reusable provider defaults live in the ARD provider common role. Deployment +workspaces normally only select the image/flavor by name instead of embedding +the full registry. + +Current image keys: + +```text +debian-13 +ubuntu-24.04 ``` -For an incremental and cheaper validation loop, create the VMs first and verify -SSH before converging DevStack: +Current flavor keys: +```text +devstack-control 8 vCPU, 16 GiB RAM, 80 GiB disk +devstack-compute 8 vCPU, 8 GiB RAM, 80 GiB disk ``` + +The default local image is Debian 13 genericcloud. + +## Cache and state paths + +Base cloud images are cached under: + +```text +$XDG_CACHE_HOME/ard/images +``` + +or, if `XDG_CACHE_HOME` is unset: + +```text +~/.cache/ard/images +``` + +Per-deployment libvirt disks, seed ISOs, NVRAM, and console logs live under: + +```text +$XDG_STATE_HOME/ard/libvirt/images/ +``` + +or, if `XDG_STATE_HOME` is unset: + +```text +~/.local/state/ard/libvirt/images/ +``` + +`make destroy` removes per-deployment provider resources but keeps cached base +images and generated workspace artifacts for inspection. Use +`make destroy-clean-generated` to destroy provider resources and then remove +`inventory.yaml`, `provider-state.yaml`, and `rendered/`. Use +`make clean-generated` to remove those generated files without touching +provider resources. + +## Molecule scenarios + +Top-level Molecule scenarios are full ARD/libvirt-backed DevStack validation +flows. They call the same provider playbooks used by Make and do not use +Vagrant. + +Available scenarios: + +```text +default Debian 13, controller + compute1, master +one-controller-two-compute Debian 13, controller + compute1 + compute2 +stable-2026.1 Ubuntu 24.04, controller + compute1, stable/2026.1 +``` + +Run a full scenario test: + +```bash +uv run molecule test -s default +``` + +For a cheaper loop: + +```bash uv run molecule create -s default uv run ansible -i molecule/default/deployment/inventory.yaml all -m ping uv run molecule converge -s default @@ -62,52 +254,79 @@ uv run molecule verify -s default uv run molecule destroy -s default ``` -Scenario deployment workspaces live under ``molecule//deployment``. -Generated files such as ``inventory.yaml``, ``provider-state.yaml``, and -``rendered/`` are runtime artifacts and should not be committed. +Role-level Molecule scenarios live under `ansible/roles/*/molecule` and use +Podman where containers are sufficient: -Role molecule tests -------------------- +```bash +make molecule-test +make molecule-role-ensure_kustomize +``` -The uv-managed development environment includes Molecule and the Podman -Molecule plugin. Role-level Molecule scenarios live under -``ansible/roles/*/molecule`` and are intended to avoid Vagrant for role unit -coverage. Use containers for roles that do not need full VM semantics; reserve -ARD libvirt-backed scenarios for roles that need a real VM, systemd, cloud-init, -libvirt, or DevStack node behavior. +## Troubleshooting -Run all role-level Molecule scenarios from the repository root: +### Libvirt access -``` -make molecule-test -``` +The local provider uses `qemu:///system`. Your user normally needs libvirt/qemu +group access. If bootstrap reports missing group membership, log out and back +in or use `newgrp libvirt` before running provider commands. + +### UEFI firmware -Run one role-level scenario from the repository root: +Libvirt firmware auto-selection is used for UEFI boot with secure boot +disabled. Install the OVMF/edk2 firmware package for your distribution if +libvirt cannot define or start the domain. +### SSH not ready + +`apply` creates VMs and inventory. If an immediate ping fails, wait briefly and +retry: + +```bash +make ping ARD_DEPLOYMENT=devstack-a ``` -make molecule-role-ensure_kustomize + +Molecule create waits for SSH before converging. + +### CIDR conflicts + +Use a management CIDR that does not conflict with existing host networks or +other ARD deployments: + +```bash +make render ARD_DEPLOYMENT=devstack-b ARD_NETWORK_CIDR=192.168.100.0/24 ``` -You can also run a role scenario from the role directory. Activate the uv venv -first, then run Molecule directly or via the role-local make target: +### Cleanup after failure + +Destroy provider resources while preserving generated artifacts for inspection: +```bash +make destroy ARD_DEPLOYMENT=devstack-a ``` -source ../../../.venv/bin/activate -cd ansible/roles/ensure_kustomize -molecule test -# or -make molecule-test + +Destroy provider resources and remove generated inventory/state/rendered files: + +```bash +make destroy-clean-generated ARD_DEPLOYMENT=devstack-a ``` -Top-level ``molecule/`` scenarios are larger deployment scenarios and are not -part of this role-level test workflow. +Remove generated files without touching provider resources: -License -------- +```bash +make clean-generated ARD_DEPLOYMENT=devstack-a +``` -BSD +If the workspace is no longer needed: -Author Information ------------------- +```bash +make cleanup ARD_DEPLOYMENT=devstack-a +``` -An optional section for the role authors to include contact information, or a website (HTML is not allowed). +Generated runtime files are ignored by git: + +```text +inventory.yaml +provider-state.yaml +rendered/ +logs/ +``` diff --git a/ansible/playbooks/ard-apply.yaml b/ansible/playbooks/ard-apply.yaml index 8540ec7..737e596 100644 --- a/ansible/playbooks/ard-apply.yaml +++ b/ansible/playbooks/ard-apply.yaml @@ -4,6 +4,7 @@ connection: local gather_facts: true roles: + - ard_provider_common - ard_provider_preflight - ard_provider_image - ard_provider_network diff --git a/ansible/playbooks/ard-cleanup.yaml b/ansible/playbooks/ard-cleanup.yaml index d410d36..b2c0a36 100644 --- a/ansible/playbooks/ard-cleanup.yaml +++ b/ansible/playbooks/ard-cleanup.yaml @@ -4,4 +4,5 @@ connection: local gather_facts: false roles: + - ard_provider_common - ard_provider_cleanup diff --git a/ansible/playbooks/ard-deploy-devstack.yaml b/ansible/playbooks/ard-deploy-devstack.yaml index c714e97..a989736 100644 --- a/ansible/playbooks/ard-deploy-devstack.yaml +++ b/ansible/playbooks/ard-deploy-devstack.yaml @@ -4,12 +4,14 @@ connection: local gather_facts: false roles: + - ard_provider_common - ard_provider_inventory - name: Load ARD deployment DevStack variables hosts: all gather_facts: false roles: + - ard_provider_common - ard_devstack_config - name: Wait for provider node bootstrap to finish diff --git a/ansible/playbooks/ard-destroy.yaml b/ansible/playbooks/ard-destroy.yaml index 74290e8..5ee7dce 100644 --- a/ansible/playbooks/ard-destroy.yaml +++ b/ansible/playbooks/ard-destroy.yaml @@ -4,4 +4,5 @@ connection: local gather_facts: false roles: + - ard_provider_common - ard_provider_destroy diff --git a/ansible/playbooks/ard-render.yaml b/ansible/playbooks/ard-render.yaml index d6d4697..09e3b09 100644 --- a/ansible/playbooks/ard-render.yaml +++ b/ansible/playbooks/ard-render.yaml @@ -4,4 +4,5 @@ connection: local gather_facts: false roles: + - ard_provider_common - ard_provider_render diff --git a/ansible/playbooks/ard-verify.yaml b/ansible/playbooks/ard-verify.yaml new file mode 100644 index 0000000..d32d835 --- /dev/null +++ b/ansible/playbooks/ard-verify.yaml @@ -0,0 +1,63 @@ +--- +- name: Verify ARD deployment + hosts: localhost + connection: local + gather_facts: false + roles: + - ard_provider_common + tasks: + - name: Require ARD deployment directory + ansible.builtin.assert: + that: + - ard_deployment_dir is defined + - ard_deployment_dir | length > 0 + fail_msg: "Pass -e ard_deployment_dir=/path/to/deployment" + + - name: Check generated inventory exists + ansible.builtin.stat: + path: "{{ ard_deployment_dir }}/inventory.yaml" + register: ard_verify_inventory + + - name: Require generated inventory + ansible.builtin.assert: + that: + - ard_verify_inventory.stat.exists + fail_msg: "{{ ard_deployment_dir }}/inventory.yaml does not exist; run ard-apply first." + + - name: Ping all ARD nodes + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - all + - -m + - ansible.builtin.ping + changed_when: false + + - name: Check DevStack checkout on controller + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - controller + - -m + - ansible.builtin.command + - -a + - test -d /opt/repos/devstack + changed_when: false + + - name: Run Tempest smoke when requested + ansible.builtin.command: + argv: + - ansible + - -i + - "{{ ard_deployment_dir }}/inventory.yaml" + - controller + - -m + - ansible.builtin.shell + - -a + - cd /opt/stack/tempest && .tox/tempest/bin/tempest run --smoke + changed_when: false + when: ard_verify_tempest_smoke | default(false) | bool diff --git a/ansible/roles/ard_libvirt_node/tasks/node.yml b/ansible/roles/ard_libvirt_node/tasks/node.yml index 1c9c949..f1930b4 100644 --- a/ansible/roles/ard_libvirt_node/tasks/node.yml +++ b/ansible/roles/ard_libvirt_node/tasks/node.yml @@ -12,7 +12,6 @@ ard_node_seed_local: "{{ ard_deployment_dir }}/rendered/libvirt/{{ ard_node.name }}/{{ ard_node.name }}-seed.iso" ard_node_seed: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ ard_node.name }}-seed.iso" ard_node_console_log: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ ard_node.name }}-console.log" - ard_node_nvram: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ ard_node.name }}-vars.fd" - name: Ensure rendered node directory exists file: diff --git a/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 b/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 index 712b0c3..65eafed 100644 --- a/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 +++ b/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 @@ -3,10 +3,12 @@ {{ ard_node_flavor.memory_mb | int * 1024 }} {{ ard_node_flavor.memory_mb | int * 1024 }} {{ ard_node_flavor.vcpus }} - + hvm - /usr/share/OVMF/OVMF_CODE_4M.fd - {{ ard_node_nvram }} + + + + diff --git a/ansible/roles/ard_libvirt_preflight/tasks/main.yml b/ansible/roles/ard_libvirt_preflight/tasks/main.yml index 78c6c84..fe0873e 100644 --- a/ansible/roles/ard_libvirt_preflight/tasks/main.yml +++ b/ansible/roles/ard_libvirt_preflight/tasks/main.yml @@ -1,6 +1,6 @@ --- - name: Check required libvirt provider commands - shell: "command -v {{ item }}" + ansible.builtin.shell: "command -v {{ item }}" changed_when: false loop: - virsh @@ -11,31 +11,25 @@ register: ard_libvirt_command_checks failed_when: ard_libvirt_command_checks.rc != 0 -- name: Check OVMF firmware exists - stat: - path: /usr/share/OVMF/OVMF_CODE_4M.fd - register: ard_ovmf_code - -- name: Require OVMF firmware for Debian cloud image boot - assert: - that: - - ard_ovmf_code.stat.exists - fail_msg: "OVMF firmware not found at /usr/share/OVMF/OVMF_CODE_4M.fd" - - name: Check libvirt connection - command: "virsh --connect {{ ard_libvirt_uri }} uri" + ansible.builtin.command: "virsh --connect {{ ard_libvirt_uri }} uri" + changed_when: false + +- name: Check libvirt version for firmware auto-selection + ansible.builtin.command: "virsh --connect {{ ard_libvirt_uri }} version" changed_when: false + register: ard_libvirt_version - name: Ensure stack SSH key exists - command: ssh-keygen -t ed25519 -N '' -f {{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack + ansible.builtin.command: ssh-keygen -t ed25519 -N '' -f {{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack args: creates: "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack" - name: Read stack public SSH key - slurp: + ansible.builtin.slurp: src: "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack.pub" register: ard_stack_public_key_slurp - name: Set stack public SSH key fact - set_fact: + ansible.builtin.set_fact: ard_stack_public_key: "{{ ard_stack_public_key_slurp.content | b64decode | trim }}" diff --git a/ansible/roles/ard_provider_common/defaults/main.yml b/ansible/roles/ard_provider_common/defaults/main.yml new file mode 100644 index 0000000..7217741 --- /dev/null +++ b/ansible/roles/ard_provider_common/defaults/main.yml @@ -0,0 +1,75 @@ +--- +ard_provider: libvirt +ard_topology: one-controller-one-compute + +ard_default_image: debian-13 +ard_default_controller_flavor: devstack-control +ard_default_compute_flavor: devstack-compute +ard_default_vm_preference: devstack + +ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default(lookup('env', 'HOME') + '/.cache', true) }}/ard/images" +ard_image_download: true +ard_image_checksum_required: false + +ard_libvirt_uri: qemu:///system +ard_libvirt_pool: ard +ard_libvirt_network_cidr: 192.168.96.0/24 +ard_libvirt_network_gateway: "{{ ard_libvirt_network_cidr | regex_replace('\\.0/24$', '.1') }}" +ard_libvirt_network_name: "ard-{{ ard_deployment_name | default('devstack-1') }}" +ard_libvirt_image_dir: "{{ lookup('env', 'XDG_STATE_HOME') | default(lookup('env', 'HOME') + '/.local/state', true) }}/ard/libvirt/images" +ard_libvirt_firmware: efi +ard_libvirt_secure_boot: false + +# Destroy preserves generated runtime state by default for post-failure +# inspection. Set ard_destroy_cleanup_generated=true to remove generated +# inventory/provider-state/rendered artifacts after provider resources are gone. +ard_destroy_cleanup_generated: false +ard_destroy_cleanup_logs: false + +ard_images: + debian-13: + os_family: debian + version: "13" + cloud_init: true + provider: + libvirt: + url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 + alternate_urls: + - https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2 + name: debian-13 + cache_filename: debian-13-genericcloud-amd64.qcow2 + format: qcow2 + cloud_init_datasource: NoCloud + ubuntu-24.04: + os_family: ubuntu + version: "24.04" + cloud_init: true + provider: + libvirt: + url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img + name: ubuntu-24.04 + cache_filename: noble-server-cloudimg-amd64.img + format: qcow2 + cloud_init_datasource: NoCloud + +ard_flavors: + devstack-control: + vcpus: 8 + memory: 16Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 16384 + cpu_mode: host-passthrough + devstack-compute: + vcpus: 8 + memory: 8Gi + root_disk_gb: 80 + nested_virt: true + provider: + libvirt: + vcpus: 8 + memory_mb: 8192 + cpu_mode: host-passthrough diff --git a/ansible/roles/ard_provider_common/tasks/main.yml b/ansible/roles/ard_provider_common/tasks/main.yml new file mode 100644 index 0000000..b2564f2 --- /dev/null +++ b/ansible/roles/ard_provider_common/tasks/main.yml @@ -0,0 +1,6 @@ +--- +# This role intentionally defines shared ARD provider defaults. Keeping a +# no-op task file lets playbooks include the role explicitly before provider +# dispatcher roles load deployment-specific overrides. +- name: Load ARD provider common defaults + ansible.builtin.meta: noop diff --git a/ansible/roles/ard_provider_destroy/tasks/main.yml b/ansible/roles/ard_provider_destroy/tasks/main.yml index b63904f..c4191d9 100644 --- a/ansible/roles/ard_provider_destroy/tasks/main.yml +++ b/ansible/roles/ard_provider_destroy/tasks/main.yml @@ -11,3 +11,19 @@ - name: Run provider destroy handling include_role: name: "ard_{{ ard_provider }}_destroy" + +- name: Remove generated ARD deployment artifacts + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - "{{ ard_deployment_dir }}/inventory.yaml" + - "{{ ard_deployment_dir }}/provider-state.yaml" + - "{{ ard_deployment_dir }}/rendered" + when: ard_destroy_cleanup_generated | default(false) | bool + +- name: Remove ARD deployment logs + ansible.builtin.file: + path: "{{ ard_deployment_dir }}/logs" + state: absent + when: ard_destroy_cleanup_logs | default(false) | bool diff --git a/ansible/roles/ard_provider_render/tasks/main.yml b/ansible/roles/ard_provider_render/tasks/main.yml index 29ed1c0..8774ba0 100644 --- a/ansible/roles/ard_provider_render/tasks/main.yml +++ b/ansible/roles/ard_provider_render/tasks/main.yml @@ -1,12 +1,31 @@ --- - name: Set ARD deployment render facts - set_fact: + ansible.builtin.set_fact: ard_deployment_dir: "{{ ard_deployment_dir | default('deployments/devstack-1') }}" ard_deployment_name: "{{ ard_deployment_name | default((ard_deployment_dir | default('deployments/devstack-1')) | basename) }}" ard_provider: "{{ ard_provider | default('libvirt') }}" + ard_topology: "{{ ard_topology | default('one-controller-one-compute') }}" + +- name: Validate supported local render inputs + ansible.builtin.assert: + that: + - ard_provider == 'libvirt' + - ard_topology in ['one-controller-one-compute', 'one-controller-two-compute'] + - ard_libvirt_network_cidr is match('^([0-9]{1,3}\.){3}0/24$') + fail_msg: >- + Local render currently supports libvirt and /24 CIDRs only. Requested + provider={{ ard_provider }}, topology={{ ard_topology }}, + cidr={{ ard_libvirt_network_cidr }}. + +- name: Derive local network render facts + ansible.builtin.set_fact: + ard_render_network_prefix: "{{ ard_libvirt_network_cidr | regex_replace('\\.0/24$', '') }}" + ard_libvirt_network_gateway: "{{ ard_libvirt_network_gateway | default(ard_libvirt_network_cidr | regex_replace('\\.0/24$', '.1')) }}" + ard_resource_name_prefix: "{{ ard_resource_name_prefix | default('ard-' + ard_deployment_name) }}" + ard_libvirt_network_name: "{{ ard_libvirt_network_name | default('ard-' + ard_deployment_name) }}" - name: Ensure ARD deployment directories exist - file: + ansible.builtin.file: path: "{{ item }}" state: directory mode: "0755" @@ -18,7 +37,7 @@ - "{{ ard_deployment_dir }}/logs" - name: Render deployment provider inputs - copy: + ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/deployment.yaml" force: false mode: "0644" @@ -26,85 +45,20 @@ --- ard_provider: {{ ard_provider }} ard_deployment_name: {{ ard_deployment_name }} - ard_resource_name_prefix: ard-{{ ard_deployment_name }} - - ard_default_image: debian-13 - ard_default_controller_flavor: devstack-control - ard_default_compute_flavor: devstack-compute - ard_default_vm_preference: devstack - - ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default(lookup('env', 'HOME') + '/.cache', true) }}/ard/images" - ard_image_download: true - ard_image_checksum_required: false + ard_resource_name_prefix: {{ ard_resource_name_prefix }} - ard_libvirt_uri: qemu:///system - ard_libvirt_pool: ard - ard_libvirt_network_name: ard-{{ ard_deployment_name }} - ard_libvirt_network_cidr: 192.168.96.0/24 - ard_libvirt_network_gateway: 192.168.96.1 - ard_libvirt_image_dir: "{{ lookup('env', 'XDG_STATE_HOME') | default(lookup('env', 'HOME') + '/.local/state', true) }}/ard/libvirt/images" + ard_topology: {{ ard_topology }} + ard_default_image: {{ ard_default_image }} + ard_default_controller_flavor: {{ ard_default_controller_flavor }} + ard_default_compute_flavor: {{ ard_default_compute_flavor }} + ard_default_vm_preference: {{ ard_default_vm_preference }} - ard_images: - debian-13: - os_family: debian - version: "13" - cloud_init: true - provider: - libvirt: - url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 - alternate_urls: - - https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2 - name: debian-13 - cache_filename: debian-13-genericcloud-amd64.qcow2 - format: qcow2 - cloud_init_datasource: NoCloud - ubuntu-24.04: - os_family: ubuntu - version: "24.04" - cloud_init: true - provider: - libvirt: - url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img - name: ubuntu-24.04 - cache_filename: noble-server-cloudimg-amd64.img - format: qcow2 - cloud_init_datasource: NoCloud - ubuntu-26.04: - os_family: ubuntu - version: "26.04" - cloud_init: true - provider: - libvirt: - url: null - name: ubuntu-26.04 - cache_filename: ubuntu-26.04-cloudimg-amd64.img - format: qcow2 - cloud_init_datasource: NoCloud + ard_libvirt_network_name: {{ ard_libvirt_network_name }} + ard_libvirt_network_cidr: {{ ard_libvirt_network_cidr }} + ard_libvirt_network_gateway: {{ ard_libvirt_network_gateway }} - ard_flavors: - devstack-control: - vcpus: 8 - memory: 16Gi - root_disk_gb: 80 - nested_virt: true - provider: - libvirt: - vcpus: 8 - memory_mb: 16384 - cpu_mode: host-passthrough - devstack-compute: - vcpus: 8 - memory: 8Gi - root_disk_gb: 80 - nested_virt: true - provider: - libvirt: - vcpus: 8 - memory_mb: 8192 - cpu_mode: host-passthrough - -- name: Render two-node topology inputs - copy: +- name: Render topology inputs + ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/nodes.yaml" force: false mode: "0644" @@ -113,42 +67,64 @@ ard_nodes: - name: controller hostname: controller - provider_resource_name: ard-{{ ard_deployment_name }}-controller + provider_resource_name: {{ ard_resource_name_prefix }}-controller groups: - controller - switch - image: debian-13 - flavor: devstack-control - preference: devstack + image: {{ ard_default_image }} + flavor: {{ ard_default_controller_flavor }} + preference: {{ ard_default_vm_preference }} networks: - name: ard-mgmt - ip: 192.168.96.2 - mac: "52:54:00:96:00:02" + ip: {{ ard_render_network_prefix }}.2 + mac: "52:54:00:{{ '%02x' | format((ard_render_network_prefix.split('.')[2] | int) % 256) }}:00:02" profiles: - ssh - nested_virt - ovn + {% if ard_topology in ['one-controller-one-compute', 'one-controller-two-compute'] %} - name: compute1 hostname: compute1 - provider_resource_name: ard-{{ ard_deployment_name }}-compute1 + provider_resource_name: {{ ard_resource_name_prefix }}-compute1 + groups: + - compute + - peers + - subnode + image: {{ ard_default_image }} + flavor: {{ ard_default_compute_flavor }} + preference: {{ ard_default_vm_preference }} + networks: + - name: ard-mgmt + ip: {{ ard_render_network_prefix }}.3 + mac: "52:54:00:{{ '%02x' | format((ard_render_network_prefix.split('.')[2] | int) % 256) }}:00:03" + profiles: + - ssh + - nested_virt + - ovn + {% endif %} + {% if ard_topology == 'one-controller-two-compute' %} + - name: compute2 + hostname: compute2 + provider_resource_name: {{ ard_resource_name_prefix }}-compute2 groups: - compute - peers - subnode - image: debian-13 - flavor: devstack-compute - preference: devstack + image: {{ ard_default_image }} + flavor: {{ ard_default_compute_flavor }} + preference: {{ ard_default_vm_preference }} networks: - name: ard-mgmt - ip: 192.168.96.3 - mac: "52:54:00:96:00:03" + ip: {{ ard_render_network_prefix }}.4 + mac: "52:54:00:{{ '%02x' | format((ard_render_network_prefix.split('.')[2] | int) % 256) }}:00:04" profiles: - ssh - nested_virt - ovn + {% endif %} - name: Render common DevStack vars - copy: + ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/devstack/common.yaml" force: false mode: "0644" @@ -163,7 +139,7 @@ stack_user_password: tester - name: Render controller DevStack vars - copy: + ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/devstack/group_vars/controller.yaml" force: false mode: "0644" @@ -171,10 +147,13 @@ --- controller_localrc_extra: {} controller_local_conf_extra: {} - controller_services_extra: {} + controller_services_extra:{{ '' if ard_topology == 'one-controller-two-compute' else ' {}' }} + {% if ard_topology == 'one-controller-two-compute' %} + n-cpu: false + {% endif %} - name: Render compute DevStack vars - copy: + ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/devstack/group_vars/compute.yaml" force: false mode: "0644" diff --git a/bindep.txt b/bindep.txt index 336f0a9..195a352 100644 --- a/bindep.txt +++ b/bindep.txt @@ -1,18 +1,26 @@ -build-essential [platform:dpkg] -gcc -libffi-dev [platform:dpkg] -libffi-devel [platform:rpm] +# Common development and workflow tools +git +rsync +curl +make python3 python3-pip -python3-dev [platform:dpkg] -python3-devel [platform:rpm] -python3-libvirt -libvirt-dev [platform:dpkg] -libvirt-devel [platform:rpm] -ruby-devel [platform:rpm] -redhat-rpm-config [platform:rpm] -vagrant -rsync -libvirt-daemon -libvirt-clients -libvirt-daemon-system [plathform:dpkg] +acl +podman + +# Python virtual environment support on Debian-family hosts +python3-venv [platform:dpkg] + +# Local libvirt provider dependencies on Debian/Ubuntu +qemu-utils [platform:dpkg] +libvirt-clients [platform:dpkg] +libvirt-daemon-system [platform:dpkg] +cloud-image-utils [platform:dpkg] +ovmf [platform:dpkg] + +# Local libvirt provider dependencies on Fedora/CentOS Stream +qemu-img [platform:rpm] +libvirt-client [platform:rpm] +libvirt-daemon-kvm [platform:rpm] +cloud-utils [platform:rpm] +edk2-ovmf [platform:rpm] diff --git a/bootstrap-repo.sh b/bootstrap-repo.sh index e248a6d..b105e46 100755 --- a/bootstrap-repo.sh +++ b/bootstrap-repo.sh @@ -1,22 +1,192 @@ -#!/bin/bash -set -x -which dpkg && sudo apt install -y python3-pip -which rpm && sudo dnf -y --setopt=install_weak_deps=False install python3-pip -[[ -e ".venv" ]] || python3 -m venv .venv -. .venv/bin/activate -# allow for using local dev copy of bindep if needed -which bindep || pip install bindep -pip install wheel -which dpkg && bindep -b | xargs sudo apt -y install -# Don't install weak deps, as that will pull in vagrant-libvirt, which we want -# to install manually later on. -which rpm && bindep -b | xargs sudo dnf -y --setopt=install_weak_deps=False install -#pip install ansible=2.9 -pip install ansible\<5 ansible-core\<2.12.0 \ - molecule!=3.6.1,!=3.6.0 molecule-vagrant python-vagrant netaddr molecule-openstack openstacksdk -which vagrant && vagrant plugin install vagrant-libvirt -git submodule update --init --recursive -groups | grep -E "libvirt" > /dev/null || \ -echo -e "You need to be part of the libvirt group for this to work.\nsudo usermod -a -G libvirt $USER" - -set +x +#!/usr/bin/env bash +set -euo pipefail + +DRY_RUN=${DRY_RUN:-0} +SKIP_PACKAGES=${SKIP_PACKAGES:-0} +SKIP_UV_INSTALL=${SKIP_UV_INSTALL:-0} +SKIP_SUBMODULES=${SKIP_SUBMODULES:-0} + +log() { + printf '==> %s\n' "$*" +} + +run() { + if [[ "${DRY_RUN}" == "1" ]]; then + printf '+ %q' "$1" + shift || true + for arg in "$@"; do + printf ' %q' "$arg" + done + printf '\n' + else + "$@" + fi +} + +have() { + command -v "$1" >/dev/null 2>&1 +} + +require_file() { + if [[ ! -f "$1" ]]; then + echo "Required file not found: $1" >&2 + exit 1 + fi +} + +source_os_release() { + if [[ -r /etc/os-release ]]; then + # shellcheck disable=SC1091 + . /etc/os-release + else + echo "Cannot detect host OS: /etc/os-release is missing" >&2 + exit 1 + fi +} + +select_package_manager() { + source_os_release + if have apt-get; then + PACKAGE_MANAGER=apt + elif have dnf; then + PACKAGE_MANAGER=dnf + else + echo "Unsupported host: expected apt-get or dnf" >&2 + exit 1 + fi + log "Detected ${PRETTY_NAME:-${ID:-unknown}} using ${PACKAGE_MANAGER}" +} + +install_minimal_bootstrap_packages() { + [[ "${SKIP_PACKAGES}" == "1" ]] && return 0 + + case "${PACKAGE_MANAGER}" in + apt) + run sudo apt-get update + run sudo apt-get install -y python3 python3-pip curl ca-certificates + ;; + dnf) + run sudo dnf -y --setopt=install_weak_deps=False install \ + python3 python3-pip curl ca-certificates + ;; + esac +} + +ensure_uv() { + if have uv; then + log "uv is already available: $(command -v uv)" + return 0 + fi + + if [[ "${SKIP_UV_INSTALL}" == "1" ]]; then + echo "uv is not available and SKIP_UV_INSTALL=1 was set" >&2 + exit 1 + fi + + log "Installing uv" + if [[ "${DRY_RUN}" == "1" ]]; then + echo '+ curl -LsSf https://astral.sh/uv/install.sh | sh' + else + curl -LsSf https://astral.sh/uv/install.sh | sh + fi + + export PATH="${HOME}/.local/bin:${PATH}" + if ! have uv; then + echo "uv installation completed but uv is not on PATH" >&2 + echo "Add ${HOME}/.local/bin to PATH and re-run this script." >&2 + exit 1 + fi +} + +install_bindep_packages() { + [[ "${SKIP_PACKAGES}" == "1" ]] && return 0 + require_file bindep.txt + + log "Resolving OS packages with bindep" + if [[ "${DRY_RUN}" == "1" ]]; then + echo '+ uvx --from bindep bindep -b' + echo '+ sudo install $(uvx --from bindep bindep -b)' + return 0 + fi + + mapfile -t packages < <(uvx --from bindep bindep -b) + if [[ ${#packages[@]} -eq 0 ]]; then + log "bindep reported no missing binary packages" + return 0 + fi + + log "Installing ${#packages[@]} bindep package(s): ${packages[*]}" + case "${PACKAGE_MANAGER}" in + apt) + sudo apt-get update + sudo apt-get install -y "${packages[@]}" + ;; + dnf) + sudo dnf -y --setopt=install_weak_deps=False install "${packages[@]}" + ;; + esac +} + +sync_python_environment() { + log "Syncing uv environment" + run uv sync +} + +sync_submodules() { + [[ "${SKIP_SUBMODULES}" == "1" ]] && return 0 + log "Updating git submodules" + run git submodule update --init --recursive +} + +check_command() { + if have "$1"; then + log "Found $1: $(command -v "$1")" + else + echo "Missing required command: $1" >&2 + return 1 + fi +} + +check_local_libvirt() { + log "Checking local ARD/libvirt commands" + check_command virsh + check_command qemu-img + check_command cloud-localds + check_command setfacl + + if [[ "${DRY_RUN}" != "1" ]]; then + if virsh --connect qemu:///system uri >/dev/null 2>&1; then + log "libvirt qemu:///system is reachable" + else + echo "WARNING: libvirt qemu:///system is not reachable by this user." >&2 + fi + fi + + if groups | grep -Eq '(^| )(libvirt|qemu)( |$)'; then + log "Current user appears to be in a libvirt/qemu access group" + else + cat >&2 < Date: Fri, 5 Jun 2026 03:01:56 +0100 Subject: [PATCH 06/11] Add local ARD Make convenience targets Local ARD deployments benefit from a simple way to enter nodes and from a single default rebuild workflow. This change makes the default Make target run the full local sequence of destroy cleanup, render, apply, ping, deploy, and verify. Add an ard-ssh helper that reads the generated deployment inventory and builds the SSH command for a selected node. The Makefile exposes ssh and ssh-print targets so users can either connect directly or print the command for inspection and manual reuse. Document the new default and SSH workflows in the README. Signed-off-by: Sean Mooney --- Makefile | 26 ++++++++++++++- README.md | 29 +++++++++++++++- scripts/ard-ssh | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 2 deletions(-) create mode 100755 scripts/ard-ssh diff --git a/Makefile b/Makefile index 8e43ef5..3ac5fa5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ +.DEFAULT_GOAL := default + .PHONY: \ - render apply ping deploy verify destroy destroy-clean-generated clean-generated cleanup site \ + default render apply ping ssh ssh-print deploy verify destroy destroy-clean-generated clean-generated cleanup site \ molecule-test molecule-role-tests molecule-role-% ARD_PROVIDER ?= libvirt @@ -10,6 +12,9 @@ ARD_TOPOLOGY ?= one-controller-one-compute ARD_IMAGE ?= debian-13 ARD_NETWORK_CIDR ?= 192.168.96.0/24 ARD_EXTRA_VARS ?= +ARD_NODE ?= controller +ARD_SSH_PRINT ?= 0 +ARD_SSH_ARGS ?= ARD_RENDER_EXTRA_VARS = \ ard_provider=$(ARD_PROVIDER) \ @@ -23,6 +28,15 @@ ARD_DEPLOYMENT_EXTRA_VARS = \ ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) \ $(ARD_EXTRA_VARS) +default: + -$(MAKE) destroy-clean-generated + -$(MAKE) cleanup + $(MAKE) render + $(MAKE) apply + $(MAKE) ping + $(MAKE) deploy + $(MAKE) verify + render: uv run ansible-playbook -i localhost, ansible/playbooks/ard-render.yaml \ -e "$(ARD_RENDER_EXTRA_VARS)" @@ -35,6 +49,16 @@ ping: uv run ansible -i $(ARD_DEPLOYMENT_DIR)/inventory.yaml all \ -m ansible.builtin.ping +ssh: + uv run scripts/ard-ssh \ + --inventory $(ARD_DEPLOYMENT_DIR)/inventory.yaml \ + --node $(ARD_NODE) \ + $(if $(filter 1 true yes,$(ARD_SSH_PRINT)),--print,) \ + $(if $(ARD_SSH_ARGS),-- $(ARD_SSH_ARGS),) + +ssh-print: + $(MAKE) ssh ARD_SSH_PRINT=1 + deploy: uv run ansible-playbook -i $(ARD_DEPLOYMENT_DIR)/inventory.yaml \ ansible/playbooks/ard-deploy-devstack.yaml \ diff --git a/README.md b/README.md index 53b7608..c174b9f 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,15 @@ make apply ARD_DEPLOYMENT=devstack-a make ping ARD_DEPLOYMENT=devstack-a ``` +SSH to a deployment node, or print the SSH command without running it: + +```bash +make ssh ARD_DEPLOYMENT=devstack-a ARD_NODE=controller +make ssh-print ARD_DEPLOYMENT=devstack-a ARD_NODE=compute1 +# equivalent explicit dry-run/print mode +make ssh ARD_DEPLOYMENT=devstack-a ARD_NODE=compute1 ARD_SSH_PRINT=1 +``` + Deploy DevStack: ```bash @@ -86,12 +95,27 @@ scenario before applying it. ## Make targets -The root `Makefile` wraps the provider playbooks: +The root `Makefile` wraps the provider playbooks. The default target is a full +local rebuild workflow: + +```bash +make +# equivalent to +make default +``` + +`make default` runs `destroy-clean-generated`, `cleanup`, `render`, `apply`, +`ping`, `deploy`, and `verify` in order. The initial destroy and cleanup steps +are best-effort so the target also works from a fresh checkout. + +Individual workflow targets are also available: ```bash make render make apply make ping +make ssh +make ssh-print make deploy make verify make destroy @@ -111,6 +135,9 @@ ARD_PROVIDER provider, currently libvirt ARD_TOPOLOGY topology preset ARD_IMAGE image key, default debian-13 ARD_NETWORK_CIDR libvirt management CIDR, default 192.168.96.0/24 +ARD_NODE inventory node for make ssh, default controller +ARD_SSH_PRINT print SSH command without running it when set to 1 +ARD_SSH_ARGS extra arguments passed to ssh ARD_EXTRA_VARS extra Ansible vars appended to provider commands ``` diff --git a/scripts/ard-ssh b/scripts/ard-ssh new file mode 100755 index 0000000..610b94b --- /dev/null +++ b/scripts/ard-ssh @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""SSH to an ARD deployment inventory host.""" + +from __future__ import annotations + +import argparse +import os +import shlex +import subprocess +import sys +from pathlib import Path + +import yaml + + +def load_host(inventory_path: Path, node: str) -> dict: + with inventory_path.open() as stream: + inventory = yaml.safe_load(stream) or {} + + hosts = inventory.get("all", {}).get("hosts", {}) + if node not in hosts: + available = ", ".join(sorted(hosts)) or "" + raise SystemExit( + f"Node {node!r} not found in {inventory_path}. Available hosts: {available}" + ) + return hosts[node] or {} + + +def build_ssh_command(host: dict, extra_args: list[str]) -> list[str]: + ansible_host = host.get("ansible_host") + if not ansible_host: + raise SystemExit("Selected host has no ansible_host") + + ansible_user = host.get("ansible_user") or os.environ.get("USER") or "stack" + command = ["ssh"] + + key_file = host.get("ansible_private_key_file") + if key_file: + command.extend(["-i", os.path.expanduser(str(key_file))]) + + ansible_port = host.get("ansible_port") + if ansible_port: + command.extend(["-p", str(ansible_port)]) + + common_args = host.get("ansible_ssh_common_args") + if common_args: + command.extend(shlex.split(str(common_args))) + + command.extend(extra_args) + command.append(f"{ansible_user}@{ansible_host}") + return command + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--inventory", required=True, type=Path) + parser.add_argument("--node", default="controller") + parser.add_argument( + "--print", + action="store_true", + dest="print_only", + help="print the SSH command and exit without executing it", + ) + parser.add_argument( + "ssh_args", + nargs=argparse.REMAINDER, + help="extra arguments passed to ssh after a literal --", + ) + args = parser.parse_args() + + extra_args = args.ssh_args + if extra_args and extra_args[0] == "--": + extra_args = extra_args[1:] + + host = load_host(args.inventory, args.node) + command = build_ssh_command(host, extra_args) + printable = shlex.join(command) + + if args.print_only: + print(printable) + return 0 + + print(printable, file=sys.stderr) + subprocess.run(command, check=False) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 58c6aa1b9d15ed3022c8a3bc0d70ddf50749e609 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Fri, 5 Jun 2026 16:39:14 +0100 Subject: [PATCH 07/11] Wait for ARD nodes during apply The local Make default workflow runs ping immediately after apply. The provider apply phase therefore needs to treat SSH and cloud-init readiness as part of provisioning, otherwise apply can return while the VMs are still booting. Add provider-created nodes to the active Ansible inventory when the libvirt inventory is generated. The apply playbook now follows provider creation with a readiness play that waits for Ansible SSH connectivity and cloud-init completion on all provider nodes before returning. Document that apply waits for node readiness before later ping or deploy steps. Signed-off-by: Sean Mooney --- README.md | 14 ++++++---- ansible/playbooks/ard-apply.yaml | 28 +++++++++++++++++++ .../ard_libvirt_inventory/tasks/main.yml | 19 +++++++++++++ .../ard_provider_common/defaults/main.yml | 3 ++ 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c174b9f..f494a98 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ make cleanup ARD_DEPLOYMENT=devstack-a The normal local workflow is: 1. `render`: create an editable deployment workspace. -2. `apply`: create libvirt network/domain/disk/seed resources and inventory. -3. `ping`: verify SSH/Ansible connectivity. +2. `apply`: create libvirt network/domain/disk/seed resources, generate inventory, and wait for SSH/cloud-init readiness. +3. `ping`: verify SSH/Ansible connectivity again. 4. `deploy`: run the multinode DevStack playbook. 5. `verify`: run basic post-deploy checks. 6. `destroy`: remove libvirt resources but keep the workspace and generated artifacts for inspection. @@ -305,14 +305,16 @@ libvirt cannot define or start the domain. ### SSH not ready -`apply` creates VMs and inventory. If an immediate ping fails, wait briefly and -retry: +`apply` creates VMs and inventory, then waits for SSH and cloud-init completion +before returning. If `apply` times out, inspect the serial console logs under the +libvirt deployment state directory or retry: ```bash -make ping ARD_DEPLOYMENT=devstack-a +make apply ARD_DEPLOYMENT=devstack-a ``` -Molecule create waits for SSH before converging. +Molecule create uses the same apply playbook, so it also waits for node readiness +before converging. ### CIDR conflicts diff --git a/ansible/playbooks/ard-apply.yaml b/ansible/playbooks/ard-apply.yaml index 737e596..1b5dcd4 100644 --- a/ansible/playbooks/ard-apply.yaml +++ b/ansible/playbooks/ard-apply.yaml @@ -10,3 +10,31 @@ - ard_provider_network - ard_provider_node - ard_provider_inventory + +- name: Wait for ARD provider nodes to be ready + hosts: ard_provider_nodes + gather_facts: false + roles: + - ard_provider_common + tasks: + - name: Wait for SSH and Ansible connectivity + ansible.builtin.wait_for_connection: + timeout: "{{ ard_apply_wait_timeout }}" + sleep: 5 + + - name: Wait for cloud-init completion + become: true + ansible.builtin.shell: | + if command -v cloud-init >/dev/null 2>&1; then + cloud-init status --wait + else + echo 'cloud-init not installed; assuming bootstrap already consumed it' + fi + register: ard_cloud_init_status + changed_when: false + failed_when: >- + ard_cloud_init_status.rc not in [0, 2] + or ( + 'status: done' not in ard_cloud_init_status.stdout + and 'cloud-init not installed' not in ard_cloud_init_status.stdout + ) diff --git a/ansible/roles/ard_libvirt_inventory/tasks/main.yml b/ansible/roles/ard_libvirt_inventory/tasks/main.yml index dbd9386..b9a1b39 100644 --- a/ansible/roles/ard_libvirt_inventory/tasks/main.yml +++ b/ansible/roles/ard_libvirt_inventory/tasks/main.yml @@ -40,6 +40,25 @@ {% endfor %} {% endfor %} +- name: Add ARD nodes to active inventory + ansible.builtin.add_host: + name: "{{ item.name }}" + groups: "{{ ['ard_provider_nodes'] + item.groups }}" + ansible_host: "{{ item.networks[0].ip }}" + ansible_user: stack + ansible_private_key_file: "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + ard_deployment_name: "{{ ard_deployment_name }}" + ard_provider_resource_name: "{{ item.provider_resource_name | default('ard-' + ard_deployment_name + '-' + item.name) }}" + nodepool: + private_ipv4: "{{ item.networks[0].ip }}" + public_ipv4: "{{ item.networks[0].ip }}" + zuul: + executor: + log_root: /tmp/zuul_logs + work_root: /tmp/work_root + loop: "{{ ard_nodes }}" + - name: Write ARD provider state copy: dest: "{{ ard_deployment_dir }}/provider-state.yaml" diff --git a/ansible/roles/ard_provider_common/defaults/main.yml b/ansible/roles/ard_provider_common/defaults/main.yml index 7217741..4c9aa29 100644 --- a/ansible/roles/ard_provider_common/defaults/main.yml +++ b/ansible/roles/ard_provider_common/defaults/main.yml @@ -26,6 +26,9 @@ ard_libvirt_secure_boot: false ard_destroy_cleanup_generated: false ard_destroy_cleanup_logs: false +# Apply should not finish until provider-created nodes are usable by Ansible. +ard_apply_wait_timeout: 900 + ard_images: debian-13: os_family: debian From 54ba443682c5980980cdfc4684c8b2f333dcecbd Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Fri, 5 Jun 2026 17:57:42 +0100 Subject: [PATCH 08/11] Compose ARD render presets for Molecule The render workflow needs to support reusable deployment intent instead of requiring every concrete deployment file to be committed or edited by hand. This is especially important for Molecule scenarios, where defining platforms and ARD nodes separately makes topology drift easy. This change adds generic render preset catalogs for branches, topologies, service profiles, and provider profiles. The render role composes those presets with optional render overrides and writes generated deployment, node, and DevStack variable files with a generated-file header. Molecule scenarios now define ARD intent once under provisioner.ard in molecule.yml. Their create playbooks derive render variables from that inline configuration, render an ignored deployment workspace, and then apply the ARD provider resources. The previously committed concrete Molecule deployment workspaces are removed so they are generated runtime state instead. The local Make render interface is extended with branch, service, provider profile, render file, and image override inputs. Validated with serial Molecule create, generated-inventory ping, and destroy for the default, one-controller-two-compute, and stable-2026.1 scenarios. Also validated the Make render/apply/ping/destroy-clean-generated/cleanup lifecycle. Signed-off-by: Sean Mooney --- .gitignore | 5 +- ARD_PROVIDER_DESIGN.md | 103 ++++++-- Makefile | 27 ++- README.md | 76 +++++- .../ard_provider_common/defaults/main.yml | 9 + .../files/presets/branches.yaml | 12 + .../files/presets/provider-profiles.yaml | 9 + .../files/presets/services.yaml | 30 +++ .../files/presets/topologies.yaml | 14 ++ .../roles/ard_provider_render/tasks/main.yml | 225 ++++++++++++------ molecule/default/create.yml | 72 +++++- molecule/default/deployment/deployment.yaml | 14 -- .../default/deployment/devstack/common.yaml | 8 - .../devstack/group_vars/compute.yaml | 4 - .../devstack/group_vars/controller.yaml | 4 - molecule/default/deployment/nodes.yaml | 37 --- molecule/default/molecule.yml | 17 +- .../one-controller-two-compute/create.yml | 72 +++++- .../deployment/deployment.yaml | 14 -- .../deployment/devstack/common.yaml | 8 - .../devstack/group_vars/compute.yaml | 4 - .../devstack/group_vars/controller.yaml | 5 - .../deployment/nodes.yaml | 56 ----- .../one-controller-two-compute/molecule.yml | 18 +- molecule/stable-2026.1/create.yml | 72 +++++- .../stable-2026.1/deployment/deployment.yaml | 14 -- .../deployment/devstack/common.yaml | 8 - .../devstack/group_vars/compute.yaml | 4 - .../devstack/group_vars/controller.yaml | 4 - molecule/stable-2026.1/deployment/nodes.yaml | 37 --- molecule/stable-2026.1/molecule.yml | 17 +- 31 files changed, 621 insertions(+), 378 deletions(-) create mode 100644 ansible/roles/ard_provider_common/files/presets/branches.yaml create mode 100644 ansible/roles/ard_provider_common/files/presets/provider-profiles.yaml create mode 100644 ansible/roles/ard_provider_common/files/presets/services.yaml create mode 100644 ansible/roles/ard_provider_common/files/presets/topologies.yaml delete mode 100644 molecule/default/deployment/deployment.yaml delete mode 100644 molecule/default/deployment/devstack/common.yaml delete mode 100644 molecule/default/deployment/devstack/group_vars/compute.yaml delete mode 100644 molecule/default/deployment/devstack/group_vars/controller.yaml delete mode 100644 molecule/default/deployment/nodes.yaml delete mode 100644 molecule/one-controller-two-compute/deployment/deployment.yaml delete mode 100644 molecule/one-controller-two-compute/deployment/devstack/common.yaml delete mode 100644 molecule/one-controller-two-compute/deployment/devstack/group_vars/compute.yaml delete mode 100644 molecule/one-controller-two-compute/deployment/devstack/group_vars/controller.yaml delete mode 100644 molecule/one-controller-two-compute/deployment/nodes.yaml delete mode 100644 molecule/stable-2026.1/deployment/deployment.yaml delete mode 100644 molecule/stable-2026.1/deployment/devstack/common.yaml delete mode 100644 molecule/stable-2026.1/deployment/devstack/group_vars/compute.yaml delete mode 100644 molecule/stable-2026.1/deployment/devstack/group_vars/controller.yaml delete mode 100644 molecule/stable-2026.1/deployment/nodes.yaml diff --git a/.gitignore b/.gitignore index 6ac91f2..44d8d88 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,4 @@ okd/output/ pull-secret deployments/* !deployments/.keep -molecule/*/deployment/inventory.yaml -molecule/*/deployment/provider-state.yaml -molecule/*/deployment/rendered/ -molecule/*/deployment/logs/ +molecule/*/deployment/ diff --git a/ARD_PROVIDER_DESIGN.md b/ARD_PROVIDER_DESIGN.md index 09b50fb..26a9601 100644 --- a/ARD_PROVIDER_DESIGN.md +++ b/ARD_PROVIDER_DESIGN.md @@ -436,11 +436,65 @@ If `ard_deployment_name` is passed explicitly, it must match the basename of `ar The workflow is: -1. **Render**: create `deployments//` from provider-neutral defaults and templates. This does not contact libvirt or OpenShift. -2. **Modify**: edit `deployment.yaml`, `nodes.yaml`, and the layered files under `devstack/` on disk. This is where scenario-specific DevStack `local.conf` inputs are changed per deployment, per group, or per node. -3. **Apply**: read the deployment folder, create or update provider resources, wait for SSH, generate `inventory.yaml`, and write `provider-state.yaml`. +1. **Render**: create `deployments//` from provider-neutral presets, provider profiles, service profiles, and optional overlays. This does not contact libvirt or OpenShift. +2. **Customize**: keep custom intent in a render file or overlay, rather than editing generated concrete files directly. Render is intentionally kustomize-like but simpler: presets are bases, overlays are ordinary YAML dictionaries, and later layers deep-merge into earlier layers. +3. **Apply**: read the deployment folder, create or update provider resources, wait for SSH and cloud-init, generate `inventory.yaml`, and write `provider-state.yaml`. 4. **Destroy**: use `provider-state.yaml`, the deployment name, and provider labels/name prefixes to destroy provider resources. Keep the deployment folder and logs. -5. **Cleanup**: remove the local deployment folder after destroy when the user no longer needs the rendered scenario inputs or state. +5. **Cleanup**: remove generated local deployment artifacts after destroy when the user no longer needs rendered inputs or state. + +Render composition order is: + +```text +role defaults + -> branch preset + -> topology preset + -> service profiles, in requested order + -> provider profile + -> render intent file + -> deployment-local overlay file + -> CLI / Make extra-vars +``` + +The intent file is small and should be the primary committed interface for Molecule scenarios and reusable examples: + +```yaml +# render.yaml +ard_provider: libvirt +ard_provider_profile: local-libvirt +ard_target_branch: master +ard_topology: one-controller-two-compute +ard_service_profiles: + - devstack + - ovn + - tempest +``` + +Topology presets are named convenience bases such as `all-in-one`, `one-controller-one-compute`, and `one-controller-two-compute`. They normalize into counts and roles, and advanced users may override the normalized values with `ard_topology_overrides` when a curated name is close but not exact. + +Generated concrete files such as `deployment.yaml`, `nodes.yaml`, and `devstack/*.yaml` are render output. They should include a generated-file header and may be overwritten by subsequent renders. Local customizations belong in the render intent or an overlay such as `overrides/render.yaml`. + +The supported explicit overlay dictionary is: + +```yaml +ard_render_overrides: + provider_defaults: + image: ubuntu-24.04 + controller_flavor: devstack-control + compute_flavor: devstack-compute + vm_preference: devstack + topology: + compute_count: 2 + devstack: + common: + enable_ceph: true + controller: + controller_localrc_extra: + DEBUG_LIBVIRT_COREDUMPS: true + compute: + compute_localrc_extra: {} +``` + +For command-line convenience, `ard_render_image`, `ard_render_controller_flavor`, `ard_render_compute_flavor`, and `ard_render_vm_preference` can override the composed provider defaults without changing the eventual provider input names written to `deployment.yaml`. Example deployment inputs: @@ -1390,40 +1444,41 @@ The KubeVirt provider should use `VirtualMachineInstancetype` and `VirtualMachin ## 14. Molecule Integration -Molecule should use ansible-native or delegated mode and call the same provider-neutral playbooks. +Molecule should use ansible-native or delegated mode and call the same provider-neutral playbooks. Molecule should not own provider-specific VM creation logic. It should be a workflow runner and verifier. + +To avoid defining topology and node names in multiple places, top-level Molecule scenarios should put ARD intent inline under `provisioner.ard` in `molecule.yml`. Molecule `platforms` are optional and should be omitted for these scenarios; ARD render presets generate the node list and provider inventory. Example scenario structure: ```text molecule/libvirt-multinode/ - molecule.yml - create.yml + molecule.yml # includes provisioner.ard + create.yml # reads provisioner.ard, renders, applies converge.yml verify.yml destroy.yml + deployment/ # generated/ignored ``` -`create.yml`: - -```yaml -- import_playbook: ../../ansible/playbooks/ard-create.yaml -``` - -`converge.yml`: +Example `molecule.yml` fragment: ```yaml -- import_playbook: ../../ansible/playbooks/ard-deploy-devstack.yaml -``` - -`destroy.yml`: - -```yaml -- import_playbook: ../../ansible/playbooks/ard-destroy.yaml +provisioner: + name: ansible + ard: + provider: libvirt + provider_profile: local-libvirt + target_branch: master + topology: one-controller-two-compute + service_profiles: + - devstack + - ovn + - tempest + libvirt: + network_cidr: 192.168.99.0/24 ``` -The same pattern applies to KubeVirt. - -Molecule should not own provider-specific VM creation logic. It should be a workflow runner and verifier. +`create.yml` reads `molecule.yml`, writes generated render variables under the ignored deployment directory, calls `ard-render.yaml`, and then calls `ard-apply.yaml`. The same pattern applies to KubeVirt once that provider is implemented. ## 15. Zuul Integration diff --git a/Makefile b/Makefile index 3ac5fa5..7b7d331 100644 --- a/Makefile +++ b/Makefile @@ -9,19 +9,35 @@ ARD_DEPLOYMENT ?= devstack-1 ARD_DEPLOYMENTS_DIR ?= $(CURDIR)/deployments ARD_DEPLOYMENT_DIR ?= $(ARD_DEPLOYMENTS_DIR)/$(ARD_DEPLOYMENT) ARD_TOPOLOGY ?= one-controller-one-compute -ARD_IMAGE ?= debian-13 +ARD_TARGET_BRANCH ?= master +ARD_SERVICES ?= devstack,ovn,tempest +ARD_PROVIDER_PROFILE ?= local-libvirt +ARD_IMAGE ?= ARD_NETWORK_CIDR ?= 192.168.96.0/24 +ARD_RENDER_FILE ?= ARD_EXTRA_VARS ?= ARD_NODE ?= controller ARD_SSH_PRINT ?= 0 ARD_SSH_ARGS ?= +ARD_RENDER_FILE_ARG = $(if $(ARD_RENDER_FILE),-e @$(ARD_RENDER_FILE),) +ARD_RENDER_PROVIDER_VAR = $(if $(filter command line environment override,$(origin ARD_PROVIDER)),ard_provider=$(ARD_PROVIDER),) +ARD_RENDER_PROVIDER_PROFILE_VAR = $(if $(filter command line environment override,$(origin ARD_PROVIDER_PROFILE)),ard_provider_profile=$(ARD_PROVIDER_PROFILE),) +ARD_RENDER_TARGET_BRANCH_VAR = $(if $(filter command line environment override,$(origin ARD_TARGET_BRANCH)),ard_target_branch=$(ARD_TARGET_BRANCH),) +ARD_RENDER_TOPOLOGY_VAR = $(if $(filter command line environment override,$(origin ARD_TOPOLOGY)),ard_topology=$(ARD_TOPOLOGY),) +ARD_RENDER_SERVICES_VAR = $(if $(filter command line environment override,$(origin ARD_SERVICES)),ard_service_profiles=$(ARD_SERVICES),) +ARD_RENDER_IMAGE_VAR = $(if $(ARD_IMAGE),ard_render_image=$(ARD_IMAGE),) +ARD_RENDER_NETWORK_VAR = $(if $(filter command line environment override,$(origin ARD_NETWORK_CIDR)),ard_libvirt_network_cidr=$(ARD_NETWORK_CIDR),) + ARD_RENDER_EXTRA_VARS = \ - ard_provider=$(ARD_PROVIDER) \ ard_deployment_dir=$(ARD_DEPLOYMENT_DIR) \ - ard_topology=$(ARD_TOPOLOGY) \ - ard_default_image=$(ARD_IMAGE) \ - ard_libvirt_network_cidr=$(ARD_NETWORK_CIDR) \ + $(ARD_RENDER_PROVIDER_VAR) \ + $(ARD_RENDER_PROVIDER_PROFILE_VAR) \ + $(ARD_RENDER_TARGET_BRANCH_VAR) \ + $(ARD_RENDER_TOPOLOGY_VAR) \ + $(ARD_RENDER_SERVICES_VAR) \ + $(ARD_RENDER_IMAGE_VAR) \ + $(ARD_RENDER_NETWORK_VAR) \ $(ARD_EXTRA_VARS) ARD_DEPLOYMENT_EXTRA_VARS = \ @@ -39,6 +55,7 @@ default: render: uv run ansible-playbook -i localhost, ansible/playbooks/ard-render.yaml \ + $(ARD_RENDER_FILE_ARG) \ -e "$(ARD_RENDER_EXTRA_VARS)" apply: diff --git a/README.md b/README.md index f494a98..fa34181 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ make cleanup ARD_DEPLOYMENT=devstack-a The normal local workflow is: -1. `render`: create an editable deployment workspace. +1. `render`: create a concrete deployment workspace from presets and optional overlays. 2. `apply`: create libvirt network/domain/disk/seed resources, generate inventory, and wait for SSH/cloud-init readiness. 3. `ping`: verify SSH/Ansible connectivity again. 4. `deploy`: run the multinode DevStack playbook. @@ -89,9 +89,7 @@ deployments// logs/ ``` -`render` writes files with overwrite protection, so local edits are preserved. -After rendering, edit `nodes.yaml` or the files under `devstack/` to customize a -scenario before applying it. +`render` treats `deployment.yaml`, `nodes.yaml`, and `devstack/*.yaml` as generated output and may overwrite them. Keep custom intent in a render file or deployment-local overlay such as `overrides/render.yaml`. ## Make targets @@ -133,8 +131,12 @@ ARD_DEPLOYMENTS_DIR deployment parent dir, default ./deployments ARD_DEPLOYMENT_DIR full workspace path ARD_PROVIDER provider, currently libvirt ARD_TOPOLOGY topology preset -ARD_IMAGE image key, default debian-13 +ARD_TARGET_BRANCH DevStack target branch, default master +ARD_SERVICES comma-separated service profiles, default devstack,ovn,tempest +ARD_PROVIDER_PROFILE provider profile, default local-libvirt +ARD_IMAGE optional image key override ARD_NETWORK_CIDR libvirt management CIDR, default 192.168.96.0/24 +ARD_RENDER_FILE optional render intent file loaded before Make vars ARD_NODE inventory node for make ssh, default controller ARD_SSH_PRINT print SSH command without running it when set to 1 ARD_SSH_ARGS extra arguments passed to ssh @@ -146,7 +148,9 @@ Example: ```bash make render \ ARD_DEPLOYMENT=devstack-a \ + ARD_TARGET_BRANCH=master \ ARD_TOPOLOGY=one-controller-two-compute \ + ARD_SERVICES=devstack,ovn,tempest \ ARD_NETWORK_CIDR=192.168.99.0/24 ``` @@ -174,10 +178,17 @@ Inventory hostnames remain logical names such as `controller`, `compute1`, and Supported local render presets: ```text +all-in-one one-controller-one-compute one-controller-two-compute ``` +`all-in-one` renders: + +```text +controller +``` + `one-controller-one-compute` renders: ```text @@ -193,8 +204,55 @@ compute1 compute2 ``` -The two-compute topology disables `nova-compute` on the controller through the -rendered controller group vars. +Multinode topologies disable `nova-compute` on the controller through the rendered controller group vars. The `all-in-one` topology leaves controller compute services enabled. + +## Render intent and service profiles + +Render can start from a small intent file: + +```yaml +--- +ard_provider: libvirt +ard_provider_profile: local-libvirt +ard_target_branch: stable/2026.1 +ard_topology: one-controller-one-compute +ard_service_profiles: + - devstack + - ovn + - tempest +ard_libvirt_network_cidr: 192.168.98.0/24 +``` + +Use it with: + +```bash +make render ARD_DEPLOYMENT=stable-test ARD_RENDER_FILE=examples/render.yaml +``` + +Use `ard_render_overrides` for simple kustomize-like customizations: + +```yaml +ard_render_overrides: + provider_defaults: + image: ubuntu-24.04 + topology: + compute_count: 2 + devstack: + common: + enable_ceph: true + controller: + controller_localrc_extra: + DEBUG_LIBVIRT_COREDUMPS: true +``` + +Current service profiles are: + +```text +devstack +ovn +tempest +ceph +``` ## Images and flavors @@ -255,7 +313,9 @@ provider resources. Top-level Molecule scenarios are full ARD/libvirt-backed DevStack validation flows. They call the same provider playbooks used by Make and do not use -Vagrant. +Vagrant. The scenario source of truth lives in `molecule.yml` under +`provisioner.ard`; Molecule `platforms` are intentionally omitted so topology +and node names are defined only once by ARD render presets. Available scenarios: diff --git a/ansible/roles/ard_provider_common/defaults/main.yml b/ansible/roles/ard_provider_common/defaults/main.yml index 4c9aa29..b923f01 100644 --- a/ansible/roles/ard_provider_common/defaults/main.yml +++ b/ansible/roles/ard_provider_common/defaults/main.yml @@ -1,6 +1,15 @@ --- ard_provider: libvirt +ard_provider_profile: local-libvirt ard_topology: one-controller-one-compute +ard_target_branch: master +ard_service_profiles: + - devstack + - ovn + - tempest +ard_render_generated_header: | + # Generated by ard-render. Do not edit directly. + # Put local customizations in render.yaml or overrides/render.yaml. ard_default_image: debian-13 ard_default_controller_flavor: devstack-control diff --git a/ansible/roles/ard_provider_common/files/presets/branches.yaml b/ansible/roles/ard_provider_common/files/presets/branches.yaml new file mode 100644 index 0000000..899c739 --- /dev/null +++ b/ansible/roles/ard_provider_common/files/presets/branches.yaml @@ -0,0 +1,12 @@ +--- +ard_render_branches: + master: + devstack: + common: + devstack_branch: master + stable/2026.1: + devstack: + common: + devstack_branch: stable/2026.1 + provider_defaults: + image: ubuntu-24.04 diff --git a/ansible/roles/ard_provider_common/files/presets/provider-profiles.yaml b/ansible/roles/ard_provider_common/files/presets/provider-profiles.yaml new file mode 100644 index 0000000..7d1621e --- /dev/null +++ b/ansible/roles/ard_provider_common/files/presets/provider-profiles.yaml @@ -0,0 +1,9 @@ +--- +ard_render_provider_profiles: + local-libvirt: + provider: libvirt + provider_defaults: + image: debian-13 + controller_flavor: devstack-control + compute_flavor: devstack-compute + vm_preference: devstack diff --git a/ansible/roles/ard_provider_common/files/presets/services.yaml b/ansible/roles/ard_provider_common/files/presets/services.yaml new file mode 100644 index 0000000..5d61e0e --- /dev/null +++ b/ansible/roles/ard_provider_common/files/presets/services.yaml @@ -0,0 +1,30 @@ +--- +ard_render_services: + devstack: + devstack: + common: + run_devstack: true + enable_ceph: false + configure_vdpa: false + external_bridge_mtu: 1450 + devstack_libvirt_type: kvm + stack_user_password: tester + controller: + controller_localrc_extra: {} + controller_local_conf_extra: {} + compute: + compute_localrc_extra: {} + compute_local_conf_extra: {} + compute_services_extra: {} + ovn: + node_profiles: + - ovn + tempest: + devstack: + common: {} + ceph: + node_profiles: + - ceph + devstack: + common: + enable_ceph: true diff --git a/ansible/roles/ard_provider_common/files/presets/topologies.yaml b/ansible/roles/ard_provider_common/files/presets/topologies.yaml new file mode 100644 index 0000000..e6181a0 --- /dev/null +++ b/ansible/roles/ard_provider_common/files/presets/topologies.yaml @@ -0,0 +1,14 @@ +--- +ard_render_topologies: + all-in-one: + controller_count: 1 + compute_count: 0 + controller_runs_compute: true + one-controller-one-compute: + controller_count: 1 + compute_count: 1 + controller_runs_compute: true + one-controller-two-compute: + controller_count: 1 + compute_count: 2 + controller_runs_compute: false diff --git a/ansible/roles/ard_provider_render/tasks/main.yml b/ansible/roles/ard_provider_render/tasks/main.yml index 8774ba0..569b423 100644 --- a/ansible/roles/ard_provider_render/tasks/main.yml +++ b/ansible/roles/ard_provider_render/tasks/main.yml @@ -1,28 +1,142 @@ --- -- name: Set ARD deployment render facts +- name: Set initial ARD deployment render facts + ansible.builtin.set_fact: + ard_deployment_dir: "{{ ard_deployment_dir | default('deployments/devstack-1') }}" + ard_deployment_name: "{{ ard_deployment_name | default((ard_deployment_dir | default('deployments/devstack-1')) | basename) }}" + +- name: Set ARD render input paths + ansible.builtin.set_fact: + ard_render_file: "{{ ard_render_file | default(ard_deployment_dir + '/render.yaml') }}" + ard_render_overlay_file: "{{ ard_render_overlay_file | default(ard_deployment_dir + '/overrides/render.yaml') }}" + +- name: Check for ARD render intent file + ansible.builtin.stat: + path: "{{ ard_render_file }}" + register: ard_render_file_stat + +- name: Load ARD render intent file + ansible.builtin.include_vars: + file: "{{ ard_render_file }}" + when: ard_render_file_stat.stat.exists + +- name: Check for ARD render overlay file + ansible.builtin.stat: + path: "{{ ard_render_overlay_file }}" + register: ard_render_overlay_file_stat + +- name: Load ARD render overlay file + ansible.builtin.include_vars: + file: "{{ ard_render_overlay_file }}" + when: ard_render_overlay_file_stat.stat.exists + +- name: Reload ARD deployment render facts after intent files ansible.builtin.set_fact: ard_deployment_dir: "{{ ard_deployment_dir | default('deployments/devstack-1') }}" ard_deployment_name: "{{ ard_deployment_name | default((ard_deployment_dir | default('deployments/devstack-1')) | basename) }}" ard_provider: "{{ ard_provider | default('libvirt') }}" + ard_provider_profile: "{{ ard_provider_profile | default('local-libvirt') }}" ard_topology: "{{ ard_topology | default('one-controller-one-compute') }}" + ard_target_branch: "{{ ard_target_branch | default('master') }}" -- name: Validate supported local render inputs +- name: Load ARD render topology presets + ansible.builtin.include_vars: + file: "{{ role_path }}/../ard_provider_common/files/presets/topologies.yaml" + +- name: Load ARD render branch presets + ansible.builtin.include_vars: + file: "{{ role_path }}/../ard_provider_common/files/presets/branches.yaml" + +- name: Load ARD render service presets + ansible.builtin.include_vars: + file: "{{ role_path }}/../ard_provider_common/files/presets/services.yaml" + +- name: Load ARD render provider profile presets + ansible.builtin.include_vars: + file: "{{ role_path }}/../ard_provider_common/files/presets/provider-profiles.yaml" + +- name: Normalize ARD render service profile list + ansible.builtin.set_fact: + ard_render_service_profile_list: >- + {{ + (ard_service_profiles.split(',') | map('trim') | reject('equalto', '') | list) + if ard_service_profiles is string + else (ard_service_profiles | default(['devstack', 'ovn', 'tempest'])) + }} + +- name: Validate supported ARD render presets ansible.builtin.assert: that: - ard_provider == 'libvirt' - - ard_topology in ['one-controller-one-compute', 'one-controller-two-compute'] + - ard_topology in ard_render_topologies + - ard_provider_profile in ard_render_provider_profiles + - ard_render_service_profile_list | difference(ard_render_services.keys() | list) | length == 0 - ard_libvirt_network_cidr is match('^([0-9]{1,3}\.){3}0/24$') fail_msg: >- - Local render currently supports libvirt and /24 CIDRs only. Requested - provider={{ ard_provider }}, topology={{ ard_topology }}, - cidr={{ ard_libvirt_network_cidr }}. + Unsupported render input. provider={{ ard_provider }}, + provider_profile={{ ard_provider_profile }}, topology={{ ard_topology }}, + services={{ ard_render_service_profile_list }}, cidr={{ ard_libvirt_network_cidr }}. + +- name: Compose ARD topology preset + ansible.builtin.set_fact: + ard_render_topology_config: >- + {{ ard_render_topologies[ard_topology] | + combine((ard_render_overrides | default({})).get('topology', {}), recursive=True) | + combine(ard_topology_overrides | default({}), recursive=True) }} + +- name: Initialize ARD service profile composition + ansible.builtin.set_fact: + ard_render_services_config: {} + ard_render_node_profiles: [] -- name: Derive local network render facts +- name: Compose requested ARD service profiles + ansible.builtin.set_fact: + ard_render_services_config: >- + {{ ard_render_services_config | combine(ard_render_services[item], recursive=True, list_merge='append') }} + ard_render_node_profiles: >- + {{ (ard_render_node_profiles + (ard_render_services[item].node_profiles | default([]))) | unique }} + loop: "{{ ard_render_service_profile_list }}" + +- name: Compose ARD branch and provider profile presets + ansible.builtin.set_fact: + ard_render_branch_config: >- + {{ {'devstack': {'common': {'devstack_branch': ard_target_branch}}} | + combine(ard_render_branches[ard_target_branch] | default({}), recursive=True) }} + ard_render_provider_profile_config: "{{ ard_render_provider_profiles[ard_provider_profile] }}" + +- name: Compose ARD provider defaults + ansible.builtin.set_fact: + ard_render_provider_defaults: >- + {{ ard_render_provider_profile_config.provider_defaults | + combine(ard_render_branch_config.provider_defaults | default({}), recursive=True) | + combine((ard_render_overrides | default({})).get('provider_defaults', {}), recursive=True) }} + +- name: Derive local network and provider defaults ansible.builtin.set_fact: ard_render_network_prefix: "{{ ard_libvirt_network_cidr | regex_replace('\\.0/24$', '') }}" ard_libvirt_network_gateway: "{{ ard_libvirt_network_gateway | default(ard_libvirt_network_cidr | regex_replace('\\.0/24$', '.1')) }}" ard_resource_name_prefix: "{{ ard_resource_name_prefix | default('ard-' + ard_deployment_name) }}" ard_libvirt_network_name: "{{ ard_libvirt_network_name | default('ard-' + ard_deployment_name) }}" + ard_render_default_image: "{{ ard_render_image | default(ard_render_provider_defaults.image) }}" + ard_render_controller_flavor: "{{ ard_render_controller_flavor | default(ard_render_provider_defaults.controller_flavor) }}" + ard_render_compute_flavor: "{{ ard_render_compute_flavor | default(ard_render_provider_defaults.compute_flavor) }}" + ard_render_vm_preference: "{{ ard_render_vm_preference | default(ard_render_provider_defaults.vm_preference) }}" + +- name: Compose rendered DevStack variables + ansible.builtin.set_fact: + ard_render_devstack_common: >- + {{ (ard_render_services_config.devstack.common | default({})) | + combine(ard_render_branch_config.devstack.common | default({}), recursive=True) | + combine((ard_render_overrides | default({})).get('devstack', {}).get('common', {}), recursive=True) | + combine(ard_devstack_common_overrides | default({}), recursive=True) }} + ard_render_devstack_controller: >- + {{ {'controller_services_extra': ({'n-cpu': false} if not ard_render_topology_config.controller_runs_compute else {})} | + combine(ard_render_services_config.devstack.controller | default({}), recursive=True) | + combine((ard_render_overrides | default({})).get('devstack', {}).get('controller', {}), recursive=True) | + combine(ard_devstack_controller_overrides | default({}), recursive=True) }} + ard_render_devstack_compute: >- + {{ (ard_render_services_config.devstack.compute | default({})) | + combine((ard_render_overrides | default({})).get('devstack', {}).get('compute', {}), recursive=True) | + combine(ard_devstack_compute_overrides | default({}), recursive=True) }} - name: Ensure ARD deployment directories exist ansible.builtin.file: @@ -39,19 +153,22 @@ - name: Render deployment provider inputs ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/deployment.yaml" - force: false mode: "0644" content: | + {{ ard_render_generated_header }} --- ard_provider: {{ ard_provider }} + ard_provider_profile: {{ ard_provider_profile }} ard_deployment_name: {{ ard_deployment_name }} ard_resource_name_prefix: {{ ard_resource_name_prefix }} + ard_target_branch: {{ ard_target_branch }} ard_topology: {{ ard_topology }} - ard_default_image: {{ ard_default_image }} - ard_default_controller_flavor: {{ ard_default_controller_flavor }} - ard_default_compute_flavor: {{ ard_default_compute_flavor }} - ard_default_vm_preference: {{ ard_default_vm_preference }} + ard_service_profiles: {{ ard_render_service_profile_list | to_json }} + ard_default_image: {{ ard_render_default_image }} + ard_default_controller_flavor: {{ ard_render_controller_flavor }} + ard_default_compute_flavor: {{ ard_render_compute_flavor }} + ard_default_vm_preference: {{ ard_render_vm_preference }} ard_libvirt_network_name: {{ ard_libvirt_network_name }} ard_libvirt_network_cidr: {{ ard_libvirt_network_cidr }} @@ -60,9 +177,9 @@ - name: Render topology inputs ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/nodes.yaml" - force: false mode: "0644" content: | + {{ ard_render_generated_header }} --- ard_nodes: - name: controller @@ -71,9 +188,9 @@ groups: - controller - switch - image: {{ ard_default_image }} - flavor: {{ ard_default_controller_flavor }} - preference: {{ ard_default_vm_preference }} + image: {{ ard_render_default_image }} + flavor: {{ ard_render_controller_flavor }} + preference: {{ ard_render_vm_preference }} networks: - name: ard-mgmt ip: {{ ard_render_network_prefix }}.2 @@ -81,84 +198,52 @@ profiles: - ssh - nested_virt - - ovn - {% if ard_topology in ['one-controller-one-compute', 'one-controller-two-compute'] %} - - name: compute1 - hostname: compute1 - provider_resource_name: {{ ard_resource_name_prefix }}-compute1 - groups: - - compute - - peers - - subnode - image: {{ ard_default_image }} - flavor: {{ ard_default_compute_flavor }} - preference: {{ ard_default_vm_preference }} - networks: - - name: ard-mgmt - ip: {{ ard_render_network_prefix }}.3 - mac: "52:54:00:{{ '%02x' | format((ard_render_network_prefix.split('.')[2] | int) % 256) }}:00:03" - profiles: - - ssh - - nested_virt - - ovn - {% endif %} - {% if ard_topology == 'one-controller-two-compute' %} - - name: compute2 - hostname: compute2 - provider_resource_name: {{ ard_resource_name_prefix }}-compute2 + {% for profile in ard_render_node_profiles %} + - {{ profile }} + {% endfor %} + {% for index in range(1, (ard_render_topology_config.compute_count | int) + 1) %} + - name: compute{{ index }} + hostname: compute{{ index }} + provider_resource_name: {{ ard_resource_name_prefix }}-compute{{ index }} groups: - compute - peers - subnode - image: {{ ard_default_image }} - flavor: {{ ard_default_compute_flavor }} - preference: {{ ard_default_vm_preference }} + image: {{ ard_render_default_image }} + flavor: {{ ard_render_compute_flavor }} + preference: {{ ard_render_vm_preference }} networks: - name: ard-mgmt - ip: {{ ard_render_network_prefix }}.4 - mac: "52:54:00:{{ '%02x' | format((ard_render_network_prefix.split('.')[2] | int) % 256) }}:00:04" + ip: {{ ard_render_network_prefix }}.{{ index + 2 }} + mac: "52:54:00:{{ '%02x' | format((ard_render_network_prefix.split('.')[2] | int) % 256) }}:00:{{ '%02x' | format(index + 2) }}" profiles: - ssh - nested_virt - - ovn - {% endif %} + {% for profile in ard_render_node_profiles %} + - {{ profile }} + {% endfor %} + {% endfor %} - name: Render common DevStack vars ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/devstack/common.yaml" - force: false mode: "0644" content: | - --- - devstack_branch: master - run_devstack: true - enable_ceph: false - configure_vdpa: false - external_bridge_mtu: 1450 - devstack_libvirt_type: kvm - stack_user_password: tester + {{ ard_render_generated_header }} + {{ ard_render_devstack_common | to_nice_yaml(indent=2, sort_keys=False) }} - name: Render controller DevStack vars ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/devstack/group_vars/controller.yaml" - force: false mode: "0644" content: | - --- - controller_localrc_extra: {} - controller_local_conf_extra: {} - controller_services_extra:{{ '' if ard_topology == 'one-controller-two-compute' else ' {}' }} - {% if ard_topology == 'one-controller-two-compute' %} - n-cpu: false - {% endif %} + {{ ard_render_generated_header }} + {{ ard_render_devstack_controller | to_nice_yaml(indent=2, sort_keys=False) }} - name: Render compute DevStack vars ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/devstack/group_vars/compute.yaml" - force: false mode: "0644" content: | - --- - compute_localrc_extra: {} - compute_local_conf_extra: {} - compute_services_extra: {} + {{ ard_render_generated_header }} + {{ ard_render_devstack_compute | to_nice_yaml(indent=2, sort_keys=False) }} diff --git a/molecule/default/create.yml b/molecule/default/create.yml index f1bd5a2..3b2a8ec 100644 --- a/molecule/default/create.yml +++ b/molecule/default/create.yml @@ -5,16 +5,64 @@ gather_facts: false vars: ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" - ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_scenario_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}" + ard_deployment_dir: "{{ ard_scenario_dir }}/deployment" + ard_molecule_render_vars_file: "{{ ard_deployment_dir }}/.molecule-render-vars.yaml" ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" tasks: - - name: Apply ARD provider resources + - name: Load Molecule scenario configuration + ansible.builtin.include_vars: + file: "{{ ard_scenario_dir }}/molecule.yml" + name: ard_molecule_config + + - name: Validate Molecule ARD scenario configuration + ansible.builtin.assert: + that: + - ard_molecule_config.provisioner.ard is defined + - ard_molecule_config.provisioner.ard.topology is defined + - ard_molecule_config.provisioner.ard.libvirt.network_cidr is defined + fail_msg: >- + Molecule scenario must define provisioner.ard.topology and + provisioner.ard.libvirt.network_cidr in molecule.yml. + + - name: Ensure ARD deployment directory exists for generated render vars + ansible.builtin.file: + path: "{{ ard_deployment_dir }}" + state: directory + mode: "0755" + + - name: Write generated ARD render vars from Molecule scenario + ansible.builtin.copy: + dest: "{{ ard_molecule_render_vars_file }}" + mode: "0644" + content: | + --- + ard_provider: {{ ard_molecule_config.provisioner.ard.provider | default('libvirt') }} + ard_provider_profile: {{ ard_molecule_config.provisioner.ard.provider_profile | default('local-libvirt') }} + ard_deployment_name: {{ ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename) }} + ard_resource_name_prefix: {{ ard_molecule_config.provisioner.ard.resource_name_prefix | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} + ard_target_branch: {{ ard_molecule_config.provisioner.ard.target_branch | default('master') }} + ard_topology: {{ ard_molecule_config.provisioner.ard.topology }} + ard_service_profiles: {{ ard_molecule_config.provisioner.ard.service_profiles | default(['devstack', 'ovn', 'tempest']) | to_json }} + ard_libvirt_network_name: {{ ard_molecule_config.provisioner.ard.libvirt.network_name | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} + ard_libvirt_network_cidr: {{ ard_molecule_config.provisioner.ard.libvirt.network_cidr }} + {% if ard_molecule_config.provisioner.ard.render_image is defined %} + ard_render_image: {{ ard_molecule_config.provisioner.ard.render_image }} + {% endif %} + {% if ard_molecule_config.provisioner.ard.render_overrides is defined %} + ard_render_overrides: + {{ ard_molecule_config.provisioner.ard.render_overrides | to_nice_yaml(indent=2, sort_keys=False) | indent(2, true) }} + {% endif %} + + - name: Render ARD deployment workspace ansible.builtin.command: argv: - ansible-playbook - -i - localhost, - - ansible/playbooks/ard-apply.yaml + - ansible/playbooks/ard-render.yaml + - -e + - "@{{ ard_molecule_render_vars_file }}" - -e - ard_deployment_dir={{ ard_deployment_dir }} environment: @@ -24,20 +72,20 @@ chdir: "{{ ard_project_dir }}" changed_when: true - - name: Wait for ARD provider SSH access + - name: Apply ARD provider resources + tags: + - apply ansible.builtin.command: argv: - - ansible + - ansible-playbook - -i - - "{{ ard_deployment_dir }}/inventory.yaml" - - all - - -m - - ansible.builtin.wait_for_connection - - -a - - timeout=600 sleep=5 + - localhost, + - ansible/playbooks/ard-apply.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} environment: ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" args: chdir: "{{ ard_project_dir }}" - changed_when: false + changed_when: true diff --git a/molecule/default/deployment/deployment.yaml b/molecule/default/deployment/deployment.yaml deleted file mode 100644 index f25421f..0000000 --- a/molecule/default/deployment/deployment.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -ard_provider: libvirt -ard_deployment_name: molecule-default -ard_resource_name_prefix: ard-molecule-default - -ard_topology: one-controller-one-compute -ard_default_image: debian-13 -ard_default_controller_flavor: devstack-control -ard_default_compute_flavor: devstack-compute -ard_default_vm_preference: devstack - -ard_libvirt_network_name: ard-molecule-default -ard_libvirt_network_cidr: 192.168.96.0/24 -ard_libvirt_network_gateway: 192.168.96.1 diff --git a/molecule/default/deployment/devstack/common.yaml b/molecule/default/deployment/devstack/common.yaml deleted file mode 100644 index 61f74c6..0000000 --- a/molecule/default/deployment/devstack/common.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -devstack_branch: master -run_devstack: true -enable_ceph: false -configure_vdpa: false -external_bridge_mtu: 1450 -devstack_libvirt_type: kvm -stack_user_password: tester diff --git a/molecule/default/deployment/devstack/group_vars/compute.yaml b/molecule/default/deployment/devstack/group_vars/compute.yaml deleted file mode 100644 index 051f914..0000000 --- a/molecule/default/deployment/devstack/group_vars/compute.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -compute_localrc_extra: {} -compute_local_conf_extra: {} -compute_services_extra: {} diff --git a/molecule/default/deployment/devstack/group_vars/controller.yaml b/molecule/default/deployment/devstack/group_vars/controller.yaml deleted file mode 100644 index aa7587a..0000000 --- a/molecule/default/deployment/devstack/group_vars/controller.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -controller_localrc_extra: {} -controller_local_conf_extra: {} -controller_services_extra: {} diff --git a/molecule/default/deployment/nodes.yaml b/molecule/default/deployment/nodes.yaml deleted file mode 100644 index 8f5e6ae..0000000 --- a/molecule/default/deployment/nodes.yaml +++ /dev/null @@ -1,37 +0,0 @@ ---- -ard_nodes: - - name: controller - hostname: controller - provider_resource_name: ard-molecule-default-controller - groups: - - controller - - switch - image: debian-13 - flavor: devstack-control - preference: devstack - networks: - - name: ard-mgmt - ip: 192.168.96.2 - mac: "52:54:00:96:00:02" - profiles: - - ssh - - nested_virt - - ovn - - name: compute1 - hostname: compute1 - provider_resource_name: ard-molecule-default-compute1 - groups: - - compute - - peers - - subnode - image: debian-13 - flavor: devstack-compute - preference: devstack - networks: - - name: ard-mgmt - ip: 192.168.96.3 - mac: "52:54:00:96:00:03" - profiles: - - ssh - - nested_virt - - ovn diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml index 115dff4..40c1d1a 100644 --- a/molecule/default/molecule.yml +++ b/molecule/default/molecule.yml @@ -4,13 +4,24 @@ dependency: enabled: false driver: name: default -platforms: - - name: controller - - name: compute1 provisioner: name: ansible env: ANSIBLE_STDOUT_CALLBACK: yaml + ard: + provider: libvirt + provider_profile: local-libvirt + deployment_name: molecule-default + resource_name_prefix: ard-molecule-default + target_branch: master + topology: one-controller-one-compute + service_profiles: + - devstack + - ovn + - tempest + libvirt: + network_name: ard-molecule-default + network_cidr: 192.168.97.0/24 verifier: name: ansible scenario: diff --git a/molecule/one-controller-two-compute/create.yml b/molecule/one-controller-two-compute/create.yml index f1bd5a2..3b2a8ec 100644 --- a/molecule/one-controller-two-compute/create.yml +++ b/molecule/one-controller-two-compute/create.yml @@ -5,16 +5,64 @@ gather_facts: false vars: ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" - ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_scenario_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}" + ard_deployment_dir: "{{ ard_scenario_dir }}/deployment" + ard_molecule_render_vars_file: "{{ ard_deployment_dir }}/.molecule-render-vars.yaml" ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" tasks: - - name: Apply ARD provider resources + - name: Load Molecule scenario configuration + ansible.builtin.include_vars: + file: "{{ ard_scenario_dir }}/molecule.yml" + name: ard_molecule_config + + - name: Validate Molecule ARD scenario configuration + ansible.builtin.assert: + that: + - ard_molecule_config.provisioner.ard is defined + - ard_molecule_config.provisioner.ard.topology is defined + - ard_molecule_config.provisioner.ard.libvirt.network_cidr is defined + fail_msg: >- + Molecule scenario must define provisioner.ard.topology and + provisioner.ard.libvirt.network_cidr in molecule.yml. + + - name: Ensure ARD deployment directory exists for generated render vars + ansible.builtin.file: + path: "{{ ard_deployment_dir }}" + state: directory + mode: "0755" + + - name: Write generated ARD render vars from Molecule scenario + ansible.builtin.copy: + dest: "{{ ard_molecule_render_vars_file }}" + mode: "0644" + content: | + --- + ard_provider: {{ ard_molecule_config.provisioner.ard.provider | default('libvirt') }} + ard_provider_profile: {{ ard_molecule_config.provisioner.ard.provider_profile | default('local-libvirt') }} + ard_deployment_name: {{ ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename) }} + ard_resource_name_prefix: {{ ard_molecule_config.provisioner.ard.resource_name_prefix | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} + ard_target_branch: {{ ard_molecule_config.provisioner.ard.target_branch | default('master') }} + ard_topology: {{ ard_molecule_config.provisioner.ard.topology }} + ard_service_profiles: {{ ard_molecule_config.provisioner.ard.service_profiles | default(['devstack', 'ovn', 'tempest']) | to_json }} + ard_libvirt_network_name: {{ ard_molecule_config.provisioner.ard.libvirt.network_name | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} + ard_libvirt_network_cidr: {{ ard_molecule_config.provisioner.ard.libvirt.network_cidr }} + {% if ard_molecule_config.provisioner.ard.render_image is defined %} + ard_render_image: {{ ard_molecule_config.provisioner.ard.render_image }} + {% endif %} + {% if ard_molecule_config.provisioner.ard.render_overrides is defined %} + ard_render_overrides: + {{ ard_molecule_config.provisioner.ard.render_overrides | to_nice_yaml(indent=2, sort_keys=False) | indent(2, true) }} + {% endif %} + + - name: Render ARD deployment workspace ansible.builtin.command: argv: - ansible-playbook - -i - localhost, - - ansible/playbooks/ard-apply.yaml + - ansible/playbooks/ard-render.yaml + - -e + - "@{{ ard_molecule_render_vars_file }}" - -e - ard_deployment_dir={{ ard_deployment_dir }} environment: @@ -24,20 +72,20 @@ chdir: "{{ ard_project_dir }}" changed_when: true - - name: Wait for ARD provider SSH access + - name: Apply ARD provider resources + tags: + - apply ansible.builtin.command: argv: - - ansible + - ansible-playbook - -i - - "{{ ard_deployment_dir }}/inventory.yaml" - - all - - -m - - ansible.builtin.wait_for_connection - - -a - - timeout=600 sleep=5 + - localhost, + - ansible/playbooks/ard-apply.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} environment: ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" args: chdir: "{{ ard_project_dir }}" - changed_when: false + changed_when: true diff --git a/molecule/one-controller-two-compute/deployment/deployment.yaml b/molecule/one-controller-two-compute/deployment/deployment.yaml deleted file mode 100644 index 625d740..0000000 --- a/molecule/one-controller-two-compute/deployment/deployment.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -ard_provider: libvirt -ard_deployment_name: molecule-octc -ard_resource_name_prefix: ard-molecule-octc - -ard_topology: one-controller-two-compute -ard_default_image: debian-13 -ard_default_controller_flavor: devstack-control -ard_default_compute_flavor: devstack-compute -ard_default_vm_preference: devstack - -ard_libvirt_network_name: ard-molecule-octc -ard_libvirt_network_cidr: 192.168.97.0/24 -ard_libvirt_network_gateway: 192.168.97.1 diff --git a/molecule/one-controller-two-compute/deployment/devstack/common.yaml b/molecule/one-controller-two-compute/deployment/devstack/common.yaml deleted file mode 100644 index 61f74c6..0000000 --- a/molecule/one-controller-two-compute/deployment/devstack/common.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -devstack_branch: master -run_devstack: true -enable_ceph: false -configure_vdpa: false -external_bridge_mtu: 1450 -devstack_libvirt_type: kvm -stack_user_password: tester diff --git a/molecule/one-controller-two-compute/deployment/devstack/group_vars/compute.yaml b/molecule/one-controller-two-compute/deployment/devstack/group_vars/compute.yaml deleted file mode 100644 index 051f914..0000000 --- a/molecule/one-controller-two-compute/deployment/devstack/group_vars/compute.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -compute_localrc_extra: {} -compute_local_conf_extra: {} -compute_services_extra: {} diff --git a/molecule/one-controller-two-compute/deployment/devstack/group_vars/controller.yaml b/molecule/one-controller-two-compute/deployment/devstack/group_vars/controller.yaml deleted file mode 100644 index 33e00a3..0000000 --- a/molecule/one-controller-two-compute/deployment/devstack/group_vars/controller.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -controller_localrc_extra: {} -controller_local_conf_extra: {} -controller_services_extra: - n-cpu: false diff --git a/molecule/one-controller-two-compute/deployment/nodes.yaml b/molecule/one-controller-two-compute/deployment/nodes.yaml deleted file mode 100644 index 82d52eb..0000000 --- a/molecule/one-controller-two-compute/deployment/nodes.yaml +++ /dev/null @@ -1,56 +0,0 @@ ---- -ard_nodes: - - name: controller - hostname: controller - provider_resource_name: ard-molecule-octc-controller - groups: - - controller - - switch - image: debian-13 - flavor: devstack-control - preference: devstack - networks: - - name: ard-mgmt - ip: 192.168.97.2 - mac: "52:54:00:97:00:02" - profiles: - - ssh - - nested_virt - - ovn - - name: compute1 - hostname: compute1 - provider_resource_name: ard-molecule-octc-compute1 - groups: - - compute - - peers - - subnode - image: debian-13 - flavor: devstack-compute - preference: devstack - networks: - - name: ard-mgmt - ip: 192.168.97.3 - mac: "52:54:00:97:00:03" - profiles: - - ssh - - nested_virt - - ovn - - - name: compute2 - hostname: compute2 - provider_resource_name: ard-molecule-octc-compute2 - groups: - - compute - - peers - - subnode - image: debian-13 - flavor: devstack-compute - preference: devstack - networks: - - name: ard-mgmt - ip: 192.168.97.4 - mac: "52:54:00:97:00:04" - profiles: - - ssh - - nested_virt - - ovn diff --git a/molecule/one-controller-two-compute/molecule.yml b/molecule/one-controller-two-compute/molecule.yml index 7ccaf1b..707fc54 100644 --- a/molecule/one-controller-two-compute/molecule.yml +++ b/molecule/one-controller-two-compute/molecule.yml @@ -4,14 +4,24 @@ dependency: enabled: false driver: name: default -platforms: - - name: controller - - name: compute1 - - name: compute2 provisioner: name: ansible env: ANSIBLE_STDOUT_CALLBACK: yaml + ard: + provider: libvirt + provider_profile: local-libvirt + deployment_name: molecule-one-controller-two-compute + resource_name_prefix: ard-molecule-one-controller-two-compute + target_branch: master + topology: one-controller-two-compute + service_profiles: + - devstack + - ovn + - tempest + libvirt: + network_name: ard-molecule-one-controller-two-compute + network_cidr: 192.168.99.0/24 verifier: name: ansible scenario: diff --git a/molecule/stable-2026.1/create.yml b/molecule/stable-2026.1/create.yml index f1bd5a2..3b2a8ec 100644 --- a/molecule/stable-2026.1/create.yml +++ b/molecule/stable-2026.1/create.yml @@ -5,16 +5,64 @@ gather_facts: false vars: ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" - ard_deployment_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}/deployment" + ard_scenario_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}" + ard_deployment_dir: "{{ ard_scenario_dir }}/deployment" + ard_molecule_render_vars_file: "{{ ard_deployment_dir }}/.molecule-render-vars.yaml" ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" tasks: - - name: Apply ARD provider resources + - name: Load Molecule scenario configuration + ansible.builtin.include_vars: + file: "{{ ard_scenario_dir }}/molecule.yml" + name: ard_molecule_config + + - name: Validate Molecule ARD scenario configuration + ansible.builtin.assert: + that: + - ard_molecule_config.provisioner.ard is defined + - ard_molecule_config.provisioner.ard.topology is defined + - ard_molecule_config.provisioner.ard.libvirt.network_cidr is defined + fail_msg: >- + Molecule scenario must define provisioner.ard.topology and + provisioner.ard.libvirt.network_cidr in molecule.yml. + + - name: Ensure ARD deployment directory exists for generated render vars + ansible.builtin.file: + path: "{{ ard_deployment_dir }}" + state: directory + mode: "0755" + + - name: Write generated ARD render vars from Molecule scenario + ansible.builtin.copy: + dest: "{{ ard_molecule_render_vars_file }}" + mode: "0644" + content: | + --- + ard_provider: {{ ard_molecule_config.provisioner.ard.provider | default('libvirt') }} + ard_provider_profile: {{ ard_molecule_config.provisioner.ard.provider_profile | default('local-libvirt') }} + ard_deployment_name: {{ ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename) }} + ard_resource_name_prefix: {{ ard_molecule_config.provisioner.ard.resource_name_prefix | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} + ard_target_branch: {{ ard_molecule_config.provisioner.ard.target_branch | default('master') }} + ard_topology: {{ ard_molecule_config.provisioner.ard.topology }} + ard_service_profiles: {{ ard_molecule_config.provisioner.ard.service_profiles | default(['devstack', 'ovn', 'tempest']) | to_json }} + ard_libvirt_network_name: {{ ard_molecule_config.provisioner.ard.libvirt.network_name | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} + ard_libvirt_network_cidr: {{ ard_molecule_config.provisioner.ard.libvirt.network_cidr }} + {% if ard_molecule_config.provisioner.ard.render_image is defined %} + ard_render_image: {{ ard_molecule_config.provisioner.ard.render_image }} + {% endif %} + {% if ard_molecule_config.provisioner.ard.render_overrides is defined %} + ard_render_overrides: + {{ ard_molecule_config.provisioner.ard.render_overrides | to_nice_yaml(indent=2, sort_keys=False) | indent(2, true) }} + {% endif %} + + - name: Render ARD deployment workspace ansible.builtin.command: argv: - ansible-playbook - -i - localhost, - - ansible/playbooks/ard-apply.yaml + - ansible/playbooks/ard-render.yaml + - -e + - "@{{ ard_molecule_render_vars_file }}" - -e - ard_deployment_dir={{ ard_deployment_dir }} environment: @@ -24,20 +72,20 @@ chdir: "{{ ard_project_dir }}" changed_when: true - - name: Wait for ARD provider SSH access + - name: Apply ARD provider resources + tags: + - apply ansible.builtin.command: argv: - - ansible + - ansible-playbook - -i - - "{{ ard_deployment_dir }}/inventory.yaml" - - all - - -m - - ansible.builtin.wait_for_connection - - -a - - timeout=600 sleep=5 + - localhost, + - ansible/playbooks/ard-apply.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} environment: ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" args: chdir: "{{ ard_project_dir }}" - changed_when: false + changed_when: true diff --git a/molecule/stable-2026.1/deployment/deployment.yaml b/molecule/stable-2026.1/deployment/deployment.yaml deleted file mode 100644 index 983082a..0000000 --- a/molecule/stable-2026.1/deployment/deployment.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -ard_provider: libvirt -ard_deployment_name: molecule-stable-2026-1 -ard_resource_name_prefix: ard-molecule-stable-2026-1 - -ard_topology: one-controller-one-compute -ard_default_image: ubuntu-24.04 -ard_default_controller_flavor: devstack-control -ard_default_compute_flavor: devstack-compute -ard_default_vm_preference: devstack - -ard_libvirt_network_name: ard-molecule-stable-2026-1 -ard_libvirt_network_cidr: 192.168.98.0/24 -ard_libvirt_network_gateway: 192.168.98.1 diff --git a/molecule/stable-2026.1/deployment/devstack/common.yaml b/molecule/stable-2026.1/deployment/devstack/common.yaml deleted file mode 100644 index 9196dc5..0000000 --- a/molecule/stable-2026.1/deployment/devstack/common.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -devstack_branch: stable/2026.1 -run_devstack: true -enable_ceph: false -configure_vdpa: false -external_bridge_mtu: 1450 -devstack_libvirt_type: kvm -stack_user_password: tester diff --git a/molecule/stable-2026.1/deployment/devstack/group_vars/compute.yaml b/molecule/stable-2026.1/deployment/devstack/group_vars/compute.yaml deleted file mode 100644 index 051f914..0000000 --- a/molecule/stable-2026.1/deployment/devstack/group_vars/compute.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -compute_localrc_extra: {} -compute_local_conf_extra: {} -compute_services_extra: {} diff --git a/molecule/stable-2026.1/deployment/devstack/group_vars/controller.yaml b/molecule/stable-2026.1/deployment/devstack/group_vars/controller.yaml deleted file mode 100644 index aa7587a..0000000 --- a/molecule/stable-2026.1/deployment/devstack/group_vars/controller.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -controller_localrc_extra: {} -controller_local_conf_extra: {} -controller_services_extra: {} diff --git a/molecule/stable-2026.1/deployment/nodes.yaml b/molecule/stable-2026.1/deployment/nodes.yaml deleted file mode 100644 index 1f97063..0000000 --- a/molecule/stable-2026.1/deployment/nodes.yaml +++ /dev/null @@ -1,37 +0,0 @@ ---- -ard_nodes: - - name: controller - hostname: controller - provider_resource_name: ard-molecule-stable-2026-1-controller - groups: - - controller - - switch - image: ubuntu-24.04 - flavor: devstack-control - preference: devstack - networks: - - name: ard-mgmt - ip: 192.168.98.2 - mac: "52:54:00:98:00:02" - profiles: - - ssh - - nested_virt - - ovn - - name: compute1 - hostname: compute1 - provider_resource_name: ard-molecule-stable-2026-1-compute1 - groups: - - compute - - peers - - subnode - image: ubuntu-24.04 - flavor: devstack-compute - preference: devstack - networks: - - name: ard-mgmt - ip: 192.168.98.3 - mac: "52:54:00:98:00:03" - profiles: - - ssh - - nested_virt - - ovn diff --git a/molecule/stable-2026.1/molecule.yml b/molecule/stable-2026.1/molecule.yml index 115dff4..4b01085 100644 --- a/molecule/stable-2026.1/molecule.yml +++ b/molecule/stable-2026.1/molecule.yml @@ -4,13 +4,24 @@ dependency: enabled: false driver: name: default -platforms: - - name: controller - - name: compute1 provisioner: name: ansible env: ANSIBLE_STDOUT_CALLBACK: yaml + ard: + provider: libvirt + provider_profile: local-libvirt + deployment_name: molecule-stable-2026-1 + resource_name_prefix: ard-molecule-stable-2026-1 + target_branch: stable/2026.1 + topology: one-controller-one-compute + service_profiles: + - devstack + - ovn + - tempest + libvirt: + network_name: ard-molecule-stable-2026-1 + network_cidr: 192.168.98.0/24 verifier: name: ansible scenario: From 7ef20e0f0ecf5fd29795b6ead9eae828da5191ed Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Fri, 5 Jun 2026 19:29:07 +0100 Subject: [PATCH 09/11] Generalize ARD topology rendering The render workflow needs to model more than fixed controller and compute counts. Future development environments may include storage, EDPM, Kubernetes, or other node kinds, and those nodes need type-level defaults, pool-level sizing, and per-node overrides without teaching provider roles about topology semantics. This change converts topology presets to generic node pools and adds node type and network preset catalogs. The render role now normalizes node types, node pools, service profiles, network attachments, and node overrides into the existing concrete ard_nodes provider contract. Counted node pools use readable hyphenated names such as compute-1 and compute-2 by default, while singleton pools can keep explicit names such as controller. The documentation describes the new node type, node pool, network, and override model. Validated with syntax checks, serial Molecule create/ping/destroy for the default, one-controller-two-compute, and stable-2026.1 scenarios, a Make render/apply/ping/destroy-clean-generated/cleanup lifecycle, and a full one-controller-two-compute DevStack deployment with Tempest smoke. Signed-off-by: Sean Mooney --- ARD_PROVIDER_DESIGN.md | 115 ++++++++++++++---- README.md | 47 ++++--- .../files/presets/networks.yaml | 5 + .../files/presets/node-types.yaml | 20 +++ .../files/presets/topologies.yaml | 46 ++++++- .../roles/ard_provider_render/tasks/main.yml | 63 +++------- .../templates/nodes.yaml.j2 | 46 +++++++ 7 files changed, 251 insertions(+), 91 deletions(-) create mode 100644 ansible/roles/ard_provider_common/files/presets/networks.yaml create mode 100644 ansible/roles/ard_provider_common/files/presets/node-types.yaml create mode 100644 ansible/roles/ard_provider_render/templates/nodes.yaml.j2 diff --git a/ARD_PROVIDER_DESIGN.md b/ARD_PROVIDER_DESIGN.md index 26a9601..ea0d659 100644 --- a/ARD_PROVIDER_DESIGN.md +++ b/ARD_PROVIDER_DESIGN.md @@ -44,13 +44,13 @@ controller: - controller - switch -compute1: +compute-1: groups: - compute - peers - subnode -compute2: +compute-2: groups: - compute - peers @@ -166,7 +166,7 @@ deployments/ compute.yaml # compute group vars using existing compute_* names host_vars/ controller.yaml # optional per-node Ansible vars - compute1.yaml + compute-1.yaml inventory.yaml # generated provider inventory provider-state.yaml # provider resource names/ids for destroy rendered/ @@ -362,8 +362,8 @@ ard_nodes: - nested_virt - ovn - - name: compute1 - hostname: compute1 + - name: compute-1 + hostname: compute-1 groups: - compute - peers @@ -379,8 +379,8 @@ ard_nodes: - nested_virt - ovn - - name: compute2 - hostname: compute2 + - name: compute-2 + hostname: compute-2 groups: - compute - peers @@ -414,7 +414,7 @@ deployments// compute.yaml host_vars/ controller.yaml - compute1.yaml + compute-1.yaml inventory.yaml provider-state.yaml rendered/ @@ -469,7 +469,52 @@ ard_service_profiles: - tempest ``` -Topology presets are named convenience bases such as `all-in-one`, `one-controller-one-compute`, and `one-controller-two-compute`. They normalize into counts and roles, and advanced users may override the normalized values with `ard_topology_overrides` when a curated name is close but not exact. +Topology presets are named convenience bases such as `all-in-one`, `one-controller-one-compute`, and `one-controller-two-compute`. They normalize generic node pools into concrete `ard_nodes`. Provider roles consume only concrete `ard_nodes`; they do not need to understand topology, node types, or pool semantics. + +The generic topology model has three layers: + +1. **node types** define reusable defaults for a kind of node, such as `controller`, `compute`, `storage`, `edpm`, or `k8s-worker`. +2. **node pools** instantiate a type with a count, naming pattern, network attachments, and optional pool-level overrides. +3. **node overrides** apply final per-node customization after names have been generated. + +Example node types: + +```yaml +ard_render_node_types: + controller: + groups: [controller, switch] + profiles: [ssh, nested_virt] + flavor: devstack-control + compute: + groups: [compute, peers, subnode] + profiles: [ssh, nested_virt] + flavor: devstack-compute +``` + +Example topology preset: + +```yaml +ard_render_topologies: + one-controller-two-compute: + controller_runs_compute: false + node_pools: + - type: controller + count: 1 + name: controller + hostname: controller + networks: + - name: ard-mgmt + ip_start: 2 + - type: compute + count: 2 + name_format: "compute-{index}" + hostname_format: "compute-{index}" + networks: + - name: ard-mgmt + ip_start: 3 +``` + +Counted pools default to readable hyphenated names like `{type}-{index}`. Singleton pools may set explicit names such as `controller`. The current presets render `compute-1` and `compute-2` rather than the older `compute-1` and `compute-2` spelling. Generated concrete files such as `deployment.yaml`, `nodes.yaml`, and `devstack/*.yaml` are render output. They should include a generated-file header and may be overwritten by subsequent renders. Local customizations belong in the render intent or an overlay such as `overrides/render.yaml`. @@ -482,8 +527,16 @@ ard_render_overrides: controller_flavor: devstack-control compute_flavor: devstack-compute vm_preference: devstack - topology: - compute_count: 2 + node_pools: + compute: + count: 3 + flavor: larger-compute + profiles: + - performance + networks: + storage: + cidr: 192.168.120.0/24 + provider_network: ard-storage devstack: common: enable_ceph: true @@ -492,6 +545,18 @@ ard_render_overrides: DEBUG_LIBVIRT_COREDUMPS: true compute: compute_localrc_extra: {} + +ard_render_node_overrides: + compute-2: + image: ubuntu-24.04 + flavor: gpu-compute + profiles: + add: [gpu] + groups: + add: [special] + networks: + ard-mgmt: + ip: 192.168.96.50 ``` For command-line convenience, `ard_render_image`, `ard_render_controller_flavor`, `ard_render_compute_flavor`, and `ard_render_vm_preference` can override the composed provider defaults without changing the eventual provider input names written to `deployment.yaml`. @@ -517,7 +582,7 @@ ard_nodes: image: debian-13 flavor: devstack-control preference: devstack - - name: compute1 + - name: compute-1 groups: [compute, peers, subnode] image: debian-13 flavor: devstack-compute @@ -552,7 +617,7 @@ compute_services_extra: {} ``` ```yaml -# deployments/devstack-a/devstack/host_vars/compute1.yaml +# deployments/devstack-a/devstack/host_vars/compute-1.yaml compute_localrc_extra: LIBVIRT_TYPE: qemu ``` @@ -578,8 +643,8 @@ For libvirt, include the deployment name in domains, volumes, cloud-init seed IS $XDG_STATE_HOME/ard/libvirt/images// # or ~/.local/state/ard/libvirt/images when XDG_STATE_HOME is unset controller.qcow2 controller-seed.iso - compute1.qcow2 - compute1-seed.iso + compute-1.qcow2 + compute-1-seed.iso ``` `provider-state.yaml` should record native resource names and identifiers, but destroy must also support a label/name-prefix fallback so cleanup is possible if state is partially missing. @@ -607,7 +672,7 @@ deployments//devstack/ compute.yaml host_vars/ controller.yaml - compute1.yaml + compute-1.yaml ``` Merge order for each host: @@ -660,10 +725,10 @@ zuul: work_root: /tmp/work_root ``` -For `compute1`: +For `compute-1`: ```yaml -inventory_hostname: compute1 +inventory_hostname: compute-1 ansible_host: 192.168.96.3 ansible_user: stack ansible_private_key_file: ~/.ssh/id_ed25519_stack @@ -674,7 +739,7 @@ nodepool: Groups come from `ard_nodes[*].groups`. -For deployment workspaces, `inventory.yaml` is generated inside `deployments//` and can be re-read by later apply, deploy, verify, collect-log, and destroy phases. Inventory hostnames remain stable logical names such as `controller` and `compute1`; provider resource names are tracked separately through `ard_provider_resource_name` and `provider-state.yaml`. +For deployment workspaces, `inventory.yaml` is generated inside `deployments//` and can be re-read by later apply, deploy, verify, collect-log, and destroy phases. Inventory hostnames remain stable logical names such as `controller` and `compute-1`; provider resource names are tracked separately through `ard_provider_resource_name` and `provider-state.yaml`. This lets existing ARD defaults continue to evaluate expressions such as: @@ -852,8 +917,8 @@ $XDG_CACHE_HOME/ard/images/ # or ~/.cache/ard/images when XDG_CACHE_HOME is uns $XDG_STATE_HOME/ard/libvirt/images/devstack-a/ # or ~/.local/state/ard/libvirt/images/devstack-a when XDG_STATE_HOME is unset controller.qcow2 controller-seed.iso - compute1.qcow2 - compute1-seed.iso + compute-1.qcow2 + compute-1-seed.iso ``` ### 10.5 Cloud-init @@ -1429,7 +1494,7 @@ Usage: ```bash make render ARD_PROVIDER=kubevirt ARD_DEPLOYMENT=devstack-a vi deployments/devstack-a/devstack/controller.yaml -vi deployments/devstack-a/devstack/nodes/compute1.yaml +vi deployments/devstack-a/devstack/nodes/compute-1.yaml make apply ARD_DEPLOYMENT=devstack-a make deploy ARD_DEPLOYMENT=devstack-a make destroy ARD_DEPLOYMENT=devstack-a @@ -1548,8 +1613,8 @@ For KubeVirt, post-run log collection should work from the OpenShift API even if ### Phase 3: Libvirt multinode -- Create controller + compute1. -- Then controller + compute1 + compute2. +- Create controller + compute-1. +- Then controller + compute-1 + compute-2. - Match current Vagrant scenario groups. - Run existing `deploy_multinode_devstack.yaml`. @@ -1628,7 +1693,7 @@ These decisions should be resolved before or during the first libvirt prototype: - Image cache location: follow XDG cache conventions with `$XDG_CACHE_HOME/ard/images`, falling back to `~/.cache/ard/images`. - Checksum policy: support checksums when configured, but do not require them for the first prototype unless reproducibility/security requirements demand it. - Libvirt URI: prototype against `qemu:///system`; defer `qemu:///session` support. The first prototype assumes the invoking user has sufficient `libvirt`/`qemu` group access and should not use Ansible `become` by default. -- VM naming: use `ard--` as the provider resource name, e.g. `ard-devstack-1-controller`, while keeping inventory hostnames as `controller`, `compute1`, etc. +- VM naming: use `ard--` as the provider resource name, e.g. `ard-devstack-1-controller`, while keeping inventory hostnames as `controller`, `compute-1`, etc. - Destroy semantics: delete per-deployment overlays, seed ISOs, domains, and generated networks by default, but keep cached base images. - Filesystem paths: store downloaded base images in XDG cache and libvirt per-deployment base copies, overlays, and seed ISOs in XDG state. The provider should not write to `/var/lib/libvirt/images` by default and should not silently sudo provider operations. - Network default: use NAT `192.168.96.0/24` for the libvirt prototype. diff --git a/README.md b/README.md index fa34181..e9833c9 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,9 @@ SSH to a deployment node, or print the SSH command without running it: ```bash make ssh ARD_DEPLOYMENT=devstack-a ARD_NODE=controller -make ssh-print ARD_DEPLOYMENT=devstack-a ARD_NODE=compute1 +make ssh-print ARD_DEPLOYMENT=devstack-a ARD_NODE=compute-1 # equivalent explicit dry-run/print mode -make ssh ARD_DEPLOYMENT=devstack-a ARD_NODE=compute1 ARD_SSH_PRINT=1 +make ssh ARD_DEPLOYMENT=devstack-a ARD_NODE=compute-1 ARD_SSH_PRINT=1 ``` Deploy DevStack: @@ -167,11 +167,11 @@ Provider resources are named with the deployment name, for example: ```text ard-devstack-a-controller -ard-devstack-a-compute1 +ard-devstack-a-compute-1 ``` -Inventory hostnames remain logical names such as `controller`, `compute1`, and -`compute2`. +Inventory hostnames remain logical names such as `controller`, `compute-1`, and +`compute-2`. ## Topology presets @@ -193,18 +193,18 @@ controller ```text controller -compute1 +compute-1 ``` `one-controller-two-compute` renders: ```text controller -compute1 -compute2 +compute-1 +compute-2 ``` -Multinode topologies disable `nova-compute` on the controller through the rendered controller group vars. The `all-in-one` topology leaves controller compute services enabled. +Topology presets are built from generic node pools. Singleton pools can set an explicit name such as `controller`; counted pools default to readable hyphenated names such as `{type}-{index}`. Multinode topologies disable `nova-compute` on the controller through the rendered controller group vars when the topology says the controller does not run compute services. ## Render intent and service profiles @@ -235,14 +235,33 @@ Use `ard_render_overrides` for simple kustomize-like customizations: ard_render_overrides: provider_defaults: image: ubuntu-24.04 - topology: - compute_count: 2 + node_pools: + compute: + count: 2 + flavor: devstack-compute + profiles: + - performance + networks: + storage: + cidr: 192.168.120.0/24 + provider_network: ard-storage devstack: common: enable_ceph: true controller: controller_localrc_extra: DEBUG_LIBVIRT_COREDUMPS: true + +ard_render_node_overrides: + compute-2: + image: ubuntu-24.04 + flavor: devstack-compute + profiles: + add: + - gpu + networks: + ard-mgmt: + ip: 192.168.98.50 ``` Current service profiles are: @@ -320,9 +339,9 @@ and node names are defined only once by ARD render presets. Available scenarios: ```text -default Debian 13, controller + compute1, master -one-controller-two-compute Debian 13, controller + compute1 + compute2 -stable-2026.1 Ubuntu 24.04, controller + compute1, stable/2026.1 +default Debian 13, controller + compute-1, master +one-controller-two-compute Debian 13, controller + compute-1 + compute-2 +stable-2026.1 Ubuntu 24.04, controller + compute-1, stable/2026.1 ``` Run a full scenario test: diff --git a/ansible/roles/ard_provider_common/files/presets/networks.yaml b/ansible/roles/ard_provider_common/files/presets/networks.yaml new file mode 100644 index 0000000..e6978f9 --- /dev/null +++ b/ansible/roles/ard_provider_common/files/presets/networks.yaml @@ -0,0 +1,5 @@ +--- +ard_render_networks: + ard-mgmt: + provider_network: ard-mgmt + mac_prefix: "52:54:00" diff --git a/ansible/roles/ard_provider_common/files/presets/node-types.yaml b/ansible/roles/ard_provider_common/files/presets/node-types.yaml new file mode 100644 index 0000000..1873d14 --- /dev/null +++ b/ansible/roles/ard_provider_common/files/presets/node-types.yaml @@ -0,0 +1,20 @@ +--- +ard_render_node_types: + controller: + groups: + - controller + - switch + profiles: + - ssh + - nested_virt + flavor: devstack-control + + compute: + groups: + - compute + - peers + - subnode + profiles: + - ssh + - nested_virt + flavor: devstack-compute diff --git a/ansible/roles/ard_provider_common/files/presets/topologies.yaml b/ansible/roles/ard_provider_common/files/presets/topologies.yaml index e6181a0..28fcdcf 100644 --- a/ansible/roles/ard_provider_common/files/presets/topologies.yaml +++ b/ansible/roles/ard_provider_common/files/presets/topologies.yaml @@ -1,14 +1,48 @@ --- ard_render_topologies: all-in-one: - controller_count: 1 - compute_count: 0 controller_runs_compute: true + node_pools: + - type: controller + count: 1 + name: controller + hostname: controller + networks: + - name: ard-mgmt + ip_start: 2 + one-controller-one-compute: - controller_count: 1 - compute_count: 1 controller_runs_compute: true + node_pools: + - type: controller + count: 1 + name: controller + hostname: controller + networks: + - name: ard-mgmt + ip_start: 2 + - type: compute + count: 1 + name_format: "compute-{index}" + hostname_format: "compute-{index}" + networks: + - name: ard-mgmt + ip_start: 3 + one-controller-two-compute: - controller_count: 1 - compute_count: 2 controller_runs_compute: false + node_pools: + - type: controller + count: 1 + name: controller + hostname: controller + networks: + - name: ard-mgmt + ip_start: 2 + - type: compute + count: 2 + name_format: "compute-{index}" + hostname_format: "compute-{index}" + networks: + - name: ard-mgmt + ip_start: 3 diff --git a/ansible/roles/ard_provider_render/tasks/main.yml b/ansible/roles/ard_provider_render/tasks/main.yml index 569b423..44c8eec 100644 --- a/ansible/roles/ard_provider_render/tasks/main.yml +++ b/ansible/roles/ard_provider_render/tasks/main.yml @@ -50,6 +50,14 @@ ansible.builtin.include_vars: file: "{{ role_path }}/../ard_provider_common/files/presets/services.yaml" +- name: Load ARD render node type presets + ansible.builtin.include_vars: + file: "{{ role_path }}/../ard_provider_common/files/presets/node-types.yaml" + +- name: Load ARD render network presets + ansible.builtin.include_vars: + file: "{{ role_path }}/../ard_provider_common/files/presets/networks.yaml" + - name: Load ARD render provider profile presets ansible.builtin.include_vars: file: "{{ role_path }}/../ard_provider_common/files/presets/provider-profiles.yaml" @@ -121,6 +129,13 @@ ard_render_compute_flavor: "{{ ard_render_compute_flavor | default(ard_render_provider_defaults.compute_flavor) }}" ard_render_vm_preference: "{{ ard_render_vm_preference | default(ard_render_provider_defaults.vm_preference) }}" +- name: Compose ARD render networks + ansible.builtin.set_fact: + ard_render_networks_config: >- + {{ ard_render_networks | + combine({'ard-mgmt': {'cidr': ard_libvirt_network_cidr, 'gateway': ard_libvirt_network_gateway}}, recursive=True) | + combine((ard_render_overrides | default({})).get('networks', {}), recursive=True) }} + - name: Compose rendered DevStack variables ansible.builtin.set_fact: ard_render_devstack_common: >- @@ -175,54 +190,10 @@ ard_libvirt_network_gateway: {{ ard_libvirt_network_gateway }} - name: Render topology inputs - ansible.builtin.copy: + ansible.builtin.template: + src: nodes.yaml.j2 dest: "{{ ard_deployment_dir }}/nodes.yaml" mode: "0644" - content: | - {{ ard_render_generated_header }} - --- - ard_nodes: - - name: controller - hostname: controller - provider_resource_name: {{ ard_resource_name_prefix }}-controller - groups: - - controller - - switch - image: {{ ard_render_default_image }} - flavor: {{ ard_render_controller_flavor }} - preference: {{ ard_render_vm_preference }} - networks: - - name: ard-mgmt - ip: {{ ard_render_network_prefix }}.2 - mac: "52:54:00:{{ '%02x' | format((ard_render_network_prefix.split('.')[2] | int) % 256) }}:00:02" - profiles: - - ssh - - nested_virt - {% for profile in ard_render_node_profiles %} - - {{ profile }} - {% endfor %} - {% for index in range(1, (ard_render_topology_config.compute_count | int) + 1) %} - - name: compute{{ index }} - hostname: compute{{ index }} - provider_resource_name: {{ ard_resource_name_prefix }}-compute{{ index }} - groups: - - compute - - peers - - subnode - image: {{ ard_render_default_image }} - flavor: {{ ard_render_compute_flavor }} - preference: {{ ard_render_vm_preference }} - networks: - - name: ard-mgmt - ip: {{ ard_render_network_prefix }}.{{ index + 2 }} - mac: "52:54:00:{{ '%02x' | format((ard_render_network_prefix.split('.')[2] | int) % 256) }}:00:{{ '%02x' | format(index + 2) }}" - profiles: - - ssh - - nested_virt - {% for profile in ard_render_node_profiles %} - - {{ profile }} - {% endfor %} - {% endfor %} - name: Render common DevStack vars ansible.builtin.copy: diff --git a/ansible/roles/ard_provider_render/templates/nodes.yaml.j2 b/ansible/roles/ard_provider_render/templates/nodes.yaml.j2 new file mode 100644 index 0000000..7799d9e --- /dev/null +++ b/ansible/roles/ard_provider_render/templates/nodes.yaml.j2 @@ -0,0 +1,46 @@ +{{ ard_render_generated_header }} +--- +ard_nodes: +{% set pool_overrides = (ard_render_overrides | default({})).get('node_pools', {}) %} +{% set node_overrides = ard_render_node_overrides | default({}) %} +{% for raw_pool in ard_render_topology_config.node_pools | default([]) %} +{% set pool_key = raw_pool.name | default(raw_pool.type) %} +{% set pool = raw_pool | combine(pool_overrides.get(raw_pool.type, {}), recursive=True) | combine(pool_overrides.get(pool_key, {}), recursive=True) %} +{% set node_type = ard_render_node_types[pool.type] %} +{% for index in range(1, (pool.count | int) + 1) %} +{% set generated_name = (pool.name if (pool.name is defined and (pool.count | int) == 1) else (pool.name_format | default('{type}-{index}') | replace('{type}', pool.type) | replace('{index}', index | string))) %} +{% set generated_hostname = (pool.hostname if (pool.hostname is defined and (pool.count | int) == 1) else (pool.hostname_format | default(pool.name_format | default('{type}-{index}')) | replace('{type}', pool.type) | replace('{index}', index | string))) %} +{% set node_override = node_overrides.get(generated_name, {}) %} + - name: {{ node_override.name | default(generated_name) }} + hostname: {{ node_override.hostname | default(generated_hostname) }} + provider_resource_name: {{ node_override.provider_resource_name | default(ard_resource_name_prefix + '-' + (node_override.name | default(generated_name))) }} + groups: +{% set base_groups = node_type.groups | default([]) %} +{% set pool_groups = pool.groups if (pool.groups is defined and pool.groups is sequence and pool.groups is not string) else [] %} +{% set group_add = (node_override.get('groups', {}).get('add', []) if node_override.get('groups', {}) is mapping else []) %} +{% for group in (base_groups + pool_groups + group_add) | unique %} + - {{ group }} +{% endfor %} + image: {{ node_override.image | default(pool.image | default(node_type.image | default(ard_render_default_image))) }} + flavor: {{ node_override.flavor | default(pool.flavor | default(node_type.flavor | default(ard_render_compute_flavor))) }} + preference: {{ node_override.preference | default(pool.preference | default(node_type.preference | default(ard_render_vm_preference))) }} + networks: +{% set node_network_overrides = node_override.networks | default({}) %} +{% for network_ref in pool.networks | default([{'name': 'ard-mgmt', 'ip_start': 2 + loop.index0}]) %} +{% set network_config = ard_render_networks_config[network_ref.name] %} +{% set network_prefix = network_config.cidr | regex_replace('\.0/24$', '') %} +{% set ip_host = (network_ref.ip_start | int) + index - 1 %} +{% set network_override = node_network_overrides.get(network_ref.name, {}) %} + - name: {{ network_ref.name }} + ip: {{ network_override.ip | default(network_prefix + '.' + (ip_host | string)) }} + mac: "{{ network_override.mac | default((network_config.mac_prefix | default('52:54:00')) + ':' + ('%02x' | format((network_prefix.split('.')[2] | int) % 256)) + ':' + ('%02x' | format(loop.index0)) + ':' + ('%02x' | format(ip_host))) }}" +{% endfor %} + profiles: +{% set base_profiles = node_type.profiles | default([]) %} +{% set pool_profiles = pool.profiles if (pool.profiles is defined and pool.profiles is sequence and pool.profiles is not string) else [] %} +{% set profile_add = (node_override.get('profiles', {}).get('add', []) if node_override.get('profiles', {}) is mapping else []) %} +{% for profile in (base_profiles + ard_render_node_profiles + pool_profiles + profile_add) | unique %} + - {{ profile }} +{% endfor %} +{% endfor %} +{% endfor %} From 6b928cad78bf1d2342be164e710c6c029090cbc3 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Fri, 5 Jun 2026 21:07:01 +0100 Subject: [PATCH 10/11] Refine ARD network and Molecule seams The provider framework needs clearer seams between workflow runners, render normalization, and provider implementation. The Molecule scenarios were still carrying duplicated create logic, and the libvirt provider only fully supported a single management network even though render intent can now model multiple network attachments. This change moves the shared Molecule create workflow into a reusable playbook and leaves scenario create.yml files as thin imports. Rendered deployments now include an explicit management network and concrete network catalog. Inventory generation uses the named management network instead of assuming the first network attachment. The libvirt provider now renders, starts, records, and destroys all rendered networks. Domains attach every node network as a separate interface, and cloud-init writes deterministic interface configuration for each attachment. The network model also supports an opt-in isolated mode for bridge-only networks with no host-side IP, DHCP, or NAT. A built-in tenant network preset provides this behavior without being attached by default, allowing guests to run their own VLAN, DHCP, or overlay experiments. Validated with syntax checks, a temporary multi-network libvirt deployment, an isolated tenant-network deployment, default Molecule create/ping/destroy, the full serial Molecule create/ping/destroy suite, and a Make lifecycle using a non-conflicting CIDR. Signed-off-by: Sean Mooney --- ARD_PROVIDER_DESIGN.md | 37 ++++++- README.md | 32 +++++- ansible/playbooks/ard-molecule-create.yaml | 98 +++++++++++++++++++ .../roles/ard_libvirt_destroy/tasks/main.yml | 17 +++- .../ard_libvirt_inventory/tasks/main.yml | 41 ++++++-- .../roles/ard_libvirt_network/tasks/main.yml | 47 +++++---- .../templates/network.xml.j2 | 20 ++++ ansible/roles/ard_libvirt_node/tasks/node.yml | 28 ++++-- .../ard_libvirt_node/templates/domain.xml.j2 | 6 +- .../ard_provider_common/defaults/main.yml | 1 + .../files/presets/networks.yaml | 6 ++ .../roles/ard_provider_render/tasks/main.yml | 56 ++++++++++- .../templates/nodes.yaml.j2 | 41 ++++---- molecule/default/create.yml | 91 +---------------- .../one-controller-two-compute/create.yml | 91 +---------------- molecule/stable-2026.1/create.yml | 91 +---------------- 16 files changed, 355 insertions(+), 348 deletions(-) create mode 100644 ansible/playbooks/ard-molecule-create.yaml create mode 100644 ansible/roles/ard_libvirt_network/templates/network.xml.j2 diff --git a/ARD_PROVIDER_DESIGN.md b/ARD_PROVIDER_DESIGN.md index ea0d659..8aad67d 100644 --- a/ARD_PROVIDER_DESIGN.md +++ b/ARD_PROVIDER_DESIGN.md @@ -514,13 +514,14 @@ ard_render_topologies: ip_start: 3 ``` -Counted pools default to readable hyphenated names like `{type}-{index}`. Singleton pools may set explicit names such as `controller`. The current presets render `compute-1` and `compute-2` rather than the older `compute-1` and `compute-2` spelling. +Counted pools default to readable hyphenated names like `{type}-{index}`. Singleton pools may set explicit names such as `controller`. The current presets render `compute-1` and `compute-2` rather than the older unhyphenated `compute1` and `compute2` spelling. Generated concrete files such as `deployment.yaml`, `nodes.yaml`, and `devstack/*.yaml` are render output. They should include a generated-file header and may be overwritten by subsequent renders. Local customizations belong in the render intent or an overlay such as `overrides/render.yaml`. -The supported explicit overlay dictionary is: +The supported explicit overlay dictionary uses ordinary recursive dictionary merge semantics. Later dictionaries replace scalar and list values for the relevant section; there is no separate patch language with `add`, `remove`, or `replace` operations. ```yaml +ard_management_network: ard-mgmt ard_render_overrides: provider_defaults: image: ubuntu-24.04 @@ -532,7 +533,14 @@ ard_render_overrides: count: 3 flavor: larger-compute profiles: + - ssh + - nested_virt - performance + networks: + - name: ard-mgmt + ip_start: 3 + - name: storage + ip_start: 20 networks: storage: cidr: 192.168.120.0/24 @@ -551,14 +559,33 @@ ard_render_node_overrides: image: ubuntu-24.04 flavor: gpu-compute profiles: - add: [gpu] - groups: - add: [special] + - ssh + - nested_virt + - gpu networks: ard-mgmt: ip: 192.168.96.50 ``` +`ard_management_network` selects the network used for generated SSH inventory and `nodepool.private_ipv4`. Additional networks are valid provider contract data; the libvirt provider renders one libvirt network per entry and attaches all node networks as VM interfaces. + +Networks support two initial modes: + +```yaml +ard_render_overrides: + networks: + storage: + mode: nat + cidr: 192.168.120.0/24 + provider_network: ard-storage + tenant: + mode: isolated + provider_network: tenant + mac_id: 130 +``` + +`nat` networks have host-side IP/DHCP/NAT and require a CIDR. `isolated` networks are bridge-only libvirt networks with no host-side IP, NAT, or DHCP. The built-in `tenant` preset is isolated and opt-in; no current topology attaches it by default. + For command-line convenience, `ard_render_image`, `ard_render_controller_flavor`, `ard_render_compute_flavor`, and `ard_render_vm_preference` can override the composed provider defaults without changing the eventual provider input names written to `deployment.yaml`. Example deployment inputs: diff --git a/README.md b/README.md index e9833c9..2d04c27 100644 --- a/README.md +++ b/README.md @@ -229,9 +229,10 @@ Use it with: make render ARD_DEPLOYMENT=stable-test ARD_RENDER_FILE=examples/render.yaml ``` -Use `ard_render_overrides` for simple kustomize-like customizations: +Use `ard_render_overrides` for simple kustomize-like customizations. Overrides use ordinary recursive dictionary merge semantics: later dictionaries replace scalar and list values for the relevant section. ```yaml +ard_management_network: ard-mgmt ard_render_overrides: provider_defaults: image: ubuntu-24.04 @@ -240,7 +241,14 @@ ard_render_overrides: count: 2 flavor: devstack-compute profiles: + - ssh + - nested_virt - performance + networks: + - name: ard-mgmt + ip_start: 3 + - name: storage + ip_start: 20 networks: storage: cidr: 192.168.120.0/24 @@ -257,13 +265,31 @@ ard_render_node_overrides: image: ubuntu-24.04 flavor: devstack-compute profiles: - add: - - gpu + - ssh + - nested_virt + - gpu networks: ard-mgmt: ip: 192.168.98.50 ``` +The libvirt provider supports multiple rendered networks. `ard_management_network` selects which attached network is used for SSH inventory and `nodepool.private_ipv4`; additional networks are rendered as extra libvirt networks and attached as additional VM interfaces. + +The built-in `tenant` network preset is isolated and opt-in. It is not attached by default, but can be added to a node pool when guests need a bridge-only network for their own VLANs, DHCP, or overlay experiments: + +```yaml +ard_render_overrides: + node_pools: + compute: + networks: + - name: ard-mgmt + ip_start: 3 + - name: tenant + mac_start: 20 +``` + +Isolated networks render as libvirt networks without host-side IP, NAT, or DHCP. Guest interfaces are still given deterministic MAC addresses and stable interface names. + Current service profiles are: ```text diff --git a/ansible/playbooks/ard-molecule-create.yaml b/ansible/playbooks/ard-molecule-create.yaml new file mode 100644 index 0000000..7afbd45 --- /dev/null +++ b/ansible/playbooks/ard-molecule-create.yaml @@ -0,0 +1,98 @@ +--- +- name: Create ARD Molecule deployment + hosts: localhost + connection: local + gather_facts: false + vars: + ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" + ard_scenario_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}" + ard_deployment_dir: "{{ ard_scenario_dir }}/deployment" + ard_molecule_render_vars_file: "{{ ard_deployment_dir }}/.molecule-render-vars.yaml" + ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" + tasks: + - name: Load Molecule scenario configuration + ansible.builtin.include_vars: + file: "{{ ard_scenario_dir }}/molecule.yml" + name: ard_molecule_config + + - name: Validate Molecule ARD scenario configuration + ansible.builtin.assert: + that: + - ard_molecule_config.provisioner.ard is defined + - ard_molecule_config.provisioner.ard.topology is defined + - ard_molecule_config.provisioner.ard.libvirt.network_cidr is defined + fail_msg: >- + Molecule scenario must define provisioner.ard.topology and + provisioner.ard.libvirt.network_cidr in molecule.yml. + + - name: Ensure ARD deployment directory exists for generated render vars + ansible.builtin.file: + path: "{{ ard_deployment_dir }}" + state: directory + mode: "0755" + + - name: Write generated ARD render vars from Molecule scenario + ansible.builtin.copy: + dest: "{{ ard_molecule_render_vars_file }}" + mode: "0644" + content: | + --- + ard_provider: {{ ard_molecule_config.provisioner.ard.provider | default('libvirt') }} + ard_provider_profile: {{ ard_molecule_config.provisioner.ard.provider_profile | default('local-libvirt') }} + ard_deployment_name: {{ ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename) }} + ard_resource_name_prefix: {{ ard_molecule_config.provisioner.ard.resource_name_prefix | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} + ard_target_branch: {{ ard_molecule_config.provisioner.ard.target_branch | default('master') }} + ard_topology: {{ ard_molecule_config.provisioner.ard.topology }} + ard_service_profiles: {{ ard_molecule_config.provisioner.ard.service_profiles | default(['devstack', 'ovn', 'tempest']) | to_json }} + ard_libvirt_network_name: {{ ard_molecule_config.provisioner.ard.libvirt.network_name | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} + ard_libvirt_network_cidr: {{ ard_molecule_config.provisioner.ard.libvirt.network_cidr }} + {% if ard_molecule_config.provisioner.ard.management_network is defined %} + ard_management_network: {{ ard_molecule_config.provisioner.ard.management_network }} + {% endif %} + {% if ard_molecule_config.provisioner.ard.render_image is defined %} + ard_render_image: {{ ard_molecule_config.provisioner.ard.render_image }} + {% endif %} + {% if ard_molecule_config.provisioner.ard.render_overrides is defined %} + ard_render_overrides: + {{ ard_molecule_config.provisioner.ard.render_overrides | to_nice_yaml(indent=2, sort_keys=False) | indent(2, true) }} + {% endif %} + {% if ard_molecule_config.provisioner.ard.node_overrides is defined %} + ard_render_node_overrides: + {{ ard_molecule_config.provisioner.ard.node_overrides | to_nice_yaml(indent=2, sort_keys=False) | indent(2, true) }} + {% endif %} + + - name: Render ARD deployment workspace + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - localhost, + - ansible/playbooks/ard-render.yaml + - -e + - "@{{ ard_molecule_render_vars_file }}" + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true + + - name: Apply ARD provider resources + tags: + - apply + ansible.builtin.command: + argv: + - ansible-playbook + - -i + - localhost, + - ansible/playbooks/ard-apply.yaml + - -e + - ard_deployment_dir={{ ard_deployment_dir }} + environment: + ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" + ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" + args: + chdir: "{{ ard_project_dir }}" + changed_when: true diff --git a/ansible/roles/ard_libvirt_destroy/tasks/main.yml b/ansible/roles/ard_libvirt_destroy/tasks/main.yml index 1d94365..d27f64e 100644 --- a/ansible/roles/ard_libvirt_destroy/tasks/main.yml +++ b/ansible/roles/ard_libvirt_destroy/tasks/main.yml @@ -46,14 +46,23 @@ changed_when: ard_undefine_domain.rc == 0 failed_when: false -- name: Destroy libvirt network - command: "virsh --connect {{ ard_libvirt_uri }} net-destroy {{ ard_libvirt_network_name }}" +- name: Build destroy network list + set_fact: + ard_destroy_networks: >- + {{ ard_state.networks | default([{'name': ard_management_network | default('ard-mgmt'), 'libvirt_name': ard_libvirt_network_name}]) + if ard_provider_state_stat.stat.exists + else [{'name': ard_management_network | default('ard-mgmt'), 'libvirt_name': ard_libvirt_network_name}] }} + +- name: Destroy libvirt networks + command: "virsh --connect {{ ard_libvirt_uri }} net-destroy {{ item.libvirt_name }}" + loop: "{{ ard_destroy_networks | default([]) }}" register: ard_net_destroy changed_when: ard_net_destroy.rc == 0 failed_when: false -- name: Undefine libvirt network - command: "virsh --connect {{ ard_libvirt_uri }} net-undefine {{ ard_libvirt_network_name }}" +- name: Undefine libvirt networks + command: "virsh --connect {{ ard_libvirt_uri }} net-undefine {{ item.libvirt_name }}" + loop: "{{ ard_destroy_networks | default([]) }}" register: ard_net_undefine changed_when: ard_net_undefine.rc == 0 failed_when: false diff --git a/ansible/roles/ard_libvirt_inventory/tasks/main.yml b/ansible/roles/ard_libvirt_inventory/tasks/main.yml index b9a1b39..609eae3 100644 --- a/ansible/roles/ard_libvirt_inventory/tasks/main.yml +++ b/ansible/roles/ard_libvirt_inventory/tasks/main.yml @@ -8,16 +8,17 @@ all: hosts: {% for node in ard_nodes %} + {% set management_network = (node.networks | selectattr('name', 'equalto', ard_management_network) | list | first) %} {{ node.name }}: - ansible_host: {{ node.networks[0].ip }} + ansible_host: {{ management_network.ip }} ansible_user: stack ansible_private_key_file: {{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" ard_deployment_name: {{ ard_deployment_name }} ard_provider_resource_name: {{ node.provider_resource_name | default('ard-' + ard_deployment_name + '-' + node.name) }} nodepool: - private_ipv4: {{ node.networks[0].ip }} - public_ipv4: {{ node.networks[0].ip }} + private_ipv4: {{ management_network.ip }} + public_ipv4: {{ management_network.ip }} zuul: executor: log_root: /tmp/zuul_logs @@ -44,19 +45,21 @@ ansible.builtin.add_host: name: "{{ item.name }}" groups: "{{ ['ard_provider_nodes'] + item.groups }}" - ansible_host: "{{ item.networks[0].ip }}" + ansible_host: "{{ ard_inventory_management_network.ip }}" ansible_user: stack ansible_private_key_file: "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519_stack" ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" ard_deployment_name: "{{ ard_deployment_name }}" ard_provider_resource_name: "{{ item.provider_resource_name | default('ard-' + ard_deployment_name + '-' + item.name) }}" nodepool: - private_ipv4: "{{ item.networks[0].ip }}" - public_ipv4: "{{ item.networks[0].ip }}" + private_ipv4: "{{ ard_inventory_management_network.ip }}" + public_ipv4: "{{ ard_inventory_management_network.ip }}" zuul: executor: log_root: /tmp/zuul_logs work_root: /tmp/work_root + vars: + ard_inventory_management_network: "{{ item.networks | selectattr('name', 'equalto', ard_management_network) | list | first }}" loop: "{{ ard_nodes }}" - name: Write ARD provider state @@ -68,16 +71,38 @@ ard_provider: libvirt ard_deployment_name: {{ ard_deployment_name }} ard_libvirt_uri: {{ ard_libvirt_uri }} + ard_management_network: {{ ard_management_network }} ard_libvirt_network_name: {{ ard_libvirt_network_name }} ard_libvirt_image_dir: {{ ard_libvirt_image_dir }}/{{ ard_deployment_name }} ard_cached_base_image: {{ ard_cached_base_image | default('') }} ard_libvirt_base_image: {{ ard_libvirt_base_image | default('') }} + networks: + {% for network_name, network in ard_networks.items() %} + - name: {{ network_name }} + libvirt_name: {{ network.provider.libvirt.name }} + mode: {{ network.mode | default('nat') }} + {% if network.cidr is defined %} + cidr: {{ network.cidr }} + {% endif %} + {% if network.gateway is defined %} + gateway: {{ network.gateway }} + {% endif %} + {% endfor %} domains: {% for node in ard_nodes %} + {% set management_network = (node.networks | selectattr('name', 'equalto', ard_management_network) | list | first) %} - inventory_name: {{ node.name }} name: {{ node.provider_resource_name | default('ard-' + ard_deployment_name + '-' + node.name) }} - ip: {{ node.networks[0].ip }} - mac: {{ node.networks[0].mac }} + ip: {{ management_network.ip }} + mac: {{ management_network.mac }} + networks: + {% for node_network in node.networks %} + - name: {{ node_network.name }} + {% if node_network.ip is defined %} + ip: {{ node_network.ip }} + {% endif %} + mac: {{ node_network.mac }} + {% endfor %} overlay: {{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ node.name }}.qcow2 seed: {{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ node.name }}-seed.iso console_log: {{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ node.name }}-console.log diff --git a/ansible/roles/ard_libvirt_network/tasks/main.yml b/ansible/roles/ard_libvirt_network/tasks/main.yml index 4e2f3fc..afa366d 100644 --- a/ansible/roles/ard_libvirt_network/tasks/main.yml +++ b/ansible/roles/ard_libvirt_network/tasks/main.yml @@ -1,43 +1,40 @@ --- -- name: Create rendered libvirt directory +- name: Create rendered libvirt network directory file: - path: "{{ ard_deployment_dir }}/rendered/libvirt" + path: "{{ ard_deployment_dir }}/rendered/libvirt/networks" state: directory mode: "0755" - name: Render libvirt network XML - copy: - dest: "{{ ard_deployment_dir }}/rendered/libvirt/network.xml" + template: + src: network.xml.j2 + dest: "{{ ard_deployment_dir }}/rendered/libvirt/networks/{{ item.key }}.xml" mode: "0644" - content: | - - {{ ard_libvirt_network_name }} - - - - {% for node in ard_nodes %} - - {% endfor %} - - - + vars: + ard_libvirt_render_network_name: "{{ item.key }}" + ard_libvirt_render_network: "{{ item.value }}" + loop: "{{ ard_networks | dict2items }}" -- name: Check whether libvirt network exists - command: "virsh --connect {{ ard_libvirt_uri }} net-info {{ ard_libvirt_network_name }}" +- name: Check whether libvirt networks exist + command: "virsh --connect {{ ard_libvirt_uri }} net-info {{ item.value.provider.libvirt.name }}" register: ard_libvirt_network_info changed_when: false failed_when: false + loop: "{{ ard_networks | dict2items }}" -- name: Define libvirt network - command: "virsh --connect {{ ard_libvirt_uri }} net-define {{ ard_deployment_dir }}/rendered/libvirt/network.xml" - when: ard_libvirt_network_info.rc != 0 +- name: Define libvirt networks + command: "virsh --connect {{ ard_libvirt_uri }} net-define {{ ard_deployment_dir }}/rendered/libvirt/networks/{{ item.item.key }}.xml" + when: item.rc != 0 + loop: "{{ ard_libvirt_network_info.results }}" -- name: Start libvirt network - command: "virsh --connect {{ ard_libvirt_uri }} net-start {{ ard_libvirt_network_name }}" +- name: Start libvirt networks + command: "virsh --connect {{ ard_libvirt_uri }} net-start {{ item.value.provider.libvirt.name }}" register: ard_libvirt_net_start changed_when: ard_libvirt_net_start.rc == 0 failed_when: ard_libvirt_net_start.rc != 0 and ('already active' not in (ard_libvirt_net_start.stderr | default(''))) + loop: "{{ ard_networks | dict2items }}" -- name: Mark libvirt network autostart - command: "virsh --connect {{ ard_libvirt_uri }} net-autostart {{ ard_libvirt_network_name }}" +- name: Mark libvirt networks autostart + command: "virsh --connect {{ ard_libvirt_uri }} net-autostart {{ item.value.provider.libvirt.name }}" changed_when: false + loop: "{{ ard_networks | dict2items }}" diff --git a/ansible/roles/ard_libvirt_network/templates/network.xml.j2 b/ansible/roles/ard_libvirt_network/templates/network.xml.j2 new file mode 100644 index 0000000..533581b --- /dev/null +++ b/ansible/roles/ard_libvirt_network/templates/network.xml.j2 @@ -0,0 +1,20 @@ + + {{ ard_libvirt_render_network.provider.libvirt.name }} +{% if ard_libvirt_render_network.provider.libvirt.bridge_name is defined %} + +{% endif %} +{% if ard_libvirt_render_network.mode | default('nat') == 'nat' %} + + + +{% for node in ard_nodes %} +{% for node_network in node.networks | selectattr('name', 'equalto', ard_libvirt_render_network_name) %} +{% if node_network.ip is defined %} + +{% endif %} +{% endfor %} +{% endfor %} + + +{% endif %} + diff --git a/ansible/roles/ard_libvirt_node/tasks/node.yml b/ansible/roles/ard_libvirt_node/tasks/node.yml index f1930b4..7914ca8 100644 --- a/ansible/roles/ard_libvirt_node/tasks/node.yml +++ b/ansible/roles/ard_libvirt_node/tasks/node.yml @@ -5,8 +5,10 @@ ard_node_resource_name: "{{ ard_node.provider_resource_name | default('ard-' + ard_deployment_name + '-' + ard_node.name) }}" ard_node_flavor: "{{ ard_flavors[ard_node.flavor].provider.libvirt }}" ard_node_root_disk_gb: "{{ ard_flavors[ard_node.flavor].root_disk_gb | default(80) }}" - ard_node_ip: "{{ ard_node.networks[0].ip }}" - ard_node_mac: "{{ ard_node.networks[0].mac }}" + ard_node_management_network: "{{ ard_node.networks | selectattr('name', 'equalto', ard_management_network) | list | first }}" + ard_node_networks: "{{ ard_node.networks }}" + ard_node_ip: "{{ (ard_node.networks | selectattr('name', 'equalto', ard_management_network) | list | first).ip }}" + ard_node_mac: "{{ (ard_node.networks | selectattr('name', 'equalto', ard_management_network) | list | first).mac }}" ard_node_render_dir: "{{ ard_deployment_dir }}/rendered/libvirt/{{ ard_node.name }}" ard_node_overlay: "{{ ard_libvirt_image_dir }}/{{ ard_deployment_name }}/{{ ard_node.name }}.qcow2" ard_node_seed_local: "{{ ard_deployment_dir }}/rendered/libvirt/{{ ard_node.name }}/{{ ard_node.name }}-seed.iso" @@ -127,19 +129,29 @@ content: | version: 2 ethernets: - ard-mgmt: + {% for node_network in ard_node_networks %} + ard-{{ loop.index0 }}: match: - macaddress: "{{ ard_node_mac }}" - set-name: eth0 + macaddress: "{{ node_network.mac }}" + set-name: eth{{ loop.index0 }} + {% if node_network.ip is defined %} addresses: - - {{ ard_node_ip }}/24 + - {{ node_network.ip }}/24 + {% else %} + dhcp4: false + dhcp6: false + optional: true + {% endif %} + {% if node_network.name == ard_management_network %} routes: - to: default - via: {{ ard_libvirt_network_gateway }} + via: {{ ard_networks[ard_management_network].gateway }} nameservers: addresses: - - {{ ard_libvirt_network_gateway }} + - {{ ard_networks[ard_management_network].gateway }} - 1.1.1.1 + {% endif %} + {% endfor %} - name: Create cloud-init seed ISO locally command: "cloud-localds --network-config={{ ard_node_render_dir }}/network-config {{ ard_node_seed_local }} {{ ard_node_render_dir }}/user-data {{ ard_node_render_dir }}/meta-data" diff --git a/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 b/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 index 65eafed..f11daf0 100644 --- a/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 +++ b/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 @@ -33,11 +33,13 @@ +{% for node_network in ard_node_networks %} - - + + +{% endfor %} diff --git a/ansible/roles/ard_provider_common/defaults/main.yml b/ansible/roles/ard_provider_common/defaults/main.yml index b923f01..2dd51a8 100644 --- a/ansible/roles/ard_provider_common/defaults/main.yml +++ b/ansible/roles/ard_provider_common/defaults/main.yml @@ -15,6 +15,7 @@ ard_default_image: debian-13 ard_default_controller_flavor: devstack-control ard_default_compute_flavor: devstack-compute ard_default_vm_preference: devstack +ard_management_network: ard-mgmt ard_image_cache_dir: "{{ lookup('env', 'XDG_CACHE_HOME') | default(lookup('env', 'HOME') + '/.cache', true) }}/ard/images" ard_image_download: true diff --git a/ansible/roles/ard_provider_common/files/presets/networks.yaml b/ansible/roles/ard_provider_common/files/presets/networks.yaml index e6978f9..81c9ca6 100644 --- a/ansible/roles/ard_provider_common/files/presets/networks.yaml +++ b/ansible/roles/ard_provider_common/files/presets/networks.yaml @@ -1,5 +1,11 @@ --- ard_render_networks: ard-mgmt: + mode: nat provider_network: ard-mgmt mac_prefix: "52:54:00" + tenant: + mode: isolated + provider_network: tenant + mac_prefix: "52:54:00" + mac_id: 130 diff --git a/ansible/roles/ard_provider_render/tasks/main.yml b/ansible/roles/ard_provider_render/tasks/main.yml index 44c8eec..485b0e3 100644 --- a/ansible/roles/ard_provider_render/tasks/main.yml +++ b/ansible/roles/ard_provider_render/tasks/main.yml @@ -133,9 +133,38 @@ ansible.builtin.set_fact: ard_render_networks_config: >- {{ ard_render_networks | - combine({'ard-mgmt': {'cidr': ard_libvirt_network_cidr, 'gateway': ard_libvirt_network_gateway}}, recursive=True) | + combine({'ard-mgmt': {'mode': 'nat', 'cidr': ard_libvirt_network_cidr, 'gateway': ard_libvirt_network_gateway}}, recursive=True) | combine((ard_render_overrides | default({})).get('networks', {}), recursive=True) }} +- name: Validate ARD render networks + ansible.builtin.assert: + that: + - item.value.mode | default('nat') in ['nat', 'isolated'] + - item.value.mode | default('nat') == 'isolated' or item.value.cidr is defined + fail_msg: "Network {{ item.key }} must use mode nat or isolated; nat networks require cidr." + loop: "{{ ard_render_networks_config | dict2items }}" + +- name: Normalize rendered ARD nodes + ansible.builtin.set_fact: + ard_render_nodes_doc: "{{ lookup('ansible.builtin.template', 'nodes.yaml.j2') | from_yaml }}" + +- name: Set normalized ARD node list + ansible.builtin.set_fact: + ard_render_nodes: "{{ ard_render_nodes_doc.ard_nodes }}" + +- name: Validate normalized ARD nodes + ansible.builtin.assert: + that: + - ard_render_nodes | length > 0 + - ard_render_nodes | map(attribute='name') | list | unique | length == ard_render_nodes | length + - ard_render_nodes | map(attribute='networks') | flatten | selectattr('ip', 'defined') | map(attribute='ip') | list | unique | length == ard_render_nodes | map(attribute='networks') | flatten | selectattr('ip', 'defined') | list | length + - ard_render_nodes | map(attribute='networks') | flatten | map(attribute='mac') | list | unique | length == ard_render_nodes | map(attribute='networks') | flatten | list | length + - ard_management_network in ard_render_networks_config + - (ard_render_networks_config[ard_management_network].mode | default('nat')) == 'nat' + fail_msg: >- + Rendered topology is invalid. Check for duplicate node names, IPs, MACs, + or an undefined management network. + - name: Compose rendered DevStack variables ansible.builtin.set_fact: ard_render_devstack_common: >- @@ -185,15 +214,36 @@ ard_default_compute_flavor: {{ ard_render_compute_flavor }} ard_default_vm_preference: {{ ard_render_vm_preference }} + ard_management_network: {{ ard_management_network }} ard_libvirt_network_name: {{ ard_libvirt_network_name }} ard_libvirt_network_cidr: {{ ard_libvirt_network_cidr }} ard_libvirt_network_gateway: {{ ard_libvirt_network_gateway }} + ard_networks: + {% for network_name, network in ard_render_networks_config.items() %} + {{ network_name }}: + mode: {{ network.mode | default('nat') }} + {% if (network.mode | default('nat')) == 'nat' %} + cidr: {{ network.cidr }} + gateway: {{ network.gateway | default(network.cidr | regex_replace('0/24$', '1')) }} + {% endif %} + provider_network: {{ network.provider_network | default(network_name) }} + mac_prefix: "{{ network.mac_prefix | default('52:54:00') }}" + {% if network.mac_id is defined %} + mac_id: {{ network.mac_id }} + {% endif %} + provider: + libvirt: + name: {{ (network.provider | default({})).get('libvirt', {}).get('name', ard_libvirt_network_name if network_name == ard_management_network else ard_libvirt_network_name + '-' + network_name) }} + {% endfor %} - name: Render topology inputs - ansible.builtin.template: - src: nodes.yaml.j2 + ansible.builtin.copy: dest: "{{ ard_deployment_dir }}/nodes.yaml" mode: "0644" + content: | + {{ ard_render_generated_header }} + --- + {{ {'ard_nodes': ard_render_nodes} | to_nice_yaml(indent=2, sort_keys=False) }} - name: Render common DevStack vars ansible.builtin.copy: diff --git a/ansible/roles/ard_provider_render/templates/nodes.yaml.j2 b/ansible/roles/ard_provider_render/templates/nodes.yaml.j2 index 7799d9e..c3cb8c5 100644 --- a/ansible/roles/ard_provider_render/templates/nodes.yaml.j2 +++ b/ansible/roles/ard_provider_render/templates/nodes.yaml.j2 @@ -10,36 +10,37 @@ ard_nodes: {% for index in range(1, (pool.count | int) + 1) %} {% set generated_name = (pool.name if (pool.name is defined and (pool.count | int) == 1) else (pool.name_format | default('{type}-{index}') | replace('{type}', pool.type) | replace('{index}', index | string))) %} {% set generated_hostname = (pool.hostname if (pool.hostname is defined and (pool.count | int) == 1) else (pool.hostname_format | default(pool.name_format | default('{type}-{index}')) | replace('{type}', pool.type) | replace('{index}', index | string))) %} +{% set generated_node = {'name': generated_name, 'hostname': generated_hostname, 'provider_resource_name': ard_resource_name_prefix + '-' + generated_name} %} {% set node_override = node_overrides.get(generated_name, {}) %} - - name: {{ node_override.name | default(generated_name) }} - hostname: {{ node_override.hostname | default(generated_hostname) }} - provider_resource_name: {{ node_override.provider_resource_name | default(ard_resource_name_prefix + '-' + (node_override.name | default(generated_name))) }} +{% set node = node_type | combine(pool, recursive=True) | combine(generated_node, recursive=True) | combine(node_override, recursive=True) %} + - name: {{ node.name }} + hostname: {{ node.hostname }} + provider_resource_name: {{ node.provider_resource_name }} groups: -{% set base_groups = node_type.groups | default([]) %} -{% set pool_groups = pool.groups if (pool.groups is defined and pool.groups is sequence and pool.groups is not string) else [] %} -{% set group_add = (node_override.get('groups', {}).get('add', []) if node_override.get('groups', {}) is mapping else []) %} -{% for group in (base_groups + pool_groups + group_add) | unique %} +{% for group in node.groups | default([]) | unique %} - {{ group }} {% endfor %} - image: {{ node_override.image | default(pool.image | default(node_type.image | default(ard_render_default_image))) }} - flavor: {{ node_override.flavor | default(pool.flavor | default(node_type.flavor | default(ard_render_compute_flavor))) }} - preference: {{ node_override.preference | default(pool.preference | default(node_type.preference | default(ard_render_vm_preference))) }} + image: {{ node.image | default(ard_render_default_image) }} + flavor: {{ node.flavor | default(ard_render_compute_flavor) }} + preference: {{ node.preference | default(ard_render_vm_preference) }} networks: -{% set node_network_overrides = node_override.networks | default({}) %} -{% for network_ref in pool.networks | default([{'name': 'ard-mgmt', 'ip_start': 2 + loop.index0}]) %} +{% set node_network_overrides = node_override.networks | default({}) if node_override.networks is mapping else {} %} +{% for network_ref in node.networks | default([{'name': 'ard-mgmt', 'ip_start': 2 + loop.index0}]) %} {% set network_config = ard_render_networks_config[network_ref.name] %} -{% set network_prefix = network_config.cidr | regex_replace('\.0/24$', '') %} -{% set ip_host = (network_ref.ip_start | int) + index - 1 %} +{% set network_mode = network_config.mode | default('nat') %} +{% set network_prefix = (network_config.cidr | default('0.0.0.0/24')) | regex_replace('\.0/24$', '') %} +{% set ip_host = (network_ref.ip_start | default(network_ref.mac_start | default(2)) | int) + index - 1 %} +{% set mac_host = (network_ref.mac_start | default(network_ref.ip_start | default(2)) | int) + index - 1 %} +{% set mac_id = network_config.mac_id | default((network_prefix.split('.')[2] | int) if network_mode == 'nat' else loop.index0) %} {% set network_override = node_network_overrides.get(network_ref.name, {}) %} - name: {{ network_ref.name }} - ip: {{ network_override.ip | default(network_prefix + '.' + (ip_host | string)) }} - mac: "{{ network_override.mac | default((network_config.mac_prefix | default('52:54:00')) + ':' + ('%02x' | format((network_prefix.split('.')[2] | int) % 256)) + ':' + ('%02x' | format(loop.index0)) + ':' + ('%02x' | format(ip_host))) }}" +{% if network_mode == 'nat' or network_override.ip is defined or network_ref.ip is defined %} + ip: {{ network_override.ip | default(network_ref.ip | default(network_prefix + '.' + (ip_host | string))) }} +{% endif %} + mac: "{{ network_override.mac | default(network_ref.mac | default((network_config.mac_prefix | default('52:54:00')) + ':' + ('%02x' | format(mac_id | int)) + ':' + ('%02x' | format(loop.index0)) + ':' + ('%02x' | format(mac_host)))) }}" {% endfor %} profiles: -{% set base_profiles = node_type.profiles | default([]) %} -{% set pool_profiles = pool.profiles if (pool.profiles is defined and pool.profiles is sequence and pool.profiles is not string) else [] %} -{% set profile_add = (node_override.get('profiles', {}).get('add', []) if node_override.get('profiles', {}) is mapping else []) %} -{% for profile in (base_profiles + ard_render_node_profiles + pool_profiles + profile_add) | unique %} +{% for profile in ((node.profiles | default([])) + ard_render_node_profiles) | unique %} - {{ profile }} {% endfor %} {% endfor %} diff --git a/molecule/default/create.yml b/molecule/default/create.yml index 3b2a8ec..910838b 100644 --- a/molecule/default/create.yml +++ b/molecule/default/create.yml @@ -1,91 +1,2 @@ --- -- name: Create ARD libvirt deployment - hosts: localhost - connection: local - gather_facts: false - vars: - ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" - ard_scenario_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}" - ard_deployment_dir: "{{ ard_scenario_dir }}/deployment" - ard_molecule_render_vars_file: "{{ ard_deployment_dir }}/.molecule-render-vars.yaml" - ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" - tasks: - - name: Load Molecule scenario configuration - ansible.builtin.include_vars: - file: "{{ ard_scenario_dir }}/molecule.yml" - name: ard_molecule_config - - - name: Validate Molecule ARD scenario configuration - ansible.builtin.assert: - that: - - ard_molecule_config.provisioner.ard is defined - - ard_molecule_config.provisioner.ard.topology is defined - - ard_molecule_config.provisioner.ard.libvirt.network_cidr is defined - fail_msg: >- - Molecule scenario must define provisioner.ard.topology and - provisioner.ard.libvirt.network_cidr in molecule.yml. - - - name: Ensure ARD deployment directory exists for generated render vars - ansible.builtin.file: - path: "{{ ard_deployment_dir }}" - state: directory - mode: "0755" - - - name: Write generated ARD render vars from Molecule scenario - ansible.builtin.copy: - dest: "{{ ard_molecule_render_vars_file }}" - mode: "0644" - content: | - --- - ard_provider: {{ ard_molecule_config.provisioner.ard.provider | default('libvirt') }} - ard_provider_profile: {{ ard_molecule_config.provisioner.ard.provider_profile | default('local-libvirt') }} - ard_deployment_name: {{ ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename) }} - ard_resource_name_prefix: {{ ard_molecule_config.provisioner.ard.resource_name_prefix | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} - ard_target_branch: {{ ard_molecule_config.provisioner.ard.target_branch | default('master') }} - ard_topology: {{ ard_molecule_config.provisioner.ard.topology }} - ard_service_profiles: {{ ard_molecule_config.provisioner.ard.service_profiles | default(['devstack', 'ovn', 'tempest']) | to_json }} - ard_libvirt_network_name: {{ ard_molecule_config.provisioner.ard.libvirt.network_name | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} - ard_libvirt_network_cidr: {{ ard_molecule_config.provisioner.ard.libvirt.network_cidr }} - {% if ard_molecule_config.provisioner.ard.render_image is defined %} - ard_render_image: {{ ard_molecule_config.provisioner.ard.render_image }} - {% endif %} - {% if ard_molecule_config.provisioner.ard.render_overrides is defined %} - ard_render_overrides: - {{ ard_molecule_config.provisioner.ard.render_overrides | to_nice_yaml(indent=2, sort_keys=False) | indent(2, true) }} - {% endif %} - - - name: Render ARD deployment workspace - ansible.builtin.command: - argv: - - ansible-playbook - - -i - - localhost, - - ansible/playbooks/ard-render.yaml - - -e - - "@{{ ard_molecule_render_vars_file }}" - - -e - - ard_deployment_dir={{ ard_deployment_dir }} - environment: - ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" - ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" - args: - chdir: "{{ ard_project_dir }}" - changed_when: true - - - name: Apply ARD provider resources - tags: - - apply - ansible.builtin.command: - argv: - - ansible-playbook - - -i - - localhost, - - ansible/playbooks/ard-apply.yaml - - -e - - ard_deployment_dir={{ ard_deployment_dir }} - environment: - ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" - ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" - args: - chdir: "{{ ard_project_dir }}" - changed_when: true +- import_playbook: ../../ansible/playbooks/ard-molecule-create.yaml diff --git a/molecule/one-controller-two-compute/create.yml b/molecule/one-controller-two-compute/create.yml index 3b2a8ec..910838b 100644 --- a/molecule/one-controller-two-compute/create.yml +++ b/molecule/one-controller-two-compute/create.yml @@ -1,91 +1,2 @@ --- -- name: Create ARD libvirt deployment - hosts: localhost - connection: local - gather_facts: false - vars: - ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" - ard_scenario_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}" - ard_deployment_dir: "{{ ard_scenario_dir }}/deployment" - ard_molecule_render_vars_file: "{{ ard_deployment_dir }}/.molecule-render-vars.yaml" - ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" - tasks: - - name: Load Molecule scenario configuration - ansible.builtin.include_vars: - file: "{{ ard_scenario_dir }}/molecule.yml" - name: ard_molecule_config - - - name: Validate Molecule ARD scenario configuration - ansible.builtin.assert: - that: - - ard_molecule_config.provisioner.ard is defined - - ard_molecule_config.provisioner.ard.topology is defined - - ard_molecule_config.provisioner.ard.libvirt.network_cidr is defined - fail_msg: >- - Molecule scenario must define provisioner.ard.topology and - provisioner.ard.libvirt.network_cidr in molecule.yml. - - - name: Ensure ARD deployment directory exists for generated render vars - ansible.builtin.file: - path: "{{ ard_deployment_dir }}" - state: directory - mode: "0755" - - - name: Write generated ARD render vars from Molecule scenario - ansible.builtin.copy: - dest: "{{ ard_molecule_render_vars_file }}" - mode: "0644" - content: | - --- - ard_provider: {{ ard_molecule_config.provisioner.ard.provider | default('libvirt') }} - ard_provider_profile: {{ ard_molecule_config.provisioner.ard.provider_profile | default('local-libvirt') }} - ard_deployment_name: {{ ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename) }} - ard_resource_name_prefix: {{ ard_molecule_config.provisioner.ard.resource_name_prefix | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} - ard_target_branch: {{ ard_molecule_config.provisioner.ard.target_branch | default('master') }} - ard_topology: {{ ard_molecule_config.provisioner.ard.topology }} - ard_service_profiles: {{ ard_molecule_config.provisioner.ard.service_profiles | default(['devstack', 'ovn', 'tempest']) | to_json }} - ard_libvirt_network_name: {{ ard_molecule_config.provisioner.ard.libvirt.network_name | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} - ard_libvirt_network_cidr: {{ ard_molecule_config.provisioner.ard.libvirt.network_cidr }} - {% if ard_molecule_config.provisioner.ard.render_image is defined %} - ard_render_image: {{ ard_molecule_config.provisioner.ard.render_image }} - {% endif %} - {% if ard_molecule_config.provisioner.ard.render_overrides is defined %} - ard_render_overrides: - {{ ard_molecule_config.provisioner.ard.render_overrides | to_nice_yaml(indent=2, sort_keys=False) | indent(2, true) }} - {% endif %} - - - name: Render ARD deployment workspace - ansible.builtin.command: - argv: - - ansible-playbook - - -i - - localhost, - - ansible/playbooks/ard-render.yaml - - -e - - "@{{ ard_molecule_render_vars_file }}" - - -e - - ard_deployment_dir={{ ard_deployment_dir }} - environment: - ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" - ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" - args: - chdir: "{{ ard_project_dir }}" - changed_when: true - - - name: Apply ARD provider resources - tags: - - apply - ansible.builtin.command: - argv: - - ansible-playbook - - -i - - localhost, - - ansible/playbooks/ard-apply.yaml - - -e - - ard_deployment_dir={{ ard_deployment_dir }} - environment: - ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" - ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" - args: - chdir: "{{ ard_project_dir }}" - changed_when: true +- import_playbook: ../../ansible/playbooks/ard-molecule-create.yaml diff --git a/molecule/stable-2026.1/create.yml b/molecule/stable-2026.1/create.yml index 3b2a8ec..910838b 100644 --- a/molecule/stable-2026.1/create.yml +++ b/molecule/stable-2026.1/create.yml @@ -1,91 +1,2 @@ --- -- name: Create ARD libvirt deployment - hosts: localhost - connection: local - gather_facts: false - vars: - ard_project_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | default(playbook_dir ~ '/../..', true) }}" - ard_scenario_dir: "{{ lookup('env', 'MOLECULE_SCENARIO_DIRECTORY') | default(playbook_dir, true) }}" - ard_deployment_dir: "{{ ard_scenario_dir }}/deployment" - ard_molecule_render_vars_file: "{{ ard_deployment_dir }}/.molecule-render-vars.yaml" - ard_ansible_roles_path: "{{ ard_project_dir }}/ansible/roles:{{ ard_project_dir }}/submodules/zuul-jobs/roles:{{ ard_project_dir }}/submodules/devstack/roles:{{ ard_project_dir }}/submodules/openstack-zuul-jobs/roles" - tasks: - - name: Load Molecule scenario configuration - ansible.builtin.include_vars: - file: "{{ ard_scenario_dir }}/molecule.yml" - name: ard_molecule_config - - - name: Validate Molecule ARD scenario configuration - ansible.builtin.assert: - that: - - ard_molecule_config.provisioner.ard is defined - - ard_molecule_config.provisioner.ard.topology is defined - - ard_molecule_config.provisioner.ard.libvirt.network_cidr is defined - fail_msg: >- - Molecule scenario must define provisioner.ard.topology and - provisioner.ard.libvirt.network_cidr in molecule.yml. - - - name: Ensure ARD deployment directory exists for generated render vars - ansible.builtin.file: - path: "{{ ard_deployment_dir }}" - state: directory - mode: "0755" - - - name: Write generated ARD render vars from Molecule scenario - ansible.builtin.copy: - dest: "{{ ard_molecule_render_vars_file }}" - mode: "0644" - content: | - --- - ard_provider: {{ ard_molecule_config.provisioner.ard.provider | default('libvirt') }} - ard_provider_profile: {{ ard_molecule_config.provisioner.ard.provider_profile | default('local-libvirt') }} - ard_deployment_name: {{ ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename) }} - ard_resource_name_prefix: {{ ard_molecule_config.provisioner.ard.resource_name_prefix | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} - ard_target_branch: {{ ard_molecule_config.provisioner.ard.target_branch | default('master') }} - ard_topology: {{ ard_molecule_config.provisioner.ard.topology }} - ard_service_profiles: {{ ard_molecule_config.provisioner.ard.service_profiles | default(['devstack', 'ovn', 'tempest']) | to_json }} - ard_libvirt_network_name: {{ ard_molecule_config.provisioner.ard.libvirt.network_name | default('ard-' ~ (ard_molecule_config.provisioner.ard.deployment_name | default(ard_scenario_dir | basename))) }} - ard_libvirt_network_cidr: {{ ard_molecule_config.provisioner.ard.libvirt.network_cidr }} - {% if ard_molecule_config.provisioner.ard.render_image is defined %} - ard_render_image: {{ ard_molecule_config.provisioner.ard.render_image }} - {% endif %} - {% if ard_molecule_config.provisioner.ard.render_overrides is defined %} - ard_render_overrides: - {{ ard_molecule_config.provisioner.ard.render_overrides | to_nice_yaml(indent=2, sort_keys=False) | indent(2, true) }} - {% endif %} - - - name: Render ARD deployment workspace - ansible.builtin.command: - argv: - - ansible-playbook - - -i - - localhost, - - ansible/playbooks/ard-render.yaml - - -e - - "@{{ ard_molecule_render_vars_file }}" - - -e - - ard_deployment_dir={{ ard_deployment_dir }} - environment: - ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" - ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" - args: - chdir: "{{ ard_project_dir }}" - changed_when: true - - - name: Apply ARD provider resources - tags: - - apply - ansible.builtin.command: - argv: - - ansible-playbook - - -i - - localhost, - - ansible/playbooks/ard-apply.yaml - - -e - - ard_deployment_dir={{ ard_deployment_dir }} - environment: - ANSIBLE_CONFIG: "{{ ard_project_dir }}/ansible.cfg" - ANSIBLE_ROLES_PATH: "{{ ard_ansible_roles_path }}" - args: - chdir: "{{ ard_project_dir }}" - changed_when: true +- import_playbook: ../../ansible/playbooks/ard-molecule-create.yaml From 2bae6c7dd6a2aa1b45d3badd8a0a0571bf0c580d Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Sun, 7 Jun 2026 17:28:46 +0100 Subject: [PATCH 11/11] Update developer guide for Molecule and network seams --- README.md | 3 + developer-guide.md | 1472 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1475 insertions(+) create mode 100644 developer-guide.md diff --git a/README.md b/README.md index 2d04c27..d0249a0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ existing DevStack deployment roles inside those VMs. The current local provider target is libvirt. KubeVirt/OpenShift Virtualization is design work for a later phase. +For contributor-oriented architecture and workflow details, read the +[ARD From Scratch developer guide](developer-guide.md). + ## Quick start Bootstrap the repository and host dependencies: diff --git a/developer-guide.md b/developer-guide.md new file mode 100644 index 0000000..185ef90 --- /dev/null +++ b/developer-guide.md @@ -0,0 +1,1472 @@ +# ARD From Scratch: Developer Guide + +This guide explains how ARD is organized, how a local ARD deployment is built from scratch, and how contributors should reason about the provider, inventory, and DevStack layers. It is written for readers who know Linux, Ansible, and virtualization in general, but who may be new to ARD's local libvirt provider or to the existing DevStack/Zuul role conventions used by this repository. + +ARD is a local development and test harness for VM-backed DevStack deployments. The main workflow uses Ansible provider playbooks to provision VMs, generate an ARD/Zuul-like inventory, and then run the existing DevStack deployment roles inside those VMs. The current implemented local provider is libvirt. KubeVirt/OpenShift Virtualization is design work for a later phase. + +The intended direction is similar to a project-specific reference manual: concepts are introduced before they are used, the deployment flow is explained from scratch, and the ARD-specific roles, presets, generated files, and local state paths are documented as part of the system rather than treated as incidental YAML. + +This guide is not a replacement for the Ansible documentation, libvirt documentation, DevStack documentation, or OpenStack/Zuul role documentation. It focuses on the subset needed to understand how ARD connects those systems together. + +## How to read this guide + +The document is both a learning path and a reference. New contributors should read Parts I through IV first, then skim Part V before changing provider roles or rendered deployment data. Parts VI through XI are more reference oriented and can be read as needed. + +```text +Part I Orientation and mental model +Part II Development host preparation and quick starts +Part III Ansible, Make, and repository layout +Part IV Full deployment-from-scratch walkthrough +Part V Render presets, deployment workspaces, and inventory +Part VI Provider role reference +Part VII DevStack deployment layer reference +Part VIII Molecule, validation, and test strategy +Part IX Maintenance workflows +Part X Troubleshooting +Part XI Quick reference +``` + +Headings are intentionally not manually numbered. Use the Markdown outline or search for the referenced heading name when following cross-references. + +A practical first pass for new contributors is: + +1. Read `README.md` for the public quick start and workflow summary. +2. Run `./bootstrap-repo.sh` on a suitable Linux host. +3. Read the top-level `Makefile` from top to bottom. +4. Read the provider playbooks under `ansible/playbooks/`. +5. Read `ansible/roles/ard_provider_render/tasks/main.yml` and `ansible/roles/ard_provider_render/templates/nodes.yaml.j2`. +6. Read the preset files under `ansible/roles/ard_provider_common/files/presets/`. +7. Run `make render ARD_DEPLOYMENT=devstack-a` and inspect `deployments/devstack-a/`. +8. Run `make apply ARD_DEPLOYMENT=devstack-a` on a libvirt-capable host and inspect `inventory.yaml`, `provider-state.yaml`, and `rendered/libvirt/`. +9. Read `ansible/roles/ard_libvirt_*` when debugging provisioning. +10. Read `ansible/deploy_multinode_devstack.yaml` and the `devstack_*` roles when debugging DevStack itself. + +Existing focused docs remain useful as source material: + +```text +README.md public overview, quick start, workflow, presets, troubleshooting +ARD_PROVIDER_DESIGN.md provider framework design intent and future-provider context +ansible/roles/*/README.md role-level notes for older and focused roles +molecule/*/molecule.yml full deployment scenario definitions +ansible/playbooks/ard-molecule-create.yaml shared Molecule create/render/apply seam +ansible/roles/*/molecule/*/molecule.yml role-level Molecule scenario definitions +examples/vdpa/ example vDPA inventory/configuration +``` + +Prefer this guide for current contributor orientation. Use focused docs for local detail, then verify commands against current playbooks and roles before treating them as canonical. + +# Part I - Orientation + +## What ARD is + +ARD provides local automation for creating VM-backed DevStack environments. A typical ARD run creates one or more VMs, configures SSH access, writes an Ansible inventory that looks like the inventory expected by the existing DevStack/Zuul roles, and then deploys DevStack across the generated nodes. + +The current primary development target is a local libvirt deployment using `qemu:///system`. A rendered deployment can be all-in-one, one controller plus one compute, or one controller plus two computes. The provider framework is written to keep provider-specific logic isolated so that future providers can create equivalent inventories without changing the DevStack roles. + +ARD is useful for: + +- testing DevStack multinode changes locally, +- testing OpenStack service combinations such as OVN, Tempest, Ceph, or vDPA-related setups, +- validating provider/inventory behavior outside of Zuul, +- reproducing CI-like VM topologies on a developer workstation, +- iterating on Ansible roles that configure DevStack controllers and computes. + +ARD is not: + +- a production OpenStack deployment tool, +- a replacement for DevStack, +- a replacement for Zuul, +- currently a general multi-provider provisioning framework in implementation, even though the design keeps that direction open. + +## The core mental model + +The most important idea is that ARD has two major phases: + +1. create provider nodes and inventory, +2. use that inventory to run the existing DevStack deployment flow. + +The high-level flow is: + +```text +Make / Molecule / developer command + | + v +ARD provider playbooks + | + v +provider common + dispatcher roles + | + v +libvirt provider roles + | + v +VMs with SSH access + | + v +generated Ansible inventory and provider state + | + v +ARD DevStack configuration loader + | + v +existing DevStack/Zuul-style deployment roles + | + v +DevStack running inside VMs +``` + +The provider's job ends when: + +1. VMs exist, +2. the management network is usable, +3. SSH works, +4. cloud-init has completed, +5. `inventory.yaml` has the expected logical names, groups, host variables, and nodepool-style facts. + +After that, the DevStack roles should not need to know whether nodes came from libvirt, KubeVirt, or a future provider. + +## Source of truth versus generated state + +ARD deliberately separates persistent intent from generated deployment state. + +Persistent source of truth normally lives in: + +```text +Makefile +bootstrap-repo.sh +bindep.txt +pyproject.toml +uv.lock +ansible.cfg +ansible/playbooks/ +ansible/roles/ +ansible/roles/ard_provider_common/files/presets/ +ansible/roles/ard_provider_render/templates/ +molecule/ +examples/ +``` + +Generated or local state normally lives in: + +```text +deployments//deployment.yaml +deployments//nodes.yaml +deployments//devstack/common.yaml +deployments//devstack/group_vars/*.yaml +deployments//inventory.yaml +deployments//provider-state.yaml +deployments//rendered/ +deployments//logs/ +$XDG_CACHE_HOME/ard/images or ~/.cache/ard/images +$XDG_STATE_HOME/ard/libvirt/images/ or ~/.local/state/ard/libvirt/images/ +``` + +`render` may overwrite `deployment.yaml`, `nodes.yaml`, and generated `devstack/*.yaml` files. Keep durable custom intent in a render file or deployment-local overlay such as `overrides/render.yaml`. + +## Terminology used in this guide + +`ARD` +: This repository and its Ansible/Make/Molecule workflow for VM-backed DevStack development. + +`provider` +: The implementation that creates nodes and inventory. The implemented provider is currently `libvirt`. + +`provider dispatcher role` +: A role named like `ard_provider_node` that loads deployment data and includes a provider-specific role such as `ard_libvirt_node`. + +`deployment workspace` +: A directory under `deployments//` or a Molecule scenario deployment directory. It contains rendered inputs, generated inventory, provider state, rendered libvirt artifacts, and logs. + +`render intent` +: The small user-supplied variable set that selects provider, topology, branch, service profiles, network CIDR, and overrides. It can be passed through Make variables, `ARD_RENDER_FILE`, or deployment-local overlays. + +`preset` +: A reusable YAML definition for topologies, services, branches, networks, node types, or provider profiles. + +`topology` +: A node layout preset such as `all-in-one`, `one-controller-one-compute`, or `one-controller-two-compute`. + +`service profile` +: A preset such as `devstack`, `ovn`, `tempest`, or `ceph` that contributes node profiles and DevStack variables. + +`node type` +: A preset such as `controller` or `compute` that contributes groups, default profiles, and default flavor. + +`management network` +: The network used for Ansible SSH connectivity and `nodepool.private_ipv4`/`public_ipv4` values. The default is `ard-mgmt`. + +`provider state` +: The generated file that records provider resource names, network names, disk paths, seed ISO paths, console logs, IPs, and MAC addresses for inspection and cleanup. + +`DevStack layer` +: The playbooks and roles that configure the VMs and run DevStack after the provider has created nodes. + +With the project overview and vocabulary in place, Part II covers development host setup and quick-start commands. + +# Part II - Development host preparation and quick starts + +## Development host assumptions + +The local provider expects a Linux host capable of running libvirt/KVM VMs through `qemu:///system`. You need enough CPU, memory, and disk for the selected topology. The default one-controller-one-compute topology uses the `devstack-control` and `devstack-compute` flavors; those are intentionally large enough for DevStack rather than tiny smoke-test VMs. + +The dependency model has three layers: + +1. minimal system tools needed to install Python tooling, +2. bindep-managed operating-system packages, +3. Python dependencies managed by `uv`. + +`bootstrap-repo.sh` handles the normal setup path. It detects `apt` or `dnf`, installs minimal Python/curl dependencies, ensures `uv`, installs packages reported by bindep, runs `uv sync`, updates submodules, and checks local libvirt commands. + +Important host commands include: + +```text +virsh +qemu-img +cloud-localds +setfacl +rsync +git +podman, for role-level Molecule scenarios that use containers +``` + +Your user normally needs permission to use libvirt. If `qemu:///system` is not reachable, check group membership, libvirt daemon state, and host virtualization support before debugging ARD roles. + +## Bootstrap the repository + +From a fresh checkout: + +```bash +./bootstrap-repo.sh +``` + +Useful bootstrap toggles: + +```bash +DRY_RUN=1 ./bootstrap-repo.sh +SKIP_PACKAGES=1 ./bootstrap-repo.sh +SKIP_UV_INSTALL=1 ./bootstrap-repo.sh +SKIP_SUBMODULES=1 ./bootstrap-repo.sh +``` + +Use the skip flags only when you know that layer is already handled by your environment. + +## Quick start: render only + +A render-only run is the safest first step because it does not create VMs: + +```bash +make render ARD_DEPLOYMENT=devstack-a +``` + +Inspect: + +```bash +find deployments/devstack-a -maxdepth 4 -type f | sort +cat deployments/devstack-a/deployment.yaml +cat deployments/devstack-a/nodes.yaml +cat deployments/devstack-a/devstack/common.yaml +cat deployments/devstack-a/devstack/group_vars/controller.yaml +cat deployments/devstack-a/devstack/group_vars/compute.yaml +``` + +This teaches the core data model without requiring libvirt to work. + +## Quick start: create provider nodes + +On a libvirt-capable host: + +```bash +make apply ARD_DEPLOYMENT=devstack-a +make ping ARD_DEPLOYMENT=devstack-a +``` + +`apply` creates provider resources, writes `inventory.yaml`, adds the generated hosts to the active inventory, waits for SSH, and waits for cloud-init completion. + +Inspect: + +```bash +cat deployments/devstack-a/inventory.yaml +cat deployments/devstack-a/provider-state.yaml +find deployments/devstack-a/rendered -maxdepth 4 -type f | sort +``` + +To SSH to a node: + +```bash +make ssh ARD_DEPLOYMENT=devstack-a ARD_NODE=controller +make ssh-print ARD_DEPLOYMENT=devstack-a ARD_NODE=compute-1 +``` + +The `ssh-print` form is useful when you want to copy the command or inspect the generated SSH options. + +## Quick start: deploy and verify DevStack + +After `apply` and `ping` succeed: + +```bash +make deploy ARD_DEPLOYMENT=devstack-a +make verify ARD_DEPLOYMENT=devstack-a +``` + +`deploy` runs the DevStack deployment playbook against the generated inventory. `verify` checks inventory presence, pings all nodes, verifies that `/opt/repos/devstack` exists on the controller, and can optionally run a Tempest smoke test when requested. + +## Quick start: destroy and clean up + +To destroy provider resources while keeping generated artifacts for inspection: + +```bash +make destroy ARD_DEPLOYMENT=devstack-a +``` + +To destroy provider resources and remove generated runtime artifacts: + +```bash +make destroy-clean-generated ARD_DEPLOYMENT=devstack-a +``` + +To remove generated inventory, provider state, and rendered artifacts without touching provider resources: + +```bash +make clean-generated ARD_DEPLOYMENT=devstack-a +``` + +To remove the entire local deployment workspace: + +```bash +make cleanup ARD_DEPLOYMENT=devstack-a +``` + +Use `destroy` first when VMs or libvirt networks still exist. Use `cleanup` only when you no longer need the workspace. + +## Quick start: full rebuild + +The default target performs a full local rebuild workflow: + +```bash +make ARD_DEPLOYMENT=devstack-a +``` + +It runs best-effort destroy/cleanup steps, then `render`, `apply`, `ping`, `deploy`, and `verify`. This is convenient when the host is already prepared and failures from prior runs should be cleared. It is not the best first command for learning because it performs every stage at once. + +# Part III - Ansible, Make, and repository layout + +## Top-level repository map + +High-level source layout: + +```text +. +├── README.md public quick start and workflow documentation +├── developer-guide.md this contributor guide +├── ARD_PROVIDER_DESIGN.md provider framework design plan +├── Makefile primary local command interface +├── bootstrap-repo.sh host/repository bootstrap helper +├── bindep.txt OS package dependency list +├── pyproject.toml Python dependency metadata for uv +├── uv.lock locked Python environment +├── ansible.cfg role path and Ansible defaults +├── ansible/ +│ ├── playbooks/ ARD provider and workflow playbooks +│ ├── roles/ ARD provider, libvirt, DevStack, and helper roles +│ ├── deploy_multinode_devstack.yaml +│ ├── devstack_common.yaml +│ ├── devstack_controller.yaml +│ ├── devstack_compute.yaml +│ └── vdpa.yaml +├── molecule/ full ARD/libvirt-backed scenarios +├── examples/ focused example inputs +├── scripts/ small developer helpers +├── deployments/ local deployment workspaces, mostly generated +├── submodules/ DevStack and Zuul role submodules +└── okd/ older/future OpenShift-related scratch area +``` + +## Makefile as command interface + +Most contributor commands should go through the top-level `Makefile`. It centralizes defaults and ensures the same playbooks are used consistently. + +Important variables: + +```text +ARD_PROVIDER provider, currently libvirt +ARD_DEPLOYMENT deployment name, default devstack-1 +ARD_DEPLOYMENTS_DIR parent directory for deployment workspaces +ARD_DEPLOYMENT_DIR full deployment workspace path +ARD_TOPOLOGY topology preset +ARD_TARGET_BRANCH DevStack branch preset/branch string +ARD_SERVICES comma-separated service profile list +ARD_PROVIDER_PROFILE provider profile preset +ARD_IMAGE optional image key override +ARD_NETWORK_CIDR management network CIDR +ARD_RENDER_FILE optional render intent file +ARD_NODE node selected by make ssh +ARD_SSH_PRINT print SSH command instead of executing it +ARD_SSH_ARGS extra arguments passed to ssh +ARD_EXTRA_VARS extra Ansible variables appended to provider commands +``` + +Common targets: + +```text +render generate deployment.yaml, nodes.yaml, and DevStack vars +apply create provider resources and inventory +ping run Ansible ping against generated inventory +ssh SSH to a generated inventory host +ssh-print print the generated SSH command +deploy deploy DevStack +verify run post-deploy checks +destroy remove provider resources +clean-generated remove inventory/state/rendered artifacts +cleanup delete the deployment workspace +site render, apply, deploy, verify +molecule-test run role-level Molecule tests +molecule-role- run one role-level Molecule scenario +``` + +The default target is intentionally broad. Prefer individual targets while developing a specific layer. + +## Ansible configuration and submodules + +`ansible.cfg` sets the role search path to: + +```text +ansible/roles +submodules/devstack/roles +submodules/zuul-jobs/roles +submodules/openstack-zuul-jobs/roles +``` + +This matters because some role names referenced by ARD playbooks are local, while others come from upstream DevStack or Zuul job repositories. If a role is not found, verify that submodules were initialized and that Ansible is running with the repository `ansible.cfg`. + +The submodules are: + +```text +submodules/devstack +submodules/zuul-jobs +submodules/openstack-zuul-jobs +``` + +Run this if they are missing: + +```bash +git submodule update --init --recursive +``` + +## Python environment + +ARD uses `uv` for the Python environment. The project metadata declares `ansible-core`, `passlib`, `netaddr`, `molecule`, and Molecule Podman plugins. The repository is not packaged as an installable Python package; `tool.uv.package = false` keeps it as a tooling environment. + +Use: + +```bash +uv sync +uv run ansible --version +uv run ansible-playbook --version +uv run molecule --version +``` + +Prefer `uv run ...` in documentation and scripts unless you intentionally rely on an activated virtual environment. + +# Part IV - Full deployment-from-scratch walkthrough + +This section walks through what happens when you run: + +```bash +make render apply ping deploy verify ARD_DEPLOYMENT=devstack-a +``` + +The exact Make invocation above is shorthand for the individual targets. In practice, run them one at a time when debugging. + +## Stage 1: Make variable resolution + +The Makefile resolves defaults such as: + +```text +ARD_PROVIDER=libvirt +ARD_DEPLOYMENT=devstack-1 +ARD_TOPOLOGY=one-controller-one-compute +ARD_TARGET_BRANCH=master +ARD_SERVICES=devstack,ovn,tempest +ARD_PROVIDER_PROFILE=local-libvirt +ARD_NETWORK_CIDR=192.168.96.0/24 +``` + +It then converts command-line Make variables into Ansible extra vars. Render-specific values are passed to `ard-render.yaml`; deployment-stage values are passed to apply/deploy/verify/destroy/cleanup playbooks. + +## Stage 2: render starts and loads intent + +`ansible/playbooks/ard-render.yaml` runs on localhost and includes: + +```text +ard_provider_common +ard_provider_render +``` + +The render role first establishes `ard_deployment_dir` and `ard_deployment_name`. It then looks for: + +```text +/render.yaml +/overrides/render.yaml +``` + +If `ARD_RENDER_FILE` is provided through Make, it is passed as an extra vars file before the ordinary render variables. Render intent can therefore come from Make variables, a checked-in example file, a local deployment file, or an overlay. + +## Stage 3: render loads presets + +The render role loads these preset groups: + +```text +topologies.yaml +branches.yaml +services.yaml +node-types.yaml +networks.yaml +provider-profiles.yaml +``` + +Then it validates the selected provider, topology, provider profile, service profiles, and network CIDR. + +As of this guide, render validates `ard_provider == 'libvirt'`. If you are adding another provider, the render validation and provider-specific render output are among the first things that need to change. + +## Stage 4: render composes services, branch, provider defaults, and networks + +Service profiles are merged in the requested order. Profiles can contribute DevStack variables and node profiles. For example, the default service list `devstack,ovn,tempest` enables DevStack defaults, adds OVN node profile data, and includes Tempest-related configuration hooks. + +Branch presets contribute DevStack branch defaults. Provider profile presets contribute image/flavor/preference defaults. Network presets define management and optional tenant networks. + +The render role then composes: + +```text +ard_render_services_config +ard_render_node_profiles +ard_render_branch_config +ard_render_provider_profile_config +ard_render_provider_defaults +ard_render_networks_config +``` + +## Stage 5: render normalizes nodes + +The `nodes.yaml.j2` template turns topology pools and node type presets into concrete nodes. + +For each pool, it derives: + +```text +node name +hostname +provider resource name +groups +image +flavor +preference +attached networks +IP addresses where applicable +MAC addresses +profiles +``` + +For the default `one-controller-one-compute` topology, the logical nodes are: + +```text +controller +compute-1 +``` + +The controller is in groups such as `controller` and `switch`. The compute is in groups such as `compute`, `peers`, and `subnode`. These group names are part of the existing DevStack/Zuul role contract, so treat them carefully. + +## Stage 6: render writes deployment files + +The render role writes: + +```text +deployments//deployment.yaml +deployments//nodes.yaml +deployments//devstack/common.yaml +deployments//devstack/group_vars/controller.yaml +deployments//devstack/group_vars/compute.yaml +``` + +It also ensures directories such as: + +```text +deployments//devstack/group_vars +deployments//devstack/host_vars +deployments//rendered/libvirt +deployments//logs +``` + +At this point no VMs have been created. + +## Stage 7: apply loads deployment data + +`ansible/playbooks/ard-apply.yaml` runs on localhost, gathers facts, and includes: + +```text +ard_provider_common +ard_provider_preflight +ard_provider_image +ard_provider_network +ard_provider_node +ard_provider_inventory +``` + +The provider dispatcher roles load `deployment.yaml` and `nodes.yaml`, then include the provider-specific roles. With `ard_provider: libvirt`, they call libvirt roles. + +## Stage 8: apply performs preflight + +The preflight layer exists to verify provider-specific requirements before expensive creation work. For libvirt, this is where provider prerequisites should be checked and normalized. When adding checks, keep them actionable: a contributor should know which missing command, daemon, permission, or variable caused the failure. + +## Stage 9: apply prepares the base image + +The libvirt image role selects the configured cloud image from `ard_images`, resolves the image cache directory, downloads the base image when needed, verifies it exists, ensures the per-deployment image directory exists, and grants libvirt access to the relevant paths. + +By default, base images are cached under: + +```text +$XDG_CACHE_HOME/ard/images +``` + +or: + +```text +~/.cache/ard/images +``` + +Per-deployment disks and seed ISOs are placed under: + +```text +$XDG_STATE_HOME/ard/libvirt/images/ +``` + +or: + +```text +~/.local/state/ard/libvirt/images/ +``` + +## Stage 10: apply creates libvirt networks + +The libvirt network role renders network XML under the deployment workspace, checks whether each libvirt network already exists, defines missing networks, starts them, and marks them autostart. + +The default management network is a NAT network. Additional networks can be NAT or isolated depending on render configuration. + +## Stage 11: apply creates VM nodes + +For each rendered node, the libvirt node role: + +1. resolves provider facts such as resource name, flavor, management IP, MAC, render directory, disk path, seed path, and console log path, +2. writes cloud-init `user-data`, +3. writes cloud-init `meta-data`, +4. writes cloud-init `network-config`, +5. builds a NoCloud seed ISO with `cloud-localds`, +6. copies the seed ISO into the libvirt image directory, +7. creates a qcow2 disk from the cached base image, +8. resizes the disk to the selected flavor size, +9. ensures a console log file exists, +10. grants libvirt access with ACLs, +11. renders domain XML, +12. defines the libvirt domain if missing, +13. starts the domain. + +Cloud-init config creates a `stack` user, installs the ARD SSH public key, enables password auth as a fallback, installs basic packages, configures serial console logging, and restarts SSH. + +## Stage 12: apply writes inventory and provider state + +The libvirt inventory role writes: + +```text +deployments//inventory.yaml +deployments//provider-state.yaml +``` + +The inventory uses logical names such as `controller` and `compute-1`, not libvirt domain names. It sets `ansible_host`, `ansible_user`, `ansible_private_key_file`, SSH common args, `nodepool.private_ipv4`, `nodepool.public_ipv4`, and minimal `zuul.executor` facts. + +The provider state file records enough libvirt resource data to inspect or debug the deployment: + +```text +provider +libvirt URI +management network +libvirt network names +domain names +IP and MAC addresses +overlay disk paths +seed ISO paths +console log paths +``` + +## Stage 13: apply waits for nodes + +After local provider creation, `ard-apply.yaml` runs a second play against `ard_provider_nodes`. It waits for SSH/Ansible connectivity and then waits for cloud-init completion. This prevents `make apply` from returning before the generated inventory is actually usable. + +## Stage 14: ping verifies connectivity + +`make ping` runs: + +```bash +uv run ansible -i /inventory.yaml all -m ansible.builtin.ping +``` + +This is a cheap sanity check that the generated inventory and SSH credentials work outside the apply playbook. + +## Stage 15: deploy loads DevStack variables + +`ard-deploy-devstack.yaml` first refreshes provider inventory and then runs `ard_devstack_config` on all hosts. That role loads: + +```text +/devstack/common.yaml +/devstack/group_vars/.yaml +/devstack/host_vars/.yaml +``` + +This lets rendered DevStack defaults and user customizations become ordinary Ansible variables before the existing DevStack playbooks run. + +## Stage 16: deploy runs multinode DevStack + +`ard-deploy-devstack.yaml` imports `ansible/deploy_multinode_devstack.yaml`. + +That playbook: + +1. ensures local cache directories, +2. runs common DevStack preparation on all hosts, +3. optionally configures vDPA, +4. pushes apt/dnf/pip/git caches to targets, +5. deploys the controller, +6. exports nodepool/Zuul-style facts, +7. syncs controller data to subnodes, +8. optionally syncs Ceph config, +9. deploys computes, +10. runs DevStack host discovery, +11. pulls caches back for future runs. + +When debugging DevStack failures, determine whether the failure happened before or after the provider boundary. Provider failures usually involve libvirt, cloud-init, SSH, images, disks, or inventory. DevStack failures usually involve package installation, git checkouts, local.conf rendering, service startup, or OpenStack-specific configuration. + +## Stage 17: verify performs post-deploy checks + +`make verify` runs `ard-verify.yaml`. It checks that generated inventory exists, pings all nodes, checks for `/opt/repos/devstack` on the controller, and optionally runs Tempest smoke when `ard_verify_tempest_smoke` is true. + +# Part V - Render presets, deployment workspaces, and inventory + +## Render input precedence + +Render inputs can come from several places. The common sources are: + +1. role defaults, +2. preset files, +3. render intent files, +4. deployment-local overlay files, +5. Make command-line variables, +6. explicit `ARD_EXTRA_VARS` or direct `ansible-playbook -e` values. + +The exact Ansible variable precedence rules still apply. When debugging surprising values, print the rendered `deployment.yaml`, `nodes.yaml`, and DevStack group vars first. Those files show what the provider and DevStack layers will consume. + +## Topology presets + +Current built-in topology presets are: + +```text +all-in-one +one-controller-one-compute +one-controller-two-compute +``` + +`all-in-one` creates only `controller`. + +`one-controller-one-compute` creates: + +```text +controller +compute-1 +``` + +`one-controller-two-compute` creates: + +```text +controller +compute-1 +compute-2 +``` + +Topology presets are composed from node pools. A singleton pool can provide an explicit node name. A counted pool can provide name and hostname formats such as `compute-{index}`. + +If a topology says the controller does not run compute services, render disables `n-cpu` on the controller through controller group vars. + +## Node type presets + +Node types define group membership, default profiles, and default flavor. + +The controller type contributes groups expected by the controller/switch side of the DevStack multinode flow. The compute type contributes groups expected by subnode/peer/compute flows. + +Changing group names can break existing roles. If you need new groups, add them without removing compatibility groups unless you are deliberately changing the DevStack contract. + +## Service profiles + +Service profiles let a small render intent select related node profiles and DevStack variable sets. + +Current service profiles are: + +```text +devstack +ovn +tempest +ceph +``` + +The default service list is: + +```text +devstack,ovn,tempest +``` + +A service profile can contribute: + +```text +node_profiles +devstack.common +devstack.controller +devstack.compute +``` + +Use service profiles for reusable feature-level bundles. Use render overrides for one-off local experiments. + +## Branch presets + +Branch presets map an ARD target branch to DevStack variables and, when needed, provider defaults. The default branch is `master`. The `stable/2026.1` preset currently selects `stable/2026.1` and changes the default image to Ubuntu 24.04. + +When adding a branch preset, document why any image or service default differs from master. + +## Network presets + +The default `ard-mgmt` network is NAT-backed and used for SSH/inventory. The built-in `tenant` network is isolated and opt-in. Isolated networks render without host-side IP, NAT, or DHCP, but VM interfaces still get deterministic MAC addresses and stable interface names. + +`ard_management_network` selects which attached network is used for SSH and `nodepool` facts. The management network must be a NAT network with IP addresses. + +## Provider profile presets + +The default provider profile is `local-libvirt`. It selects: + +```text +provider: libvirt +default image: debian-13 +controller flavor: devstack-control +compute flavor: devstack-compute +VM preference: devstack +``` + +Provider profiles should describe reusable provider-level defaults, not one-off local deployment decisions. + +## Render overrides + +Use `ard_render_overrides` for recursive dictionary merges into provider defaults, topology, node pools, networks, and DevStack variables. Later dictionaries replace scalar and list values for the relevant section. + +Use `ard_render_node_overrides` for per-node changes after the node is generated from topology and node type presets. + +Example pattern: + +```yaml +--- +ard_provider: libvirt +ard_provider_profile: local-libvirt +ard_target_branch: master +ard_topology: one-controller-one-compute +ard_service_profiles: + - devstack + - ovn + - tempest +ard_libvirt_network_cidr: 192.168.98.0/24 + +ard_render_overrides: + provider_defaults: + image: ubuntu-24.04 + node_pools: + compute: + count: 2 + devstack: + common: + enable_ceph: false + +ard_render_node_overrides: + compute-2: + networks: + ard-mgmt: + ip: 192.168.98.50 +``` + +Keep examples small. If an override becomes reusable, consider making it a preset. + +## Inventory contract + +Generated inventory must preserve the logical names and groups expected by the DevStack deployment layer. Inventory hostnames remain `controller`, `compute-1`, and `compute-2` even though provider resources are named with deployment-specific prefixes such as `ard-devstack-a-controller`. + +Each generated host should include: + +```text +ansible_host +ansible_user +ansible_private_key_file +ansible_ssh_common_args +ard_deployment_name +ard_provider_resource_name +nodepool.private_ipv4 +nodepool.public_ipv4 +zuul.executor.log_root +zuul.executor.work_root +``` + +Group membership is not just cosmetic. It determines which DevStack playbooks and roles run on each node. + +# Part VI - Provider role reference + +## Provider common + +`ard_provider_common` supplies defaults shared across provider workflows. Its task file is intentionally a no-op so playbooks can include the role explicitly and get role defaults loaded before dispatcher roles run. + +Important defaults include: + +```text +ard_provider +ard_provider_profile +ard_topology +ard_target_branch +ard_service_profiles +ard_default_image +ard_default_controller_flavor +ard_default_compute_flavor +ard_management_network +ard_image_cache_dir +ard_libvirt_uri +ard_libvirt_pool +ard_libvirt_network_cidr +ard_libvirt_image_dir +ard_apply_wait_timeout +ard_images +ard_flavors +``` + +## Provider render + +`ard_provider_render` is the highest-leverage role in the repository. It converts small human intent into concrete provider and DevStack inputs. + +Responsibilities: + +- load render intent and overlay files, +- load presets, +- normalize service profile lists, +- validate supported inputs, +- compose topology, service, branch, provider, and network configs, +- normalize nodes with the `nodes.yaml.j2` template, +- validate node uniqueness, IP uniqueness, MAC uniqueness, and management network correctness, +- compose DevStack common/controller/compute variables, +- write generated deployment files. + +If render output is wrong, fix render before debugging lower provider layers. + +## Provider preflight + +`ard_provider_preflight` loads deployment and node variables, then dispatches to provider-specific preflight. Use this layer for checks that should fail fast before image downloads or VM creation. + +Good preflight checks are: + +- required host commands exist, +- provider connection is reachable, +- selected provider values are supported, +- required credentials or permissions are present, +- impossible topology/provider combinations are rejected early. + +## Provider image + +`ard_provider_image` dispatches to image handling for the selected provider. The libvirt implementation resolves the selected cloud image and ensures the cached base image is available. + +When adding image support, decide whether image selection is deployment-wide or per-node. The current libvirt image role uses the deployment default image as the selected base image, while nodes can still carry image metadata from render. If you introduce true mixed-image deployments, audit this role carefully. + +## Provider network + +`ard_provider_network` dispatches to provider-specific network creation. The libvirt implementation renders one XML file per rendered network, defines missing networks, starts them, and marks them autostart. + +NAT networks render ``, a gateway IP, and static DHCP host entries for every node interface on that network that has an IP address. Isolated networks render without host-side IP, NAT, or DHCP. The libvirt network template also honors provider-specific details such as a `provider.libvirt.bridge_name` value if that value is present in the consumed deployment data, which is useful for specialized local experiments. + +When changing network behavior, keep the management-network contract front and center. Ansible must be able to SSH to every node through the selected management network, and `ard_management_network` must continue to point at a NAT network with deterministic guest IPs. Extra networks should be additive: attach them to nodes through topology/pool/node overrides, generate stable MAC addresses, and avoid changing the logical inventory address unless the management network itself changes. + +## Provider node + +`ard_provider_node` dispatches to provider-specific node creation. The libvirt implementation loops through `ard_nodes` and includes `node.yml` per node. + +For each node network, cloud-init matches the deterministic MAC address, assigns a stable interface name such as `eth0`, and configures a static address only when render produced one. Interfaces without an IP are explicitly marked optional with DHCP disabled, which is important for isolated tenant-style networks that guests may configure themselves later. The management network receives the default route and DNS configuration. + +Node creation is the stage most likely to fail because it touches many host systems: + +- file permissions and ACLs, +- base image formats, +- disk creation and resizing, +- cloud-init seed generation, +- libvirt domain XML, +- firmware selection, +- network attachment, +- SSH readiness. + +Debug one node at a time by inspecting its rendered directory and console log. + +## Provider inventory + +`ard_provider_inventory` dispatches to provider-specific inventory generation. The libvirt implementation writes both persistent inventory and active in-memory Ansible hosts for the second apply play. + +If `apply` creates VMs but the wait play cannot find hosts, inspect this role and the generated `inventory.yaml`. + +## Provider destroy and cleanup + +Destroy removes provider resources. Cleanup removes the local deployment workspace. Generated provider state exists to make destroy/debug flows more transparent. + +The libvirt destroy path is state-first. When `provider-state.yaml` exists, destroy uses the recorded domain and network list rather than guessing from current presets. If state is missing, it falls back to finding domains with the deployment resource-name prefix and to the default management network name. This makes ordinary cleanup robust while still allowing manual recovery from partial or old workspaces. + +Preserve generated artifacts by default after failures. They often contain exactly what you need: rendered XML, cloud-init data, inventory, state files, and log paths. Use `destroy-clean-generated` only when you no longer need those artifacts, or when you want a fresh generated workspace after resources are gone. + +# Part VII - DevStack deployment layer reference + +## DevStack configuration loading + +Before running DevStack, `ard_devstack_config` loads deployment-specific variable files: + +```text +devstack/common.yaml +devstack/group_vars/.yaml +devstack/host_vars/.yaml +``` + +This is the bridge between rendered ARD intent and ordinary Ansible variables used by DevStack roles. + +## Common host preparation + +`ansible/devstack_common.yaml` runs on all hosts and applies local roles plus upstream roles. It prepares stack user access, development tools, common DevStack settings, multinode SSH/bridge configuration, pip, and swap. + +When a failure happens before DevStack itself runs, check this layer for missing OS packages, user setup problems, SSH peer issues, bridge setup, or role path/submodule problems. + +## Controller deployment + +`ansible/devstack_controller.yaml` runs the `devstack_controller` role on the `controller` group. This is where controller-side localrc/local.conf behavior and DevStack execution are managed. + +Controller failures are usually visible in DevStack logs on the controller VM. SSH to the controller and inspect `/opt/stack`, `/opt/repos/devstack`, and DevStack log output. + +## Compute deployment + +`ansible/devstack_compute.yaml` runs the `devstack_compute` role on the `compute` group. Compute failures usually involve service enablement, controller connectivity, virtualization support, networking, or branch-specific DevStack behavior. + +## Cache push and pull + +`deploy_multinode_devstack.yaml` has local cache handling for apt/dnf, pip, and git repositories. The goal is to speed repeat deployments by pushing known caches to nodes and pulling caches back after a successful run. + +Cache behavior can hide or reveal network issues. When debugging reproducibility, know whether a run used local cache data. + +## vDPA and optional services + +`vdpa.yaml` and the `configure_vdpa` role exist for vDPA-specific configuration. Service profiles and render overrides can enable related behavior. Treat specialized features as layers on top of the core render/apply/deploy flow. + +# Part VIII - Molecule, validation, and test strategy + +## Top-level Molecule scenarios + +Top-level Molecule scenarios are full ARD/libvirt-backed DevStack validation flows. They use the same provider playbooks as Make and define ARD scenario intent under `provisioner.ard`. + +Current top-level scenarios include: + +```text +default +one-controller-two-compute +stable-2026.1 +``` + +The create step is intentionally thin in each scenario: `molecule//create.yml` imports the shared `ansible/playbooks/ard-molecule-create.yaml` playbook. That shared playbook reads `molecule.yml`, validates the required `provisioner.ard` keys, writes generated render variables to `deployment/.molecule-render-vars.yaml`, runs `ard-render.yaml`, and then runs `ard-apply.yaml`. This keeps Molecule scenarios declarative and avoids duplicating render/apply command construction across scenarios. + +The supported `provisioner.ard` fields include the provider, provider profile, deployment name, resource-name prefix, target branch, topology, service profiles, libvirt network name/CIDR, optional management network, optional render image, render overrides, and node overrides. Prefer adding scenario intent to `molecule.yml` instead of editing generated files under `molecule//deployment/`. + +Run a full scenario with: + +```bash +uv run molecule test -s default +``` + +For a cheaper loop: + +```bash +uv run molecule create -s default +uv run ansible -i molecule/default/deployment/inventory.yaml all -m ping +uv run molecule converge -s default +uv run molecule verify -s default +uv run molecule destroy -s default +``` + +These scenarios require a host capable of running the local libvirt provider. + +## Role-level Molecule scenarios + +Some roles have their own Molecule scenarios under `ansible/roles/*/molecule`. These are intended for role-level validation where containers are sufficient. + +Run all role scenarios discovered by the Makefile: + +```bash +make molecule-test +``` + +Run one role scenario: + +```bash +make molecule-role-ensure_kustomize +``` + +Role-level Molecule tests may require Podman. + +## Cheap checks before expensive tests + +Before running full libvirt/DevStack validation, use cheaper checks: + +```bash +uv run ansible-playbook --syntax-check -i localhost, ansible/playbooks/ard-render.yaml +uv run ansible-playbook --syntax-check -i localhost, ansible/playbooks/ard-apply.yaml +uv run ansible-playbook --syntax-check -i localhost, ansible/playbooks/ard-destroy.yaml +make render ARD_DEPLOYMENT=syntax-devstack +``` + +A render-only test catches many data-model mistakes without starting VMs. + +## Manual validation matrix + +For provider/render changes, consider at least: + +```text +make render ARD_TOPOLOGY=all-in-one +make render ARD_TOPOLOGY=one-controller-one-compute +make render ARD_TOPOLOGY=one-controller-two-compute +make render ARD_TARGET_BRANCH=stable/2026.1 +make render ARD_SERVICES=devstack,ovn,tempest +make render ARD_SERVICES=devstack,ovn,tempest,ceph +make render with a custom ARD_RENDER_FILE +make render with ard_render_overrides +make apply + ping for the default topology +make deploy + verify for one representative full deployment +``` + +For DevStack role changes, a full deploy is more valuable than render-only checks. + +# Part IX - Maintenance workflows + +## Adding a topology preset + +1. Add the topology to `ansible/roles/ard_provider_common/files/presets/topologies.yaml`. +2. Use existing node types where possible. +3. Ensure node names and IP/MAC generation are deterministic. +4. Decide whether the controller runs compute services. +5. Run render-only tests for the new topology. +6. Inspect `nodes.yaml`, controller group vars, and generated inventory. +7. Run at least `apply` and `ping` before declaring the topology usable. + +Avoid inventing new group names unless necessary. Existing DevStack roles depend on current groups. + +## Adding a service profile + +1. Add the profile to `services.yaml`. +2. Decide whether it contributes node profiles, DevStack common vars, controller vars, compute vars, or a combination. +3. Keep profile behavior reusable and small. +4. Add README/developer-guide notes if the profile is user-facing. +5. Render with the new service in combination with default services. +6. Run a full deployment if the profile changes DevStack behavior. + +## Adding an image or flavor + +Images and flavors live in provider common defaults today. + +When adding an image: + +- include a stable key, +- specify OS family/version metadata, +- define provider-specific URL/name/cache filename/format, +- consider whether checksums should be required, +- test image download and cloud-init behavior. + +When adding a flavor: + +- document CPU, memory, and disk expectations, +- include provider-specific libvirt values, +- verify the size is realistic for DevStack, +- avoid making defaults too small for successful deployments. + +## Adding a provider + +The design supports provider isolation, but the current implementation is libvirt-oriented. A new provider likely needs: + +```text +ard__preflight +ard__image +ard__network +ard__node +ard__inventory +ard__destroy +``` + +You may also need provider-specific render output and validation changes. The provider must eventually produce the same logical inventory contract used by the DevStack layer. + +Keep the DevStack roles provider-agnostic. Do not make `devstack_common`, `devstack_controller`, or `devstack_compute` know about libvirt/KubeVirt unless there is no other option. + +## Updating submodules + +Submodules provide DevStack and Zuul roles. When updating them: + +1. update the submodule pointer, +2. run syntax checks, +3. run render-only checks, +4. run at least one full deployment scenario, +5. inspect role-name conflicts or behavior changes, +6. document any required variable changes. + +Submodule updates can change role behavior even if ARD code did not change. + +## Editing generated examples + +Do not manually edit generated files under `deployments//` and commit them as source unless the repository intentionally starts tracking a fixture. Most deployment workspaces are local scratch state. + +Prefer examples under `examples/` or Molecule scenario definitions when you need checked-in user-facing intent. + +# Part X - Troubleshooting + +## Decide which layer failed + +First classify the failure: + +```text +bootstrap failure host packages, uv, bindep, submodules, libvirt commands +render failure bad variables, missing presets, invalid topology/network/service input +image failure image URL/cache/checksum/download/path/ACL issue +network failure libvirt network XML, net-define, net-start, CIDR conflict +node failure cloud-init seed, qcow2, ACLs, firmware, domain XML, virsh start +inventory failure wrong management IP, missing groups, generated inventory syntax +readiness failure SSH, cloud-init, guest networking, stack user/key setup +DevStack common failure OS packages, users, bridges, role path, upstream roles +controller failure local.conf/localrc, run-devstack, OpenStack services +compute failure subnode sync, nova-compute, networking, controller reachability +verify failure inventory, DevStack checkout, Tempest smoke +``` + +Do not debug DevStack logs before confirming that provider nodes are reachable and cloud-init completed. + +## Libvirt access + +Symptoms: + +```text +virsh cannot connect to qemu:///system +permission denied on libvirt socket +network/domain define fails unexpectedly +``` + +Checks: + +```bash +virsh --connect qemu:///system uri +groups +systemctl status libvirtd || systemctl status virtqemud +``` + +Your user may need membership in a libvirt or qemu group. After changing group membership, log out and back in or use `newgrp`. + +## CIDR conflicts + +If a libvirt network fails to start or nodes cannot route, check whether the selected `ARD_NETWORK_CIDR` conflicts with existing host networks or another ARD deployment. + +Use unique deployment names and CIDRs for parallel deployments: + +```bash +make render ARD_DEPLOYMENT=devstack-a ARD_NETWORK_CIDR=192.168.99.0/24 +make render ARD_DEPLOYMENT=devstack-b ARD_NETWORK_CIDR=192.168.100.0/24 +``` + +## UEFI firmware problems + +Libvirt firmware auto-selection is used for UEFI boot with secure boot disabled. If domain definition or boot fails because firmware cannot be found, install the OVMF/edk2 firmware package for your distribution. + +## SSH not ready + +`apply` waits for SSH and cloud-init. If it times out: + +1. inspect the generated inventory IP, +2. inspect `provider-state.yaml`, +3. inspect the node's rendered cloud-init files, +4. inspect the serial console log path recorded in provider state, +5. verify the libvirt domain is running, +6. verify the libvirt network is active, +7. retry `make apply` after fixing the underlying issue. + +## Cloud-init problems + +Inspect per-node rendered files: + +```text +deployments//rendered/libvirt//user-data +deployments//rendered/libvirt//meta-data +deployments//rendered/libvirt//network-config +``` + +Inside the guest, inspect: + +```bash +sudo cloud-init status --long +sudo journalctl -u cloud-init --no-pager +sudo journalctl -u ssh --no-pager +ip addr +ip route +``` + +## DevStack failures + +Once SSH works and cloud-init is complete, switch your attention to DevStack logs and role output. + +Useful locations on the controller commonly include: + +```text +/opt/repos/devstack +/opt/stack +/tmp/zuul_logs +``` + +Use `make ssh` to enter the controller, then inspect service logs and DevStack output according to the failing role/task. + +## Cleaning up stuck resources + +Prefer ARD cleanup first: + +```bash +make destroy ARD_DEPLOYMENT= +make destroy-clean-generated ARD_DEPLOYMENT= +``` + +If manual cleanup is required, inspect `provider-state.yaml` before deleting resources. It records the libvirt resource names and local disk/seed/log paths. + +Manual libvirt commands can be destructive. Verify names before running commands such as `virsh destroy`, `virsh undefine`, `virsh net-destroy`, or `virsh net-undefine`. + +# Part XI - Quick reference + +## Core commands + +```bash +./bootstrap-repo.sh +make render ARD_DEPLOYMENT=devstack-a +make apply ARD_DEPLOYMENT=devstack-a +make ping ARD_DEPLOYMENT=devstack-a +make ssh ARD_DEPLOYMENT=devstack-a ARD_NODE=controller +make ssh-print ARD_DEPLOYMENT=devstack-a ARD_NODE=compute-1 +make deploy ARD_DEPLOYMENT=devstack-a +make verify ARD_DEPLOYMENT=devstack-a +make destroy ARD_DEPLOYMENT=devstack-a +make destroy-clean-generated ARD_DEPLOYMENT=devstack-a +make cleanup ARD_DEPLOYMENT=devstack-a +``` + +## Render examples + +```bash +make render \ + ARD_DEPLOYMENT=devstack-a \ + ARD_TARGET_BRANCH=master \ + ARD_TOPOLOGY=one-controller-two-compute \ + ARD_SERVICES=devstack,ovn,tempest \ + ARD_NETWORK_CIDR=192.168.99.0/24 +``` + +```bash +make render \ + ARD_DEPLOYMENT=stable-test \ + ARD_RENDER_FILE=path/to/render.yaml +``` + +## Topology names + +```text +all-in-one +one-controller-one-compute +one-controller-two-compute +``` + +## Service profile names + +```text +devstack +ovn +tempest +ceph +``` + +## Image keys + +```text +debian-13 +ubuntu-24.04 +``` + +## Flavor keys + +```text +devstack-control +devstack-compute +``` + +## Important generated files + +```text +deployments//deployment.yaml +deployments//nodes.yaml +deployments//devstack/common.yaml +deployments//devstack/group_vars/controller.yaml +deployments//devstack/group_vars/compute.yaml +deployments//inventory.yaml +deployments//provider-state.yaml +deployments//rendered/libvirt//user-data +deployments//rendered/libvirt//meta-data +deployments//rendered/libvirt//network-config +deployments//rendered/libvirt//domain.xml +molecule//deployment/.molecule-render-vars.yaml +``` + +## Important source files + +```text +Makefile +bootstrap-repo.sh +bindep.txt +pyproject.toml +ansible.cfg +ansible/playbooks/ard-render.yaml +ansible/playbooks/ard-apply.yaml +ansible/playbooks/ard-molecule-create.yaml +ansible/playbooks/ard-deploy-devstack.yaml +ansible/playbooks/ard-verify.yaml +ansible/playbooks/ard-destroy.yaml +ansible/playbooks/ard-cleanup.yaml +ansible/roles/ard_provider_common/defaults/main.yml +ansible/roles/ard_provider_common/files/presets/*.yaml +ansible/roles/ard_provider_render/tasks/main.yml +ansible/roles/ard_provider_render/templates/nodes.yaml.j2 +ansible/roles/ard_libvirt_image/tasks/main.yml +ansible/roles/ard_libvirt_network/tasks/main.yml +ansible/roles/ard_libvirt_node/tasks/main.yml +ansible/roles/ard_libvirt_node/tasks/node.yml +ansible/roles/ard_libvirt_inventory/tasks/main.yml +ansible/deploy_multinode_devstack.yaml +ansible/devstack_common.yaml +ansible/devstack_controller.yaml +ansible/devstack_compute.yaml +ansible/roles/ard_devstack_config/tasks/main.yml +scripts/ard-ssh +molecule/*/molecule.yml +``` + +## Contributor rule of thumb + +When changing ARD, identify the layer first: + +```text +render data model presets, render tasks, nodes template, generated workspace files +provider behavior ard_provider_* dispatchers and ard_libvirt_* roles +inventory contract ard_libvirt_inventory and DevStack group expectations +DevStack behavior ard_devstack_config, deploy_multinode_devstack, devstack_* roles +local UX Makefile, bootstrap-repo.sh, scripts/ard-ssh, README.md +validation Molecule scenarios and role-level Molecule tests +``` + +Then choose the cheapest validation that exercises that layer. Render changes usually deserve render-only checks across topologies. Provider changes deserve `apply` and `ping`. DevStack changes deserve a full `deploy` and `verify` on at least one representative topology.