diff --git a/.gitignore b/.gitignore index 5fc83c8..44d8d88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -.venv +.venv/ +.tmp/ clouds.yaml okd/build/ okd/output/ -pull-secret \ No newline at end of file +pull-secret +deployments/* +!deployments/.keep +molecule/*/deployment/ diff --git a/ARD_PROVIDER_DESIGN.md b/ARD_PROVIDER_DESIGN.md new file mode 100644 index 0000000..8aad67d --- /dev/null +++ b/ARD_PROVIDER_DESIGN.md @@ -0,0 +1,1779 @@ +# 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 + +compute-1: + groups: + - compute + - peers + - subnode + +compute-2: + 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 + compute-1.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: compute-1 + hostname: compute-1 + 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: compute-2 + hostname: compute-2 + 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 + compute-1.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 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 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 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 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 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 + controller_flavor: devstack-control + compute_flavor: devstack-compute + vm_preference: devstack + node_pools: + compute: + 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 + provider_network: ard-storage + devstack: + common: + enable_ceph: true + controller: + controller_localrc_extra: + DEBUG_LIBVIRT_COREDUMPS: true + compute: + compute_localrc_extra: {} + +ard_render_node_overrides: + compute-2: + image: ubuntu-24.04 + flavor: gpu-compute + profiles: + - 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: + +```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: compute-1 + 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/compute-1.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 + 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. + +### 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 + compute-1.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 `compute-1`: + +```yaml +inventory_hostname: compute-1 +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 `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: + +```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 + compute-1.qcow2 + compute-1-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/compute-1.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. 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 # includes provisioner.ard + create.yml # reads provisioner.ard, renders, applies + converge.yml + verify.yml + destroy.yml + deployment/ # generated/ignored +``` + +Example `molecule.yml` fragment: + +```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 +``` + +`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 + +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 + compute-1. +- Then controller + compute-1 + compute-2. +- 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`, `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. + +## 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/Makefile b/Makefile new file mode 100644 index 0000000..7b7d331 --- /dev/null +++ b/Makefile @@ -0,0 +1,121 @@ +.DEFAULT_GOAL := default + +.PHONY: \ + 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 +ARD_DEPLOYMENT ?= devstack-1 +ARD_DEPLOYMENTS_DIR ?= $(CURDIR)/deployments +ARD_DEPLOYMENT_DIR ?= $(ARD_DEPLOYMENTS_DIR)/$(ARD_DEPLOYMENT) +ARD_TOPOLOGY ?= one-controller-one-compute +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_deployment_dir=$(ARD_DEPLOYMENT_DIR) \ + $(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 = \ + 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 \ + $(ARD_RENDER_FILE_ARG) \ + -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 + +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 \ + -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))) + +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..d0249a0 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,469 @@ -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. +For contributor-oriented architecture and workflow details, read the +[ARD From Scratch developer guide](developer-guide.md). -Role Variables --------------- +## Quick start -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. +Bootstrap the repository and host dependencies: -Dependencies ------------- +```bash +./bootstrap-repo.sh +``` + +Render a named deployment workspace: + +```bash +make render ARD_DEPLOYMENT=devstack-a +``` + +Create the VMs and generated inventory: + +```bash +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=compute-1 +# equivalent explicit dry-run/print mode +make ssh ARD_DEPLOYMENT=devstack-a ARD_NODE=compute-1 ARD_SSH_PRINT=1 +``` + +Deploy DevStack: + +```bash +make deploy ARD_DEPLOYMENT=devstack-a +make verify ARD_DEPLOYMENT=devstack-a +``` + +Destroy provider resources when finished: + +```bash +make destroy ARD_DEPLOYMENT=devstack-a +``` + +Remove the local deployment workspace only when you no longer need the rendered +inputs or generated state: + +```bash +make cleanup ARD_DEPLOYMENT=devstack-a +``` + +## Local deployment workflow + +The normal local workflow is: + +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. +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/ +``` + +`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 + +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 +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_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 +ARD_EXTRA_VARS extra Ansible vars appended to provider commands +``` + +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 +``` + +## 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-compute-1 +``` + +Inventory hostnames remain logical names such as `controller`, `compute-1`, and +`compute-2`. + +## Topology presets + +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 +controller +compute-1 +``` + +`one-controller-two-compute` renders: + +```text +controller +compute-1 +compute-2 +``` + +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. -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. +## Render intent and service profiles -Example Playbook ----------------- +Render can start from a small intent file: -Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: +```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. 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 + node_pools: + compute: + 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 + 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: + - 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 +devstack +ovn +tempest +ceph +``` + +## 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 +``` + +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: - - hosts: servers - roles: - - { role: username.rolename, x: 42 } +```text +$XDG_STATE_HOME/ard/libvirt/images/ +``` -Playbook to test Microshift deployment on Fedora CoreOS -------------------------------------------------------- +or, if `XDG_STATE_HOME` is unset: +```text +~/.local/state/ard/libvirt/images/ ``` -- 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 + +`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. 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: + +```text +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 ``` -Bootstrap OpenShift crc in a vm -------------------------------- +Run a full scenario test: -This should create ``~/.ssh/id_ed25519_stack`` SSH key to login to VM as a stack user: +```bash +uv run molecule test -s default ``` -molecule destroy -s shift-stack -molecule create -s shift-stack -molecule converge -s shift-stack + +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 ``` -Verify login to CRC VM and RHCOS k8s worker node. + +Role-level Molecule scenarios live under `ansible/roles/*/molecule` and use +Podman where containers are sufficient: + +```bash +make molecule-test +make molecule-role-ensure_kustomize ``` -cd ~/.cache/molecule/ansible_role_devstack/shift-stack -vagrant ssh crc -[stack@crc ~]$ ssh -i ~/.crc/machines/crc/id_ecdsa core@`crc ip` +## Troubleshooting + +### Libvirt access + +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 + +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, 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 apply ARD_DEPLOYMENT=devstack-a ``` -Configure localhost to access deployed OpenShift crc ----------------------------------------------------- -> **NOTE**: This overwrites ``~/.kube/config`` ! +Molecule create uses the same apply playbook, so it also waits for node readiness +before converging. + +### CIDR conflicts -Install shuttle first. +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 ``` -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 + +### Cleanup after failure + +Destroy provider resources while preserving generated artifacts for inspection: + +```bash +make destroy ARD_DEPLOYMENT=devstack-a ``` -License -------- +Destroy provider resources and remove generated inventory/state/rendered files: -BSD +```bash +make destroy-clean-generated ARD_DEPLOYMENT=devstack-a +``` -Author Information ------------------- +Remove generated files without touching provider resources: -An optional section for the role authors to include contact information, or a website (HTML is not allowed). +```bash +make clean-generated ARD_DEPLOYMENT=devstack-a +``` + +If the workspace is no longer needed: + +```bash +make cleanup ARD_DEPLOYMENT=devstack-a +``` + +Generated runtime files are ignored by git: + +```text +inventory.yaml +provider-state.yaml +rendered/ +logs/ +``` 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/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/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..1b5dcd4 --- /dev/null +++ b/ansible/playbooks/ard-apply.yaml @@ -0,0 +1,40 @@ +--- +- name: Apply ARD provider deployment + hosts: localhost + connection: local + gather_facts: true + roles: + - ard_provider_common + - ard_provider_preflight + - ard_provider_image + - 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/playbooks/ard-cleanup.yaml b/ansible/playbooks/ard-cleanup.yaml new file mode 100644 index 0000000..b2c0a36 --- /dev/null +++ b/ansible/playbooks/ard-cleanup.yaml @@ -0,0 +1,8 @@ +--- +- name: Cleanup ARD deployment workspace + hosts: localhost + 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 new file mode 100644 index 0000000..a989736 --- /dev/null +++ b/ansible/playbooks/ard-deploy-devstack.yaml @@ -0,0 +1,39 @@ +--- +- name: Discover ARD provider nodes + hosts: localhost + 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 + 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/playbooks/ard-destroy.yaml b/ansible/playbooks/ard-destroy.yaml new file mode 100644 index 0000000..5ee7dce --- /dev/null +++ b/ansible/playbooks/ard-destroy.yaml @@ -0,0 +1,8 @@ +--- +- name: Destroy ARD provider deployment + hosts: localhost + connection: local + gather_facts: false + roles: + - ard_provider_common + - ard_provider_destroy 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/playbooks/ard-render.yaml b/ansible/playbooks/ard-render.yaml new file mode 100644 index 0000000..09e3b09 --- /dev/null +++ b/ansible/playbooks/ard-render.yaml @@ -0,0 +1,8 @@ +--- +- name: Render ARD deployment workspace + hosts: localhost + 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_devstack_config/tasks/main.yml b/ansible/roles/ard_devstack_config/tasks/main.yml new file mode 100644 index 0000000..2afcc88 --- /dev/null +++ b/ansible/roles/ard_devstack_config/tasks/main.yml @@ -0,0 +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 + ansible.builtin.include_vars: + file: "{{ ard_devstack_common_vars_file.stat.path }}" + when: ard_devstack_common_vars_file.stat.exists + +- name: Check deployment group DevStack var files + ansible.builtin.stat: + path: "{{ ard_deployment_dir }}/devstack/group_vars/{{ item }}.yaml" + loop: "{{ group_names }}" + 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 + 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/ard_libvirt_destroy/tasks/main.yml b/ansible/roles/ard_libvirt_destroy/tasks/main.yml new file mode 100644 index 0000000..d27f64e --- /dev/null +++ b/ansible/roles/ard_libvirt_destroy/tasks/main.yml @@ -0,0 +1,73 @@ +--- +- 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: 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 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 + +- 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..609eae3 --- /dev/null +++ b/ansible/roles/ard_libvirt_inventory/tasks/main.yml @@ -0,0 +1,109 @@ +--- +- name: Write ARD deployment inventory + copy: + dest: "{{ ard_deployment_dir }}/inventory.yaml" + mode: "0644" + content: | + --- + all: + hosts: + {% for node in ard_nodes %} + {% set management_network = (node.networks | selectattr('name', 'equalto', ard_management_network) | list | first) %} + {{ node.name }}: + 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: {{ management_network.ip }} + public_ipv4: {{ management_network.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: Add ARD nodes to active inventory + ansible.builtin.add_host: + name: "{{ item.name }}" + groups: "{{ ['ard_provider_nodes'] + item.groups }}" + 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: "{{ 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 + 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_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: {{ 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 + {% 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..afa366d --- /dev/null +++ b/ansible/roles/ard_libvirt_network/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Create rendered libvirt network directory + file: + path: "{{ ard_deployment_dir }}/rendered/libvirt/networks" + state: directory + mode: "0755" + +- name: Render libvirt network XML + template: + src: network.xml.j2 + dest: "{{ ard_deployment_dir }}/rendered/libvirt/networks/{{ item.key }}.xml" + mode: "0644" + vars: + ard_libvirt_render_network_name: "{{ item.key }}" + ard_libvirt_render_network: "{{ item.value }}" + loop: "{{ ard_networks | dict2items }}" + +- 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 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 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 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/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..7914ca8 --- /dev/null +++ b/ansible/roles/ard_libvirt_node/tasks/node.yml @@ -0,0 +1,218 @@ +--- +- 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_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" + 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" + +- 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: + {% for node_network in ard_node_networks %} + ard-{{ loop.index0 }}: + match: + macaddress: "{{ node_network.mac }}" + set-name: eth{{ loop.index0 }} + {% if node_network.ip is defined %} + addresses: + - {{ node_network.ip }}/24 + {% else %} + dhcp4: false + dhcp6: false + optional: true + {% endif %} + {% if node_network.name == ard_management_network %} + routes: + - to: default + via: {{ ard_networks[ard_management_network].gateway }} + nameservers: + addresses: + - {{ 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" + 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..f11daf0 --- /dev/null +++ b/ansible/roles/ard_libvirt_node/templates/domain.xml.j2 @@ -0,0 +1,58 @@ + + {{ ard_node_resource_name }} + {{ ard_node_flavor.memory_mb | int * 1024 }} + {{ ard_node_flavor.memory_mb | int * 1024 }} + {{ ard_node_flavor.vcpus }} + + hvm + + + + + + + + + + + + + destroy + restart + destroy + + /usr/bin/qemu-system-x86_64 + + + + + + + + + + + +{% for node_network in ard_node_networks %} + + + + + +{% endfor %} + + + + + + + + + + + + + /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..fe0873e --- /dev/null +++ b/ansible/roles/ard_libvirt_preflight/tasks/main.yml @@ -0,0 +1,35 @@ +--- +- name: Check required libvirt provider commands + ansible.builtin.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 libvirt connection + 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 + 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 + 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 + ansible.builtin.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_common/defaults/main.yml b/ansible/roles/ard_provider_common/defaults/main.yml new file mode 100644 index 0000000..2dd51a8 --- /dev/null +++ b/ansible/roles/ard_provider_common/defaults/main.yml @@ -0,0 +1,88 @@ +--- +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 +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 +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 + +# Apply should not finish until provider-created nodes are usable by Ansible. +ard_apply_wait_timeout: 900 + +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/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/networks.yaml b/ansible/roles/ard_provider_common/files/presets/networks.yaml new file mode 100644 index 0000000..81c9ca6 --- /dev/null +++ b/ansible/roles/ard_provider_common/files/presets/networks.yaml @@ -0,0 +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_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/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..28fcdcf --- /dev/null +++ b/ansible/roles/ard_provider_common/files/presets/topologies.yaml @@ -0,0 +1,48 @@ +--- +ard_render_topologies: + all-in-one: + 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_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_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_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 new file mode 100644 index 0000000..c4191d9 --- /dev/null +++ b/ansible/roles/ard_provider_destroy/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- 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" + +- 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_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..485b0e3 --- /dev/null +++ b/ansible/roles/ard_provider_render/tasks/main.yml @@ -0,0 +1,270 @@ +--- +- 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: 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 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" + +- 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 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: >- + 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: 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 ARD render networks + ansible.builtin.set_fact: + ard_render_networks_config: >- + {{ ard_render_networks | + 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: >- + {{ (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: + 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 + ansible.builtin.copy: + dest: "{{ ard_deployment_dir }}/deployment.yaml" + 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_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_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.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: + dest: "{{ ard_deployment_dir }}/devstack/common.yaml" + mode: "0644" + content: | + {{ 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" + mode: "0644" + content: | + {{ 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" + mode: "0644" + content: | + {{ ard_render_generated_header }} + {{ ard_render_devstack_compute | to_nice_yaml(indent=2, sort_keys=False) }} 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..c3cb8c5 --- /dev/null +++ b/ansible/roles/ard_provider_render/templates/nodes.yaml.j2 @@ -0,0 +1,47 @@ +{{ 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 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, {}) %} +{% 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: +{% for group in node.groups | default([]) | unique %} + - {{ group }} +{% endfor %} + 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({}) 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_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 }} +{% 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: +{% for profile in ((node.profiles | default([])) + ard_render_node_profiles) | unique %} + - {{ profile }} +{% endfor %} +{% endfor %} +{% endfor %} 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/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_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/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/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/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 </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. 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..910838b --- /dev/null +++ b/molecule/default/create.yml @@ -0,0 +1,2 @@ +--- +- import_playbook: ../../ansible/playbooks/ard-molecule-create.yaml 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..40c1d1a 100644 --- a/molecule/default/molecule.yml +++ b/molecule/default/molecule.yml @@ -1,49 +1,41 @@ --- 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' -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 + name: default 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" + 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: + 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..910838b --- /dev/null +++ b/molecule/one-controller-two-compute/create.yml @@ -0,0 +1,2 @@ +--- +- import_playbook: ../../ansible/playbooks/ard-molecule-create.yaml 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..707fc54 100644 --- a/molecule/one-controller-two-compute/molecule.yml +++ b/molecule/one-controller-two-compute/molecule.yml @@ -1,66 +1,41 @@ --- 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' -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 + name: default 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" + 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: + 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..910838b --- /dev/null +++ b/molecule/stable-2026.1/create.yml @@ -0,0 +1,2 @@ +--- +- import_playbook: ../../ansible/playbooks/ard-molecule-create.yaml 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..4b01085 --- /dev/null +++ b/molecule/stable-2026.1/molecule.yml @@ -0,0 +1,41 @@ +--- +dependency: + name: galaxy + enabled: false +driver: + name: default +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: + 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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..08a2d4b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "ard" +version = "0.1.0" +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", + "molecule>=25.0.0", + "molecule-plugins[podman]>=23.7.0", +] + +[tool.uv] +package = false 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()) 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..3a9f04b --- /dev/null +++ b/uv.lock @@ -0,0 +1,592 @@ +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" +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" }, + { name = "molecule" }, + { name = "molecule-plugins" }, + { name = "netaddr" }, + { name = "passlib" }, +] + +[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" +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 = "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" +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 = "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" +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 = "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" +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 = "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" +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" +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 = "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 = "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" +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 = "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" +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 = "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" +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" }, +] + +[[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" }, +]