From 1e9ffa7f2bf82e5738910a0e8a51f81f5b626b00 Mon Sep 17 00:00:00 2001 From: Abhishek S A Date: Tue, 28 Jul 2026 16:24:04 +0530 Subject: [PATCH 1/3] examples, input and utils update Signed-off-by: Abhishek S A --- .../examples/catalog/catalog_rhel.json | 0 .../catalog_rhel_aarch64_with_slurm_only.json | 0 .../catalog_rhel_with_nfs_provisioner.json | 0 .../examples/catalog/catalog_rhel_x86_64.json | 0 .../catalog_rhel_x86_64_with_slurm_only.json | 0 .../pxe_mapping_file.csv | 0 .../software_config.json | 0 .../catalog_rhel_json/pxe_mapping_file.csv | 0 .../catalog_rhel_json/software_config.json | 0 .../pxe_mapping_file.csv | 0 .../software_config.json | 0 .../pxe_mapping_file.csv | 0 .../software_config.json | 0 .../additional_packages.json | 0 .../pxe_mapping_file.csv | 0 .../software_config.json | 0 src/examples/inventory/bmc_inventory_file | 3 - ..._rhel_10.0_multi_arch_software_config.json | 38 -- ...late_rhel_10.0_x86-64_software_config.json | 38 -- src/input/telemetry_config.yml | 534 ------------------ src/input/telemetry_storage_config.yml | 218 ------- .../examples/additional_cloud_init.yml | 0 .../CSI_driver/secret.yaml | 0 .../CSI_driver/values.yaml | 0 .../powerscale_metrics/values.yaml | 0 .../examples/pxe_mapping_file.csv | 0 .../examples/rhel_software_config.json | 0 .../examples/slurm_conf/cgroup.conf | 0 .../examples/slurm_conf/slurm.conf | 0 .../examples/slurm_conf/slurmdbd.conf | 0 .../storage_config/storage_config.yml | 0 .../utils/log_collector/ansible.cfg | 0 .../utils/log_collector/collect.yml | 0 .../roles/log_collector/tasks/bundle.yml | 0 .../roles/log_collector/tasks/k8s_logs.yml | 0 .../roles/log_collector/tasks/main.yml | 0 .../roles/log_collector/tasks/prepare.yml | 0 .../roles/log_collector/tasks/setup.yml | 0 .../roles/log_collector/tasks/slurm_logs.yml | 0 .../log_collector/templates/metadata.json.j2 | 0 .../roles/log_collector/vars/main.yml | 0 .../tasks/generate_bmc_inventory.yml | 0 .../utils/roles/idrac_pxe_boot/tasks/main.yml | 0 .../roles/idrac_pxe_boot/tasks/pre_checks.yml | 0 .../idrac_pxe_boot/templates/bmc_inventory.j2 | 0 .../utils/roles/idrac_pxe_boot/vars/main.yml | 0 .../tasks/cloudinit_phone_home.yml | 0 .../tasks/compute_effective_inventory.yml | 0 .../tasks/gitlab_host.yml | 0 .../pxe_buildstream_manager/tasks/main.yml | 0 .../tasks/update_restart_state.yml | 0 .../tasks/upload_to_gitlab.yml | 0 .../tasks/write_node_results.yml | 0 .../pxe_buildstream_manager/vars/main.yml | 0 src/{ => utils}/rpm_build/README.md | 0 src/{ => utils}/rpm_build/build_rpm.sh | 0 .../ldms/build_ldms.rockylinux10.bash | 0 src/{ => utils}/rpm_build/ldms/configure.sh | 0 .../rpm_build/ldms/rpm_postuninstall.txt | 0 .../start_build_container.rockylinux10.bash | 0 src/{playbooks => }/utils/set_pxe_boot.yml | 0 61 files changed, 831 deletions(-) rename src/{ => build_stream}/examples/catalog/catalog_rhel.json (100%) rename src/{ => build_stream}/examples/catalog/catalog_rhel_aarch64_with_slurm_only.json (100%) rename src/{ => build_stream}/examples/catalog/catalog_rhel_with_nfs_provisioner.json (100%) rename src/{ => build_stream}/examples/catalog/catalog_rhel_x86_64.json (100%) rename src/{ => build_stream}/examples/catalog/catalog_rhel_x86_64_with_slurm_only.json (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/pxe_mapping_file.csv (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/software_config.json (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_json/pxe_mapping_file.csv (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_json/software_config.json (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/pxe_mapping_file.csv (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/software_config.json (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/pxe_mapping_file.csv (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/software_config.json (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/additional_packages.json (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/pxe_mapping_file.csv (100%) rename src/{ => build_stream}/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/software_config.json (100%) delete mode 100644 src/examples/inventory/bmc_inventory_file delete mode 100644 src/examples/software_config_template/template_rhel_10.0_multi_arch_software_config.json delete mode 100644 src/examples/software_config_template/template_rhel_10.0_x86-64_software_config.json delete mode 100644 src/input/telemetry_config.yml delete mode 100644 src/input/telemetry_storage_config.yml rename src/{ => orchestrator}/examples/additional_cloud_init.yml (100%) rename src/{ => orchestrator}/examples/powerscale_reference_files/CSI_driver/secret.yaml (100%) rename src/{ => orchestrator}/examples/powerscale_reference_files/CSI_driver/values.yaml (100%) rename src/{ => orchestrator}/examples/powerscale_reference_files/powerscale_metrics/values.yaml (100%) rename src/{ => orchestrator}/examples/pxe_mapping_file.csv (100%) rename src/{ => orchestrator}/examples/rhel_software_config.json (100%) rename src/{ => orchestrator}/examples/slurm_conf/cgroup.conf (100%) rename src/{ => orchestrator}/examples/slurm_conf/slurm.conf (100%) rename src/{ => orchestrator}/examples/slurm_conf/slurmdbd.conf (100%) rename src/{ => orchestrator}/examples/storage_config/storage_config.yml (100%) rename src/{playbooks => }/utils/log_collector/ansible.cfg (100%) rename src/{playbooks => }/utils/log_collector/collect.yml (100%) rename src/{playbooks => }/utils/log_collector/roles/log_collector/tasks/bundle.yml (100%) rename src/{playbooks => }/utils/log_collector/roles/log_collector/tasks/k8s_logs.yml (100%) rename src/{playbooks => }/utils/log_collector/roles/log_collector/tasks/main.yml (100%) rename src/{playbooks => }/utils/log_collector/roles/log_collector/tasks/prepare.yml (100%) rename src/{playbooks => }/utils/log_collector/roles/log_collector/tasks/setup.yml (100%) rename src/{playbooks => }/utils/log_collector/roles/log_collector/tasks/slurm_logs.yml (100%) rename src/{playbooks => }/utils/log_collector/roles/log_collector/templates/metadata.json.j2 (100%) rename src/{playbooks => }/utils/log_collector/roles/log_collector/vars/main.yml (100%) rename src/{playbooks => }/utils/roles/idrac_pxe_boot/tasks/generate_bmc_inventory.yml (100%) rename src/{playbooks => }/utils/roles/idrac_pxe_boot/tasks/main.yml (100%) rename src/{playbooks => }/utils/roles/idrac_pxe_boot/tasks/pre_checks.yml (100%) rename src/{playbooks => }/utils/roles/idrac_pxe_boot/templates/bmc_inventory.j2 (100%) rename src/{playbooks => }/utils/roles/idrac_pxe_boot/vars/main.yml (100%) rename src/{playbooks => }/utils/roles/pxe_buildstream_manager/tasks/cloudinit_phone_home.yml (100%) rename src/{playbooks => }/utils/roles/pxe_buildstream_manager/tasks/compute_effective_inventory.yml (100%) rename src/{playbooks => }/utils/roles/pxe_buildstream_manager/tasks/gitlab_host.yml (100%) rename src/{playbooks => }/utils/roles/pxe_buildstream_manager/tasks/main.yml (100%) rename src/{playbooks => }/utils/roles/pxe_buildstream_manager/tasks/update_restart_state.yml (100%) rename src/{playbooks => }/utils/roles/pxe_buildstream_manager/tasks/upload_to_gitlab.yml (100%) rename src/{playbooks => }/utils/roles/pxe_buildstream_manager/tasks/write_node_results.yml (100%) rename src/{playbooks => }/utils/roles/pxe_buildstream_manager/vars/main.yml (100%) rename src/{ => utils}/rpm_build/README.md (100%) rename src/{ => utils}/rpm_build/build_rpm.sh (100%) mode change 100755 => 100644 rename src/{ => utils}/rpm_build/ldms/build_ldms.rockylinux10.bash (100%) mode change 100755 => 100644 rename src/{ => utils}/rpm_build/ldms/configure.sh (100%) mode change 100755 => 100644 rename src/{ => utils}/rpm_build/ldms/rpm_postuninstall.txt (100%) rename src/{ => utils}/rpm_build/ldms/start_build_container.rockylinux10.bash (100%) mode change 100755 => 100644 rename src/{playbooks => }/utils/set_pxe_boot.yml (100%) diff --git a/src/examples/catalog/catalog_rhel.json b/src/build_stream/examples/catalog/catalog_rhel.json similarity index 100% rename from src/examples/catalog/catalog_rhel.json rename to src/build_stream/examples/catalog/catalog_rhel.json diff --git a/src/examples/catalog/catalog_rhel_aarch64_with_slurm_only.json b/src/build_stream/examples/catalog/catalog_rhel_aarch64_with_slurm_only.json similarity index 100% rename from src/examples/catalog/catalog_rhel_aarch64_with_slurm_only.json rename to src/build_stream/examples/catalog/catalog_rhel_aarch64_with_slurm_only.json diff --git a/src/examples/catalog/catalog_rhel_with_nfs_provisioner.json b/src/build_stream/examples/catalog/catalog_rhel_with_nfs_provisioner.json similarity index 100% rename from src/examples/catalog/catalog_rhel_with_nfs_provisioner.json rename to src/build_stream/examples/catalog/catalog_rhel_with_nfs_provisioner.json diff --git a/src/examples/catalog/catalog_rhel_x86_64.json b/src/build_stream/examples/catalog/catalog_rhel_x86_64.json similarity index 100% rename from src/examples/catalog/catalog_rhel_x86_64.json rename to src/build_stream/examples/catalog/catalog_rhel_x86_64.json diff --git a/src/examples/catalog/catalog_rhel_x86_64_with_slurm_only.json b/src/build_stream/examples/catalog/catalog_rhel_x86_64_with_slurm_only.json similarity index 100% rename from src/examples/catalog/catalog_rhel_x86_64_with_slurm_only.json rename to src/build_stream/examples/catalog/catalog_rhel_x86_64_with_slurm_only.json diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/pxe_mapping_file.csv b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/pxe_mapping_file.csv similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/pxe_mapping_file.csv rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/pxe_mapping_file.csv diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/software_config.json b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/software_config.json similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/software_config.json rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_aarch64_with_slurm_only_json/software_config.json diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_json/pxe_mapping_file.csv b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_json/pxe_mapping_file.csv similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_json/pxe_mapping_file.csv rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_json/pxe_mapping_file.csv diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_json/software_config.json b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_json/software_config.json similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_json/software_config.json rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_json/software_config.json diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/pxe_mapping_file.csv b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/pxe_mapping_file.csv similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/pxe_mapping_file.csv rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/pxe_mapping_file.csv diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/software_config.json b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/software_config.json similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/software_config.json rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_with_nfs_provisioner_json/software_config.json diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/pxe_mapping_file.csv b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/pxe_mapping_file.csv similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/pxe_mapping_file.csv rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/pxe_mapping_file.csv diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/software_config.json b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/software_config.json similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/software_config.json rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_json/software_config.json diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/additional_packages.json b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/additional_packages.json similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/additional_packages.json rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/additional_packages.json diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/pxe_mapping_file.csv b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/pxe_mapping_file.csv similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/pxe_mapping_file.csv rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/pxe_mapping_file.csv diff --git a/src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/software_config.json b/src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/software_config.json similarity index 100% rename from src/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/software_config.json rename to src/build_stream/examples/catalog/mapping_file_software_config/catalog_rhel_x86_64_with_slurm_only_json/software_config.json diff --git a/src/examples/inventory/bmc_inventory_file b/src/examples/inventory/bmc_inventory_file deleted file mode 100644 index 5dbe0bdb0d..0000000000 --- a/src/examples/inventory/bmc_inventory_file +++ /dev/null @@ -1,3 +0,0 @@ -[bmc] -10.x.0.2 -10.x.0.3 diff --git a/src/examples/software_config_template/template_rhel_10.0_multi_arch_software_config.json b/src/examples/software_config_template/template_rhel_10.0_multi_arch_software_config.json deleted file mode 100644 index 0be0ccce4a..0000000000 --- a/src/examples/software_config_template/template_rhel_10.0_multi_arch_software_config.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "cluster_os_type": "rhel", - "cluster_os_version": "10.0", - "repo_config": "partial", - "softwares": [ - {"name": "default_packages", "arch": ["x86_64","aarch64"]}, - {"name": "admin_debug_packages", "arch": ["x86_64","aarch64"]}, - {"name": "openldap", "arch": ["x86_64","aarch64"]}, - {"name": "slurm_custom", "arch": ["x86_64","aarch64"]}, - {"name": "service_k8s", "version": "1.35.1", "arch": ["x86_64"]}, - {"name": "ucx", "version": "1.19.0", "arch": ["x86_64","aarch64"]}, - {"name": "openmpi", "version": "5.0.8", "arch": ["x86_64","aarch64"]}, - {"name": "csi_driver_powerscale", "version":"v2.16.0", "arch": ["x86_64"]}, - {"name": "ldms", "arch": ["x86_64","aarch64"]}, - {"name": "additional_packages", "arch": ["x86_64","aarch64"]} - ], - "slurm_custom": [ - {"name": "slurm_control_node"}, - {"name": "slurm_node"}, - {"name": "login_node"}, - {"name": "login_compiler_node"} - ], - "service_k8s": [ - {"name": "service_kube_control_plane_first"}, - {"name": "service_kube_control_plane"}, - {"name": "service_kube_node"} - ], - "additional_packages":[ - {"name": "service_kube_control_plane_first"}, - {"name": "service_kube_control_plane"}, - {"name": "service_kube_node"}, - {"name": "slurm_control_node"}, - {"name": "slurm_node"}, - {"name": "login_node"}, - {"name": "login_compiler_node"}, - {"name": "os"} - ] -} diff --git a/src/examples/software_config_template/template_rhel_10.0_x86-64_software_config.json b/src/examples/software_config_template/template_rhel_10.0_x86-64_software_config.json deleted file mode 100644 index 2bd29156c3..0000000000 --- a/src/examples/software_config_template/template_rhel_10.0_x86-64_software_config.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "cluster_os_type": "rhel", - "cluster_os_version": "10.0", - "repo_config": "partial", - "softwares": [ - {"name": "default_packages", "arch": ["x86_64"]}, - {"name": "admin_debug_packages", "arch": ["x86_64"]}, - {"name": "openldap", "arch": ["x86_64"]}, - {"name": "slurm_custom", "arch": ["x86_64"]}, - {"name": "service_k8s", "version": "1.35.1", "arch": ["x86_64"]}, - {"name": "ucx", "version": "1.19.0", "arch": ["x86_64"]}, - {"name": "openmpi", "version": "5.0.8", "arch": ["x86_64"]}, - {"name": "csi_driver_powerscale", "version":"v2.16.0", "arch": ["x86_64"]}, - {"name": "ldms", "arch": ["x86_64"]}, - {"name": "additional_packages", "arch": ["x86_64"]} - ], - "slurm_custom": [ - {"name": "slurm_control_node"}, - {"name": "slurm_node"}, - {"name": "login_node"}, - {"name": "login_compiler_node"} - ], - "service_k8s": [ - {"name": "service_kube_control_plane_first"}, - {"name": "service_kube_control_plane"}, - {"name": "service_kube_node"} - ], - "additional_packages":[ - {"name": "service_kube_control_plane_first"}, - {"name": "service_kube_control_plane"}, - {"name": "service_kube_node"}, - {"name": "slurm_control_node"}, - {"name": "slurm_node"}, - {"name": "login_node"}, - {"name": "login_compiler_node"}, - {"name": "os"} - ] -} diff --git a/src/input/telemetry_config.yml b/src/input/telemetry_config.yml deleted file mode 100644 index 765b227786..0000000000 --- a/src/input/telemetry_config.yml +++ /dev/null @@ -1,534 +0,0 @@ -# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. ---- - -# *********************************************************************** -# DO NOT REMOVE OR COMMENT OUT ANY LINES IN THIS FILE. -# SIMPLY APPEND THE REQUIRED VALUES AGAINST THE PARAMETER OF YOUR CHOICE. -# *********************************************************************** - -# ============================================================================ -# TELEMETRY CONFIGURATION OVERVIEW -# ============================================================================ -# This file configures telemetry data collection, routing, and storage for Dell Omnia. -# -# ARCHITECTURE: -# SOURCES (Collectors) → BRIDGES (Vector) → SINKS (Storage) -# -# SECTIONS: -# 1. Telemetry Sources : Data collectors (iDRAC, LDMS, DCGM, PowerScale, UFM, VAST) -# 2. Telemetry Bridges : Data routers (Vector pipelines) -# 3. Telemetry Sinks : Storage backends (victoria_metrics, victoria_logs, Kafka) -# 4. Source Configurations: Detailed settings per source (PowerScale, LDMS, UFM, VAST) -# -# ============================================================================ -# STORAGE REQUIREMENTS SUMMARY -# ============================================================================ -# -# victoria_metrics STORAGE: -# ┌─────────────────┬──────────────────┬─────────────────┬──────────────────┐ -# │ Deployment Mode │ Per-Pod Storage │ Number of Pods │ Total Storage │ -# ├─────────────────┼──────────────────┼─────────────────┼──────────────────┤ -# │ Cluster │ persistence_size │ 3 vmstorage │ 3× storage │ -# └─────────────────┴──────────────────┴─────────────────┴──────────────────┘ -# Example: 8Gi per pod → Cluster: 24Gi total -# -# victoria_logs STORAGE: -# ┌─────────────────┬──────────────────┬─────────────────┬──────────────────┐ -# │ Component │ Per-Pod Storage │ Number of Pods │ Total Storage │ -# ├─────────────────┼──────────────────┼─────────────────┼──────────────────┤ -# │ vlstorage │ storage_size │ 3 pods │ 3× storage │ -# ├─────────────────┼──────────────────┼─────────────────┼──────────────────┤ -# │ VLAgent buffer │ 5Gi (fixed) │ 1 pod │ 5Gi │ -# └─────────────────┴──────────────────┴─────────────────┴──────────────────┘ -# Example: 8Gi × 3 vlstorage = 24Gi + 5Gi VLAgent = 29Gi total -# -# KAFKA STORAGE: -# ┌─────────────────┬──────────────────┬─────────────────┬──────────────────┐ -# │ Component │ Per-Pod Storage │ Number of Pods │ Total Storage │ -# ├─────────────────┼──────────────────┼─────────────────┼──────────────────┤ -# │ Kafka Broker │ persistence_size │ 3 pods │ 3× storage │ -# ├─────────────────┼──────────────────┼─────────────────┼──────────────────┤ -# │ Kafka Controller│ persistence_size │ 3 pods │ 3× storage │ -# ├─────────────────┼──────────────────┼─────────────────┼──────────────────┤ -# │ TOTAL KAFKA │ persistence_size │ 6 pods │ 6× storage │ -# └─────────────────┴──────────────────┴─────────────────┴──────────────────┘ -# Example: 8Gi per pod → 48Gi total Kafka storage -# -# COMBINED STORAGE EXAMPLES: -# Default (8Gi each): victoria_metrics (24Gi) + victoria_logs (29Gi) + Kafka (48Gi) = 101Gi total -# ============================================================================ - -# ============================================================================ -# TELEMETRY SOURCES (Data Collectors) -# ============================================================================ -# Each source can be independently enabled/disabled. -# Sources produce telemetry data that flows through bridges to sinks. -# -# Supported sources: idrac, ldms, dcgm, powerscale, ufm, vast, ome - -telemetry_sources: - - # -------------------------------------------------------------------------- - # iDRAC — Hardware metrics from Dell PowerEdge servers - # -------------------------------------------------------------------------- - # Collects: temperature, power, fan speed, storage health, CPU/memory errors - # Requires: iDRAC-enabled Dell PowerEdge servers in inventory - # Data path: iDRAC Receiver → ActiveMQ → KafkaPump → Kafka 'idrac' topic - # iDRAC Receiver → ActiveMQ → VictoriaPump → vmagent → victoria_metrics - idrac: - # Enable or disable iDRAC metrics collection - # Default: true - metrics_enabled: true - - # Collection targets define where iDRAC data is sent BEFORE Vector processing - # Supported values: "victoria_metrics", "kafka" - # Multiple targets: ["victoria_metrics", "kafka"] - # Default: ["victoria_metrics", "kafka"] - collection_targets: - - "victoria_metrics" - - "kafka" - - # -------------------------------------------------------------------------- - # LDMS — Lightweight Distributed Metric Service - # -------------------------------------------------------------------------- - # Collects: CPU, memory, network, disk metrics from compute nodes - # Requires: LDMS software in software_config.json - # Data path: LDMS samplers → LDMS aggregator → store_avro_kafka → Kafka 'ldms' topic - ldms: - # Enable or disable LDMS metrics collection - # Default: true - metrics_enabled: true - - # LDMS only supports Kafka collection (no direct victoria_metrics path) - # Vector-LDMS bridge consumes from Kafka and routes to victoria_metrics - collection_targets: - - "kafka" - - # -------------------------------------------------------------------------- - # DCGM — NVIDIA Data Center GPU Manager - # -------------------------------------------------------------------------- - # Collects: GPU temperature, utilization, memory, ECC errors, power - # Requires: NVIDIA GPU driver installed on compute nodes - dcgm: - # Enable or disable DCGM metrics collection - # Default: true - metrics_enabled: true - - # -------------------------------------------------------------------------- - # PowerScale — Dell PowerScale (OneFS) storage telemetry - # -------------------------------------------------------------------------- - # Collects: Storage metrics from Dell PowerScale clusters - # Requires: CSM Observability (Karavi) values file configured - # Data path: CSM Metrics PowerScale → OTEL Collector → vmagent(shared) → victoria_metrics - powerscale: - # Enable or disable PowerScale metrics collection - # Default: true - metrics_enabled: true - - # Enable or disable PowerScale logs collection - # Default: true - logs_enabled: true - - # PowerScale uses vmagent(shared) (no Kafka, no Vector) - collection_targets: - - "victoria_metrics" - - "victoria_logs" - - # -------------------------------------------------------------------------- - # UFM — NVIDIA UFM InfiniBand Fabric Telemetry - # -------------------------------------------------------------------------- - # Collects: IB port state, transmit/receive data, error counters, fabric topology - # Requires: NVIDIA UFM appliance with Prometheus exporter enabled (port 9090) - # Data path: UFM Prometheus Exporter → vmagent(shared) → victoria_metrics - # - # NOTE: Omnia does NOT deploy UFM itself. Omnia configures the existing - # vmagent to scrape the UFM Prometheus endpoint for IB fabric metrics. - ufm: - # Enable or disable UFM InfiniBand metrics collection - # Default: false - metrics_enabled: false - - # Enable or disable UFM syslog logs collection - # Default: false - logs_enabled: false - - # UFM uses vmagent(shared) for metrics and VLAgent for logs - collection_targets: - - "victoria_metrics" - - "victoria_logs" - - # -------------------------------------------------------------------------- - # VAST — VAST Data Storage Telemetry - # -------------------------------------------------------------------------- - # Collects: Storage performance metrics from VAST Data clusters via Prometheus API - # Requires: VAST Data cluster with Prometheus metrics endpoint enabled - # Data path: VAST Prometheus API → vmagent(shared) → victoria_metrics - # - # NOTE: Omnia does NOT deploy VAST itself. Omnia configures the existing - # vmagent to scrape the VAST Prometheus endpoint for storage metrics. - vast: - # Enable or disable VAST storage metrics collection - # Default: false - metrics_enabled: false - - # Enable or disable VAST syslog logs collection - # Default: false - logs_enabled: false - - # VAST uses vmagent(shared) for metrics and VLAgent for logs - collection_targets: - - "victoria_metrics" - - "victoria_logs" - - # -------------------------------------------------------------------------- - # OME — Dell OpenManage Enterprise Telemetry - # -------------------------------------------------------------------------- - # Collects: Server inventory, health, alerts, and firmware metrics from OME - # Requires: Dell OpenManage Enterprise configured to publish to Kafka - # Data path: OME → Kafka 'ome.*' topics → Vector-OME → victoria_metrics/victoria_logs - # - # NOTE: OME does NOT push directly to VictoriaMetrics. OME publishes data to - # Kafka topics, and the Vector-OME bridge consumes from Kafka and routes to - # Victoria sinks. If OME source is disabled, Vector-OME bridge must also be disabled. - ome: - # Enable or disable OME metrics collection - # Default: true - metrics_enabled: true - - # Enable or disable OME logs collection - # Default: true - logs_enabled: true - - # OME only supports Kafka collection (no direct victoria_metrics/victoria_logs path) - # Vector-OME bridge consumes from Kafka and routes to victoria_metrics/victoria_logs - collection_targets: - - "kafka" - - -# ============================================================================ -# TELEMETRY BRIDGES (Data Routers) -# ============================================================================ -# Bridges route data from Kafka topics to Victoria sinks. -# Vector is the primary bridge technology, consuming from Kafka and producing -# to victoria_metrics (metrics) and victoria_logs (logs/events). -# -# ARCHITECTURE: -# Kafka topics → Vector pods → vmagent-vector/vlagent-vector → Victoria sinks -# - -telemetry_bridges: - - # -------------------------------------------------------------------------- - # Vector-LDMS — Kafka-to-victoria_metrics bridge for LDMS metrics - # -------------------------------------------------------------------------- - # Purpose: Consume LDMS metrics from Kafka 'ldms' topic, transform NERSC - # schema to Prometheus format, and write to victoria_metrics - # Data flow: Kafka 'ldms' topic → Vector-LDMS → vmagent-vector → victoria_metrics - vector_ldms: - # Enable or disable Vector-LDMS bridge - # Requires: telemetry_sources.ldms.enabled = true - # Default: true - metrics_enabled: true - - # -------------------------------------------------------------------------- - # Vector-OME — Kafka-to-Victoria bridge for OME metrics and logs - # -------------------------------------------------------------------------- - # Purpose: Consume OME data from Kafka 'ome.*' topics and route to victoria_metrics/victoria_logs - # Data flow: Kafka 'ome.*' topics → Vector-OME → vmagent-vector (metrics) / vlagent-vector (logs) - vector_ome: - # Enable or disable Vector-OME metrics routing - # Requires: OME to be configured with kafka - # Default: true - metrics_enabled: true - - # Enable or disable Vector-OME logs routing - # Default: true - logs_enabled: true - - # Identifier used by Vector-OME for topic identification and routing. - # Default: "ome" — internally used to match topics with the prefix (e.g., "^ome\\..*$") - # Change only if your OME Kafka topics use a different prefix. - ome_identifier: "ome" - -# ============================================================================ -# TELEMETRY SINKS (Storage Backends) -# ============================================================================ -# Sinks are auto-enabled when at least one source targets them. -# Explicit 'enabled: false' here overrides source routing (disables the sink). - -telemetry_sinks: - - # -------------------------------------------------------------------------- - # victoria_metrics — Time-series database for metrics - # -------------------------------------------------------------------------- - victoria_metrics: - # Storage per vmstorage pod PVC - # Cluster: total = persistence_size × 3 vmstorage pods - # Accepted values: in the form of "X[Ki|Mi|Gi|Ti|Pi|Ei]" - # Default: 8Gi (results in 24Gi total storage for cluster mode) - persistence_size: "8Gi" - - # Metric retention period in hours - # Default: 168 (7 days) - retention_period: 168 - - # Additional remote write endpoints for metrics (optional) - # Metrics will be sent to the Omnia-managed VictoriaMetrics AND to these endpoints. - # Each entry requires a 'url' field (must start with http:// or https://). - # Set tls_insecure_skip_verify: true to skip TLS certificate verification. - # Default: [] (only Omnia VictoriaMetrics receives metrics) - # Example: - # additional_metric_remote_write_endpoints: - # - url: https://external-metrics-server:8480/insert/0/prometheus/api/v1/write - # tls_insecure_skip_verify: false - additional_metric_remote_write_endpoints: [] - - # -------------------------------------------------------------------------- - # victoria_logs — Centralized log storage and querying - # -------------------------------------------------------------------------- - # Co-deployed with victoria_metrics when victoria_metrics sink is active. - # Provides structured log collection via vlagent-vector (JSON Lines receiver). - victoria_logs: - # Storage per vlstorage pod PVC - # Total = storage_size × 3 vlstorage pods - # Accepted values: in the form of "X[Ki|Mi|Gi|Ti|Pi|Ei]" - # Default: 8Gi (results in 24Gi total storage) - storage_size: "8Gi" - - # Log retention period in hours - # Default: 168 (7 days) - retention_period: 168 - - # Additional remote write endpoints for logs (optional) - # Logs will be sent to the Omnia-managed VictoriaLogs AND to these endpoints. - # Each entry requires a 'url' field (must start with http:// or https://). - # Set tls_insecure_skip_verify: true to skip TLS certificate verification. - # Default: [] (only Omnia VictoriaLogs receives logs) - # Example: - # additional_log_write_endpoints: - # - url: https://external-logs-server:9481/internal/insert - # tls_insecure_skip_verify: false - additional_log_write_endpoints: [] - - # -------------------------------------------------------------------------- - # Kafka — Distributed streaming platform - # -------------------------------------------------------------------------- - kafka: - # Storage per Kafka pod PVC - # Total = persistence_size × 6 pods (3 brokers + 3 controllers) - # Accepted values: in the form of "X[Ki|Mi|Gi|Ti|Pi|Ei]" - # Default: 8Gi (results in 48Gi total storage) - persistence_size: "8Gi" - - # Log retention - # Default: 168 (7 days) - log_retention_hours: 168 - - # Maximum size of Kafka logs (in bytes) before deletion - # Default: -1 (unlimited) - log_retention_bytes: -1 - - # Maximum size of Kafka log segments (in bytes) - # Default: 1073741824 (1 GB) - log_segment_bytes: 1073741824 - - # Topic partitions per source (auto-created for enabled sources targeting kafka) - # Only sources with kafka in collection_targets get topics. - # Topic name = source name (e.g., "idrac", "ldms") - topic_partitions: - idrac: 1 - ldms: 2 - -# ============================================================================ -# SOURCE-SPECIFIC CONFIGURATIONS -# ============================================================================ -# Detailed configurations for each telemetry source. -# Only relevant when the corresponding source is enabled above. - -# -------------------------------------------------------------------------- -# iDRAC Telemetry Storage Configuration -# -------------------------------------------------------------------------- -# Storage configuration for iDRAC telemetry MySQL database -idrac_telemetry_configurations: - mysqldb_storage: "1Gi" # MySQL database storage for iDRAC telemetry - -# -------------------------------------------------------------------------- -# LDMS Configuration -# -------------------------------------------------------------------------- -ldms_configurations: - # Aggregator port on service K8s cluster (valid: 6001-6100) - agg_port: 6001 - - # Store daemon port (valid: 6001-6100) - store_port: 6001 - - # Sampler port on compute nodes (valid: 10001-10100) - sampler_port: 10001 - - # Sampler plugins — which metrics to collect from compute nodes - # Parameters: - # - plugin_name: Name of the LDMS sampler plugin - # - config_parameters: Plugin-specific configuration (as a single string) - # - activation_parameters: Collection schedule in MICROSECONDS - # Format: "interval= offset=" - # Example: "interval=30000000" (30 seconds) - sampler_plugins: - - plugin_name: meminfo - config_parameters: "" - activation_parameters: "interval=30000000" - - - plugin_name: procstat2 - config_parameters: "" - activation_parameters: "interval=30000000" - - - plugin_name: vmstat - config_parameters: "" - activation_parameters: "interval=30000000" - - - plugin_name: loadavg - config_parameters: "" - activation_parameters: "interval=30000000" - - - plugin_name: procnetdev2 - config_parameters: "" - activation_parameters: "interval=30000000 offset=0" - -# -------------------------------------------------------------------------- -# PowerScale Telemetry Configuration -# -------------------------------------------------------------------------- -# PowerScale telemetry collects storage metrics from Dell PowerScale (OneFS) -# clusters using the CSM (Container Storage Modules) Metrics PowerScale exporter. -# -# DATA PIPELINE: -# CSM Metrics PowerScale → OTEL Collector → vmagent(shared) → victoria_metrics -# -# NOTE: PowerScale does NOT use Vector bridges. It uses the shared vmagent instance -# that writes directly to victoria_metrics. -powerscale_configurations: - # PVC size for OTEL Collector metric batching and buffering - # Accepted values: in the form of "X[Ki|Mi|Gi|Ti|Pi|Ei]" - # Default: "5Gi" - otel_collector_storage_size: "5Gi" - - # Path to the CSM Observability (Karavi Observability) values.yaml file - # Required when powerscale_configurations.powerscale_telemetry_support: true - # Reference: https://raw.githubusercontent.com/dell/helm-charts/refs/heads/release-v1.17.1/charts/karavi-observability/values.yaml - csm_observability_values_file_path: "" - -# -------------------------------------------------------------------------- -# UFM Telemetry Configuration -# -------------------------------------------------------------------------- -# UFM telemetry collects InfiniBand fabric metrics from NVIDIA UFM appliances -# using Prometheus scraping. -# -# DATA PIPELINE: -# UFM Prometheus Exporter → vmagent(shared) → victoria_metrics -# -# NOTE: UFM does NOT use Vector bridges. It uses the shared vmagent instance -# for metrics collection. -ufm_configuration: - # UFM appliance IP address or hostname - # Required when telemetry_sources.ufm.metrics_enabled is true - # Example: "172.20.44.180" or "ufm.example.com" - ufm_endpoint: "" - - # UFM Prometheus exporter port - # Default: 9001 (UFM default Prometheus port) - ufm_metrics_port: 9001 - - # Prometheus scrape interval for UFM metrics - # Accepted values: Prometheus duration format (e.g., "15s", "30s", "1m") - # Default: "30s" - scrape_interval: "30s" - - # Prometheus scrape timeout (must be <= scrape_interval) - # Accepted values: Prometheus duration format (e.g., "10s", "15s") - # Default: "15s" - scrape_timeout: "15s" - - # TLS mode for connecting to UFM Prometheus endpoint - # Accepted values: "self_signed", "ca_signed" - # - self_signed: Skip TLS verification (insecure_skip_verify=true) - # - ca_signed: Use CA certificate for TLS verification - # Default: "self_signed" - tls_mode: "self_signed" - - # Path to CA certificate file for UFM TLS verification - # Required when tls_mode is "ca_signed" - # Must be a valid PEM-format certificate file - # Default: "" (empty — not used when tls_mode is "self_signed") - ufm_ca_cert_path: "" - - # Authentication mode for UFM Prometheus endpoint - # Accepted values: "basic", "none" - # - basic: Use ufm_username/ufm_password from omnia_config_credentials.yml - # - none: No authentication (UFM endpoint is open) - # Default: "basic" - auth_mode: "basic" - -# -------------------------------------------------------------------------- -# VAST Telemetry Configuration -# -------------------------------------------------------------------------- -# VAST telemetry collects storage metrics from VAST Data clusters -# using Prometheus scraping via the VAST Prometheus metrics API. -# -# DATA PIPELINE: -# VAST Prometheus API (HTTPS) → vmagent(shared) → victoria_metrics -# -# NOTE: VAST does NOT use Vector bridges. It uses the shared vmagent instance -# for metrics collection. -vast_configuration: - # VAST cluster IP address or hostname - # Required when telemetry_sources.vast.metrics_enabled is true - # Example: "172.18.44.171" or "vast.example.com" - vast_endpoint: "" - - # VAST Prometheus metrics port - # Default: 443 (VAST default HTTPS port) - vast_metrics_port: 443 - - # Prometheus metrics path for VAST API - # Default: "/api/prometheusmetrics/all" - metrics_path: "/api/prometheusmetrics/all" - - # Prometheus scrape interval for VAST metrics - # Accepted values: Prometheus duration format (e.g., "15s", "30s", "1m") - # Default: "30s" - scrape_interval: "30s" - - # Prometheus scrape timeout (must be <= scrape_interval) - # Accepted values: Prometheus duration format (e.g., "10s", "15s") - # Default: "15s" - scrape_timeout: "15s" - - # TLS mode for connecting to VAST Prometheus endpoint - # Accepted values: "self_signed", "ca_signed" - # - self_signed: Skip TLS verification (insecure_skip_verify=true) - # - ca_signed: Use CA certificate for TLS verification - # Default: "self_signed" - tls_mode: "self_signed" - - # Path to CA certificate file for VAST TLS verification - # Required when tls_mode is "ca_signed" - # Must be a valid PEM-format certificate file - # Default: "" (empty — not used when tls_mode is "self_signed") - vast_ca_cert_path: "" - - # Authentication mode for VAST Prometheus endpoint - # Accepted values: "basic", "none" - # - basic: Use vast_username/vast_password from omnia_config_credentials.yml - # - none: No authentication (VAST endpoint is open) - # Default: "basic" - auth_mode: "basic" diff --git a/src/input/telemetry_storage_config.yml b/src/input/telemetry_storage_config.yml deleted file mode 100644 index c80dbdde65..0000000000 --- a/src/input/telemetry_storage_config.yml +++ /dev/null @@ -1,218 +0,0 @@ -# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. ---- -# Telemetry Storage Configuration -# This file contains resource and replica configurations for VictoriaMetrics, VictoriaLogs, and Vector components - -# VictoriaMetrics Cluster -victoria_cluster_storage: - vmstorage: - replicas: 3 - resources: - requests: - memory: "1Gi" - cpu: "250m" - limits: - memory: "2Gi" - cpu: "1000m" - vminsert: - replicas: 2 - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" - vmselect: - replicas: 2 - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" - vmagent: - replicas: 2 - resources: - requests: - memory: "128Mi" - cpu: "50m" - limits: - memory: "512Mi" - cpu: "250m" - -# VictoriaLogs Cluster -victoria_logs_cluster_storage: - vlstorage: - replicas: 3 - resources: - requests: - memory: "512Mi" - cpu: "100m" - limits: - memory: "1Gi" - cpu: "500m" - vlinsert: - replicas: 2 - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" - vlselect: - replicas: 2 - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" - vlagent: - replicas: 2 - pvc_size: "5Gi" - resources: - requests: - memory: "64Mi" - cpu: "25m" - limits: - memory: "256Mi" - cpu: "100m" - -# Vector components -vector_storage: - ldms: - replicas: 2 - resources: - requests: - memory: "128Mi" - cpu: "50m" - limits: - memory: "256Mi" - cpu: "250m" - ome: - replicas: 2 - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" - vlagent_vector: - replicas: 2 - pvc_size: "5Gi" - resources: - requests: - memory: "128Mi" - cpu: "50m" - limits: - memory: "256Mi" - cpu: "250m" - vmagent_vector: - replicas: 2 - pvc_size: "5Gi" - resources: - requests: - memory: "128Mi" - cpu: "50m" - limits: - memory: "256Mi" - cpu: "250m" - -# CSI Volume Exporter -csi_volume_exporter_storage: - resources: - requests: - cpu: "50m" - memory: "64Mi" - limits: - cpu: "200m" - memory: "256Mi" - -# CSM Metrics PowerScale resource limits -csm_metrics_powerscale_storage: - requests: - cpu: "100m" - memory: "128Mi" - limits: - cpu: "500m" - memory: "512Mi" - -# iDRAC Telemetry Container resources -idrac_telemetry_storage: - mysqldb: - resources: - requests: - cpu: "100m" - memory: "256Mi" - limits: - cpu: "500m" - memory: "512Mi" - activemq: - resources: - requests: - cpu: "100m" - memory: "512Mi" - limits: - cpu: "500m" - memory: "1536Mi" - receiver: - resources: - requests: - cpu: "100m" - memory: "128Mi" - limits: - cpu: "500m" - memory: "256Mi" - kafka_pump: - resources: - requests: - cpu: "50m" - memory: "128Mi" - limits: - cpu: "200m" - memory: "512Mi" - victoria_pump: - resources: - requests: - cpu: "50m" - memory: "128Mi" - limits: - cpu: "200m" - memory: "512Mi" - -# Kafka Storage resources -kafka_storage: - kafka: - resources: - requests: - memory: "512Mi" - cpu: "200m" - limits: - memory: "1Gi" - cpu: "1000m" - entity_operator: - user_operator: - resources: - requests: - memory: "512Mi" - cpu: "200m" - limits: - memory: "512Mi" - cpu: "1000m" diff --git a/src/examples/additional_cloud_init.yml b/src/orchestrator/examples/additional_cloud_init.yml similarity index 100% rename from src/examples/additional_cloud_init.yml rename to src/orchestrator/examples/additional_cloud_init.yml diff --git a/src/examples/powerscale_reference_files/CSI_driver/secret.yaml b/src/orchestrator/examples/powerscale_reference_files/CSI_driver/secret.yaml similarity index 100% rename from src/examples/powerscale_reference_files/CSI_driver/secret.yaml rename to src/orchestrator/examples/powerscale_reference_files/CSI_driver/secret.yaml diff --git a/src/examples/powerscale_reference_files/CSI_driver/values.yaml b/src/orchestrator/examples/powerscale_reference_files/CSI_driver/values.yaml similarity index 100% rename from src/examples/powerscale_reference_files/CSI_driver/values.yaml rename to src/orchestrator/examples/powerscale_reference_files/CSI_driver/values.yaml diff --git a/src/examples/powerscale_reference_files/powerscale_metrics/values.yaml b/src/orchestrator/examples/powerscale_reference_files/powerscale_metrics/values.yaml similarity index 100% rename from src/examples/powerscale_reference_files/powerscale_metrics/values.yaml rename to src/orchestrator/examples/powerscale_reference_files/powerscale_metrics/values.yaml diff --git a/src/examples/pxe_mapping_file.csv b/src/orchestrator/examples/pxe_mapping_file.csv similarity index 100% rename from src/examples/pxe_mapping_file.csv rename to src/orchestrator/examples/pxe_mapping_file.csv diff --git a/src/examples/rhel_software_config.json b/src/orchestrator/examples/rhel_software_config.json similarity index 100% rename from src/examples/rhel_software_config.json rename to src/orchestrator/examples/rhel_software_config.json diff --git a/src/examples/slurm_conf/cgroup.conf b/src/orchestrator/examples/slurm_conf/cgroup.conf similarity index 100% rename from src/examples/slurm_conf/cgroup.conf rename to src/orchestrator/examples/slurm_conf/cgroup.conf diff --git a/src/examples/slurm_conf/slurm.conf b/src/orchestrator/examples/slurm_conf/slurm.conf similarity index 100% rename from src/examples/slurm_conf/slurm.conf rename to src/orchestrator/examples/slurm_conf/slurm.conf diff --git a/src/examples/slurm_conf/slurmdbd.conf b/src/orchestrator/examples/slurm_conf/slurmdbd.conf similarity index 100% rename from src/examples/slurm_conf/slurmdbd.conf rename to src/orchestrator/examples/slurm_conf/slurmdbd.conf diff --git a/src/examples/storage_config/storage_config.yml b/src/orchestrator/examples/storage_config/storage_config.yml similarity index 100% rename from src/examples/storage_config/storage_config.yml rename to src/orchestrator/examples/storage_config/storage_config.yml diff --git a/src/playbooks/utils/log_collector/ansible.cfg b/src/utils/log_collector/ansible.cfg similarity index 100% rename from src/playbooks/utils/log_collector/ansible.cfg rename to src/utils/log_collector/ansible.cfg diff --git a/src/playbooks/utils/log_collector/collect.yml b/src/utils/log_collector/collect.yml similarity index 100% rename from src/playbooks/utils/log_collector/collect.yml rename to src/utils/log_collector/collect.yml diff --git a/src/playbooks/utils/log_collector/roles/log_collector/tasks/bundle.yml b/src/utils/log_collector/roles/log_collector/tasks/bundle.yml similarity index 100% rename from src/playbooks/utils/log_collector/roles/log_collector/tasks/bundle.yml rename to src/utils/log_collector/roles/log_collector/tasks/bundle.yml diff --git a/src/playbooks/utils/log_collector/roles/log_collector/tasks/k8s_logs.yml b/src/utils/log_collector/roles/log_collector/tasks/k8s_logs.yml similarity index 100% rename from src/playbooks/utils/log_collector/roles/log_collector/tasks/k8s_logs.yml rename to src/utils/log_collector/roles/log_collector/tasks/k8s_logs.yml diff --git a/src/playbooks/utils/log_collector/roles/log_collector/tasks/main.yml b/src/utils/log_collector/roles/log_collector/tasks/main.yml similarity index 100% rename from src/playbooks/utils/log_collector/roles/log_collector/tasks/main.yml rename to src/utils/log_collector/roles/log_collector/tasks/main.yml diff --git a/src/playbooks/utils/log_collector/roles/log_collector/tasks/prepare.yml b/src/utils/log_collector/roles/log_collector/tasks/prepare.yml similarity index 100% rename from src/playbooks/utils/log_collector/roles/log_collector/tasks/prepare.yml rename to src/utils/log_collector/roles/log_collector/tasks/prepare.yml diff --git a/src/playbooks/utils/log_collector/roles/log_collector/tasks/setup.yml b/src/utils/log_collector/roles/log_collector/tasks/setup.yml similarity index 100% rename from src/playbooks/utils/log_collector/roles/log_collector/tasks/setup.yml rename to src/utils/log_collector/roles/log_collector/tasks/setup.yml diff --git a/src/playbooks/utils/log_collector/roles/log_collector/tasks/slurm_logs.yml b/src/utils/log_collector/roles/log_collector/tasks/slurm_logs.yml similarity index 100% rename from src/playbooks/utils/log_collector/roles/log_collector/tasks/slurm_logs.yml rename to src/utils/log_collector/roles/log_collector/tasks/slurm_logs.yml diff --git a/src/playbooks/utils/log_collector/roles/log_collector/templates/metadata.json.j2 b/src/utils/log_collector/roles/log_collector/templates/metadata.json.j2 similarity index 100% rename from src/playbooks/utils/log_collector/roles/log_collector/templates/metadata.json.j2 rename to src/utils/log_collector/roles/log_collector/templates/metadata.json.j2 diff --git a/src/playbooks/utils/log_collector/roles/log_collector/vars/main.yml b/src/utils/log_collector/roles/log_collector/vars/main.yml similarity index 100% rename from src/playbooks/utils/log_collector/roles/log_collector/vars/main.yml rename to src/utils/log_collector/roles/log_collector/vars/main.yml diff --git a/src/playbooks/utils/roles/idrac_pxe_boot/tasks/generate_bmc_inventory.yml b/src/utils/roles/idrac_pxe_boot/tasks/generate_bmc_inventory.yml similarity index 100% rename from src/playbooks/utils/roles/idrac_pxe_boot/tasks/generate_bmc_inventory.yml rename to src/utils/roles/idrac_pxe_boot/tasks/generate_bmc_inventory.yml diff --git a/src/playbooks/utils/roles/idrac_pxe_boot/tasks/main.yml b/src/utils/roles/idrac_pxe_boot/tasks/main.yml similarity index 100% rename from src/playbooks/utils/roles/idrac_pxe_boot/tasks/main.yml rename to src/utils/roles/idrac_pxe_boot/tasks/main.yml diff --git a/src/playbooks/utils/roles/idrac_pxe_boot/tasks/pre_checks.yml b/src/utils/roles/idrac_pxe_boot/tasks/pre_checks.yml similarity index 100% rename from src/playbooks/utils/roles/idrac_pxe_boot/tasks/pre_checks.yml rename to src/utils/roles/idrac_pxe_boot/tasks/pre_checks.yml diff --git a/src/playbooks/utils/roles/idrac_pxe_boot/templates/bmc_inventory.j2 b/src/utils/roles/idrac_pxe_boot/templates/bmc_inventory.j2 similarity index 100% rename from src/playbooks/utils/roles/idrac_pxe_boot/templates/bmc_inventory.j2 rename to src/utils/roles/idrac_pxe_boot/templates/bmc_inventory.j2 diff --git a/src/playbooks/utils/roles/idrac_pxe_boot/vars/main.yml b/src/utils/roles/idrac_pxe_boot/vars/main.yml similarity index 100% rename from src/playbooks/utils/roles/idrac_pxe_boot/vars/main.yml rename to src/utils/roles/idrac_pxe_boot/vars/main.yml diff --git a/src/playbooks/utils/roles/pxe_buildstream_manager/tasks/cloudinit_phone_home.yml b/src/utils/roles/pxe_buildstream_manager/tasks/cloudinit_phone_home.yml similarity index 100% rename from src/playbooks/utils/roles/pxe_buildstream_manager/tasks/cloudinit_phone_home.yml rename to src/utils/roles/pxe_buildstream_manager/tasks/cloudinit_phone_home.yml diff --git a/src/playbooks/utils/roles/pxe_buildstream_manager/tasks/compute_effective_inventory.yml b/src/utils/roles/pxe_buildstream_manager/tasks/compute_effective_inventory.yml similarity index 100% rename from src/playbooks/utils/roles/pxe_buildstream_manager/tasks/compute_effective_inventory.yml rename to src/utils/roles/pxe_buildstream_manager/tasks/compute_effective_inventory.yml diff --git a/src/playbooks/utils/roles/pxe_buildstream_manager/tasks/gitlab_host.yml b/src/utils/roles/pxe_buildstream_manager/tasks/gitlab_host.yml similarity index 100% rename from src/playbooks/utils/roles/pxe_buildstream_manager/tasks/gitlab_host.yml rename to src/utils/roles/pxe_buildstream_manager/tasks/gitlab_host.yml diff --git a/src/playbooks/utils/roles/pxe_buildstream_manager/tasks/main.yml b/src/utils/roles/pxe_buildstream_manager/tasks/main.yml similarity index 100% rename from src/playbooks/utils/roles/pxe_buildstream_manager/tasks/main.yml rename to src/utils/roles/pxe_buildstream_manager/tasks/main.yml diff --git a/src/playbooks/utils/roles/pxe_buildstream_manager/tasks/update_restart_state.yml b/src/utils/roles/pxe_buildstream_manager/tasks/update_restart_state.yml similarity index 100% rename from src/playbooks/utils/roles/pxe_buildstream_manager/tasks/update_restart_state.yml rename to src/utils/roles/pxe_buildstream_manager/tasks/update_restart_state.yml diff --git a/src/playbooks/utils/roles/pxe_buildstream_manager/tasks/upload_to_gitlab.yml b/src/utils/roles/pxe_buildstream_manager/tasks/upload_to_gitlab.yml similarity index 100% rename from src/playbooks/utils/roles/pxe_buildstream_manager/tasks/upload_to_gitlab.yml rename to src/utils/roles/pxe_buildstream_manager/tasks/upload_to_gitlab.yml diff --git a/src/playbooks/utils/roles/pxe_buildstream_manager/tasks/write_node_results.yml b/src/utils/roles/pxe_buildstream_manager/tasks/write_node_results.yml similarity index 100% rename from src/playbooks/utils/roles/pxe_buildstream_manager/tasks/write_node_results.yml rename to src/utils/roles/pxe_buildstream_manager/tasks/write_node_results.yml diff --git a/src/playbooks/utils/roles/pxe_buildstream_manager/vars/main.yml b/src/utils/roles/pxe_buildstream_manager/vars/main.yml similarity index 100% rename from src/playbooks/utils/roles/pxe_buildstream_manager/vars/main.yml rename to src/utils/roles/pxe_buildstream_manager/vars/main.yml diff --git a/src/rpm_build/README.md b/src/utils/rpm_build/README.md similarity index 100% rename from src/rpm_build/README.md rename to src/utils/rpm_build/README.md diff --git a/src/rpm_build/build_rpm.sh b/src/utils/rpm_build/build_rpm.sh old mode 100755 new mode 100644 similarity index 100% rename from src/rpm_build/build_rpm.sh rename to src/utils/rpm_build/build_rpm.sh diff --git a/src/rpm_build/ldms/build_ldms.rockylinux10.bash b/src/utils/rpm_build/ldms/build_ldms.rockylinux10.bash old mode 100755 new mode 100644 similarity index 100% rename from src/rpm_build/ldms/build_ldms.rockylinux10.bash rename to src/utils/rpm_build/ldms/build_ldms.rockylinux10.bash diff --git a/src/rpm_build/ldms/configure.sh b/src/utils/rpm_build/ldms/configure.sh old mode 100755 new mode 100644 similarity index 100% rename from src/rpm_build/ldms/configure.sh rename to src/utils/rpm_build/ldms/configure.sh diff --git a/src/rpm_build/ldms/rpm_postuninstall.txt b/src/utils/rpm_build/ldms/rpm_postuninstall.txt similarity index 100% rename from src/rpm_build/ldms/rpm_postuninstall.txt rename to src/utils/rpm_build/ldms/rpm_postuninstall.txt diff --git a/src/rpm_build/ldms/start_build_container.rockylinux10.bash b/src/utils/rpm_build/ldms/start_build_container.rockylinux10.bash old mode 100755 new mode 100644 similarity index 100% rename from src/rpm_build/ldms/start_build_container.rockylinux10.bash rename to src/utils/rpm_build/ldms/start_build_container.rockylinux10.bash diff --git a/src/playbooks/utils/set_pxe_boot.yml b/src/utils/set_pxe_boot.yml similarity index 100% rename from src/playbooks/utils/set_pxe_boot.yml rename to src/utils/set_pxe_boot.yml From cabfb62c57b510745de6bab2a17a9d2b793d31f2 Mon Sep 17 00:00:00 2001 From: Abhishek S A Date: Tue, 28 Jul 2026 17:39:21 +0530 Subject: [PATCH 2/3] Image build manager fixes for galaxy collection Signed-off-by: Abhishek S A --- docs/README.rst | 15 +- docs/code-style/README.md | 20 + docs/code-style/ansible.md | 166 + docs/code-style/general.md | 44 + docs/code-style/jinja2.md | 50 + docs/code-style/python.md | 100 + docs/code-style/test_automation.md | 419 +++ docs/design/ansible-collections-migration.md | 1295 +++++++ docs/design/omnia-domain-repo-design.md | 767 ++++ docs/design/test-automation-design.md | 352 ++ src/ansible.cfg | 23 - src/image_build_manager/README.md | 306 ++ src/image_build_manager/ansible.cfg | 22 +- .../containers/image_builder/README.md | 51 + src/image_build_manager/docs/architecture.md | 123 + .../contracts/input-contract.md} | 83 +- .../contracts/output-contract.md} | 0 .../design/image-builder-design.md} | 6 +- .../docs/design/standalone-design.md | 1076 ++++++ .../docs/design/standalone-mode-a.md | 454 +++ .../docs/package-mapping-guide.md | 164 + .../docs/troubleshooting.md | 178 + src/image_build_manager/galaxy.yml | 40 + .../image_build_manager.yml | 286 -- .../image_build_config.yml | 56 +- .../library/MIGRATED_FROM_COMMON.md | 63 - src/image_build_manager/meta/runtime.yml | 2 + src/image_build_manager/output/.gitkeep | 0 src/image_build_manager/playbooks/ansible.cfg | 23 +- .../playbooks/build/ansible.cfg | 31 + .../{ => build}/build_image_aarch64.yml | 97 +- .../{ => build}/build_image_x86_64.yml | 98 +- .../playbooks/cleanup/ansible.cfg | 30 + .../cleanup_image_build_manager.yml | 14 +- .../playbooks/credentials/ansible.cfg | 30 + .../get_build_credentials.yml | 8 +- .../playbooks/image_build_manager.yml | 151 + .../playbooks/prepare/ansible.cfg | 30 + .../prepare_image_build_manager.yml | 52 +- .../playbooks/rollback/ansible.cfg | 30 + .../rollback_image_build_manager.yml | 4 +- .../playbooks/upgrade/ansible.cfg | 30 + .../upgrade_image_build_manager.yml | 4 +- .../playbooks/validate/ansible.cfg | 30 + .../validate_image_build_config.yml | 4 +- .../callback}/omnia_default.py | 0 .../module_utils/build_image/__init__.py | 0 .../build_image/common_functions.py | 0 .../module_utils/build_image/config.py | 3 +- .../image_build_validation/__init__.py | 0 .../image_build_validation_flow.py | 13 +- .../image_build_validation/schema/__init__.py | 0 .../schema/config.schema.json | 48 + .../schema/functional_groups_config.json | 0 .../schema/image_build_config.json | 19 +- .../schema/image_build_credentials.json | 10 +- .../modules/base_image_package_collector.py | 0 .../modules/functional_group_parser.py | 0 .../modules/generate_functional_groups.py | 0 .../modules/image_package_collector.py | 0 .../modules/validate_image_build_config.py | 14 +- .../plugins/modules/validate_yaml_schema.py | 147 + src/image_build_manager/requirements.txt | 19 + src/image_build_manager/requirements.yml | 20 + .../tasks/build_base_image_aarch64.yml | 41 +- .../tasks/build_base_image_x86_64.yml | 41 +- .../tasks/build_compute_image_aarch64.yml | 51 +- .../tasks/build_compute_image_x86_64.yml | 49 +- .../tasks/build_image_common_aarch64.yml | 5 +- .../tasks/build_image_common_x86_64.yml | 5 +- .../tasks/configure_registry.yml | 35 + .../roles/build_os_images/tasks/main.yml | 5 - .../build_os_images/tasks/set_s3_acl.yml | 6 +- .../tasks/write_build_status.yml | 8 +- .../templates/images/rhel-base-config.yaml.j2 | 4 +- .../images/rhel-compute-config.yaml.j2 | 8 +- .../roles/build_os_images/vars/main.yml | 15 +- .../tasks/cleanup_omnia_target.yml | 42 - .../cleanup_build_artifacts/tasks/main.yml | 4 - .../cleanup_build_artifacts/vars/main.yml | 24 +- .../templates/image_build_credential.j2 | 4 +- .../collect_build_credentials/vars/main.yml | 7 +- .../roles/deploy_minio/tasks/main.yml | 14 + .../roles/deploy_minio/templates/s3/s3cfg.j2 | 4 +- .../roles/deploy_minio/vars/main.yml | 27 +- .../roles/deploy_registry/vars/main.yml | 6 +- .../roles/fetch_build_packages/tasks/main.yml | 85 +- .../roles/fetch_build_packages/vars/main.yml | 11 +- .../generate_functional_groups/tasks/main.yml | 41 - .../generate_functional_groups/vars/main.yml | 19 - .../image_build_setup/tasks/load_config.yml | 159 + .../tasks/load_repo_status.yml | 133 + .../roles/image_build_setup/tasks/main.yml | 138 +- .../tasks/validate_prereqs.yml | 42 + .../image_build_setup/tasks/validate_tags.yml | 53 + .../roles/image_build_setup/vars/main.yml | 77 +- .../tasks/gather_oim_data.yml | 24 +- .../roles/prepare_aarch64_node/tasks/main.yml | 15 +- .../roles/prepare_aarch64_node/vars/main.yml | 8 +- .../validate_build_runtime/tasks/main.yml | 27 +- .../tasks/powerscale_check.yml | 11 +- .../tasks/validate_aarch64_host.yml | 2 +- .../validate_build_runtime/vars/main.yml | 16 +- .../validate_image_build_input/tasks/main.yml | 3 +- .../validate_image_build_input/vars/main.yml | 4 +- src/image_build_manager/samples/README.md | 122 - .../samples/build_status.yml | 46 - .../functional_group_packages.yml | 258 ++ .../{ => repo_manager_output}/repo_status.yml | 36 +- src/image_build_manager/test/.gitkeep | 0 src/image_build_manager/vars/image_vars.yml | 4 + src/main/containers/README.md | 208 -- src/main/containers/build_images.sh | 264 -- src/main/containers/omnia_auth/Containerfile | 23 - src/main/containers/omnia_core/Containerfile | 184 - src/main/containers/omnia_core/cert-copy.sh | 25 - src/main/containers/omnia_core/entrypoint.sh | 12 - src/main/containers/omnia_core/pyproject.toml | 43 - src/main/containers/omnia_core/uv.lock | 1549 -------- src/main/omnia-cli | 714 ++++ src/main/omnia.env | 64 + src/main/omnia.sh | 3125 ++--------------- 122 files changed, 8555 insertions(+), 6515 deletions(-) create mode 100644 docs/code-style/README.md create mode 100644 docs/code-style/ansible.md create mode 100644 docs/code-style/general.md create mode 100644 docs/code-style/jinja2.md create mode 100644 docs/code-style/python.md create mode 100644 docs/code-style/test_automation.md create mode 100644 docs/design/ansible-collections-migration.md create mode 100644 docs/design/omnia-domain-repo-design.md create mode 100644 docs/design/test-automation-design.md delete mode 100644 src/ansible.cfg create mode 100644 src/image_build_manager/README.md create mode 100644 src/image_build_manager/containers/image_builder/README.md create mode 100644 src/image_build_manager/docs/architecture.md rename src/image_build_manager/{INPUT_CONTRACT.md => docs/contracts/input-contract.md} (64%) rename src/image_build_manager/{OUTPUT_CONTRACT.md => docs/contracts/output-contract.md} (100%) rename src/image_build_manager/{IMAGE_BUILDER_DESIGN.md => docs/design/image-builder-design.md} (98%) create mode 100644 src/image_build_manager/docs/design/standalone-design.md create mode 100644 src/image_build_manager/docs/design/standalone-mode-a.md create mode 100644 src/image_build_manager/docs/package-mapping-guide.md create mode 100644 src/image_build_manager/docs/troubleshooting.md create mode 100644 src/image_build_manager/galaxy.yml delete mode 100644 src/image_build_manager/image_build_manager.yml rename src/image_build_manager/input/{ => project_default}/image_build_config.yml (54%) delete mode 100644 src/image_build_manager/library/MIGRATED_FROM_COMMON.md create mode 100644 src/image_build_manager/meta/runtime.yml delete mode 100644 src/image_build_manager/output/.gitkeep create mode 100644 src/image_build_manager/playbooks/build/ansible.cfg rename src/image_build_manager/playbooks/{ => build}/build_image_aarch64.yml (62%) rename src/image_build_manager/playbooks/{ => build}/build_image_x86_64.yml (56%) create mode 100644 src/image_build_manager/playbooks/cleanup/ansible.cfg rename src/image_build_manager/playbooks/{ => cleanup}/cleanup_image_build_manager.yml (90%) create mode 100644 src/image_build_manager/playbooks/credentials/ansible.cfg rename src/image_build_manager/playbooks/{ => credentials}/get_build_credentials.yml (88%) create mode 100644 src/image_build_manager/playbooks/image_build_manager.yml create mode 100644 src/image_build_manager/playbooks/prepare/ansible.cfg rename src/image_build_manager/playbooks/{ => prepare}/prepare_image_build_manager.yml (52%) create mode 100644 src/image_build_manager/playbooks/rollback/ansible.cfg rename src/image_build_manager/playbooks/{ => rollback}/rollback_image_build_manager.yml (91%) create mode 100644 src/image_build_manager/playbooks/upgrade/ansible.cfg rename src/image_build_manager/playbooks/{ => upgrade}/upgrade_image_build_manager.yml (91%) create mode 100644 src/image_build_manager/playbooks/validate/ansible.cfg rename src/image_build_manager/playbooks/{ => validate}/validate_image_build_config.yml (93%) rename src/image_build_manager/{callback_plugins => plugins/callback}/omnia_default.py (100%) rename src/image_build_manager/{library => plugins}/module_utils/build_image/__init__.py (100%) rename src/image_build_manager/{library => plugins}/module_utils/build_image/common_functions.py (100%) rename src/image_build_manager/{library => plugins}/module_utils/build_image/config.py (93%) rename src/image_build_manager/{library => plugins}/module_utils/image_build_validation/__init__.py (100%) rename src/image_build_manager/{library => plugins}/module_utils/image_build_validation/image_build_validation_flow.py (88%) rename src/image_build_manager/{library => plugins}/module_utils/image_build_validation/schema/__init__.py (100%) create mode 100644 src/image_build_manager/plugins/module_utils/image_build_validation/schema/config.schema.json rename src/image_build_manager/{library => plugins}/module_utils/image_build_validation/schema/functional_groups_config.json (100%) rename src/image_build_manager/{library => plugins}/module_utils/image_build_validation/schema/image_build_config.json (74%) rename src/image_build_manager/{library => plugins}/module_utils/image_build_validation/schema/image_build_credentials.json (77%) rename src/image_build_manager/{library => plugins}/modules/base_image_package_collector.py (100%) rename src/image_build_manager/{library => plugins}/modules/functional_group_parser.py (100%) rename src/image_build_manager/{library => plugins}/modules/generate_functional_groups.py (100%) rename src/image_build_manager/{library => plugins}/modules/image_package_collector.py (100%) rename src/image_build_manager/{library => plugins}/modules/validate_image_build_config.py (94%) create mode 100644 src/image_build_manager/plugins/modules/validate_yaml_schema.py create mode 100644 src/image_build_manager/requirements.txt create mode 100644 src/image_build_manager/requirements.yml create mode 100644 src/image_build_manager/roles/build_os_images/tasks/configure_registry.yml delete mode 100644 src/image_build_manager/roles/cleanup_build_artifacts/tasks/cleanup_omnia_target.yml delete mode 100644 src/image_build_manager/roles/generate_functional_groups/tasks/main.yml delete mode 100644 src/image_build_manager/roles/generate_functional_groups/vars/main.yml create mode 100644 src/image_build_manager/roles/image_build_setup/tasks/load_config.yml create mode 100644 src/image_build_manager/roles/image_build_setup/tasks/load_repo_status.yml create mode 100644 src/image_build_manager/roles/image_build_setup/tasks/validate_prereqs.yml create mode 100644 src/image_build_manager/roles/image_build_setup/tasks/validate_tags.yml delete mode 100644 src/image_build_manager/samples/README.md delete mode 100644 src/image_build_manager/samples/build_status.yml create mode 100644 src/image_build_manager/samples/repo_manager_output/functional_group_packages.yml rename src/image_build_manager/samples/{ => repo_manager_output}/repo_status.yml (80%) delete mode 100644 src/image_build_manager/test/.gitkeep delete mode 100644 src/main/containers/README.md delete mode 100755 src/main/containers/build_images.sh delete mode 100644 src/main/containers/omnia_auth/Containerfile delete mode 100644 src/main/containers/omnia_core/Containerfile delete mode 100755 src/main/containers/omnia_core/cert-copy.sh delete mode 100755 src/main/containers/omnia_core/entrypoint.sh delete mode 100644 src/main/containers/omnia_core/pyproject.toml delete mode 100644 src/main/containers/omnia_core/uv.lock create mode 100644 src/main/omnia-cli create mode 100644 src/main/omnia.env diff --git a/docs/README.rst b/docs/README.rst index 1d49fc1b6d..dda298f49b 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -1,6 +1,17 @@ Omnia Documentation ------------------- -**Omnia** is an open source project hosted on `GitHub `_. Go to `GitHub `_ to view the source, open issues, ask questions, and participate in the project. +**Omnia** is an open source project hosted on `GitHub `_. -The Omnia docs are hosted here: https://omnia.readthedocs.io/en/latest/index.html and are written in reStructuredText (`.rst`). +This directory contains **generic** Omnia documentation shared across all domains. +Domain-specific docs live inside each domain (e.g. ``src/image_build_manager/docs/``). + +:: + + docs/ + ├── code-style/ Code style guides (Ansible, Python, Jinja2) + ├── design/ Cross-domain design documents + │ ├── ansible-collections-migration.md + │ ├── omnia-domain-repo-design.md + │ └── test-automation-design.md + └── logos/ Omnia branding assets diff --git a/docs/code-style/README.md b/docs/code-style/README.md new file mode 100644 index 0000000000..0f9a3a7303 --- /dev/null +++ b/docs/code-style/README.md @@ -0,0 +1,20 @@ +# Code Style Guides — Image Build Manager + +Coding standards per language/tool. All code PRs are reviewed against these guides. +Based on [Dell Omnia Code Style Guides](https://github.com/dell/omnia). + +| Guide | What It Covers | +|-------|---------------| +| [ansible.md](ansible.md) | Playbook structure, FQCN modules, role layout, linting | +| [python.md](python.md) | Naming, docstrings, pylint rules, Ansible module patterns | +| [jinja2.md](jinja2.md) | Template syntax, filters, whitespace, error prevention | +| [general.md](general.md) | Copyright headers, readability, consistency, documentation | + +## Validated Environment + +| Component | Minimum Version | Validated Version | +|-----------|----------------|-------------------| +| Python | 3.12+ | 3.12.8 | +| Ansible Core | 2.20+ | 2.20.0 | +| RHEL | 10.0+ | 10.0 | +| Podman | 5.0+ | 5.3.1 | diff --git a/docs/code-style/ansible.md b/docs/code-style/ansible.md new file mode 100644 index 0000000000..bbf6e2aee2 --- /dev/null +++ b/docs/code-style/ansible.md @@ -0,0 +1,166 @@ +# Ansible / YAML Style Guide — Image Build Manager + +Based on [Dell Omnia Ansible Style Guide](https://github.com/dell/omnia). + +## 1. File Conventions + +### 1.1 File Headers +- Every YAML file SHALL start with the Dell Apache 2.0 copyright header (see `general.md`) +- Copyright header SHALL be followed by `---` +- Every playbook SHALL have a descriptive `name:` in the play definition + +### 1.2 File Naming +- Playbooks: `.yml` (e.g., `image_build_manager.yml`) +- Role tasks: `main.yml`, `.yml` (e.g., `validate_tags.yml`, `load_config.yml`) +- Variables: `main.yml` in `defaults/` and `vars/` +- Templates: `.j2` in `templates/` + +### 1.3 Indentation +- **2-space indentation** for all YAML files +- No tabs — spaces only +- No trailing whitespace + +## 2. Playbook Standards + +### 2.1 Play Structure +```yaml +--- +# playbook_name.yml — Brief description of purpose +- name: Descriptive play name + hosts: localhost + connection: local + gather_facts: false + + roles: + - role_name +``` + +### 2.2 Entry Point Rules +- `src/.yml` MUST contain only `roles:` and `import_playbook:` — NO inline `tasks:`. +- All `hosts:` MUST be `localhost` with `connection: local`. +- Each play must have a step comment (`# Step N: ...`). + +### 2.3 Task Standards +- Every task SHALL have a descriptive `name:` field +- Use **FQCN** (Fully Qualified Collection Names): `ansible.builtin.copy`, not `copy` +- Use `become: true` explicitly — never rely on implicit privilege escalation +- Set `changed_when` for all `command`/`shell` tasks +- Set `failed_when` for tasks with non-standard failure conditions + +### 2.4 Example Task +```yaml +- name: Ensure log directory exists + ansible.builtin.file: + path: "{{ log_dir }}" + state: directory + mode: "0755" + become: true + +- name: Check if service is running + ansible.builtin.command: + cmd: systemctl is-active minio + register: service_status + changed_when: false + failed_when: false +``` + +## 3. Role Structure + +``` +roles// +├── defaults/main.yml # Default variables (overridable) +├── vars/main.yml # Role variables (internal) — ALL error messages here +├── tasks/ +│ ├── main.yml # Entry point (include other task files) +│ ├── validate_tags.yml # Tag validation +│ ├── load_config.yml # Configuration loading +│ └── validate_prereqs.yml # Prerequisite validation +├── templates/ # Jinja2 templates +├── files/ # Static files +├── handlers/main.yml # Handlers (restart/reload) +└── meta/main.yml # Role metadata and dependencies +``` + +## 4. Variable Standards + +### 4.1 Naming +- Use **snake_case** for all variable names +- Prefix private variables with `_`: `_prereq_checks`, `_pulp_cert_check` +- Use descriptive names: `repo_manager_output_dir`, not `dir` + +### 4.2 Defaults vs Vars +- `defaults/main.yml` — Values the user CAN override +- `vars/main.yml` — Internal values the user SHOULD NOT change +- **ALL error messages** go in `vars/main.yml` — never inline + +### 4.3 Boolean Values +- Use `true` / `false` — never `yes` / `no` + +### 4.4 String Quoting +- Always quote strings that could be misinterpreted as booleans or numbers +- Always quote strings with special characters: `{{ }}`, `:`, `#` + +### 4.5 Fact Caching +- Use `cacheable: true` on `set_fact` when the fact is needed across plays +- Use `verbosity: 1` or `2` on debug tasks (not shown by default) +- Use `quiet: true` on assert tasks to suppress verbose output + +## 5. Error Handling + +### 5.1 Block/Rescue/Always +```yaml +- name: Deploy component with error handling + block: + - name: Install packages + ansible.builtin.dnf: + name: "{{ packages }}" + state: present + rescue: + - name: Log failure + ansible.builtin.debug: + msg: "Deployment failed: {{ ansible_failed_result.msg }}" +``` + +### 5.2 Assertions +- Use `ansible.builtin.assert` for prerequisite validation +- Provide actionable `fail_msg` messages defined in `vars/main.yml` + +## 6. Linting + +### 6.1 Required Tools +- **ansible-lint** with production profile +- **yamllint** for raw YAML files + +### 6.2 Enforced Rules +- No unnamed tasks +- No bare variables in `when:` clauses +- Use FQCN for `ansible.builtin.*` modules +- No `command`/`shell` when a module exists + +### 6.3 Collection Requirements +Required collections in `requirements.yml`: +- `ansible.utils` (IP validation) +- `containers.podman` (container management) +- `community.crypto` (certificate handling) +- `community.general` (general utilities) + +## 7. Idempotency + +- All roles SHALL be idempotent — running twice produces no changes on second run +- Use `creates:` parameter for `command` tasks that generate files +- Use `stat` + `when` for conditional execution +- Never use `shell` with `rm -rf` without explicit guards + +## 8. Secrets Management + +- **Never** hardcode credentials in playbooks or variable files +- Use Ansible Vault for sensitive data +- Reference secrets via `{{ vault_secret_name }}` + +## 9. Validated Environment + +| Component | Minimum | Validated | +|-----------|---------|-----------| +| Ansible Core | 2.20+ | 2.20.0 | +| Python | 3.12+ | 3.12.8 | +| RHEL | 10.0+ | 10.0 | diff --git a/docs/code-style/general.md b/docs/code-style/general.md new file mode 100644 index 0000000000..8ad5f03e0c --- /dev/null +++ b/docs/code-style/general.md @@ -0,0 +1,44 @@ +# General Code Style — Image Build Manager + +Based on [Dell Omnia General Style Guide](https://github.com/dell/omnia). + +## 1. Copyright Header + +Every source file (`.yml`, `.py`, `.sh`) SHALL include this header. `.j2` templates are excluded (the parent role carries the header): + +``` +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +``` + +## 2. Principles + +- **Readability**: Code should read like prose — clear variable names, step comments +- **Consistency**: Follow project conventions, not personal preference +- **Simplicity**: Prefer simple over clever +- **Maintainability**: Write for the next developer +- **Documentation**: Document the *why*, not just the *what* + +## 3. File Organization + +- Group related files in directories +- Use descriptive file names in `snake_case` +- Keep files focused on a single responsibility +- Maximum file length: ~300 lines (split if larger) + +## 4. Version Control + +- One logical change per commit +- Clear, descriptive commit messages +- Branch naming: `feature/` or `fix/` diff --git a/docs/code-style/jinja2.md b/docs/code-style/jinja2.md new file mode 100644 index 0000000000..5764ceef94 --- /dev/null +++ b/docs/code-style/jinja2.md @@ -0,0 +1,50 @@ +# Jinja2 Template Style Guide — Image Build Manager + +Based on [Dell Omnia Jinja2 Style Guide](https://github.com/dell/omnia). + +## 1. File Conventions + +- Template files SHALL use `.j2` extension +- Name SHALL match the target file: `slurm.conf.j2` → `slurm.conf` +- Store in role's `templates/` directory +- Start with `# {{ ansible_managed }}` comment +- **No copyright header in templates** — the role carries it + +## 2. Syntax + +| Delimiter | Purpose | Example | +|-----------|---------|---------| +| `{{ }}` | Variable output | `{{ admin_nic_ip }}` | +| `{% %}` | Logic (if/for) | `{% if enable_gpu %}` | +| `{# #}` | Comments | `{# Registry config #}` | + +## 3. Variables + +- Use dot notation: `{{ node.hostname }}` +- Bracket notation for special chars: `{{ config['my-key'] }}` +- Always provide defaults for optional variables: + ```jinja + {{ telemetry_port | default(9090) }} + ``` + +## 4. Whitespace Control + +- Use `{%-` and `-%}` for clean output in loops +- Use standard `{% %}` when whitespace is acceptable + +## 5. Filters + +| Filter | Purpose | +|--------|---------| +| `default(val)` | Fallback value | +| `join(',')` | List to string | +| `to_yaml` | Dict to YAML | +| `bool` | Convert to boolean | +| `int` | Convert to integer | +| `lower` / `upper` | Case conversion | + +## 6. Error Prevention + +- Use `{{ var }}` without `default()` for required variables +- Use `{{ var | default(omit) }}` to skip optional Ansible parameters +- Use `| int` when arithmetic is needed diff --git a/docs/code-style/python.md b/docs/code-style/python.md new file mode 100644 index 0000000000..66d94d938d --- /dev/null +++ b/docs/code-style/python.md @@ -0,0 +1,100 @@ +# Python Style Guide — Image Build Manager + +Based on [Dell Omnia Python Style Guide](https://github.com/dell/omnia), adapted from Google Python Style Guide. + +## 1. Environment + +| Component | Minimum | Validated | +|-----------|---------|-----------| +| Python | 3.12+ | 3.12.8 | +| pylint score | ≥ 8.0 | — | + +## 2. Language Rules + +### 2.1 Linting +- Use `pylint` for static analysis — target score ≥ 8.0 +- Use `flake8` as secondary checker + +### 2.2 Imports +- Standard library first, then third-party, then local +- One import per line +- No wildcard imports (`from module import *`) + +### 2.3 Type Annotations +- Type hints on all public functions +- Use `typing` module for complex types + +### 2.4 Exceptions +- Catch specific exceptions — never bare `except:` +- Re-raise with context: `raise ... from e` + +## 3. Style Rules + +### 3.1 Naming +| Type | Convention | Example | +|------|-----------|---------| +| Modules | `snake_case` | `validate_config.py` | +| Functions | `snake_case` | `load_repo_status()` | +| Classes | `PascalCase` | `ImageBuildConfig` | +| Constants | `ALL_CAPS` | `MAX_RETRIES` | +| Private | `_prefix` | `_validate_input()` | + +### 3.2 Line Length +- Maximum 100 characters per line + +### 3.3 Indentation +- 4 spaces — no tabs + +### 3.4 Docstrings +- Google-style docstrings for all public functions/classes +```python +def validate_config(config_path: str) -> dict: + """Validate and load configuration file. + + Args: + config_path: Absolute path to config.yml. + + Returns: + Parsed configuration dictionary. + + Raises: + FileNotFoundError: If config file does not exist. + """ +``` + +## 4. Ansible Module Patterns + +### 4.1 Module Structure +```python +#!/usr/bin/python +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 + +"""Module docstring describing purpose.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """Main module entry point.""" + module = AnsibleModule( + argument_spec=dict( + param1=dict(type='str', required=True), + ), + supports_check_mode=True, + ) + # Logic here + module.exit_json(changed=False, result="ok") + + +if __name__ == '__main__': + main() +``` + +### 4.2 Module Utilities +- Place shared code in `src/module_utils/` +- Import as `from ansible.module_utils. import ` + +### 4.3 Error Handling +- Use `module.fail_json(msg="...")` for failures — never `sys.exit()` +- Always set `changed=True/False` accurately diff --git a/docs/code-style/test_automation.md b/docs/code-style/test_automation.md new file mode 100644 index 0000000000..57b79c66b6 --- /dev/null +++ b/docs/code-style/test_automation.md @@ -0,0 +1,419 @@ +# Test Automation — Coding Rules + +> Adapted from [Dell Omnia Automation Framework Rules](https://github.com/dell/omnia-containers). +> All test automation code under `test/` MUST follow these rules. + +--- + +## 1. Module Architecture + +### 1.1 Directory Structure (MANDATORY) + +``` +test/ +├── fvt/ # Functional Verification Tests +│ ├── / # One folder per test scenario +│ │ ├── deploy/test_deploy.py # Playbook execution (deploy command) +│ │ ├── /test_.py +│ │ └── ... +│ └── image_builder/ # Full verification (verify command) +│ ├── container/ +│ ├── s3/ +│ ├── registry/ +│ └── image_verification/ +├── library/ # Shared automation library +│ ├── functions/ # Verification logic +│ │ ├── __init__.py +│ │ ├── build_image_func.py # Domain-specific checks +│ │ ├── host_func.py # Connection, sync, config loading +│ │ ├── formatting_func.py # TestLogger, Colors, Symbols +│ │ ├── runner_func.py # PlaybookRunner +│ │ └── report_func.py # TestReport, HTML generation +│ ├── vars/ # Constants and configuration +│ │ ├── __init__.py +│ │ ├── common_vars.py # CMDS dict, paths, ports, retries +│ │ └── runner_vars.py # PlaybookRunner constants +│ ├── messages/ # All user-facing strings +│ │ ├── __init__.py +│ │ └── build_image_msgs.py # TEST_NAMES, LOG, ASSERT, SKIP +│ └── validation/ # Config validation +│ └── functions/ +│ └── validation_func.py +├── datasets/ # Input datasets +│ └── project_default/ +│ ├── config.yml # Top-level project config +│ └── input/ # Files synced to target +│ ├── image_build_config.yml +│ ├── image_build_credentials.yml +│ └── repo_manager_output/ +├── conftest.py # pytest hooks and fixtures +├── test_config.yml # Server connection and sync settings +├── test_run_config.yml # Suite enable/disable and markers +├── test_creds.yml # SSH credentials (vault-encrypted) +├── run_validation.sh # Main entry point +├── setup_env.sh # Environment setup script +└── requirements.txt # Python dependencies +``` + +### 1.2 Strict Separation Rules + +| Content | Location | Never In | +|---------|----------|----------| +| Shell commands | `CMDS` dict in `common_vars.py` | Test files, function files | +| Test names | `TEST_NAMES` in `build_image_msgs.py` | Test files | +| Log messages | `TEST_LOG_MSGS` in `build_image_msgs.py` | Test files | +| Assert messages | `TEST_ASSERT_MSGS` in `build_image_msgs.py` | Test files | +| Skip messages | `TEST_LOG_MSGS` (skip keys) in `build_image_msgs.py` | Test files | +| Constants | `common_vars.py` or `runner_vars.py` | Function or test files | +| Verification logic | `functions/*.py` | Test files | + +### 1.3 `__init__.py` Requirements + +Every `__init__.py` MUST: +1. Include Apache 2.0 license header (current year) +2. Provide a module docstring +3. Import and re-export specific items (no `import *`) +4. Group imports: functions, then vars, then messages + +--- + +## 2. Test Writing Rules + +### 2.1 Test File Docstring (MANDATORY) + +Every test file MUST start with a module docstring listing all test cases: + +```python +""" +Image Builder — Registry Image Verification Tests. + +TC_IB_006: Verify x86_64 images in registry +TC_IB_007: Verify aarch64 images in registry +TC_IB_008: Verify build_status.yml exists +TC_IB_009: Verify x86_64 functional groups built +TC_IB_010: Verify aarch64 functional groups built +""" +``` + +### 2.2 Test Function Structure (MANDATORY) + +```python +@pytest.mark.x86_64 +@pytest.mark.sanity +@pytest.mark.order(6) +def test_registry_images_x86_64(host): + """TC_IB_006: Verify x86_64 images in registry.""" + # 1. Initialize logger with centralized test name + tl = TestLogger(TEST_NAMES["registry_images"].format(arch="x86_64")) + + # 2. Call verification function (returns dict) + result = check_registry_images(host, arch="x86_64") + + # 3. Handle skip (optional features) + if result.get("skipped"): + tl.skipped(result["details"]) + pytest.skip(result["details"]) + + # 4. Build details string + details = format_result(result) + + # 5. Log pass or fail + if result["success"]: + tl.passed(LOG["registry_images_ok"].format(arch="x86_64"), details) + else: + tl.failed(LOG["registry_images_missing"].format(...), details) + + # 6. Assert with centralized message + assert result["success"], ASSERT["registry_images_missing"].format(...) +``` + +### 2.3 Test Naming Convention + +| Type | Convention | Example | +|------|-----------|---------| +| Test function | `test__` | `test_registry_images_x86_64` | +| Test file | `test_.py` | `test_registry_images.py` | +| Test case ID | `TC__` | `TC_IB_006` | + +### 2.4 Test Case ID Areas + +| Area | Prefix | Description | +|------|--------|-------------| +| Validate | `TC_VL_` | Validation tag tests | +| Prepare | `TC_PR_` | Prepare tag tests | +| Build | `TC_BD_` | Build tag tests | +| Cleanup | `TC_CL_` | Cleanup tag tests | +| Image Builder | `TC_IB_` | Full suite verification | + +### 2.5 Test Output Format + +Use `✓`/`✗` format with grouping: + +``` + ▶ Verify x86_64 images in registry + ✔ PASS: All x86_64 images found in registry + │ Registry: abhoim.vm.cluster:5000 + │ ✓ rhel-x86_64-base + │ ✓ rhel-slurm_node_x86_64 +``` + +### 2.6 Import Structure for Test Files + +```python +# Third-party +import pytest + +# Local — Functions +from library.functions import TestLogger, check_registry_images + +# Local — Messages +from library.messages import ( + TEST_NAMES, + TEST_LOG_MSGS as LOG, + TEST_ASSERT_MSGS as ASSERT, +) +``` + +--- + +## 3. Functions Module Rules + +### 3.1 Return Dictionary Pattern (MANDATORY) + +All verification functions MUST return a dict: + +```python +def check_something(host) -> Dict[str, Any]: + """Verify something. + + Returns: + Dict with 'success', 'error', and component-specific keys. + """ + return { + "success": True, # REQUIRED — bool + "error": None, # REQUIRED — None or error string + "details": "...", # Human-readable details + "skipped": False, # True if feature not configured + } +``` + +### 3.2 Dynamic Input Rules (CRITICAL) + +**NEVER hardcode:** +- IP addresses or hostnames +- File paths that vary by environment +- Credentials or secrets +- Port numbers (use constants from `common_vars.py`) + +**ALWAYS:** +- Read from `test_config.yml` via `load_test_config()` +- Use `CMDS` dict for shell commands +- Use constants from `common_vars.py` for paths and ports + +### 3.3 Skip Pattern for Optional Features + +```python +if not groups: + return { + "success": True, + "skipped": True, + "details": f"No {arch} functional groups configured", + } +``` + +--- + +## 4. Variables Module Rules + +### 4.1 Command Dictionary (MANDATORY) + +All shell commands MUST be in `CMDS`: + +```python +CMDS: Dict[str, str] = { + "check_container": "podman ps --format '{{{{.Names}}}}' --filter name={}", + "list_s3_objects": "s3cmd ls s3://{bucket}/ --host={host}", +} +``` + +### 4.2 Constants + +```python +# Ports +MINIO_API_PORT = 9000 +REGISTRY_PORT = 5000 + +# Retries +CONTAINER_WAIT_RETRIES = 5 +CONTAINER_WAIT_DELAY = 3 +``` + +--- + +## 5. Messages Module Rules (MANDATORY) + +### 5.1 Required Dictionaries + +```python +TEST_NAMES: Dict[str, str] = { + "registry_images": "Verify {arch} images in registry", +} + +TEST_LOG_MSGS: Dict[str, str] = { + "registry_images_ok": "All {arch} images found in registry", + "build_status_not_found": "build_status.yml not found ...", +} + +TEST_ASSERT_MSGS: Dict[str, str] = { + "registry_images_missing": ( + "REGISTRY IMAGES MISSING\n" + "...\n" + "HOW TO FIX:\n" + " 1. Check registry: regctl repo ls ...\n" + ), +} +``` + +### 5.2 Rules +- Use `{placeholder}` for dynamic values +- Include **HOW TO FIX** in assertion messages +- Never hardcode IPs in messages — use `` format + +--- + +## 6. Code Standards + +### 6.1 License Header (MANDATORY) + +Every Python file MUST start with: + +```python +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# ... +``` + +### 6.2 Python Standards + +- Python 3.12+ compatibility +- pylint score ≥ 8.7 +- Max line length: 100 characters +- 4-space indentation, no tabs +- Type hints on all public functions +- Google-style docstrings on all functions + +### 6.3 Import Organization + +```python +# 1. Standard library +import os +from typing import Dict, Any + +# 2. Third-party +import pytest + +# 3. Local +from library.functions import TestLogger +from library.vars.common_vars import CMDS +from library.messages import TEST_NAMES +``` + +### 6.4 Shell Scripts + +- Start with `set -euo pipefail` +- Use functions for reusable logic +- Support `--help` flag + +--- + +## 7. Git Commit Rules (MANDATORY) + +### 7.1 Commit Format + +All commits **MUST** use `--signoff` and `--author`: + +```bash +git commit --signoff \ + --author="Your Name " \ + -m "Issue #: Short description of change" +``` + +**Example:** + +```bash +git commit --signoff \ + --author="Your Name " \ + -m "Issue #42: Add x86_64 S3 image verification tests" +``` + +### 7.2 Commit Message Rules + +- **First line**: `Issue #: ` (max 72 chars) +- **Body** (optional): Blank line, then details in bullet points +- **Signed-off-by**: Auto-added by `--signoff` flag + +``` +Issue #42: Add x86_64 S3 image verification tests + +- Add TC_IB_004 and TC_IB_005 for S3 image checks +- Add check_s3_bucket_images function +- Update TEST_NAMES and TEST_LOG_MSGS + +Signed-off-by: Your Name +``` + +### 7.3 Branch Naming + +``` +feature/- +bugfix/- +``` + +### 7.4 Before Push Checklist + +- [ ] `pylint score ≥ 8.7` — run `pylint` on all changed files +- [ ] No hardcoded credentials or secrets in code +- [ ] `test_creds.yml` is encrypted (not plain text) +- [ ] Dataset files contain only keys, no actual secret values +- [ ] `.test_creds.key` and `.image_build_credentials_key` are in `.gitignore` +- [ ] All tests pass: `./run_validation.sh image_builder verify --marker sanity` + +--- + +## 8. Quality Checklist + +Before submitting code: + +### Module Structure +- [ ] Follows `functions/`, `vars/`, `messages/` structure +- [ ] All `__init__.py` files properly export items +- [ ] License headers in all files (current year) + +### Functions +- [ ] Return dicts with `success`, `error` keys +- [ ] Use dynamic inputs (no hardcoded IPs, paths) +- [ ] Docstrings with Args/Returns + +### Variables +- [ ] All shell commands in `CMDS` dict +- [ ] All constants in `vars/` directory +- [ ] No magic numbers in functions or tests + +### Messages +- [ ] `TEST_NAMES`, `TEST_LOG_MSGS`, `TEST_ASSERT_MSGS` defined +- [ ] Assert messages include HOW TO FIX +- [ ] All messages use `{placeholder}` syntax + +### Tests +- [ ] Module docstring lists all test cases with IDs +- [ ] Use `TestLogger` with `tc_id` parameter +- [ ] Skip logic for optional features +- [ ] All messages imported from `messages/` +- [ ] All constants imported from `vars/` +- [ ] pylint score ≥ 8.7 + +### Git +- [ ] Commit with `--signoff --author` +- [ ] No secrets in committed files +- [ ] Dataset values sanitized (keys only) diff --git a/docs/design/ansible-collections-migration.md b/docs/design/ansible-collections-migration.md new file mode 100644 index 0000000000..174e5e8728 --- /dev/null +++ b/docs/design/ansible-collections-migration.md @@ -0,0 +1,1295 @@ +# Ansible Collections Migration — Design & Plan + +**Version**: 1.0 +**Status**: PROPOSAL +**Audience**: Omnia platform team, domain developers +**Purpose**: Evaluate and plan migration from flat playbook repositories to Ansible Collections. + +--- + +## 1. What Is an Ansible Collection? + +An Ansible Collection is the **standard distribution format** for Ansible content — modules, +plugins, roles, and playbooks bundled into a namespaced, versioned package installable via +`ansible-galaxy`. + +``` +// # e.g. dell/omnia_image_build +├── galaxy.yml # Metadata + version + dependencies +├── plugins/ +│ ├── modules/ # Custom modules +│ ├── module_utils/ # Shared Python utilities for modules +│ ├── callback/ # Callback plugins +│ ├── filter/ # Jinja2 filter plugins (optional) +│ └── inventory/ # Inventory plugins (optional) +├── roles/ +│ ├── setup/ # Roles (referenced by FQCN) +│ ├── deploy_minio/ +│ └── ... +├── playbooks/ # Playbooks callable via FQCN +├── docs/ +├── tests/ +│ ├── unit/ +│ ├── integration/ +│ └── sanity/ +└── meta/ + └── runtime.yml # Ansible version compatibility + redirects +``` + +### Key Concepts + +| Concept | Description | +|---------|-------------| +| **Namespace** | Top-level vendor identifier (e.g., `dell`, `community`, `ansible`) | +| **Collection name** | The collection within the namespace (e.g., `omnia_common`, `omnia_image_build`) | +| **FQCN** | Fully Qualified Collection Name — `dell.omnia_image_build.validate_yaml_schema` | +| **galaxy.yml** | Metadata file defining name, version, dependencies, and authors | +| **meta/runtime.yml** | Ansible version compatibility and module redirects | + +--- + +## 2. Current State — Omnia Mono-Repo + +### Asset Inventory + +| Asset Type | `common/` (shared) | `image_build_manager` | Other domains | +|---|---|---|---| +| **Custom modules** | 49 (`common/library/modules/`) | 6 (`library/modules/`) | Varies | +| **Module utils** | 68 items (`common/library/module_utils/`) | 2 packages | Varies | +| **Callback plugins** | 1 (`omnia_default.py`) | 1 (copy of same) | Copies | +| **Roles** | N/A (tasks only) | 11 | 50+ across domains | +| **Playbooks** | N/A | 9 sub-playbooks + 1 entry point | Hundreds | + +### Current Pain Points + +1. **`common/` coupling** — Every domain has `ansible.cfg` pointing to `../common/library/`. Any change in `common/` can break any domain. +2. **Module duplication** — Domains copy modules from `common/` to work standalone. No single source of truth. +3. **No versioning** — Modules are at "whatever commit you have". No way to pin versions. +4. **Bare module names** — `validate_yaml_schema:` could collide with another domain's module. +5. **`ansible.cfg` path hacks** — Every `ansible.cfg` has relative paths to `library/`, `module_utils/`, `callback_plugins/`. + +--- + +## 3. Advantages of Collections + +| # | Advantage | Detail | +|---|-----------|--------| +| 1 | **Namespaced FQCN** | `dell.omnia_image_build.validate_yaml_schema` instead of bare `validate_yaml_schema`. Eliminates name collisions across domains. | +| 2 | **Versioned distribution** | `ansible-galaxy collection install dell.omnia_image_build:1.2.0` — exact version pinning. | +| 3 | **Independent release cycles** | Each domain collection is versioned separately. `image_build` v1.3 while `orchestrator` v1.1. | +| 4 | **Eliminates `common/` coupling** | Each collection ships its own modules/plugins. No cross-domain `../common/library/` imports. | +| 5 | **Standard installation** | `ansible-galaxy collection install -r requirements.yml` — same workflow as community collections. | +| 6 | **Air-gap friendly** | `ansible-galaxy collection build` → `.tar.gz` → copy to air-gapped host → `ansible-galaxy collection install ./dell-omnia_common-1.0.0.tar.gz` | +| 7 | **Automation Hub compatible** | Can be published to Private Automation Hub for enterprise distribution. | +| 8 | **Testing framework** | `ansible-test` provides sanity, unit, and integration test runners designed for collections. | +| 9 | **Galaxy dependency resolution** | Collections declare dependencies on other collections in `galaxy.yml`. | +| 10 | **LLM/AI advantage** | FQCN is self-documenting. AI agents generate `dell.omnia_image_build.validate_yaml_schema` without guessing paths. | + +--- + +## 4. Disadvantages of Collections + +| # | Disadvantage | Detail | +|---|-------------|--------| +| 1 | **Migration effort** | Restructuring roles + modules + module_utils + callback plugins. Every `ansible.cfg` path reference changes. | +| 2 | **Playbook FQCN rewrite** | Every task using a custom module needs updating: `validate_yaml_schema:` → `dell.omnia_image_build.validate_yaml_schema:` | +| 3 | **`module_utils` import path changes** | `from ansible.module_utils.build_image.common_functions import ...` → `from ansible_collections.dell.omnia_image_build.plugins.module_utils.build_image.common_functions import ...` | +| 4 | **Build & publish pipeline** | Need CI/CD to build collection tarball, test, and publish. Additional infrastructure. | +| 5 | **Role naming constraints** | Role names must be unique within the collection namespace. | +| 6 | **Callback plugin path change** | `stdout_callback = omnia_default` → `stdout_callback = dell.omnia_common.omnia_default` | +| 7 | **Dev workflow friction** | Need to install collection in editable mode, symlink, or use `COLLECTIONS_PATHS` override during development. | +| 8 | **Debugging complexity** | Stack traces show full collection paths instead of local relative paths. | +| 9 | **Mono-repo compatibility** | Need to decide: collections as separate repos OR collection build from mono-repo subdirectory. | +| 10 | **Ansible version requirement** | Full collection support requires ansible-core ≥ 2.10 (not an issue — we're on 2.20+). | + +--- + +## 5. Decision Matrix + +| Factor | Stay as Playbook Repo | Convert to Collection | +|--------|----------------------|----------------------| +| **Time to value** | Already working | 2-4 sprints migration | +| **Module reuse** | Copy modules between domains | `dell.omnia_common` shared properly | +| **Version pinning** | Git commit hash only | Semantic versioning | +| **Air-gap distribution** | Git clone or tarball | `collection build` → `.tar.gz` | +| **FQCN clarity** | Bare module names | Namespaced, unambiguous | +| **Dev workflow** | Simple `cd src && ansible-playbook` | Needs collection install step | +| **CI/CD complexity** | Simple | Build + publish pipeline | +| **Ansible ecosystem alignment** | Non-standard | Standard Galaxy/Hub format | +| **LLM/AI productivity** | Needs `ansible.cfg` context | FQCN is self-documenting | + +### Recommendation + +**Convert — but phased. Start with `dell.omnia_common` to prove the pattern before touching domain code.** + +--- + +## 6. Proposed Collection Architecture + +### Omnia Collection Namespace + +``` +dell.omnia_common ← Shared modules, module_utils, callback plugins +dell.omnia_image_build ← Image build domain (roles, playbooks, domain modules) +dell.omnia_repo_manager ← Repo manager domain +dell.omnia_orchestrator ← Orchestrator domain +dell.omnia_discovery ← Discovery domain +dell.omnia_telemetry ← Telemetry domain +``` + +### Dependency Graph + +``` + ┌─────────────────────┐ + │ dell.omnia_common │ + │ (shared modules, │ + │ utils, callback) │ + └─────────┬───────────┘ + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ dell.omnia_ │ │ dell.omnia_ │ │ dell.omnia_ │ +│ image_build │ │ orchestrator │ │ repo_manager │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ + ┌──────────┐ ┌──────────┐ + │ ansible. │ │ ansible. │ + │ utils │ │ netcommon│ + └──────────┘ └──────────┘ +``` + +--- + +## 7. Collection Structure Details + +### 7.1 `dell.omnia_common` — Shared Collection + +``` +dell/omnia_common/ +├── galaxy.yml +├── plugins/ +│ ├── modules/ +│ │ ├── generate_xname_in_mapping_file.py +│ │ ├── update_node_object.py +│ │ ├── ... (49 modules from common/library/modules/) +│ ├── module_utils/ +│ │ ├── omnia_logging.py +│ │ ├── config_manager.py +│ │ ├── ... (68 items from common/library/module_utils/) +│ ├── callback/ +│ │ └── omnia_default.py +│ └── filter/ # Optional: shared Jinja2 filters +│ └── omnia_filters.py +├── roles/ # Shared roles (if any) +├── tests/ +│ ├── unit/ +│ │ └── plugins/ +│ │ └── modules/ +│ │ └── test_generate_xname.py +│ └── sanity/ +├── docs/ +├── meta/ +│ └── runtime.yml +└── README.md +``` + +**galaxy.yml:** + +```yaml +namespace: dell +name: omnia_common +version: 1.0.0 +readme: README.md +authors: + - Dell Technologies +description: >- + Shared Ansible modules, module utilities, and callback plugins for + the Omnia HPC cluster management platform. +license: + - Apache-2.0 +tags: + - dell + - omnia + - hpc + - infrastructure +repository: https://github.com/dell/omnia +dependencies: + ansible.utils: ">=2.0.0" + community.general: ">=7.0.0" +build_ignore: + - "*.tar.gz" + - ".git" + - ".github" +``` + +### 7.2 `dell.omnia_image_build` — Domain Collection + +``` +dell/omnia_image_build/ +├── galaxy.yml +├── plugins/ +│ ├── modules/ +│ │ ├── validate_yaml_schema.py +│ │ ├── validate_image_build_config.py +│ │ ├── image_package_collector.py +│ │ ├── base_image_package_collector.py +│ │ ├── functional_group_parser.py +│ │ └── generate_functional_groups.py +│ ├── module_utils/ +│ │ ├── build_image/ +│ │ │ ├── __init__.py +│ │ │ ├── common_functions.py +│ │ │ └── config.py +│ │ └── image_build_validation/ +│ │ ├── __init__.py +│ │ ├── image_build_validation_flow.py +│ │ └── schema/ +│ │ ├── config.schema.json +│ │ ├── image_build_config.json +│ │ ├── image_build_credentials.json +│ │ └── functional_groups_config.json +│ └── callback/ # (empty — uses dell.omnia_common callback) +├── roles/ +│ ├── setup/ # image_build_setup +│ │ ├── tasks/ +│ │ ├── vars/ +│ │ └── defaults/ +│ ├── deploy_minio/ +│ ├── deploy_registry/ +│ ├── build_os_images/ +│ ├── fetch_build_packages/ +│ ├── collect_credentials/ +│ ├── validate_input/ +│ ├── validate_runtime/ +│ ├── cleanup/ +│ ├── prepare_aarch64/ +│ ├── generate_functional_groups/ +│ └── upgrade/ +├── playbooks/ +│ ├── image_build_manager.yml # Entry point +│ ├── prepare.yml +│ ├── build_x86_64.yml +│ ├── build_aarch64.yml +│ ├── cleanup.yml +│ ├── validate.yml +│ ├── get_credentials.yml +│ ├── upgrade.yml +│ └── rollback.yml +├── tests/ +├── docs/ +├── meta/ +│ └── runtime.yml +└── README.md +``` + +**galaxy.yml:** + +```yaml +namespace: dell +name: omnia_image_build +version: 1.0.0 +readme: README.md +authors: + - Dell Technologies +description: >- + Ansible collection for building OS images (RHEL/Rocky) for Omnia HPC clusters. + Deploys MinIO S3 + OCI Registry and builds bare-metal OS images via OpenCHAMI. +license: + - Apache-2.0 +tags: + - dell + - omnia + - hpc + - image-build + - openchami +repository: https://github.com/dell/omnia +dependencies: + dell.omnia_common: ">=1.0.0" + ansible.utils: ">=2.0.0" + community.general: ">=7.0.0" + containers.podman: ">=1.10.0" +build_ignore: + - "*.tar.gz" + - ".git" + - ".github" + - "test/" +``` + +--- + +## 8. How to Use `dell.omnia_common` From Other Collections + +This is the critical pattern — how domain collections consume shared modules. + +### 8.1 Declare Dependency in `galaxy.yml` + +Every domain collection that uses shared modules MUST declare the dependency: + +```yaml +# dell/omnia_image_build/galaxy.yml +dependencies: + dell.omnia_common: ">=1.0.0" +``` + +When users install `dell.omnia_image_build`, Galaxy automatically installs `dell.omnia_common` too. + +### 8.2 Use Shared Modules in Playbooks/Tasks (FQCN) + +```yaml +# BEFORE (bare module name — requires ansible.cfg path hack): +- name: Generate xnames + generate_xname_in_mapping_file: + mapping_file_path: "{{ mapping_file }}" + +# AFTER (FQCN — works anywhere, no ansible.cfg needed): +- name: Generate xnames + dell.omnia_common.generate_xname_in_mapping_file: + mapping_file_path: "{{ mapping_file }}" +``` + +### 8.3 Use Shared Module Utils in Python Code + +When writing a custom module in `dell.omnia_image_build` that needs utilities from `dell.omnia_common`: + +```python +# BEFORE (flat import — requires ansible.cfg module_utils path): +from ansible.module_utils.omnia_logging import OmniaLogger + +# AFTER (collection-aware import): +from ansible_collections.dell.omnia_common.plugins.module_utils.omnia_logging import OmniaLogger +``` + +### 8.4 Use Shared Callback Plugin + +```ini +# BEFORE (ansible.cfg — requires callback_plugins path): +[defaults] +stdout_callback = omnia_default +callback_plugins = ../common/callback_plugins + +# AFTER (FQCN — no path needed): +[defaults] +stdout_callback = dell.omnia_common.omnia_default +``` + +### 8.5 Use Shared Roles From Another Collection + +```yaml +# If dell.omnia_common had shared roles: +- name: Run common setup + ansible.builtin.include_role: + name: dell.omnia_common.base_setup +``` + +### 8.6 Use Domain Collection Playbooks + +```bash +# Run a collection playbook directly: +ansible-playbook dell.omnia_image_build.image_build_manager --tags prepare + +# Or reference in another playbook: +- name: Build images + ansible.builtin.import_playbook: dell.omnia_image_build.image_build_manager +``` + +### 8.7 Use Shared Filters (Optional) + +If `dell.omnia_common` includes custom Jinja2 filters: + +```yaml +# In any domain task: +- name: Format output + ansible.builtin.debug: + msg: "{{ my_var | dell.omnia_common.omnia_format }}" +``` + +--- + +## 9. Development Workflow + +### 9.1 Local Development (Editable Mode) + +During development, you don't want to rebuild and reinstall the collection after every change. +Use symlinks: + +```bash +# Create the collection namespace directory structure +mkdir -p ~/.ansible/collections/ansible_collections/dell + +# Symlink your local development directories +ln -s /path/to/omnia_common ~/.ansible/collections/ansible_collections/dell/omnia_common +ln -s /path/to/omnia_image_build ~/.ansible/collections/ansible_collections/dell/omnia_image_build + +# Now ansible-playbook resolves FQCN from your local source +ansible-playbook dell.omnia_image_build.image_build_manager +``` + +### 9.2 Alternative: `COLLECTIONS_PATHS` Override + +```bash +# Point Ansible to your dev collections +export ANSIBLE_COLLECTIONS_PATHS=/path/to/dev/collections:~/.ansible/collections + +# Or in ansible.cfg: +[defaults] +collections_paths = /path/to/dev/collections:~/.ansible/collections +``` + +### 9.3 Building a Collection Tarball + +```bash +cd /path/to/dell/omnia_common +ansible-galaxy collection build +# → dell-omnia_common-1.0.0.tar.gz +``` + +### 9.4 Installing From Tarball (Air-Gap) + +```bash +# On the air-gapped host: +ansible-galaxy collection install ./dell-omnia_common-1.0.0.tar.gz +ansible-galaxy collection install ./dell-omnia_image_build-1.0.0.tar.gz +``` + +### 9.5 Installing From Galaxy or Automation Hub + +```bash +# From requirements.yml: +ansible-galaxy collection install -r requirements.yml + +# Or directly: +ansible-galaxy collection install dell.omnia_image_build:1.0.0 +``` + +### 9.6 Verifying Installation + +```bash +# List installed collections +ansible-galaxy collection list | grep dell + +# Expected output: +# dell.omnia_common 1.0.0 +# dell.omnia_image_build 1.0.0 + +# Verify module is accessible +ansible-doc dell.omnia_common.generate_xname_in_mapping_file +ansible-doc dell.omnia_image_build.validate_yaml_schema +``` + +--- + +## 10. Testing Collections + +### 10.1 Sanity Tests (Built-in) + +```bash +cd /path/to/dell/omnia_common +ansible-test sanity --docker default +``` + +Checks: Python syntax, import validation, documentation, GPL license, metaclass boilerplate, etc. + +### 10.2 Unit Tests + +```bash +# tests/unit/plugins/modules/test_generate_xname.py +ansible-test units --docker default +``` + +### 10.3 Integration Tests + +```bash +# tests/integration/targets/validate_yaml_schema/tasks/main.yml +ansible-test integration --docker default validate_yaml_schema +``` + +--- + +## 11. Migration Plan + +### Phase 0: Preparation (1 sprint) + +| Step | Action | +|------|--------| +| 0.1 | **Audit module usage** — map which domain uses which `common/` modules. Build dependency matrix. | +| 0.2 | **Choose namespace** — `dell.omnia_common`, `dell.omnia_image_build`, etc. | +| 0.3 | **Register namespace** on Galaxy or Private Automation Hub. | +| 0.4 | **Set up collection CI** — `ansible-test sanity`, `ansible-test units`, build + publish pipeline. | + +### Phase 1: `dell.omnia_common` (2 sprints) + +| Step | Action | +|------|--------| +| 1.1 | Scaffold `dell.omnia_common` using `ansible-galaxy collection init dell.omnia_common` | +| 1.2 | Move 49 modules from `common/library/modules/` → `plugins/modules/` | +| 1.3 | Move 68 module_utils from `common/library/module_utils/` → `plugins/module_utils/` | +| 1.4 | Update all Python imports: `from ansible.module_utils.xyz` → `from ansible_collections.dell.omnia_common.plugins.module_utils.xyz` | +| 1.5 | Move callback plugin → `plugins/callback/omnia_default.py` | +| 1.6 | Run `ansible-test sanity` — fix all issues | +| 1.7 | Build tarball: `ansible-galaxy collection build` | +| 1.8 | Update mono-repo — add `dell.omnia_common` to `requirements.yml` | +| 1.9 | Update all domain playbooks — FQCN for every `common/` module call | +| 1.10 | Test — run all domain playbooks, verify FQCN resolution | + +### Phase 2: `dell.omnia_image_build` — Pilot (1-2 sprints) + +| Step | Action | +|------|--------| +| 2.1 | Scaffold `dell.omnia_image_build` | +| 2.2 | Move 6 domain modules → `plugins/modules/` | +| 2.3 | Move domain module_utils → `plugins/module_utils/` | +| 2.4 | Move 11 roles → `roles/` (rename to shorter names) | +| 2.5 | Move playbooks → collection `playbooks/` | +| 2.6 | Update all FQCN in tasks | +| 2.7 | Update `galaxy.yml` dependencies — `dell.omnia_common: ">=1.0.0"` | +| 2.8 | Remove all `ansible.cfg` path overrides | +| 2.9 | Run `ansible-test sanity` + existing tests | +| 2.10 | Full end-to-end test with collection-installed roles/modules | + +### Phase 3: Other Domain Collections (parallel, 1-2 sprints each) + +- `dell.omnia_orchestrator` +- `dell.omnia_discovery` +- `dell.omnia_telemetry` +- `dell.omnia_repo_manager` + +### Phase 4: Integration Repo Update + +```yaml +# dell/omnia/requirements.yml (updated) +collections: + - name: dell.omnia_common + version: ">=1.0.0" + - name: dell.omnia_image_build + version: ">=1.0.0" + - name: dell.omnia_orchestrator + version: ">=1.0.0" + - name: dell.omnia_discovery + version: ">=1.0.0" + - name: dell.omnia_repo_manager + version: ">=1.0.0" + - name: ansible.utils + version: ">=2.0.0" + - name: community.general + version: ">=7.0.0" + - name: containers.podman + version: ">=1.10.0" +``` + +```bash +# dell/omnia/omnia.sh (updated) +#!/bin/bash +python3 -m venv /opt/omnia/venv +source /opt/omnia/venv/bin/activate +pip install -r requirements.txt +ansible-galaxy collection install -r requirements.yml # installs all domain collections +ansible-playbook dell.omnia_image_build.image_build_manager --tags prepare +``` + +--- + +## 12. Before vs After — Concrete Example + +### Before: Using `common/` module in a task + +```yaml +# ansible.cfg (required path hack): +# library = ../common/library/modules +# module_utils = ../common/library/module_utils +# callback_plugins = ../common/callback_plugins + +# In a role task: +- name: Generate xnames + generate_xname_in_mapping_file: + mapping_file_path: "{{ mapping_file }}" +``` + +```python +# In a custom module (flat import): +from ansible.module_utils.omnia_logging import OmniaLogger +``` + +### After: Using `dell.omnia_common` module + +```yaml +# ansible.cfg — NO path hacks needed: +# [defaults] +# stdout_callback = dell.omnia_common.omnia_default + +# In a role task (FQCN): +- name: Generate xnames + dell.omnia_common.generate_xname_in_mapping_file: + mapping_file_path: "{{ mapping_file }}" +``` + +```python +# In a custom module (collection-aware import): +from ansible_collections.dell.omnia_common.plugins.module_utils.omnia_logging import OmniaLogger +``` + +### Before: Using domain module + +```yaml +# ansible.cfg: +# library = library/modules +# module_utils = library/module_utils + +- name: Validate config + validate_yaml_schema: + yaml_file: "{{ config_path }}" + schema_file: "{{ schema_path }}" +``` + +### After: Using domain collection module + +```yaml +# No ansible.cfg path override needed + +- name: Validate config + dell.omnia_image_build.validate_yaml_schema: + yaml_file: "{{ config_path }}" + schema_file: "{{ schema_path }}" +``` + +--- + +## 13. `ansible.cfg` Simplification + +### Before (path hacks everywhere) + +```ini +[defaults] +library = library/modules:../common/library/modules +module_utils = library/module_utils:../common/library/module_utils +callback_plugins = callback_plugins:../common/callback_plugins +roles_path = roles +stdout_callback = omnia_default +``` + +### After (collections handle everything) + +```ini +[defaults] +stdout_callback = dell.omnia_common.omnia_default +roles_path = roles + +# No library, module_utils, or callback_plugins paths needed. +# Collections are resolved via COLLECTIONS_PATHS or default ~/.ansible/collections/ +``` + +--- + +## 14. Quick Reference — Common Operations + +| Operation | Command | +|-----------|---------| +| **Init new collection** | `ansible-galaxy collection init dell.omnia_` | +| **Build tarball** | `ansible-galaxy collection build` | +| **Install from tarball** | `ansible-galaxy collection install ./dell-omnia_common-1.0.0.tar.gz` | +| **Install from Galaxy** | `ansible-galaxy collection install dell.omnia_common` | +| **Install from requirements** | `ansible-galaxy collection install -r requirements.yml` | +| **List installed** | `ansible-galaxy collection list \| grep dell` | +| **View module docs** | `ansible-doc dell.omnia_common.generate_xname_in_mapping_file` | +| **Run sanity tests** | `ansible-test sanity --docker default` | +| **Run unit tests** | `ansible-test units --docker default` | +| **Dev symlink** | `ln -s /path/to/src ~/.ansible/collections/ansible_collections/dell/omnia_common` | + +--- + +## 15. Input Handling in Collections + +### The Problem + +In the current flat repo, input files live relative to the playbook: + +``` +image-build-manager/ +├── config.yml ← User edits this (repo root) +└── src/ + ├── image_build_manager.yml ← playbook_dir + └── input/ + └── project_default/ ← {{ playbook_dir }}/input/{{ project_name }} + ├── image_build_config.yml + ├── image_build_credentials.yml + └── repo_manager_output/ + ├── repo_status.yml + └── functional_group_packages.yml +``` + +Tasks reference files via `{{ playbook_dir }}/../config.yml` and `{{ playbook_dir }}/input/...`. + +**In a collection**, playbooks are installed into a **read-only** location +(`~/.ansible/collections/ansible_collections/dell/omnia_image_build/playbooks/`). +User config files **cannot** live inside the collection directory. + +### The Solution: Workspace-Based Input + +Collections separate **code** (read-only, versioned) from **user data** (writable, per-project). + +The user works from a **workspace directory** — a local directory where they keep their config +and input files. A thin wrapper playbook or the collection playbook reads user data from the +workspace via variables. + +``` +# User's workspace (NOT inside the collection): +~/my-omnia-project/ ← User's working directory +├── config.yml ← Project config (user-editable) +├── requirements.yml ← Collection dependencies +├── site.yml ← Thin wrapper playbook (optional) +└── input/ + └── project_default/ + ├── image_build_config.yml ← Domain config + ├── image_build_credentials.yml ← Vault-encrypted credentials + └── repo_manager_output/ + ├── repo_status.yml ← From repo_manager + └── functional_group_packages.yml + +# Collection (read-only, installed via ansible-galaxy): +~/.ansible/collections/ansible_collections/dell/omnia_image_build/ +├── playbooks/ +│ └── image_build_manager.yml ← Collection playbook +├── roles/ +│ └── setup/ ← Reads config from workspace +└── plugins/ + └── modules/ ← Domain modules +``` + +### Three Input Patterns for Collections + +#### Pattern A: `--extra-vars` (Simplest) + +Pass the workspace path as an extra variable: + +```bash +# User runs from their workspace: +cd ~/my-omnia-project + +ansible-playbook dell.omnia_image_build.image_build_manager \ + -e "workspace_dir=$(pwd)" \ + --tags prepare +``` + +Inside the collection roles, all paths are derived from `workspace_dir`: + +```yaml +# roles/setup/tasks/load_config.yml (inside collection) +- name: Load config.yml from workspace + ansible.builtin.include_vars: + file: "{{ workspace_dir }}/config.yml" + name: standalone_config + +- name: Set input directory from workspace + ansible.builtin.set_fact: + input_project_dir: "{{ workspace_dir }}/input/{{ standalone_config.project_name }}" + cacheable: true +``` + +#### Pattern B: Thin Wrapper Playbook (Recommended) + +User creates a minimal `site.yml` in their workspace that calls the collection playbook: + +```yaml +# ~/my-omnia-project/site.yml — User's wrapper playbook +--- +- name: Load workspace config + hosts: localhost + connection: local + gather_facts: false + tags: always + tasks: + - name: Set workspace directory + ansible.builtin.set_fact: + workspace_dir: "{{ playbook_dir }}" + cacheable: true + +- name: Run image build manager + ansible.builtin.import_playbook: dell.omnia_image_build.image_build_manager +``` + +```bash +cd ~/my-omnia-project +ansible-playbook site.yml --tags prepare +``` + +Now `playbook_dir` resolves to the user's workspace, and the collection roles pick up +`workspace_dir` to find config files. + +#### Pattern C: Environment Variable + +```bash +export OMNIA_WORKSPACE=~/my-omnia-project +ansible-playbook dell.omnia_image_build.image_build_manager --tags prepare +``` + +```yaml +# Inside collection role: +- name: Set workspace from environment + ansible.builtin.set_fact: + workspace_dir: "{{ lookup('env', 'OMNIA_WORKSPACE') | default(playbook_dir + '/..') }}" +``` + +### Input Resolution Priority + +The setup role resolves input paths in this priority order: + +```yaml +# 1. Explicit extra-var (highest priority) +workspace_dir: "{{ workspace_dir }}" + +# 2. Environment variable +workspace_dir: "{{ lookup('env', 'OMNIA_WORKSPACE') | default('') }}" + +# 3. Wrapper playbook (playbook_dir = user's workspace) +workspace_dir: "{{ playbook_dir }}" + +# 4. Fallback — collection playbook_dir + parent (development/flat-repo mode) +workspace_dir: "{{ playbook_dir + '/..' }}" +``` + +Collection setup role implementation: + +```yaml +# dell/omnia_image_build/roles/setup/tasks/resolve_workspace.yml +- name: Resolve workspace directory + ansible.builtin.set_fact: + workspace_dir: >- + {{ workspace_dir | default( + lookup('env', 'OMNIA_WORKSPACE') | default( + playbook_dir + '/..', true + ), true + ) }} + cacheable: true + +- name: Validate workspace directory exists + ansible.builtin.stat: + path: "{{ workspace_dir }}/config.yml" + register: _ws_config + +- name: Fail if workspace config not found + ansible.builtin.fail: + msg: >- + config.yml not found in workspace '{{ workspace_dir }}'. + Either: + 1. Run from your project directory: cd ~/my-project && ansible-playbook site.yml + 2. Pass workspace: ansible-playbook dell.omnia_image_build.image_build_manager -e workspace_dir=/path/to/project + 3. Set environment: export OMNIA_WORKSPACE=/path/to/project + when: not _ws_config.stat.exists +``` + +### Input File Mapping (Flat Repo → Collection) + +| Input File | Flat Repo Path | Collection Path | +|-----------|---------------|-----------------| +| `config.yml` | `{{ playbook_dir }}/../config.yml` | `{{ workspace_dir }}/config.yml` | +| `image_build_config.yml` | `{{ playbook_dir }}/input//image_build_manager/image_build_config.yml` | `{{ workspace_dir }}/input//image_build_manager/image_build_config.yml` | +| `image_build_credentials.yml` | `{{ playbook_dir }}/input//image_build_credentials.yml` | `{{ workspace_dir }}/input//image_build_credentials.yml` | +| `repo_status.yml` | `/opt/omnia/repo_manager/output//repo_status.yml` | Same (absolute path — unchanged) | +| `functional_group_packages.yml` | `{{ playbook_dir }}/input//repo_manager_output/functional_group_packages.yml` | `{{ workspace_dir }}/input//repo_manager_output/functional_group_packages.yml` | +| JSON schemas | `{{ playbook_dir }}/library/module_utils/.../schema/*.json` | `{{ ansible_collections_path }}/.../plugins/module_utils/.../schema/*.json` (inside collection) | + +**Key insight**: User-editable files come from `workspace_dir`. Read-only assets (schemas, +templates) ship inside the collection. Upstream contract files (repo_status.yml) use absolute +paths configured in `config.yml`. + +--- + +## 16. Inter-Domain Output Handling (Stage Chaining) + +### The Problem + +Omnia domains run in sequence. Each domain produces output that the next domain consumes: + +``` +repo_manager → repo_status.yml → image_build_manager → build_status.yml → orchestrator +``` + +In the current flat repo, output paths are hardcoded or derived from `shared_path`: + +```yaml +# repo_manager writes to: +/opt/omnia/repo_manager/output/project_default/repo_status.yml + +# image_build_manager reads from: +repo_manager_output_dir: "/opt/omnia/repo_manager/output/project_default/" + +# image_build_manager writes to: +{{ shared_path }}/output/{{ project_name }}/build_status.yml + +# orchestrator reads from: +{{ shared_path }}/output/{{ project_name }}/build_status.yml +``` + +### How Collections Handle Inter-Domain Contracts + +Collections don't change the contract mechanism — **file-based YAML contracts stay the same**. +The output files live on the filesystem (under `/`), not inside any collection. + +``` +┌───────────────────────────────────┐ +│ File System │ +│ (state_path = /opt/omnia/...) │ +│ │ +│ repo_manager/ │ +│ └── output/project_default/ │ +│ └── repo_status.yml ──────────────┐ +│ │ │ +│ image_build_manager/ │ │ +│ └── output/project_default/ │ │ +│ └── build_status.yml ─────────────────┐ +│ │ │ │ +└───────────────────────────────────┘ │ │ + │ │ +┌──────────────────────────────┐ │ │ +│ dell.omnia_image_build │ │ │ +│ (collection — read-only) │◄──────────┘ │ +│ │ │ +│ roles/setup reads │ │ +│ repo_status.yml │ │ +│ from configured path │ │ +└──────────────────────────────┘ │ + │ +┌──────────────────────────────┐ │ +│ dell.omnia_orchestrator │ │ +│ (collection — read-only) │◄──────────────┘ +│ │ +│ roles/setup reads │ +│ build_status.yml │ +│ from configured path │ +└──────────────────────────────┘ +``` + +### Contract Path Configuration + +Each domain's `config.yml` declares where to find upstream contracts: + +```yaml +# ~/my-omnia-project/config.yml +project_name: "project_default" + +host: + hostname: "myhost" + shared_path: "/opt/omnia/image_build_manager" + domain_name: "local" + admin_nic_ip: "10.20.0.1" + +# Upstream contract paths (where to find other domains' output) +upstream: + repo_manager_output_dir: "/opt/omnia/repo_manager/output/project_default" + pulp_cert_path: "/opt/omnia/pulp/settings/certs/pulp_webserver.crt" +``` + +### Output Location + +Output is written to the filesystem (never inside the collection): + +```yaml +# Collection role writes output to state_path (from config.yml): +- name: Write build_status.yml + ansible.builtin.template: + src: build_status.yml.j2 + dest: "{{ output_project_dir }}/build_status.yml" + # output_project_dir = {{ shared_path }}/output/{{ project_name }} + # e.g., /opt/omnia/image_build_manager/output/project_default/build_status.yml +``` + +### Integration Repo Orchestration + +In the `dell/omnia` integration repo, `omnia.sh` chains domain collections: + +```bash +#!/bin/bash +# dell/omnia/omnia.sh — Orchestrator + +source /opt/omnia/venv/bin/activate + +# Each domain reads upstream contracts from well-known state_path locations. +# repo_manager writes to /opt/omnia/repo_manager/output/ +# image_build_manager reads from there, writes to /opt/omnia/image_build_manager/output/ +# orchestrator reads from image_build_manager output + +ansible-playbook dell.omnia_repo_manager.repo_manager \ + -e workspace_dir=/opt/omnia/workspace + +ansible-playbook dell.omnia_image_build.image_build_manager \ + -e workspace_dir=/opt/omnia/workspace \ + --tags prepare + +ansible-playbook dell.omnia_image_build.image_build_manager \ + -e workspace_dir=/opt/omnia/workspace \ + --tags build + +ansible-playbook dell.omnia_orchestrator.orchestrator \ + -e workspace_dir=/opt/omnia/workspace +``` + +### Contract Validation in Collections + +Each collection's setup role validates upstream contracts on startup: + +```yaml +# dell/omnia_image_build/roles/setup/tasks/validate_upstream.yml +- name: Check repo_status.yml exists + ansible.builtin.stat: + path: "{{ upstream_repo_manager_output_dir }}/repo_status.yml" + register: _repo_status_file + +- name: Fail if repo_status.yml not found + ansible.builtin.fail: + msg: >- + Upstream contract file not found: {{ upstream_repo_manager_output_dir }}/repo_status.yml. + Run repo_manager first: + ansible-playbook dell.omnia_repo_manager.repo_manager -e workspace_dir={{ workspace_dir }} + when: not _repo_status_file.stat.exists + +- name: Load repo_status.yml + ansible.builtin.include_vars: + file: "{{ upstream_repo_manager_output_dir }}/repo_status.yml" + name: repo_status + +- name: Validate repo_manager completed successfully + ansible.builtin.assert: + that: + - repo_status.overall_status == 'success' + fail_msg: "repo_manager did not complete successfully. Re-run repo_manager." +``` + +--- + +## 17. Tag Handling in Collections + +### How Tags Work With Collection Playbooks + +Ansible tags work **identically** in collection playbooks as in flat-repo playbooks. +The `--tags` flag is passed at the CLI and applies to all plays/tasks regardless of where +the playbook lives. + +```bash +# Flat repo (current): +ansible-playbook image_build_manager.yml --tags prepare + +# Collection (same tags, same behavior): +ansible-playbook dell.omnia_image_build.image_build_manager --tags prepare + +# Via wrapper playbook: +ansible-playbook site.yml --tags prepare +``` + +### Tag Architecture in Collection + +The entry point playbook inside the collection uses the same tag structure: + +```yaml +# dell/omnia_image_build/playbooks/image_build_manager.yml + +# Step 0: Setup (always runs — loads config, validates tags) +- name: Image build manager setup + hosts: localhost + connection: local + gather_facts: false + tags: always + roles: + - dell.omnia_image_build.setup # FQCN for collection role + +# Step 1: Validate +- name: Validate configuration + ansible.builtin.import_playbook: dell.omnia_image_build.validate + tags: + - always + - validate + +# Step 2: Credentials (skipped for cleanup/validate) +- name: Get build credentials + ansible.builtin.import_playbook: dell.omnia_image_build.get_credentials + when: not (skip_build_credentials | default(false) | bool) + tags: always + +# FLOW: prepare +- name: Prepare infrastructure + ansible.builtin.import_playbook: dell.omnia_image_build.prepare + tags: + - prepare + +# FLOW: build +- name: Build x86_64 images + ansible.builtin.import_playbook: dell.omnia_image_build.build_x86_64 + tags: + - x86_64 + - build + +- name: Build aarch64 images + ansible.builtin.import_playbook: dell.omnia_image_build.build_aarch64 + when: hostvars['localhost']['aarch64_inventory_host_ip'] | default('') | length > 0 + tags: + - aarch64 + - build + +# FLOW: cleanup (opt-in) +- name: Cleanup + ansible.builtin.import_playbook: dell.omnia_image_build.cleanup + tags: + - never + - cleanup + +# FLOW: upgrade (opt-in) +- name: Upgrade + ansible.builtin.import_playbook: dell.omnia_image_build.upgrade + tags: + - never + - upgrade + +# FLOW: rollback (opt-in) +- name: Rollback + ansible.builtin.import_playbook: dell.omnia_image_build.rollback + tags: + - never + - rollback +``` + +### Tag Validation Inside Collection Roles + +The setup role validates tags using `ansible_run_tags` — this works the same in collections: + +```yaml +# dell/omnia_image_build/roles/setup/vars/main.yml +supported_tags: + - prepare + - build + - cleanup + - validate + - upgrade + - rollback + +skip_credential_tags: + - cleanup + - validate + +invalid_tag_combinations: + - [prepare, cleanup] + - [build, cleanup] + - [prepare, validate] + - [build, validate] + - [prepare, upgrade] + - [build, upgrade] + - [cleanup, upgrade] + - [prepare, rollback] + - [build, rollback] + - [cleanup, rollback] + - [upgrade, rollback] +``` + +```yaml +# dell/omnia_image_build/roles/setup/tasks/validate_tags.yml +- name: Get provided tags + ansible.builtin.set_fact: + provided_tags: "{{ ansible_run_tags | difference(['always', 'all']) }}" + +- name: Fail if invalid tags + ansible.builtin.fail: + msg: "{{ tag_validation_fail_msg }}" + when: + - provided_tags | length > 0 + - (provided_tags | difference(supported_tags)) | length > 0 + +- name: Check invalid combinations + ansible.builtin.set_fact: + has_invalid_combination: true + when: + - provided_tags | length > 1 + - item | difference(provided_tags) | length == 0 + loop: "{{ invalid_tag_combinations }}" + +- name: Fail if invalid combination + ansible.builtin.fail: + msg: "{{ tag_combination_fail_msg }}" + when: has_invalid_combination | default(false) | bool + +- name: Set skip_credentials for cleanup/validate + ansible.builtin.set_fact: + skip_build_credentials: true + cacheable: true + when: + - provided_tags | length > 0 + - (provided_tags | intersect(skip_credential_tags)) | length > 0 +``` + +### Tag Behavior Summary + +| Tag | What Runs | Credentials | Notes | +|-----|-----------|-------------|-------| +| *(none)* | setup → validate → credentials → prepare → build | Yes | Default full flow | +| `prepare` | setup → validate → credentials → prepare | Yes | Deploy MinIO + Registry | +| `build` | setup → validate → credentials → build | Yes | Build images only | +| `validate` | setup → validate | No | Config validation only | +| `cleanup` | setup → cleanup | No | Remove all artifacts | +| `upgrade` | setup → upgrade | No | Version migration | +| `rollback` | setup → rollback | No | Revert to previous version | + +### Key Nuances for Collection Tags + +1. **`tags: always`** works the same — setup role always runs regardless of which tag is passed. +2. **`tags: [never, cleanup]`** works the same — cleanup only runs when explicitly requested. +3. **`ansible_run_tags`** variable is available inside collection roles — tag validation code is unchanged. +4. **`import_playbook` with FQCN** — `dell.omnia_image_build.prepare` resolves to the collection's `playbooks/prepare.yml`. Tags on `import_playbook` work identically. +5. **Wrapper playbook tags pass through** — when `site.yml` imports a collection playbook, `--tags prepare` applies to both the wrapper and the imported collection playbook. + +### Tag Flow Diagram (Collection) + +``` +User: ansible-playbook dell.omnia_image_build.image_build_manager --tags prepare + + ┌──────────────────────────────────────────────────────────────────────┐ + │ Collection: dell.omnia_image_build │ + │ │ + │ image_build_manager.yml │ + │ │ │ + │ ├── [tags: always] setup role │ + │ │ ├── resolve_workspace.yml ← find config.yml │ + │ │ ├── validate_tags.yml ← validate 'prepare' is valid │ + │ │ ├── load_config.yml ← load workspace/config.yml │ + │ │ └── validate_prereqs.yml ← check upstream contracts │ + │ │ │ + │ ├── [tags: always] validate playbook │ + │ │ └── schema + logic checks │ + │ │ │ + │ ├── [tags: always] credentials playbook │ + │ │ └── prompts user for S3 + provision credentials │ + │ │ │ + │ ├── [tags: prepare] ← MATCHES → prepare playbook RUNS │ + │ │ ├── deploy_minio role │ + │ │ └── deploy_registry role │ + │ │ │ + │ ├── [tags: build] ← NO MATCH → SKIPPED │ + │ ├── [tags: never, cleanup] ← NO MATCH → SKIPPED │ + │ ├── [tags: never, upgrade] ← NO MATCH → SKIPPED │ + │ └── [tags: never, rollback] ← NO MATCH → SKIPPED │ + │ │ + └──────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 18. Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|-----------| +| Migration breaks existing playbooks | All domains fail | Phase 1 only touches `common/` — domains migrate one at a time | +| `module_utils` import path change | Python ImportError in modules | Automated find-replace + `ansible-test sanity` catches all | +| Dev workflow friction | Slower development cycle | Symlink pattern for local development | +| Collection version conflicts | Incompatible versions installed | Pin versions in `requirements.yml` + CI validation | +| Air-gap distribution | Can't reach Galaxy | Build tarballs and distribute via Pulp or file copy | +| Galaxy namespace reservation | Someone else takes `dell` | Register namespace early on Galaxy | +| Large `common/` collection | Slow install, bloated deps | Split further if needed (e.g., `dell.omnia_network`, `dell.omnia_storage`) | + +--- + +## 16. References + +- [Ansible Collection Developer Guide](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html) +- [ansible-galaxy CLI](https://docs.ansible.com/ansible/latest/cli/ansible-galaxy.html) +- [Collection Structure](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections_structure.html) +- [Using Collections in Playbooks](https://docs.ansible.com/ansible/latest/collections_guide/collections_using_playbooks.html) +- [ansible-test](https://docs.ansible.com/ansible/latest/dev_guide/testing.html) +- [Private Automation Hub](https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/) diff --git a/docs/design/omnia-domain-repo-design.md b/docs/design/omnia-domain-repo-design.md new file mode 100644 index 0000000000..f207e189a5 --- /dev/null +++ b/docs/design/omnia-domain-repo-design.md @@ -0,0 +1,767 @@ +# Omnia Domain Repository — Generic Design & Coding Standard + +**Version**: 1.0 +**Audience**: All Omnia domain developers (image_build_manager, repo_manager, provision, telemetry, etc.) +**Purpose**: Standard repository structure, coding rules, and conventions that every Omnia domain repository MUST follow. + +--- + +## 1. Overview + +Each Omnia domain (e.g., `image_build_manager`, `repo_manager`, `provision`) is an **independent Ansible-based repository** that: + +- Runs standalone on a RHEL host via `ansible-playbook` +- Has its own `config.yml`, input files, and output artifacts +- Communicates with other domains only through well-defined file contracts (YAML) +- Does NOT require `omnia_core` container or the Omnia mono-repo to execute + +### Execution Mode + +All domains run directly on the RHEL host via `ansible-playbook` (bare-metal). +No container runtime is required for the playbook itself (Podman is used only for image builds). + +**Rationale:** +- **No path translation** — all file paths are host-native, no bind-mount gymnastics +- **No privilege escalation** — no need for `--privileged` containers or UID mapping +- **Simpler debugging** — logs, processes, and filesystem are directly accessible +- **Better host integration** — systemd services (MinIO, Registry via Quadlet) are managed natively +- **Air-gap friendly** — no container runtime needed for the playbook itself + +### Validated Environment + +| Component | Minimum Version | Validated Version | +|-----------|----------------|-------------------| +| **Python** | 3.12+ | 3.12.8 | +| **Ansible Core** | 2.20+ | 2.20.0 | +| **RHEL** | 10.0+ | 10.0 | +| **Podman** | 5.0+ | 5.3.1 (for container builds only) | + +> **NOTE**: All code MUST be compatible with Python 3.12+ and Ansible 2.20+. Do NOT use deprecated Ansible modules or Python 2 patterns. + +--- + +## 2. Repository Structure + +Every domain repository MUST follow this layout: + +``` +/ +├── config.yml.sample # Sample config — users copy to config.yml +├── requirements.txt # Python deps: ansible-core>=2.20, jmespath, etc. +├── requirements.yml # Ansible Galaxy collections +├── README.md # Quick start, prerequisites, usage +├── .gitignore +├── .github/ +│ └── workflows/ci.yml # CI pipeline (lint, test) +├── docs/ # Domain-specific documentation +│ ├── design/ # Architecture and design documents +│ ├── code-style/ # Code style guides (ansible, python, jinja2, etc.) +│ └── contracts/ # Input/output contracts +├── test/ # Unit + integration tests +└── src/ # All Ansible code + ├── ansible.cfg # Ansible configuration + ├── .yml # Entry point playbook — ONLY role/playbook imports + ├── roles/ # One role per responsibility + │ └── / + │ ├── tasks/main.yml # Entry point + │ ├── vars/main.yml # ALL error messages, constants + │ ├── defaults/main.yml # User-overridable defaults (rare) + │ ├── templates/ # Jinja2 templates + │ ├── files/ # Static files + │ └── handlers/main.yml # Handlers + ├── playbooks/ # Sub-playbooks per flow + ├── library/ # Custom Ansible modules (Python) + ├── module_utils/ # Shared Python module utilities + ├── callback_plugins/ # Output callback plugins + ├── vars/ # Shared cross-role variables + └── input/ # Project input files + └── / # Per-project input directory +``` + +### Key Rules + +- **`src/.yml`** is the ONLY entry point. It MUST contain only `roles:` and `import_playbook:` — no inline `tasks:`. +- **No output or log directories under `src/`** — all runtime output goes to `/`. +- Every file MUST start with the Dell Apache 2.0 copyright header. + +--- + +## 3. Configuration Design + +### `config.yml` (Repo Root) + +Every domain has a `config.yml` at the repo root with this structure: + +```yaml +project_name: "project_default" + +host: + hostname: "oim" # Short hostname (NOT FQDN), alphanumeric + hyphens + shared_path: "/opt/omnia/" # Absolute path — persistent storage + domain_name: "omnia.cluster" # Domain suffix + admin_nic_ip: "172.16.107.254" # Admin NIC IPv4 address +``` + +### Validation Rules + +| Field | Rule | +|-------|------| +| `project_name` | Non-empty string | +| `host.hostname` | Regex: `^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$` | +| `host.admin_nic_ip` | Valid IPv4 (use `ansible.utils.ipv4` filter) | +| `host.shared_path` | Absolute path starting with `/` | +| `host.domain_name` | Non-empty string | + +### Derived Paths + +From `config.yml`, the setup role derives: + +| Variable | Value | +|----------|-------| +| `input_project_dir` | `{{ playbook_dir }}/input/{{ project_name }}` | +| `output_project_dir` | `{{ shared_path }}/output/{{ project_name }}` | +| `log_dir` | `{{ shared_path }}/log/{{ project_name }}` | +| `shared_path` | `{{ config.host.shared_path }}` | +| `host_name` | `{{ config.host.hostname }}` | + +--- + +## 4. Entry Point Playbook Rules + +The main playbook (`src/.yml`) orchestrates the domain workflow. + +### Structure + +```yaml +--- +# .yml — Entry point for domain. +# +# Tags: +# validate — Validate configuration only +# prepare — Deploy infrastructure +# build — Execute main workflow +# cleanup — Remove everything +# +# Steps: +# Step 0: Setup — load config, validate host, check prereqs +# Step 1: Input validation +# Step 2: Credential collection + +# Step 0: Setup +- name: Domain setup + hosts: localhost + connection: local + gather_facts: false + tags: always + roles: + - _setup + +# Step 1: Validate +- name: Validate configuration + ansible.builtin.import_playbook: playbooks/validate_config.yml + tags: + - always + - validate + +# Step 2: Credentials (skipped for cleanup/validate) +- name: Collect credentials + ansible.builtin.import_playbook: playbooks/get_credentials.yml + when: not (skip_credentials | default(false) | bool) + tags: always + +# Flow: prepare +- name: Prepare infrastructure + ansible.builtin.import_playbook: playbooks/prepare.yml + tags: prepare + +# Flow: build/execute +- name: Execute main workflow + ansible.builtin.import_playbook: playbooks/execute.yml + tags: build +``` + +### Rules + +1. **NO `tasks:` blocks** — only `roles:` and `import_playbook:`. +2. **All `hosts:`** MUST be `localhost` with `connection: local`. +3. Each play has a clear step comment. +4. Tag validation, config loading, prereq checks — all in the setup role. + +--- + +## 5. Sub-Playbook Rules + +Sub-playbooks live in `src/playbooks/` and handle specific flows. + +### Naming + +| Pattern | Example | Purpose | +|---------|---------|---------| +| `prepare_*.yml` | `prepare_image_build_manager.yml` | Deploy infrastructure | +| `build_*.yml` | `build_image_x86_64.yml` | Build/execute main task | +| `cleanup_*.yml` | `cleanup_image_build_manager.yml` | Cleanup and teardown | +| `validate_*.yml` | `validate_image_build_config.yml` | Validation only | +| `get_*.yml` | `get_build_credentials.yml` | Credential/input collection | + +### Rules + +1. **All `hosts:`** MUST be `localhost` with `connection: local`. +2. Must NOT duplicate setup role logic (e.g., loading repo_status.yml). +3. Start with a guard if the play should be skipped conditionally. +4. `gather_facts: false` unless host facts are genuinely needed. + +--- + +## 6. Role Rules + +### Setup Role (`_setup`) + +Every domain MUST have a setup role that runs first (tag: `always`). It handles: + +1. **Tag validation** — supported tags, invalid combinations +2. **Config loading** — `config.yml` from repo root +3. **Host validation** — hostname, IP, paths +4. **Project dir setup** — create output, log dirs under `shared_path` +5. **Prerequisite checks** — fail-fast for all required files +6. **Data loading** — load domain-specific config files, upstream contracts +7. **Guard facts** — set `_setup_done: true` + +### General Role Rules + +1. Role names: `snake_case`, verb-noun (e.g., `deploy_minio`, `fetch_build_packages`). +2. Task file names: `snake_case.yml` (e.g., `install.yml`, `configure.yml`, `validate.yml`). +3. `tasks/main.yml` is the entry point — include other task files from there. +4. **ALL error messages in `vars/main.yml`** — never inline in tasks. +5. Private variables prefixed with `_` (e.g., `_prereq_checks`). +6. Each role is self-contained — no cross-role variable leaking. + +--- + +## 7. Task Rules + +### Every task MUST: + +- Have a descriptive `name:` field (sentence case, imperative verb). +- Use **FQCN** (Fully Qualified Collection Names): `ansible.builtin.file`, NOT `file`. +- Use `become: true` explicitly where needed (never at play level for entire plays). + +### Task patterns + +```yaml +# Validation — use assert (cleaner output) +- name: Validate hostname format + ansible.builtin.assert: + that: + - host_name is regex('^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$') + fail_msg: "{{ host_hostname_fail_msg }}" + quiet: true + +# File existence — use stat + fail +- name: Check required file exists + ansible.builtin.stat: + path: "{{ required_file_path }}" + register: _file_check + +- name: Fail if required file not found + ansible.builtin.fail: + msg: "{{ file_not_found_fail_msg }}" + when: not _file_check.stat.exists + +# Command tasks — always set changed_when +- name: Check service status + ansible.builtin.command: + cmd: systemctl is-active myservice + register: _service_status + changed_when: false + failed_when: false + +# Fact caching — use cacheable: true when needed across plays +- name: Set project variables + ansible.builtin.set_fact: + project_name: "{{ config.project_name }}" + cacheable: true + +# Debug output — use verbosity +- name: Display loaded config + ansible.builtin.debug: + msg: "Project: {{ project_name }}" + verbosity: 1 +``` + +### Conditional Patterns + +```yaml +# Good — simple boolean +when: standalone_mode | bool + +# Good — variable check +when: my_var | default('') | length > 0 + +# Good — defined check +when: my_var is defined + +# Bad — Jinja2 in when clause +when: "{{ some_complex_ternary }}" +``` + +### Loop Patterns + +```yaml +# Always use loop_control +loop: "{{ items | dict2items }}" +loop_control: + loop_var: item +``` + +### Error Handling + +```yaml +# Block/rescue for recoverable errors +- name: Deploy with error handling + block: + - name: Install packages + ansible.builtin.dnf: + name: "{{ packages }}" + state: present + rescue: + - name: Log failure + ansible.builtin.debug: + msg: "Installation failed: {{ ansible_failed_result.msg }}" +``` + +--- + +## 8. Variable and Message Rules + +### Variable naming + +| Pattern | Purpose | Example | +|---------|---------|---------| +| `*_dir` | Directory path | `input_project_dir` | +| `*_path` | File or directory path | `pulp_cert_host_path` | +| `*_file` | Stat register for a file | `_config_file` | +| `*_check` | Validation register | `_pulp_cert_check` | +| `_*` (underscore prefix) | Private/internal variable | `_prereq_checks` | + +### Message naming + +| Suffix | Used in | Example | +|--------|---------|---------| +| `_fail_msg` | `ansible.builtin.fail` / `assert` | `config_missing_fail_msg` | +| `_warn_msg` | `ansible.builtin.debug` (warnings) | `cert_expiry_warn_msg` | +| `_info_msg` | `ansible.builtin.debug` (info) | `build_done_info_msg` | + +### Rules + +1. **ALL error messages** go in `vars/main.yml` — never inline. +2. Use `snake_case` for all variables. +3. Role-specific variables MAY be prefixed with role name: `deploy_minio_port`. +4. Booleans: use `true` / `false`, never `yes` / `no`. +5. Always quote strings that could be misinterpreted: `{{ }}`, `:`, `#`. + +--- + +## 9. Path Conventions + +### Two Distinct Path Concepts + +| Concept | Description | Example | Configurable? | +|---------|-------------|---------|---------------| +| **Code location** | Where `dell/omnia` (or a domain) is cloned | `/root/omnia/`, `~/omnia/`, anywhere | User chooses at `git clone` time | +| **State location** (``) | Where persistent runtime data lives (MinIO data, registry, logs, output) | `/opt/omnia/image_build_manager/` | Yes — via `config.yml → host.shared_path` | + +**`/opt/omnia`** is NOT the code checkout location. It is the **default state location**. Users clone code anywhere and configure `shared_path` in `config.yml` to point at the persistent storage directory. + +### `` — Administration + +| Question | Answer | +|----------|--------| +| **In source control?** | No — runtime data, not code. Gitignored. | +| **Backed up?** | Yes — contains build artifacts, MinIO S3 data, registry images. Loss = rebuild from scratch. | +| **Sensitive information?** | Low — credentials are Ansible Vault encrypted in `src/input/`. MinIO stores OS images (not sensitive). Logs may contain hostnames/IPs. | +| **Who creates it?** | The playbook creates it automatically on first run. | +| **Permissions?** | `0755` root-owned. MinIO/Registry containers access via bind-mount. | + +### Runtime paths + +| Path | Purpose | +|------|---------| +| `/` | Root persistent storage (domain state) | +| `/output//` | Domain output artifacts | +| `/log//` | Domain logs | +| `/log/.log` | Ansible playbook log | +| `/s3/` | MinIO S3 data (if applicable) | +| `/registry/` | Local container registry storage (if applicable) | +| `/workdir/` | Build workdir (if applicable) | +| `src/input//` | Input config files (in repo) | + +### Upstream contract paths + +| Path | Purpose | +|------|---------| +| `/opt/omnia/repo_manager/output//` | repo_manager contract output | +| `/opt/omnia/pulp/settings/certs/` | Pulp TLS certificates | + +### Rules + +- **Never hardcode `/opt/omnia` in task files** — use variables from config.yml. +- Output and log directories go under `/output//` and `/log//`, NOT under `src/`. +- Certificate paths are read as absolute paths from upstream contracts. +- `*_output_dir` variables are always **directories**, never file paths. + +--- + +## 10. Input/Output Contracts + +Every domain communicates via YAML contracts. + +### Input Contract + +Document in `docs/contracts/input-contract.md`: + +```yaml +# What this domain reads +inputs: + - file: config.yml + source: user + required: true + - file: _config.yml + source: src/input// + required: true + - file: repo_status.yml + source: /opt/omnia/repo_manager/output// + required: true +``` + +### Output Contract + +Document in `docs/contracts/output-contract.md`: + +```yaml +# What this domain produces +outputs: + - file: _status.yml + location: /output// + format: YAML +``` + +### Rules + +1. Every input file has a stat check in the setup role. +2. Fail-fast with actionable error messages for each missing file. +3. Output files use a `*_status.yml` naming pattern. + +--- + +## 11. Ansible Style Guide + +### YAML Conventions + +- **2-space indentation** — no tabs, no trailing whitespace. +- Every YAML file starts with copyright header + `---`. +- Every playbook has a descriptive `name:` in the play definition. + +### Module Rules + +- **Always FQCN**: `ansible.builtin.file`, `containers.podman.podman_container`, etc. +- Set `changed_when` for all `command`/`shell` tasks. +- Set `failed_when` for non-standard failure conditions. +- Prefer `ansible.builtin.assert` over `fail` + `when` for validation. +- Use `ansible.builtin.dnf` for RHEL 10+ (not `yum`). + +### Idempotency + +- All roles SHALL be idempotent — running twice produces no changes on second run. +- Use `creates:` parameter for `command` tasks that generate files. +- Use `stat` + `when` for conditional execution. +- Never use `shell` with `rm -rf` without explicit guards. + +### Secrets Management + +- **Never** hardcode credentials in playbooks or variable files. +- Use Ansible Vault for sensitive data. +- Reference secrets via `{{ vault_secret_name }}`. + +### Linting + +- **ansible-lint** with `production` profile. +- **yamllint** for raw YAML files. +- Enforced rules: named tasks, FQCN for `ansible.builtin.*`, no bare variables in `when:`. + +--- + +## 12. Python Style Guide (Custom Modules) + +### Requirements + +- Python 3.12+ compatible +- Type hints on all public functions +- Google-style docstrings (`Args:`, `Returns:`, `Raises:`) +- `pylint` score ≥ 8.0 + +### Module Structure + +```python +#!/usr/bin/python +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 + +"""Module docstring describing purpose.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """Main module entry point.""" + module = AnsibleModule( + argument_spec=dict( + param1=dict(type='str', required=True), + ), + supports_check_mode=True, + ) + # Module logic here + module.exit_json(changed=False, result="ok") + + +if __name__ == '__main__': + main() +``` + +### Naming + +- `snake_case` for modules, functions, methods, variables. +- `PascalCase` for classes. +- `ALL_CAPS` for constants. +- `_prefix` for internal/private members. + +--- + +## 13. Jinja2 Template Rules + +- Templates use `.j2` extension, named to match target file. +- Start with `# {{ ansible_managed }}` comment. +- Use `{{ var | default(value) }}` for optional variables. +- Use `{%- -%}` for whitespace control in loops. +- No copyright header in templates (the role carries it). + +--- + +## 14. Git Conventions + +- **Branch naming**: `feature/` or `fix/` +- **Commit messages**: `(): ` + - Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore` + - Example: `feat(setup): add host validation for config.yml` +- **One logical change per commit** — don't mix refactoring with features. +- Always sync before commit: `git fetch origin && git merge origin/main`. + +--- + +## 15. Upgrade & Rollback Standard + +Every domain MUST support `upgrade` and `rollback` tags with dedicated playbooks. + +### Tags + +| Tag | Purpose | Playbook | +|-----|---------|----------| +| `upgrade` | Migrate domain to a new version | `playbooks/upgrade_.yml` | +| `rollback` | Revert domain to the previous version | `playbooks/rollback_.yml` | + +### Entry Point Integration + +```yaml +# In .yml — add upgrade/rollback alongside other flows + +# Upgrade flow +- name: Upgrade + ansible.builtin.import_playbook: playbooks/upgrade_.yml + tags: + - never + - upgrade + +# Rollback flow +- name: Rollback + ansible.builtin.import_playbook: playbooks/rollback_.yml + tags: + - never + - rollback +``` + +Both use `tags: [never, upgrade/rollback]` — they only run when explicitly requested via `--tags`. + +### Version Tracking + +Each domain writes a version file after successful deployment: + +```yaml +# /.domain_version.yml +--- +domain: "image_build_manager" +version: "1.2.0" +installed_at: "2026-07-27T10:30:00Z" +ansible_version: "2.20.0" +python_version: "3.12.8" +config_hash: "sha256:abc123..." +previous_version: "1.1.0" +``` + +### Upgrade Flow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ UPGRADE FLOW │ +└─────────────────────────────────────────────────────────────────┘ + + 1. Pre-upgrade validation + ├── Check current version from .domain_version.yml + ├── Verify target version compatibility + ├── Check disk space for snapshot + └── Validate all services are healthy + + 2. Snapshot current state + ├── Copy / → /.upgrade_snapshot/ + ├── Export service configs (MinIO, Registry) + └── Record current .domain_version.yml + + 3. Stop domain services + ├── Stop MinIO (systemctl stop minio) + ├── Stop Registry (systemctl stop registry) + └── Verify services stopped + + 4. Apply data migrations + ├── Schema changes (config file format updates) + ├── Path migrations (directory renames/moves) + └── Data transformations (format changes) + + 5. Start services with new config + ├── Deploy updated Quadlet files + ├── Reload systemd (systemctl daemon-reload) + └── Start services + + 6. Post-upgrade verification + ├── Verify services are healthy + ├── Validate data integrity + └── Run smoke tests (MinIO connectivity, registry pull) + + 7. Update version file + └── Write new .domain_version.yml +``` + +### Rollback Flow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ROLLBACK FLOW │ +└─────────────────────────────────────────────────────────────────┘ + + 1. Validate snapshot exists + └── Check /.upgrade_snapshot/ is present + + 2. Stop domain services + ├── Stop MinIO + └── Stop Registry + + 3. Restore state from snapshot + ├── Remove current state directories + ├── Restore from .upgrade_snapshot/ + └── Restore service configs + + 4. Restart services + ├── Reload systemd + └── Start services with restored config + + 5. Verify rollback + ├── Check services are healthy + ├── Validate data integrity + └── Restore .domain_version.yml from snapshot + + 6. Cleanup + └── Optionally remove .upgrade_snapshot/ after verified rollback +``` + +### Upgrade Playbook Structure + +```yaml +# playbooks/upgrade_.yml +--- +- name: Upgrade + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: Run pre-upgrade validation + ansible.builtin.include_role: + name: _upgrade + tasks_from: pre_upgrade.yml + + - name: Snapshot current state + ansible.builtin.include_role: + name: _upgrade + tasks_from: snapshot.yml + + - name: Apply data migrations + ansible.builtin.include_role: + name: _upgrade + tasks_from: migrate.yml + + - name: Post-upgrade verification + ansible.builtin.include_role: + name: _upgrade + tasks_from: verify.yml +``` + +### Rollback Playbook Structure + +```yaml +# playbooks/rollback_.yml +--- +- name: Rollback + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: Validate snapshot exists + ansible.builtin.include_role: + name: _upgrade + tasks_from: validate_snapshot.yml + + - name: Restore from snapshot + ansible.builtin.include_role: + name: _upgrade + tasks_from: restore.yml + + - name: Post-rollback verification + ansible.builtin.include_role: + name: _upgrade + tasks_from: verify.yml +``` + +### Rules + +1. **Upgrade MUST create a snapshot** before modifying any state. +2. **Rollback MUST NOT require the new version's code** — it should work with the snapshot data alone. +3. **Version file** is updated only after successful post-upgrade verification. +4. **Credentials are skipped** during upgrade/rollback (same as cleanup/validate). +5. **Data migrations** are idempotent — running upgrade twice on the same version is a no-op. +6. **Snapshot directory** is `/.upgrade_snapshot/` — only one snapshot is kept at a time. + +--- + +## 16. Validation Checklist + +Before submitting a PR, verify: + +- [ ] Entry point playbook has NO inline `tasks:` — only `roles:` / `import_playbook:` +- [ ] All `hosts:` are `localhost` with `connection: local` +- [ ] All error messages defined in `vars/main.yml`, not inline +- [ ] All prerequisite files checked in setup role (fail-fast) +- [ ] Config validation includes hostname regex, IPv4, absolute path checks +- [ ] Output and log directories created under `/output//` and `/log//` +- [ ] No `src/log/` or `src/output/` directories +- [ ] No OIM, oim, or inventory group references — everything is localhost +- [ ] All modules use FQCN (`ansible.builtin.*`) +- [ ] `changed_when` set on all `command`/`shell` tasks +- [ ] Copyright header on all source files +- [ ] `ansible-lint` passes with no errors +- [ ] Python modules pass `pylint` with score ≥ 8.0 diff --git a/docs/design/test-automation-design.md b/docs/design/test-automation-design.md new file mode 100644 index 0000000000..d6dff6c99d --- /dev/null +++ b/docs/design/test-automation-design.md @@ -0,0 +1,352 @@ +# Test Automation — Design & Architecture + +> **Scope**: All Omnia domains | **Last Updated**: Jul 2026 + +--- + +## 1. Overview + +The test automation framework provides **Functional Verification Testing (FVT)** for all +Omnia domains. Each domain uses this framework to verify that its Ansible playbooks correctly +deploy infrastructure, produce valid output artifacts, and meet contract specifications. + +### Design Goals + +- **Zero hardcoded values** — all IPs, paths, and credentials read from config +- **Centralized messages** — no inline strings in test files +- **Graceful skipping** — optional features skip cleanly (no false failures) +- **Structured output** — TestLogger produces consistent ✓/✗ formatted results +- **HTML reports** — consolidated report across multiple scenario runs +- **Remote + local** — tests run against a remote OIM server or locally + +--- + +## 2. Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ TEST AUTOMATION ARCHITECTURE │ +└─────────────────────────────────────────────────────────────────────────────┘ + + ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ + │ run_validation│ │ conftest.py │ │ Target Server │ + │ .sh │────>│ pytest hooks │────>│ (OIM / image_build_manager) │ + │ │ │ + fixtures │ │ │ + └──────┬───────┘ └──────┬───────┘ └──────────────────────────────┘ + │ │ ▲ + │ │ │ SSH / testinfra + │ ▼ │ + │ ┌──────────────┐ ┌─────┴──────┐ + │ │ fvt/ │ │ library/ │ + │ │ test files │──────────>│ functions/ │ + │ │ │ │ vars/ │ + │ └──────────────┘ │ messages/ │ + │ └────────────┘ + │ + ▼ + ┌──────────────┐ ┌──────────────┐ + │ test_config │ │ test_run │ + │ .yml │ │ _config.yml │ + │ (connection) │ │ (scenarios) │ + └──────────────┘ └──────────────┘ +``` + +--- + +## 3. Execution Flow + +### 3.1 Entry Point: `run_validation.sh` + +``` +run_validation.sh [--marker ] + │ │ + │ ├── deploy → run playbook + verify + │ ├── verify → verify only (no deploy) + │ └── test → deploy + verify (combined) + │ + ├── image_build_manager → fvt/image_build_manager/ + ├── validate → fvt/validate/ + ├── prepare → fvt/prepare/ + ├── build → fvt/build/ + └── cleanup → fvt/cleanup/ +``` + +### 3.2 Config Mode: `run_validation.sh --config` + +Reads `test_run_config.yml` and runs enabled scenarios in order: + +```yaml +scenarios: + image_build_manager: + order: 1 + run: true + suite: "" + marker: "sanity" +``` + +### 3.3 Playbook ↔ Test Scenario Mapping + +| Playbook Tag | Test Scenario | What It Verifies | +|-------------|---------------|------------------| +| `validate` | `validate` | Input config exists, credentials synced | +| `prepare` | `prepare` | Containers running, S3 buckets, ports | +| `build` | `build` | S3 images, registry images, build_status | +| `cleanup` | `cleanup` | Containers removed, artifacts cleaned | +| *(none — default)* | `image_build_manager` | Full end-to-end (deploy + verify) | + +--- + +## 4. Test Phases + +### Phase 1: Session Setup (conftest.py) + +``` +1. Validate test_config.yml (all required fields, dataset exists, paths valid) + → Fail fast with clear error if invalid (no fallback defaults) +2. Encrypt test_creds.yml (if not already encrypted) +3. Clone/pull repo on target (if remote mode) +4. Sync dataset input/ files to target (sync_image_build_input) +5. Sync config.yml to target clone root +6. Sync repo_manager_output/ to target (sync_output) +7. Initialize TestReport +``` + +### Phase 2: Deploy (deploy command) + +``` +1. PlaybookRunner connects to target (SSH or local) +2. Runs: ansible-playbook image_build_manager.yml --tags +3. Streams live output with | prefix +4. Returns success/failure dict +``` + +### Phase 3: Verify (verify command) + +``` +1. Testinfra connects to target +2. Runs verification functions (check_*, verify_*) +3. TestLogger produces structured output +4. Results collected by TestReport +``` + +### Phase 4: Report Generation + +``` +1. conftest.py pytest_sessionfinish hook +2. TestReport.save() → JSON + HTML +3. Multiple runs merge into single report +``` + +--- + +## 5. Test Case Registry + +### validate (Tag: validate) — 3 Tests + +| ID | Test | Markers | Suite | +|----|------|---------|-------| +| TC_VL_001 | Deploy playbook --tags validate | deploy, sanity | *(root)* | +| TC_VL_002 | Verify image_build_config.yml exists on target | sanity | status | +| TC_VL_003 | Verify credentials file present on target | sanity | status | + +### prepare (Tag: prepare) — 8 Tests + +| ID | Test | Markers | Suite | +|----|------|---------|-------| +| TC_PR_001 | Deploy playbook --tags prepare | deploy, sanity | *(root)* | +| TC_PR_002 | Verify S3 storage backend (MinIO) running | sanity | container | +| TC_PR_003 | Verify registry container running | sanity | container | +| TC_PR_004 | Verify systemd services active | sanity | container | +| TC_PR_005 | Verify firewall ports open | sanity | container | +| TC_PR_006 | Verify s3cmd installed and configured | sanity | container | +| TC_PR_007 | Verify registry reachable | sanity | container | +| TC_PR_008 | Verify S3 buckets created | sanity | s3 | + +### build (Tag: build) — 6 Tests + +| ID | Test | Markers | Suite | +|----|------|---------|-------| +| TC_BD_001 | Deploy playbook --tags build | deploy, sanity | *(root)* | +| TC_BD_002 | Verify x86_64 images pushed to S3 | x86_64, sanity | s3 | +| TC_BD_003 | Verify aarch64 images pushed to S3 | aarch64, sanity | s3 | +| TC_BD_004 | Verify x86_64 images in registry | x86_64, sanity | registry | +| TC_BD_005 | Verify build_status.yml after build | sanity | registry | +| TC_BD_006 | Verify x86_64 functional groups built | x86_64, sanity | registry | + +### cleanup (Tag: cleanup) — 8 Tests + +| ID | Test | Markers | Suite | +|----|------|---------|-------| +| TC_CL_001 | Deploy playbook --tags cleanup | deploy, sanity | *(root)* | +| TC_CL_002 | Verify containers removed | sanity | cleanup | +| TC_CL_003 | Verify systemd services stopped | sanity | cleanup | +| TC_CL_004 | Verify firewall ports closed | sanity | cleanup | +| TC_CL_005 | Verify S3 buckets removed | sanity | cleanup | +| TC_CL_006 | Verify s3cmd configuration removed | sanity | cleanup | +| TC_CL_007 | Verify build_status.yml removed | sanity | cleanup | +| TC_CL_008 | Verify registry has no images | sanity | cleanup | + +### image_build_manager (Full End-to-End) — 13 Tests + +| ID | Test | Markers | Suite | +|----|------|---------|-------| +| TC_IB_000 | Deploy image_build_manager.yml (no tags) | deploy, sanity | *(root)* | +| TC_IB_001 | Verify S3 storage backend (MinIO) | x86_64, aarch64, sanity | container | +| TC_IB_002 | Verify registry container running | x86_64, aarch64, sanity | container | +| TC_IB_003 | Verify required S3 buckets exist | x86_64, aarch64, sanity | s3 | +| TC_IB_004 | Verify x86_64 images pushed to S3 | x86_64, sanity | s3 | +| TC_IB_005 | Verify aarch64 images pushed to S3 | aarch64, sanity | s3 | +| TC_IB_006 | Verify x86_64 images in registry | x86_64, sanity | registry | +| TC_IB_007 | Verify aarch64 images in registry | aarch64, sanity | registry | +| TC_IB_008 | Verify build_status.yml reports success | x86_64, aarch64, sanity | registry | +| TC_IB_009 | Verify x86_64 functional groups built | x86_64, sanity | registry | +| TC_IB_010 | Verify aarch64 functional groups built | aarch64, sanity | registry | +| TC_IB_011 | Verify packages in x86_64 S3 images | x86_64, sanity | image_verification | +| TC_IB_012 | Verify packages in aarch64 S3 images | aarch64, sanity | image_verification | + +**Total: 38 test cases across 5 scenarios.** + +--- + +## 6. Data Flow + +### 6.1 Input Flow + +``` +test/datasets/data_set_01/input/ (local — automation runner) + │ + │ rsync via conftest.py (when sync_image_build_input: true) + ▼ +/src/input/project_default/ (target server) + │ + │ ansible-playbook image_build_manager.yml + ▼ +/opt/omnia/image_build_manager/output/ (target server — runtime output) +``` + +### 6.2 Dataset Structure + +``` +datasets/data_set_01/ +├── input/ # Synced to /src/input// +│ ├── config.yml # Project config (also -> /config.yml) +│ ├── image_build_config.yml # Domain input config +│ └── image_build_credentials.yml # Vault-encrypted credentials +└── repo_manager_output/ # Synced to repo_manager_output_dir (sync_output: true) + ├── repo_status.yml # RPM repo URLs, certs + ├── functional_group_packages.yml + └── certs/ + ├── pulp_webserver.crt + └── pulp_webserver.key +``` + +--- + +## 7. Report Architecture + +### 7.1 Report Generation + +``` +pytest_sessionstart → TestReport.__init__() +pytest_runtest_makereport → TestReport.add_result() +pytest_sessionfinish → TestReport.save() → JSON + HTML +``` + +### 7.2 HTML Report Sections + +1. **Header** — server info, suite, marker, duration +2. **Summary** — pass/fail/skip counts +3. **Folder Breakdown** — results grouped by test folder + suite/marker info +4. **Test Details** — expandable per-test results with details + +### 7.3 Report Merging + +Multiple scenario runs with the same `report_id` merge into one report. +Each run appears as a separate section under the same server. + +--- + +## 8. Connection Architecture + +### 8.1 Remote Mode (`oim_server_ip` set) + +``` +Automation Runner → SSH → Target Server (OIM) + │ + ├── testinfra host = ssh:// + ├── PlaybookRunner = sshpass + ssh + └── rsync for file sync +``` + +### 8.2 Local Mode (`oim_server_ip` empty) + +``` +Same Machine + ├── testinfra host = local:// + ├── PlaybookRunner = subprocess + └── cp/rsync for file sync +``` + +--- + +## 9. Security + +- **Credentials**: `test_creds.yml` encrypted with Ansible Vault on first use +- **Dataset credentials**: `image_build_credentials.yml` vault-encrypted +- **No secrets in code**: All credentials from config files +- **No secrets in git**: `.gitignore` excludes vault key files +- **SSH**: Uses `StrictHostKeyChecking=no` for automation only + +--- + +## 10. Configuration Validation + +### 10.1 No Fallback Defaults + +All required fields in `test_config.yml` must be explicitly set. The framework +never silently substitutes a default value. If a field is missing or `null`, +session startup fails immediately with a clear error listing every missing field. + +**Required fields:** `oim_server_ip`, `dataset`, `project_name`, `clone_path`, +`shared_path`, `report_path`, `report_name`. + +### 10.2 Dataset Validation + +Before tests run, the validator checks: +- Dataset directory exists: `datasets//` +- Required files present: `input/config.yml`, `input/image_build_config.yml`, + `input/image_build_credentials.yml` + +### 10.3 Report Path + +`report_path` supports both relative (to `test/`) and absolute paths. +Directories are created automatically if they do not exist. + +### 10.4 Tab Completion + +```bash +eval "$(./run_validation.sh --completion)" +``` + +Enables bash tab completion for scenarios, commands, `--suite`, and `--marker`. + +--- + +## 11. Extensibility + +### Adding a New Test Scenario + +1. Create `fvt//` directory +2. Add `test_playbook.py` at scenario root for playbook execution +3. Add `/test_.py` for verification +4. Add test names/messages to `build_image_msgs.py` +5. Add scenario to `test_run_config.yml` +6. Update this design document + +### Adding a New Verification Check + +1. Add function to `build_image_func.py` (return dict pattern) +2. Add command to `CMDS` in `common_vars.py` (if new shell command) +3. Add messages to `build_image_msgs.py` +4. Create test in appropriate `fvt/` folder +5. Run pylint and verify score ≥ 8.7 diff --git a/src/ansible.cfg b/src/ansible.cfg deleted file mode 100644 index 88d2484337..0000000000 --- a/src/ansible.cfg +++ /dev/null @@ -1,23 +0,0 @@ -[defaults] -log_path = /opt/omnia/log/core/playbooks/omnia.log -# Set the remote temporary directory to a shared path to avoid SELinux issues -remote_tmp = /opt/omnia/tmp/.ansible/tmp/ -host_key_checking = false -forks = 5 -timeout = 180 -executable = /bin/bash -display_skipped_hosts = false -deprecation_warnings = false -show_task_path_on_failure = false -stdout_callback = omnia_default -callback_plugins = src/common/callback_plugins -library = src/common/library/modules -module_utils = src/common/library/module_utils - -[persistent_connection] -command_timeout = 180 -connect_timeout = 180 - -[ssh_connection] -retries = 3 -ssh_args = -o ControlMaster=auto -o ControlPersist=60 -o ConnectTimeout=60 diff --git a/src/image_build_manager/README.md b/src/image_build_manager/README.md new file mode 100644 index 0000000000..e1428f6244 --- /dev/null +++ b/src/image_build_manager/README.md @@ -0,0 +1,306 @@ +# Omnia Image Build Manager + +**Ansible Galaxy Collection: `omnia.image_build`** + +Build OS images (x86_64 + aarch64) for HPC cluster provisioning using OpenCHAMI. +Deploys MinIO (S3) + local container registry, builds base and compute images, +and writes `build_status.yml` with S3 artifact paths. + +**Runs directly on a RHEL host** with Ansible + Python. +No container runtime required for the playbook itself (Podman is used for image builds). + +## Prerequisites + +| Requirement | Minimum | Validated | +|------------|---------|-----------| +| OS | RHEL 10.x, Rocky 10.x | RHEL 10.0 | +| Python | 3.12+ | 3.12.8 | +| Ansible | ansible-core 2.20+ | 2.20.0 | +| Container runtime | Podman 5.0+ | 5.3.1 | +| Disk space | 50 GB free | — | + +### Ansible Installation (Common Omnia Venv) + +**Recommended — use the shared Omnia venv**: + +```bash +# From the Omnia repo root: +./src/main/omnia.sh --setup-venv +source /opt/omnia/venv/bin/activate +``` + +**Manual install** (if not using the shared venv): + +```bash +python3 -m venv ~/.venvs/omnia +source ~/.venvs/omnia/bin/activate +pip install -r requirements.txt +ansible-galaxy collection install -r requirements.yml +``` + +**Verify**: + +```bash +ansible --version # ansible-core 2.20+ +ansible-galaxy collection list | grep containers.podman +``` + +## Quick Start + +```bash +# 1. Configure omnia.env (REQUIRED — do this first) +vi src/main/omnia.env # Set OMNIA_ADMIN_NIC_IP at minimum + +# 2. Set up the shared Omnia venv (one-time) +./src/main/omnia.sh --setup-venv +source /opt/omnia/venv/bin/activate + +# 3. Ensure repo_manager output is ready +./src/main/omnia-cli repo-manager # Check repo_manager status +# If repo_manager was NOT run, manually set up: +# mkdir -p /opt/omnia/repo_manager/output/project_default +# cp src/image_build_manager/samples/repo_manager_output/repo_status.yml \ +# /opt/omnia/repo_manager/output/project_default/ +# cp src/image_build_manager/samples/repo_manager_output/functional_group_packages.yml \ +# /opt/omnia/repo_manager/output/project_default/ +# Edit repo_status.yml with your actual Pulp URLs and cert paths + +# 4. Edit image_build_config.yml — enable functional groups to build +vi src/image_build_manager/input/project_default/image_build_config.yml + +# 5. Run playbooks (cd into the playbooks directory) +cd src/image_build_manager/playbooks +ansible-playbook image_build_manager.yml --tags validate # Validate config +ansible-playbook image_build_manager.yml --tags prepare # Deploy MinIO + Registry +ansible-playbook image_build_manager.yml --tags build # Build OS images +ansible-playbook image_build_manager.yml --tags cleanup # Remove everything + +# Or run sub-playbooks directly from their directory: +cd build && ansible-playbook build_image_x86_64.yml +cd validate && ansible-playbook validate_image_build_config.yml +cd prepare && ansible-playbook prepare_image_build_manager.yml +cd cleanup && ansible-playbook cleanup_image_build_manager.yml +``` + +## Input Files + +| File | Location | Required | Description | +|------|----------|----------|-------------| +| `omnia.env` | `src/main/` | Yes | Common environment variables (OMNIA_DATA_PATH, OMNIA_ADMIN_NIC_IP, etc.) | +| `image_build_config.yml` | `input/project_default/` | Yes | S3 config, functional groups, build settings | +| `repo_status.yml` | Full path via `repo_manager_output_path` | Yes | RPM repo URLs + OS metadata + cert paths + `package_list` | +| `functional_group_packages.yml` | Path from `repo_manager.package_list` in repo_status.yml | Yes | **Functional group → RPM package mapping** | +| `image_build_credentials.yml` | Auto-generated in project dir | Yes (except validate/cleanup) | S3 + aarch64 SSH credentials | + +### Certificate Handling + +Certificates are referenced by **absolute paths** in `repo_status.yml`: + +```yaml +repo_manager: + port: 2225 + certificates: + server_crt: /opt/omnia/pulp/settings/certs/pulp_webserver.crt + server_key: /opt/omnia/pulp/settings/certs/pulp_webserver.key + certs_dir: /opt/omnia/pulp/settings/certs +``` + +The playbook reads the cert path directly from `repo_status.yml` and validates the file +exists on the host. No staging or copying is needed — the cert is used as-is. + +## Package Resolution Flow + +``` +image_build_config.yml functional_group_packages.yml +┌──────────────────────────┐ ┌──────────────────────────────────┐ +│ functional_groups: │ │ base_packages: │ +│ - name: slurm_node_x86 │──────┐ │ - systemd │ +│ - name: slurm_ctrl_x86 │ │ │ - kernel │ +│ - name: os_x86_64 │ │ │ - dracut │ +└──────────────────────────┘ │ │ - ... │ + │ │ functional_groups: │ + └──>│ slurm_node_x86_64: │ + │ packages: │ + │ - munge │ + │ - slurm-slurmd │ + │ - ... │ + └──────────────────────────────────┘ + │ + ▼ + base_image_packages (all images) + compute_images_dict (per functional group) + │ + ▼ + OpenCHAMI image-build → S3 upload +``` + +**No `software_config.json` needed.** The `functional_group_packages.yml` file is the +single source of truth for which RPM packages belong to each functional group. + +## Configuration Reference + +### Environment Variables (`omnia.env`) + +Host and project settings are configured via environment variables in `src/main/omnia.env`. +You can also `export` them directly in your shell. + +| Variable | Description | Default | +|----------|-------------|---------| +| `OMNIA_DATA_PATH` | Root data directory for all Omnia persistent data | `/opt/omnia` | +| `OMNIA_PROJECT_NAME` | Project name (maps to input/output dirs) | `project_default` | +| `OMNIA_HOSTNAME` | OIM hostname (NOT FQDN) | `oim` | +| `OMNIA_DOMAIN_NAME` | Domain name of the OIM host | `omnia.cluster` | +| `OMNIA_ADMIN_NIC_IP` | Admin NIC IP (Pulp and S3 endpoint) | `172.16.107.254` | +| `IMAGE_BUILD_MANAGER_DATA_PATH` | Override image_build_manager data path | `${OMNIA_DATA_PATH}/image_build_manager` | +| `OMNIA_VENV_PATH` | Path to the shared Omnia Python venv | `/opt/omnia/venv` | + +### `image_build_config.yml` + +Per-domain configuration. Key sections: +- **`s3_configurations`** — S3 provider (minio or powerscale) +- **`repo_manager_output_path`** — full path to `repo_status.yml` (default: `/opt/omnia/repo_manager/output/project_default/repo_status.yml`) +- **`functional_groups`** — image variants to build (e.g., `os_x86_64`, `slurm_node_x86_64`) +- **`aarch64_inventory_host_ip`** — ARM build host (leave empty to skip aarch64) +- **`build_image`** — async/retry/delay settings + +### `functional_group_packages.yml` + +**Single source of truth** for RPM package mapping per functional group. +Located in `repo_manager_output/`. Structure: + +```yaml +base_packages: # RPMs installed in EVERY image (base OS layer) + - systemd + - kernel + - ... + +functional_groups: # Additional RPMs per functional group + slurm_node_x86_64: + packages: + - munge + - slurm-slurmd + - ... + os_x86_64: + packages: [] # Only base packages +``` + +**To customize**: Add or remove RPM package names under the appropriate functional group. +Package names must match what is available in the Pulp RPM repos defined in `repo_status.yml`. + +### `repo_status.yml` + +Produced by `repo_manager` at `/opt/omnia/repo_manager/output//repo_status.yml`. +Contains RPM repo URLs, OS metadata, certificate paths (absolute), and package list path. + +Key fields consumed by image_build_manager: +- **`overall_status`** — must be `"success"` for image_build_manager to proceed +- **`cluster_os_type`** / **`cluster_os_version`** — build target OS +- **`rpm_repos.x86_64`** / **`rpm_repos.aarch64`** — RPM repository URLs +- **`repo_manager.port`** — Pulp HTTPS port (default: 2225) +- **`repo_manager.certificates.server_crt`** — absolute path to Pulp TLS cert +- **`repo_manager.package_list`** — absolute path to `functional_group_packages.yml` + +See `samples/repo_manager_output/repo_status.yml` for the full structure and sample values. + +**Manual setup** (if repo_manager was not run): +```bash +# Copy sample files to the runtime output directory +mkdir -p /opt/omnia/repo_manager/output/project_default +cp samples/repo_manager_output/repo_status.yml /opt/omnia/repo_manager/output/project_default/ +cp samples/repo_manager_output/functional_group_packages.yml /opt/omnia/repo_manager/output/project_default/ + +# Edit repo_status.yml with your actual Pulp server URLs and cert paths +vi /opt/omnia/repo_manager/output/project_default/repo_status.yml + +# Verify setup +./src/main/omnia-cli repo-manager +``` + +## Tags + +| Tag | Description | +|-----|-------------| +| `validate` | Validate configuration only (no credentials required) | +| `prepare` | Deploy MinIO S3 + local container registry | +| `build` | Build x86_64 + aarch64 OS images | +| `cleanup` | Remove MinIO, registry, build artifacts, credentials | +| `upgrade` | Upgrade flow (placeholder — future release) | +| `rollback` | Rollback flow (placeholder — future release) | + +## Output Paths + +All runtime output goes to `/` (default: `/opt/omnia/image_build_manager/`): + +| Path | Purpose | +|------|---------| +| `/output//` | Build output (`build_status.yml`) | +| `/log//` | Build logs (base/compute image logs) | +| `/log/playbooks/` | Ansible playbook logs (per-subdirectory) | +| `/s3/` | MinIO S3 data | +| `/registry/` | Local container registry storage | +| `/oci/` | OCI image data | +| `/workdir/` | OpenCHAMI image build workdir | + +## CI/CD Pipeline + +The `.github/workflows/ci.yml` runs on push/PR to `main`: + +- **lint** — `ansible-lint` on all playbooks +- **test** — `pytest` on unit tests +- **validate-standalone** — Sets env vars, creates input dirs, runs `--tags validate --check` + +## Collection Structure + +``` +image_build_manager/ # omnia.image_build collection +├── galaxy.yml # Collection metadata (namespace: omnia, name: image_build) +├── meta/runtime.yml # Ansible version compatibility +├── requirements.txt # Python dependencies +├── requirements.yml # Ansible Galaxy collections +├── ansible.cfg # FQCN config (no path hacks) +├── plugins/ # Galaxy-standard plugin layout +│ ├── modules/ # Custom Ansible modules (FQCN: omnia.image_build.*) +│ ├── module_utils/ # Shared Python utilities for modules +│ └── callback/ # Callback plugins (omnia.image_build.omnia_default) +├── roles/ # All Ansible roles +├── playbooks/ # All playbooks (entry point + sub-playbooks) +│ ├── image_build_manager.yml # Entry point +│ ├── validate/ # Input validation +│ ├── credentials/ # Credential management +│ ├── prepare/ # MinIO + Registry deployment +│ ├── build/ # OS image builds (x86_64 + aarch64) +│ ├── cleanup/ # Service and artifact cleanup +│ ├── upgrade/ # Upgrade flow (placeholder) +│ └── rollback/ # Rollback flow (placeholder) +├── containers/ # Container build files (image_builder) +├── vars/ # Shared variables +├── input/ # User input (excluded from build) +│ └── project_default/ +│ └── image_build_config.yml # User configuration +├── samples/ # Reference files for manual setup/testing +│ └── repo_manager_output/ # Sample repo_status.yml + functional_group_packages.yml +├── docs/ # Domain-specific documentation +│ ├── architecture.md # Architecture overview +│ ├── package-mapping-guide.md # RPM package customization guide +│ ├── troubleshooting.md # Common issues and fixes +│ ├── contracts/ # Input/output YAML contracts +│ └── design/ # Design documents +└── README.md # This file +``` + +### Runtime Output (auto-created at `/opt/omnia/image_build_manager/`) + +``` +/opt/omnia/image_build_manager/ +├── output/project_default/ # build_status.yml, versioned copies +├── log/project_default/ # Base/compute image build logs +├── log/playbooks/ # Ansible playbook logs (validate.log, build.log, etc.) +├── s3/ # MinIO S3 data +├── registry/ # Local container registry storage +├── oci/ # OCI image data +└── workdir/ # OpenCHAMI image build workdir +``` + +## License + +Apache License, Version 2.0 \ No newline at end of file diff --git a/src/image_build_manager/ansible.cfg b/src/image_build_manager/ansible.cfg index 3b5280d282..b92938a202 100644 --- a/src/image_build_manager/ansible.cfg +++ b/src/image_build_manager/ansible.cfg @@ -1,6 +1,19 @@ +# ansible.cfg for running playbooks from the domain root. +# +# Usage: +# cd src/image_build_manager +# ansible-playbook playbooks/image_build_manager.yml --tags validate +# +# NOTE: Ansible resolves paths relative to ansible.cfg location, NOT the playbook. +# Since ansible.cfg is at image_build_manager/, roles_path is ./roles (same dir). + [defaults] -log_path = /opt/omnia/log/core/playbooks/image_build_manager.log -remote_tmp = /opt/omnia/tmp/.ansible/tmp/ +log_path = /opt/omnia/image_build_manager/log/image_build_manager.log +roles_path = roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = plugins/modules +module_utils = plugins/module_utils +callback_plugins = plugins/callback +remote_tmp = /tmp/.ansible/tmp/ host_key_checking = false forks = 5 timeout = 180 @@ -9,10 +22,7 @@ interpreter_python = /usr/bin/python3 deprecation_warnings = false show_task_path_on_failure = false stdout_callback = omnia_default -roles_path = roles -callback_plugins = callback_plugins -library = library/modules -module_utils = library/module_utils +collections_paths = .:~/.ansible/collections:/usr/share/ansible/collections [persistent_connection] command_timeout = 180 diff --git a/src/image_build_manager/containers/image_builder/README.md b/src/image_build_manager/containers/image_builder/README.md new file mode 100644 index 0000000000..1c3fdce1e7 --- /dev/null +++ b/src/image_build_manager/containers/image_builder/README.md @@ -0,0 +1,51 @@ +# Image Builder Container + +Container image used by the `image_build_manager` to build OS images via +[OpenCHAMI image-build](https://github.com/OpenCHAMI/image-build). + +## Base Image + +`docker.io/library/almalinux:10.0` + +## Contents + +- **Go** (compiled from source) for building `buildah` and `image-build` +- **Buildah** — OCI image builder (built from source with `btrfs` support) +- **OpenCHAMI image-build** — disk image generator +- **Python 3.12** — for Ansible and helper scripts +- Ansible + boto3 + cryptography (see `requirements.txt`) + +## Files + +| File | Description | +|------|-------------| +| `Containerfile.el10` | Multi-stage build for EL10 (AlmaLinux 10.0) | +| `requirements.txt` | Python packages installed inside the container | + +## Building + +The container is built by `containers/build_images.sh` during the +`prepare` phase of the image_build_manager playbook. You do not +normally need to build it manually. + +**Manual build** (for development): + +```bash +cd src/image_build_manager/containers +podman build -t image-builder:dev -f image_builder/Containerfile.el10 image_builder/ +``` + +## Usage + +The playbook launches this container via Podman to execute `image-build` +commands. The container is run with: + +- Pulp TLS certificate mounted at `/etc/pki/ca-trust/source/anchors/` +- Build config YAML mounted at `/home/builder/config.yaml` +- Privileged mode for rootless image assembly + +See `roles/build_os_images/` for the Ansible tasks that invoke this container. + +## License + +Apache License, Version 2.0 diff --git a/src/image_build_manager/docs/architecture.md b/src/image_build_manager/docs/architecture.md new file mode 100644 index 0000000000..0fc6b8d950 --- /dev/null +++ b/src/image_build_manager/docs/architecture.md @@ -0,0 +1,123 @@ +# Image Build Manager — Architecture Overview + +## System Context + +``` + ┌─────────────────────────────────────────────┐ + │ Image Build Manager │ + │ │ + ┌──────────┐ │ ┌────────────┐ ┌────────────┐ ┌────────┐│ ┌──────────┐ + │ config │─────▶│ │ Validate │─▶│ Prepare │─▶│ Build ││─────▶│ S3 / OCI │ + │ .yml │ │ │ (schema + │ │ (MinIO + │ │ (base +││ │ Artifacts│ + │ + repo │ │ │ runtime) │ │ registry) │ │ compute││ │ │ + │ _status │ │ └────────────┘ └────────────┘ └────────┘│ └──────────┘ + └──────────┘ └─────────────────────────────────────────────┘ +``` + +## Execution Mode + +**Bare-metal** — the only supported execution mode. The playbook runs +directly on the RHEL host via `ansible-playbook`. All tasks execute locally +(`connection: local`) except aarch64 builds which SSH to an ARM node. + +## Execution Flow + +### 1. Setup (`image_build_setup` role — tag: always) + +- Validate tags and tag combinations +- Load and validate `config.yml` — hostname regex, IPv4, absolute path checks +- Set project dirs, host vars +- Validate all prerequisite files exist (fail-fast): + - `image_build_config.yml` + - `repo_manager_output_path` directory + - `repo_status.yml` inside it + - `functional_group_packages.yml` inside it +- Load `repo_status.yml` → RPM repo URLs, cert paths +- Validate Pulp certificate exists at absolute path +- Build repo lists, set pulp facts, s3_endpoint + +### 2. Validate (`--tags validate`) + +- Schema validation of `image_build_config.yml` against JSON schema +- Logic validation (S3 provider, aarch64 host, repo_status pre-check) +- No credentials required + +### 3. Prepare (`--tags prepare`) + +- Collect S3 credentials (interactive prompts, Ansible Vault) +- Deploy MinIO S3 (if provider=minio) via Podman Quadlet +- Deploy local OCI container registry via Podman Quadlet + +### 4. Build (`--tags build`) + +- Write `functional_groups_config.yml` from `image_build_config.yml` +- Load `functional_group_packages.yml` → `base_image_packages` + `compute_images_dict` +- Fetch Pulp RPM repo URLs from `repo_status.yml` +- Build base OS image (OpenCHAMI image-build) +- Build compute images per functional group (OpenCHAMI image-build) +- Upload to S3 (boot-images + efi-images buckets) +- Write `build_status.yml` with artifact paths + +### 5. Cleanup (`--tags cleanup`) + +- Stop and remove MinIO + Registry containers +- Remove build artifacts, credentials, S3 data +- Remove firewall ports and systemd entries + +## Role Dependency Graph + +``` +image_build_setup ─────────────────────────────────────────┐ + │ │ + ▼ ▼ +validate_image_build_input collect_build_credentials + │ │ + ▼ ▼ +validate_build_runtime deploy_minio + deploy_registry + │ │ + ▼ ▼ +fetch_build_packages ──────────────────────▶ build_os_images + │ │ + │ ▼ + │ write_build_status + │ + ▼ +cleanup_build_artifacts +``` + +## Data Contract + +### Inputs + +| File | Source | Purpose | +|------|--------|---------| +| `config.yml` | User-created (repo root) | Host + project settings | +| `image_build_config.yml` | `input/project_default/` | S3, functional groups, build params | +| `repo_status.yml` | `/opt/omnia/repo_manager/output//` | RPM repo URLs + OS metadata + cert paths | +| `functional_group_packages.yml` | `/opt/omnia/repo_manager/output//` | Functional group → RPM package mapping | + +### Outputs + +| File | Purpose | +|------|---------| +| `build_status.yml` | S3 artifact paths per functional group | +| Validation logs | `/opt/omnia/image_build_manager/log//` | + +## Key Paths + +| Path | Purpose | +|------|---------| +| `/opt/omnia/image_build_manager/` | Shared path — MinIO data, registry, workdir, logs | +| `/opt/omnia/image_build_manager/log/playbooks/` | Ansible playbook logs | +| `/opt/omnia/repo_manager/output//` | Upstream repo_manager output directory | +| `/opt/omnia/pulp/settings/certs/pulp_webserver.crt` | Pulp TLS certificate (read as-is) | + +## Key Design Decisions + +1. **No `software_config.json`** — replaced by `functional_group_packages.yml` +2. **Bare-metal only** — no container or Omnia core required +3. **Absolute cert paths** — read directly from `repo_status.yml`, no staging +4. **Config uses `host`** — generic key name (not `build_host`), works for all repos +5. **Single mapping file** — `functional_group_packages.yml` is the single source + of truth for which RPMs go into each image variant +6. **Standalone** — no dependency on `omnia_core` container or mono-repo diff --git a/src/image_build_manager/INPUT_CONTRACT.md b/src/image_build_manager/docs/contracts/input-contract.md similarity index 64% rename from src/image_build_manager/INPUT_CONTRACT.md rename to src/image_build_manager/docs/contracts/input-contract.md index 0dd8d83565..f33b00adca 100644 --- a/src/image_build_manager/INPUT_CONTRACT.md +++ b/src/image_build_manager/docs/contracts/input-contract.md @@ -20,7 +20,7 @@ This document defines all input files consumed by the `image_build_manager` doma |-------|------|----------|---------|-------------| | `s3_configurations.provider` | string | Yes | `"minio"` | S3 backend: `minio` (deployed locally) or `powerscale` (external) | | `s3_configurations.endpoint_url` | string | No | `""` | S3 endpoint URL; auto-detected for MinIO, required for PowerScale | -| `repo_manager_output_path` | string | No | `""` | Path to `repo_status.yml`; empty = default location | +| `repo_manager_output_path` | string | Yes | `/opt/omnia/repo_manager/output/project_default/repo_status.yml` | Full path to `repo_status.yml` | | `aarch64_inventory_host_ip` | string | No | `""` | ARM build host IP; empty = skip aarch64 builds | | `aarch64_ssh_user` | string | No | `"root"` | SSH user for ARM build host | | `build_image.job_async` | int | No | `7200` | Max async wait for image build (seconds) | @@ -47,7 +47,7 @@ This document defines all input files consumed by the `image_build_manager` doma |-------|------|----------|-------------| | `s3_access_id` | string | Yes (MinIO) | S3 access key ID; defaults to `admin` for MinIO | | `s3_secret_key` | string | Yes (MinIO) | S3 secret key | -| `provision_password` | string | Yes | Root password for provisioned nodes | +| `aarch64_ssh_password` | string | Conditional | SSH password for ARM build host (only when `aarch64_inventory_host_ip` is set) | **Notes**: - Created by `credential_utility` with tag `image_build_manager` @@ -123,44 +123,65 @@ user_repos: --- -## 4. software_config.json (shared input) +## 4. functional_group_packages.yml -**Purpose**: Cluster OS type, version, and software stack definition. +**Purpose**: Single source of truth mapping functional groups to RPM packages. -**Location**: `input/project_default/software_config.json` +**Location** (runtime): `/opt/omnia/repo_manager/output//functional_group_packages.yml` -**Owner**: User (shared across domains) +**Owner**: User / repo_manager output -**Consumed Fields**: +**Replaces**: The legacy chain of `software_config.json` → `config//*.json` → `image_package_collector.py` -| Field | Usage in image_build_manager | -|-------|------------------------------| -| `cluster_os_type` | Selects base image type (e.g., `rhel`) | -| `cluster_os_version` | Sets OS version for image naming (e.g., `10.0`) | -| `softwares[].name` | Determines functional groups and packages | +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `base_packages` | list[string] | Yes | RPM packages installed in every image (base OS layer) | +| `functional_groups` | dict | Yes | Keyed by functional group name | +| `functional_groups..packages` | list[string] | Yes | Additional RPMs for that functional group (on top of base) | + +**Example**: + +```yaml +base_packages: + - systemd + - kernel + - dracut + +functional_groups: + os_x86_64: + packages: [] + slurm_node_x86_64: + packages: + - munge + - slurm-slurmd + - slurm-pam_slurm +``` + +**Validation**: Functional group names in this file must match entries in +`image_build_config.yml → functional_groups[].name`. + +> **Note**: `software_config.json` is **NOT used** in standalone mode. OS type and version +> come from `repo_status.yml`. Package lists come from this file. --- ## 5. Dependency Summary ``` - ┌─────────────────────────┐ - │ credential_utility │ - │ (tag: image_build_mgr) │ - └────────┬────────────────┘ - │ produces - ▼ - ┌─────────────────────────┐ - │ image_build_credentials │ - │ .yml │ - └────────┬────────────────┘ - │ - ┌──────────────┐ │ ┌──────────────────┐ - │ image_build │ │ │ repo_status.yml │ - │ _config.yml │───────────┼──────────▶│ (from repo_mgr) │ - └──────────────┘ │ └──────────────────┘ - │ - ┌────────▼────────────────┐ - │ image_build_manager.yml │ - └─────────────────────────┘ + ┌──────────────────┐ ┌──────────────────────────────┐ + │ image_build │ │ repo_manager_output/ │ + │ _config.yml │ │ ├── repo_status.yml │ + │ (functional │ │ ├── functional_group │ + │ groups list) │ │ │ _packages.yml │ + └────────┬─────────┘ │ └── certs/ │ + │ └──────────────┬───────────────┘ + │ │ + └──────────┬──────────────────┘ + │ + ┌────────▼────────────────┐ + │ image_build_manager.yml │ + │ │ + │ base_image_packages ◄── base_packages + │ compute_images_dict ◄── functional_groups + packages + └─────────────────────────┘ ``` diff --git a/src/image_build_manager/OUTPUT_CONTRACT.md b/src/image_build_manager/docs/contracts/output-contract.md similarity index 100% rename from src/image_build_manager/OUTPUT_CONTRACT.md rename to src/image_build_manager/docs/contracts/output-contract.md diff --git a/src/image_build_manager/IMAGE_BUILDER_DESIGN.md b/src/image_build_manager/docs/design/image-builder-design.md similarity index 98% rename from src/image_build_manager/IMAGE_BUILDER_DESIGN.md rename to src/image_build_manager/docs/design/image-builder-design.md index 6e8f88f4bf..53159f12f7 100644 --- a/src/image_build_manager/IMAGE_BUILDER_DESIGN.md +++ b/src/image_build_manager/docs/design/image-builder-design.md @@ -89,7 +89,7 @@ src/image_build_manager/ | Credential key | `.image_build_credentials_key` | | Input subdir | `input/project_default/image_build_manager/` | | Output subdir | `output/project_default/image_build_manager/` | -| Log path | `/opt/omnia/log/core/playbooks/image_build_manager.log` | +| Log path | `/opt/omnia/image_build_manager/log/image_build_manager.log` | ### Ansible Config (ansible.cfg) @@ -302,8 +302,8 @@ Other domains can adopt this pattern: | Field | Type | When Required | |-------|------|---------------| | `s3_secret_key` | Mandatory | Always (MinIO password or PowerScale S3 key) | -| `provision_password` | Mandatory | Always (SSH to ARM build host) | | `s3_access_id` | Conditional | When `s3_configurations.provider == powerscale` | +| `aarch64_ssh_password` | Conditional | When `aarch64_inventory_host_ip` is set | ### 6.3 Credential Lifecycle @@ -321,7 +321,7 @@ Other domains can adopt this pattern: |------------|---------------------|-----------------------------| | `s3_secret_key` | `omnia_config_credentials.yml` | `image_build_credentials.yml` only | | `s3_access_id` | `omnia_config_credentials.yml` | `image_build_credentials.yml` only | -| `provision_password` | `omnia_config_credentials.yml` | `image_build_credentials.yml` (mandatory) | +| `aarch64_ssh_password` | `omnia_config_credentials.yml` | `image_build_credentials.yml` (conditional) | ### 6.5 File Locations diff --git a/src/image_build_manager/docs/design/standalone-design.md b/src/image_build_manager/docs/design/standalone-design.md new file mode 100644 index 0000000000..abac8f75e6 --- /dev/null +++ b/src/image_build_manager/docs/design/standalone-design.md @@ -0,0 +1,1076 @@ +# Image Build Manager — Standalone Repository Design + +## Status: ACTIVE v3.1 + +This document describes how `image_build_manager` operates as a **fully independent domain** +running directly on a RHEL bare-metal host using Ansible + Python. +The core container is **not required**. Bare-metal is the only supported execution mode. + +--- + +## 1. Current Independence Audit + +### 1.1 Dependency Inventory + +| # | Dependency | Files Affected | Type | Status | +|---|-----------|---------------|------|--------| +| 1 | **OIM metadata** (`/opt/omnia/.data/oim_metadata.yml`) | `image_build_setup/vars/main.yml` | Config | ✅ Replaced by `config.yml` | +| 2 | **Project config** (`/opt/omnia/input/default.yml`) | `image_build_setup` role | Config | ✅ Replaced by `config.yml` | +| 3 | **Upgrade lock** (`/opt/omnia/.data/upgrade_in_progress.lock`) | `image_build_setup` role | Guard | ✅ Commented out | +| 4 | **repo_status.yml** (from `repo_manager`) | `image_build_manager.yml` Step 4 | Contract | ✅ User-provided in `repo_manager_output/` | +| 5 | **software_config.json** | `fetch_build_packages`, `image_package_collector.py` | Config | ✅ Replaced by `functional_group_packages.yml` | +| 6 | **Core container** (`omnia_core`) | Runtime assumption | Runtime | ✅ Not required — Mode A/B only | +| 7 | **OIM host group** (SSH to OIM) | All `hosts: oim` plays | Inventory | ✅ Auto-detected (local/SSH) | +| 8 | **omnia.target** (systemd) | `prepare_image_build_manager.yml`, `cleanup` | Service | ✅ Skipped in standalone | +| 9 | **`/opt/omnia/` hardcoded paths** | All role vars files | Paths | ✅ All defaults removed — use `config.yml` paths | +| 10 | **Credential utility** | Replaced by `collect_build_credentials` role | Eliminated | ✅ Done | +| 11 | **common/callback_plugins/** | Local copy at `callback_plugins/omnia_default.py` | Eliminated | ✅ Done | +| 12 | **common/library/modules/** | All 5 modules local at `library/modules/` | Eliminated | ✅ Done | +| 13 | **common/library/module_utils/** | `build_image/` + `image_build_validation/` local | Eliminated | ✅ Done | +| 14 | **common/vars/** | `common_vars` inlined into `image_build_setup/vars/main.yml` | Eliminated | ✅ Done | +| 15 | **playbooks/utils/** | All absorbed into domain roles | Eliminated | ✅ Done | + +### 1.2 Self-Containment Summary + +``` +✅ ansible.cfg — fully local paths (library, module_utils, callback_plugins, roles) +✅ callback_plugins/ — local copy of omnia_default.py +✅ library/modules/ — all 5 modules (base_image_package_collector, image_package_collector, + functional_group_parser, generate_functional_groups, validate_image_build_config) +✅ library/module_utils/ — build_image/ + image_build_validation/ fully local +✅ credential flow — collect_build_credentials role +✅ validation flow — validate_image_build_input role + validate_image_build_config module +✅ functional groups — defined in image_build_config.yml (standalone); generate_functional_groups commented out +✅ tag validation — image_build_setup role (supported_tags, skip_credential_tags, invalid_tag_combinations) +✅ zero ../common/ — no ansible.cfg references to ../common/ or ../playbooks/utils/ +``` + +### 1.3 Omnia Coupling — FULLY RESOLVED + +All 9 Omnia coupling points have been resolved: + +| # | Coupling | Resolution | +|---|---------|------------| +| 1 | OIM metadata | Replaced by `config.yml → build_host` section | +| 2 | Project config | Replaced by `config.yml → project_name` | +| 3 | Upgrade lock | Commented out in `image_build_setup/vars/main.yml` | +| 4 | `repo_status.yml` | User-provided in `repo_manager_output/` | +| 5 | `software_config.json` | **Replaced by `functional_group_packages.yml`** — direct RPM mapping, no software_config.json needed | +| 6 | Core container | Mode A (bare-metal) + Mode B (domain container) — no `omnia_core` needed | +| 7 | `omnia.target` | Skipped in standalone mode | +| 8 | `/opt/omnia/` in `ansible.cfg` | Uses `./log/` and `/tmp/.ansible/` | +| 9 | `/opt/omnia/` defaults in vars | All `/opt/omnia` fallback defaults removed from role vars | + +--- + +## 2. Input Files — Package Resolution (Standalone) + +### 2.1 Design Decision: Replace `software_config.json` With Direct Mapping + +**Problem** (Mode C): `software_config.json` is a project-level file shared across ALL domains. +It defines which software modules are enabled, then per-arch JSON files in `config////` +contain the actual RPM packages. `image_package_collector.py` and `base_image_package_collector.py` +read both to resolve the final package list. This chain has 4 files and 2 Python modules. + +**Solution** (Standalone): A single `functional_group_packages.yml` file in `repo_manager_output/` +maps functional groups directly to RPM packages. No `software_config.json`, no per-arch JSON files, +no collector modules needed. + +``` +MODE C (NOT SUPPORTED): STANDALONE (Mode A/B): +software_config.json functional_group_packages.yml + → config////*.json ├── base_packages: [systemd, kernel, ...] + → image_package_collector.py └── functional_groups: + → base_image_package_collector.py slurm_node_x86_64: + → compute_images_dict packages: [munge, slurm-slurmd, ...] + → compute_images_dict (directly) +``` + +### 2.2 Design Decision: Keep `repo_status.yml` Separate, Extend With OS Metadata + +`repo_status.yml` remains a **separate input file** — NOT merged into `config.yml`. + +**Rationale**: +- `repo_status.yml` is the **output contract of repo_manager** — it's infrastructure data, not user config +- In Omnia mode, repo_manager produces it automatically after syncing repos +- In standalone mode, user provides it manually (copy from `samples/repo_status.yml`) +- Merging it into config.yml would force users to duplicate repo URLs into a config format +- The RPM repo URLs already embed OS version in their paths (e.g. `rhel/10.0/rpms/`) + +**Extension**: Add `cluster_os_type` and `cluster_os_version` to `repo_status.yml`: + +```yaml +# repo_status.yml — UPDATED contract (v2) +--- +overall_status: "success" + +# OS metadata (NEW — tells consumers what OS the repos were synced for) +cluster_os_type: "rhel" +cluster_os_version: "10.0" + +# RPM Repository Base URLs +rpm_repos: + x86_64: + baseos: "https://10.20.0.1:2225/pulp/content/.../x86_64_rhel_10.0_baseos/" + appstream: "https://10.20.0.1:2225/pulp/content/.../x86_64_rhel_10.0_appstream/" + # ... other repos + aarch64: + baseos: "https://10.20.0.1:2225/pulp/content/.../aarch64_rhel_10.0_baseos/" + appstream: "https://10.20.0.1:2225/pulp/content/.../aarch64_rhel_10.0_appstream/" + +# Repo manager metadata (Omnia mode — optional in standalone) +repo_manager: + port: 2225 + certificates: + server_crt: "/opt/omnia/pulp/settings/certs/pulp_webserver.crt" + +# User-defined repos (optional) +user_repos: + x86_64: {} + aarch64: {} +``` + +### 2.3 Where OS Version and OS Name Come From + +**Current flow** (Omnia mode): +``` +software_config.json → fetch_build_packages/tasks/main.yml + → include_vars software_config.json + → set_fact rhel_tag: software_config.cluster_os_version + → set_fact default_json_path using cluster_os_type/cluster_os_version + → validate_build_runtime/tasks/main.yml + → validates software_config.json exists and has valid JSON + → library/modules/image_package_collector.py + → reads cluster_os_version from software_config.json + → library/modules/base_image_package_collector.py + → reads software_config.json for OS info +``` + +**Updated flow** (all modes): +``` +repo_status.yml → cluster_os_type, cluster_os_version (OS metadata — NEW) + → rpm_repos (repo URLs — existing) + → repo_manager.certificates (Omnia only — existing) + +software_config.json → softwares[] array (package catalog — KEEP) + → service_k8s version (KEEP) + → additional_packages, admin_debug (KEEP) + → cluster_os_type, cluster_os_version (DEPRECATED — use repo_status.yml) +``` + +### 2.4 Code Changes Required for OS Version Source + +**File: `roles/fetch_build_packages/tasks/main.yml`** — Change OS fact source: + +```yaml +# BEFORE (reads OS info from software_config.json): +- name: Include software config + ansible.builtin.include_vars: + file: "{{ software_config_file_path }}" + name: software_config + +- name: Set cluster OS facts + ansible.builtin.set_fact: + rhel_tag: "{{ software_config.cluster_os_version }}" + default_json_path: "{{ input_project_dir }}/config/{{ build_arch }}/{{ software_config.cluster_os_type }}/{{ software_config.cluster_os_version }}/..." + +# AFTER (reads OS info from repo_status.yml facts, with software_config.json fallback): +- name: Include software config + ansible.builtin.include_vars: + file: "{{ software_config_file_path }}" + name: software_config + +- name: Set cluster OS facts + ansible.builtin.set_fact: + rhel_tag: "{{ cluster_os_version | default(software_config.cluster_os_version) }}" + default_json_path: >- + {{ input_project_dir }}/config/{{ build_arch }}/ + {{ cluster_os_type | default(software_config.cluster_os_type) }}/ + {{ cluster_os_version | default(software_config.cluster_os_version) }}/default_packages.json + additional_json_path: >- + {{ input_project_dir }}/config/{{ build_arch }}/ + {{ cluster_os_type | default(software_config.cluster_os_type) }}/ + {{ cluster_os_version | default(software_config.cluster_os_version) }}/additional_packages.json + admin_debug_json_path: >- + {{ input_project_dir }}/config/{{ build_arch }}/ + {{ cluster_os_type | default(software_config.cluster_os_type) }}/ + {{ cluster_os_version | default(software_config.cluster_os_version) }}/admin_debug_packages.json +``` + +The `cluster_os_type` and `cluster_os_version` facts are set by `image_build_setup` +(from `repo_status.yml` in all modes). The `| default(software_config.*)` fallback +preserves backward compatibility when repo_status.yml doesn't have OS fields yet. + +### 2.5 `software_config.json` — NOT USED in Standalone Mode + +In standalone mode, `software_config.json` is **completely replaced** by +`functional_group_packages.yml`. The entire Mode C package resolution chain +(`software_config.json` → per-arch JSONs → `image_package_collector.py` → +`base_image_package_collector.py`) is commented out in `fetch_build_packages/tasks/main.yml`. + +**Standalone user provides these input files**: +1. `config.yml` — project settings + build host (replaces OIM metadata + default.yml) +2. `repo_status.yml` — RPM repo URLs + OS type/version (in `repo_manager_output/`) +3. `functional_group_packages.yml` — functional group → RPM package mapping (in `repo_manager_output/`) +4. `image_build_config.yml` — S3 provider, functional groups list, aarch64 host, build settings + +--- + +## 3. Execution Modes + +### 3.1 Three Supported Modes + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ IMAGE BUILD MANAGER — THREE EXECUTION MODES │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + + MODE A: BARE-METAL MODE B: DOMAIN CONTAINER MODE C: OMNIA MONO-REPO + ────────────────── ────────────────────── ─────────────────────── + ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ + │ Build Host │ │ image_build │ │ omnia_core │ + │ (RHEL/Rocky) │ │ _runner │ │ container │ + │ │ │ container │ │ (wolfi-base) │ + │ Ansible 2.16+ │ │ (wolfi-base) │ │ Ansible bundled │ + │ Python 3.12+ │ │ Ansible pinned │ │ All domains │ + │ Podman/Docker │ │ Long-running │ │ OIM metadata │ + └────────┬─────────┘ │ (sshd + shell) │ └────────┬─────────┘ + │ └────────┬─────────┘ │ + ┌────────▼─────────┐ ┌────────▼─────────┐ ┌────────▼─────────┐ + │ config.yml │ │ config.yml │ │ OIM metadata │ + │ repo_status.yml │ │ repo_status.yml │ │ default.yml │ + │ software_config │ │ software_config │ │ software_config │ + │ image_build_cfg │ │ image_build_cfg │ │ repo_status.yml │ + └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ + │ │ │ + └───────────────┬───────────────┘ │ + │ │ + └──────────────────┬───────────────────────────────┘ + │ + ┌────────▼────────┐ + │ SHARED BUILD │ + │ PIPELINE │ + │ (same roles, │ + │ same modules, │ + │ same playbooks)│ + └─────────────────┘ +``` + +### 3.2 Mode Comparison + +| Aspect | Mode A: Bare-Metal | Mode B: Domain Container | Mode C: Omnia Mono-Repo | +|--------|-------------------|--------------------------|------------------------| +| **Ansible source** | User-installed (`pip install ansible-core`) | Pinned in container image | Bundled in `omnia_core` | +| **Python source** | User-installed (system or venv) | Pinned in container image | Bundled in `omnia_core` | +| **Container runtime** | Required on host (Podman/Docker) | Required on host | Required on host | +| **Config source** | `config.yml` + `repo_status.yml` + `functional_group_packages.yml` | Same (volume-mounted) | OIM metadata + default.yml | +| **RPM repos** | `repo_status.yml` (user-provided) | Same (volume-mounted) | `repo_status.yml` from repo_manager | +| **OS type/version** | `repo_status.yml` | `repo_status.yml` | `software_config.json` (Mode C) | +| **Package mapping** | `functional_group_packages.yml` | `functional_group_packages.yml` | `software_config.json` + per-arch JSONs (Mode C) | +| **Core container needed** | **No** | **No** | **Yes** | +| **OIM metadata needed** | **No** | **No** | **Yes** | +| **Air-gap support** | Via local RPM mirror URLs in `repo_status.yml` | Same | Via Pulp (repo_manager) | +| **Container lifecycle** | N/A | Long-running (sshd) — `podman exec` for tags | Long-running (sshd) | +| **OIM connection** | `ansible_connection: local` | `ansible_connection: ssh` (auto-detected via `/run/.containerenv`) | SSH to core container | +| **Functional groups** | `image_build_config.yml` | `image_build_config.yml` | `generate_functional_groups` from CSV | +| **Target users** | DevOps, CI/CD, advanced users | Production, repeatable builds | **NOT SUPPORTED** | + +### 3.3 Mode Detection + +```yaml +# Priority: explicit var > config.yml detection > Omnia detection +- name: Detect execution mode + ansible.builtin.set_fact: + execution_mode: >- + {{ 'standalone' if (standalone_mode | default(false) | bool) + else ('standalone' if (lookup('file', playbook_dir + '/../config.yml', + errors='ignore') | default('') | length > 0 + and not (omnia_input_dir is defined)) + else 'omnia') }} + cacheable: true +``` + +--- + +## 4. Container Strategy — Analysis & Recommendation + +### 4.1 Should Each Domain Have Its Own Container? + +| Option | Pros | Cons | +|--------|------|------| +| **A. No container — bare-metal** | Zero container overhead, user controls Ansible/Python versions, simplest for CI/CD, works anywhere with pip | User must manage dependencies, version drift possible | +| **B. Per-domain lightweight container** | Reproducible builds, pinned deps, hermetic execution, independent release cycles | Each domain maintains a Dockerfile, ~200MB per container | +| **C. One `omnia_core` container for everything** | Single image to maintain, all domains share deps | Tight coupling, large image (~2GB), can't release domains independently, single point of failure | +| **D. Orchestrator container that git-clones repos** | Thin meta-container, always gets latest, flexible | Requires network at startup, version pinning is harder, clone failures block execution | + +### 4.2 Recommendation: **A + B Hybrid (Bare-Metal First, Optional Container)** + +``` +RECOMMENDED ARCHITECTURE: +───────────────────────── + 1. Primary: BARE-METAL (Mode A) + - User has Ansible + Python installed + - git clone → edit config.yml → ansible-playbook + - Works in CI/CD pipelines, dev environments, air-gapped hosts + + 2. Optional: PER-DOMAIN CONTAINER (Mode B) + - Containerfile in each domain repo: containers/Containerfile + - Pins exact Ansible + Python + collection versions + - Long-running container (sshd) — user runs multiple tags via exec/SSH + - ~300MB image (wolfi-base + ansible-core + deps + sshd) + + 3. NOT SUPPORTED: Monolithic core container (Mode C) + - Mode C code is guarded by `when: not standalone_mode` and will never execute + - Creates coupling between unrelated domains + - Can't release image_build_manager without rebuilding all of omnia_core + - Forces all domains to share the same Ansible/Python version + + 4. NOT RECOMMENDED: Git-clone orchestrator container (Mode D) + - Network dependency at runtime breaks air-gap + - Version pinning becomes git tags/branches — fragile + - Adds latency and failure modes (DNS, auth, rate limits) +``` + +### 4.3 Container Execution Model — Long-Running (Same as `omnia_core`) + +The domain container follows the **same pattern as `omnia_core`**: a long-running container +with sshd that stays alive. Users SSH in or use `podman exec` to run playbooks with +different tags. The container does **NOT** run-and-exit. + +**Why long-running instead of run-and-exit?** + +| Concern | Run-and-Exit | Long-Running (Recommended) | +|---------|-------------|---------------------------| +| **Multiple tags** | Must restart container for each tag | Run any tag via `podman exec` or SSH | +| **State between runs** | Lost — must remount everything | Preserved — credentials, cached facts, logs | +| **Credential prompts** | Interactive prompts require `-it` on each run | One-time setup, then reuse | +| **Debugging** | Container gone after failure — logs lost | Shell into running container, inspect state | +| **Build artifacts** | Must volume-mount everything | Artifacts persist in container filesystem | +| **MinIO/Registry** | Services need to KEEP RUNNING after prepare | Container stays alive → services stay alive | + +``` +┌─────────────────────────────────────────────────────────────────────────────────────┐ +│ DOMAIN CONTAINER LIFECYCLE │ +└─────────────────────────────────────────────────────────────────────────────────────┘ + + Host Container (image_build_runner) + ──── ────────────────────────────── + │ │ + │ podman run -d ... │ + │────────────────────────────────>│ ┌──────────────────────────┐ + │ │ │ entrypoint.sh: │ + │ │ │ 1. ssh-keygen -A │ + │ │ │ 2. Start sshd on :2230 │ + │ │ │ 3. cd /image_build_mgr │ + │ │ │ 4. Wait (sshd keeps │ + │ │ │ container alive) │ + │ │ └──────────────────────────┘ + │ │ + │ # Run prepare tag │ + │ podman exec -it \ │ + │ ansible-playbook \ │ + │ image_build_manager.yml \ │ + │ --tags prepare │ + │────────────────────────────────>│ → deploys MinIO + Registry + │ │ + │ # Run build tag (later) │ + │ podman exec -it \ │ + │ ansible-playbook \ │ + │ image_build_manager.yml \ │ + │ --tags build │ + │────────────────────────────────>│ → builds OS images + │ │ + │ # Run cleanup tag │ + │ podman exec -it \ │ + │ ansible-playbook \ │ + │ image_build_manager.yml \ │ + │ --tags cleanup │ + │────────────────────────────────>│ → cleanup + │ │ + │ # Interactive shell │ + │ podman exec -it bash │ + │────────────────────────────────>│ → debug, inspect logs + │ │ + │ # OR via SSH │ + │ ssh -p 2230 root@localhost │ + │────────────────────────────────>│ → same access via SSH + │ │ + │ podman stop │ + │────────────────────────────────>│ → container stops + │ │ +``` + +### 4.4 Per-Domain Container Design (Containerfile) + +```dockerfile +# containers/Containerfile — image_build_manager domain runner +# Pattern: Same as omnia_core (long-running, sshd, shell access) +FROM cgr.dev/chainguard/wolfi-base:latest + +ARG IMAGE_BUILD_VERSION=staging + +# ── System packages ───────────────────────────────────────── +RUN apk update && apk add --no-cache \ + python-3.12 \ + py3.12-pip \ + git \ + openssh \ + sshpass \ + openssl \ + jq \ + wget \ + rsync \ + curl \ + ca-certificates-bundle \ + ca-certificates \ + shadow \ + bash \ + nano + +RUN mkdir -p /usr/local/share/ca-certificates + +# ── Python deps ───────────────────────────────────────────── +COPY requirements.txt /opt/image_build_manager/requirements.txt +RUN pip install --no-cache-dir -r /opt/image_build_manager/requirements.txt + +# ── Ansible Galaxy collections ────────────────────────────── +COPY requirements.yml /opt/image_build_manager/requirements.yml +RUN ansible-galaxy collection install -r /opt/image_build_manager/requirements.yml + +# ── SSH configuration (same pattern as omnia_core) ────────── +RUN sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \ + sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \ + sed -i 's/^#Port 22/Port 2230/' /etc/ssh/sshd_config + +EXPOSE 2230 +RUN ssh-keygen -A + +# ── Directory setup ───────────────────────────────────────── +RUN mkdir -p /root/.ssh && chmod 700 /root/.ssh + +# ── Clone domain repo (or COPY for local build) ──────────── +# Option A: git clone at build time (production) +# RUN git clone https://github.com/dell/image-build-manager.git \ +# -b ${IMAGE_BUILD_VERSION} /image_build_manager + +# Option B: COPY local source (development) +COPY . /image_build_manager/ + +WORKDIR /image_build_manager + +RUN echo "cd /image_build_manager" >> /root/.bashrc + +# ── Entrypoint (long-running — sshd keeps container alive) ── +COPY containers/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# ── Environment ───────────────────────────────────────────── +ENV ANSIBLE_LOG_PATH=/image_build_manager/log/image_build_manager.log +ENV ANSIBLE_REMOTE_TMP=/tmp/.ansible/tmp + +ENTRYPOINT ["/entrypoint.sh"] +``` + +### 4.5 Entrypoint Script + +```bash +#!/bin/bash +# containers/entrypoint.sh — keeps container alive (same pattern as omnia_core) + +# Start sshd in background +/usr/sbin/sshd + +echo "=================================================" +echo " image_build_manager container ready" +echo " SSH: ssh -p 2230 root@" +echo " Exec: podman exec -it bash" +echo "=================================================" +echo "" +echo " Run playbooks:" +echo " ansible-playbook image_build_manager.yml" +echo " ansible-playbook image_build_manager.yml --tags prepare" +echo " ansible-playbook image_build_manager.yml --tags build" +echo " ansible-playbook image_build_manager.yml --tags cleanup" +echo " ansible-playbook image_build_manager.yml --tags validate" +echo "=================================================" + +# Keep container alive (wait forever) +exec tail -f /dev/null +``` + +### 4.6 Container Usage — Running Multiple Tags + +```bash +# ═══════════════════════════════════════════════════════════ +# Step 1: Build the domain container image +# ═══════════════════════════════════════════════════════════ +podman build -t image_build_runner:1.0 -f containers/Containerfile . + +# ═══════════════════════════════════════════════════════════ +# Step 2: Start the container (long-running — stays alive) +# ═══════════════════════════════════════════════════════════ +podman run -d --name image_build_mgr \ + --privileged \ + -p 2230:2230 \ + -v ./config.yml:/image_build_manager/config.yml:ro \ + -v ./repo_status.yml:/image_build_manager/repo_status.yml:ro \ + -v ./input:/image_build_manager/input:rw \ + -v ./output:/image_build_manager/output:rw \ + -v /run/podman/podman.sock:/run/podman/podman.sock \ + image_build_runner:1.0 + +# Container is now running with sshd — does NOT exit + +# ═══════════════════════════════════════════════════════════ +# Step 3: Run playbooks with different tags (via podman exec) +# ═══════════════════════════════════════════════════════════ + +# Validate config +podman exec -it image_build_mgr \ + ansible-playbook image_build_manager.yml --tags validate + +# Deploy MinIO + Registry +podman exec -it image_build_mgr \ + ansible-playbook image_build_manager.yml --tags prepare + +# Build images +podman exec -it image_build_mgr \ + ansible-playbook image_build_manager.yml --tags build + +# Full flow (no tags — runs prepare + build) +podman exec -it image_build_mgr \ + ansible-playbook image_build_manager.yml + +# Cleanup +podman exec -it image_build_mgr \ + ansible-playbook image_build_manager.yml --tags cleanup + +# ═══════════════════════════════════════════════════════════ +# Step 4: Interactive shell for debugging +# ═══════════════════════════════════════════════════════════ +podman exec -it image_build_mgr bash + +# OR via SSH +ssh -p 2230 root@localhost + +# ═══════════════════════════════════════════════════════════ +# Step 5: Stop and remove when done +# ═══════════════════════════════════════════════════════════ +podman stop image_build_mgr +podman rm image_build_mgr +``` + +### 4.7 Why NOT a Git-Clone Orchestrator Container + +| Problem | Detail | +|---------|--------| +| **Air-gap breaks** | Can't git clone inside air-gapped environments (Omnia first-class constraint) | +| **Version pinning** | Git branches/tags are mutable; a `main` branch changes under you | +| **Startup latency** | Cloning 5+ repos adds 30-60s before any work starts | +| **Auth complexity** | Private repos need SSH keys or tokens mounted into container | +| **Failure modes** | DNS failure, GitHub rate limits, partial clones — all block execution | +| **Testing overhead** | Can't reproduce a build if upstream repos change between runs | + +**Better alternative for multi-domain orchestration**: A `release.yml` manifest file (or git submodules) that pins exact versions: + +```yaml +# dell-omnia/release/release.yml — version pinning (NOT git-clone at runtime) +domains: + image_build_manager: "v1.2.0" + orchestrator: "v1.1.0" + discovery: "v1.0.0" + repo_manager: "v1.3.0" +``` + +--- + +## 5. Standalone Configuration + +### 5.1 Config File — `config.yml` + +`config.yml` replaces OIM metadata and project config **only**. +It does NOT contain RPM repos or OS version — those stay in `repo_status.yml`. + +```yaml +# image_build_manager/config.yml — Standalone configuration +--- +# Project settings (replaces: /opt/omnia/input/default.yml) +project_name: "my_project" +# input/output paths are auto-derived from src/input/ and src/output/ + +# Build host settings (replaces: /opt/omnia/.data/oim_metadata.yml) +build_host: + hostname: "localhost" # For cluster naming (standalone always runs locally) + shared_path: "/opt/image_build" # Persistent storage for MinIO + Registry data + domain_name: "local" + admin_nic_ip: "10.20.0.1" # Admin NIC IP — Pulp and S3 endpoint + +# Logging (standalone default: local directory) +log_dir: "./log" +``` + +### 5.2 Config Source Mapping + +| Omnia Source | Standalone Equivalent | File | +|-------------|----------------------|------| +| `/opt/omnia/.data/oim_metadata.yml` | `config.yml → build_host` | `config.yml` | +| `/opt/omnia/input/default.yml` | `config.yml → project_name, input_dir, output_dir` | `config.yml` | +| `software_config.json` (OS fields) | `repo_status.yml → cluster_os_type, cluster_os_version` | `repo_status.yml` | +| `software_config.json` (softwares) | Same file — no change | `software_config.json` | +| `repo_status.yml` (from repo_manager) | User-provided `repo_status.yml` — same format | `repo_status.yml` | +| `/opt/omnia/log/` | `config.yml → log_dir` | `config.yml` | +| `image_build_config.yml` | Same file — no change | `image_build_config.yml` | +| `image_build_credentials.yml` | Same file — no change | Ansible Vault | + +--- + +## 6. Changes Required for Standalone Mode + +### 6.1 `image_build_setup` Role — Tri-Mode Bootstrap + +The `image_build_setup` role is the **only role** that needs significant changes. +All downstream roles (deploy_minio, build_os_images, etc.) remain untouched — +they read facts set by setup, not Omnia paths directly. + +```yaml +# roles/image_build_setup/tasks/main.yml — UPDATED FOR TRI-MODE + +# --- Step 0: Tag validation (unchanged) --- +# ... existing tag validation logic ... + +# --- Step 0.5: Detect execution mode --- +- name: Detect standalone mode + ansible.builtin.set_fact: + standalone_mode: >- + {{ standalone_mode | default(false) | bool + or (lookup('file', playbook_dir + '/../config.yml', errors='ignore') + | default('') | length > 0 + and not (omnia_input_dir is defined)) }} + cacheable: true + +# --- Step 1: Upgrade guard (Omnia mode only) --- +- name: Check upgrade lock file + ansible.builtin.stat: + path: "{{ upgrade_lock_path }}" + register: upgrade_lock + when: not standalone_mode | bool + +- name: Block playbook while upgrade is in progress + ansible.builtin.fail: + msg: "{{ upgrade_in_progress_msg }}" + when: + - not standalone_mode | bool + - upgrade_lock.stat.exists + - not (upgrade_mode | default(false) | bool) + +# --- Step 2a: Load project config (Omnia mode) --- +- name: Load omnia project config + when: not standalone_mode | bool + block: + - name: Include omnia project config file + ansible.builtin.include_vars: "{{ omnia_input_config_file }}" + - name: Set input/output dirs from omnia config + ansible.builtin.set_fact: + input_project_dir: "{{ omnia_input_dir }}/{{ project_name }}" + output_project_dir: "{{ omnia_output_dir }}/{{ project_name }}" + cacheable: true + +# --- Step 2b: Load standalone config --- +- name: Load standalone config + when: standalone_mode | bool + block: + - name: Include standalone config.yml + ansible.builtin.include_vars: + file: "{{ playbook_dir }}/../config.yml" + name: standalone_config + - name: Set input/output dirs from standalone config + ansible.builtin.set_fact: + input_project_dir: "{{ standalone_config.input_dir }}/{{ standalone_config.project_name }}" + output_project_dir: "{{ standalone_config.output_dir }}/{{ standalone_config.project_name }}" + project_name: "{{ standalone_config.project_name }}" + admin_nic_ip: "{{ standalone_config.build_host.admin_nic_ip }}" + log_dir: "{{ standalone_config.log_dir | default('./log') }}" + cacheable: true + +# --- Step 3: Load OIM metadata or standalone build_host --- +- name: Include oim metadata vars (Omnia mode) + ansible.builtin.include_vars: "{{ omnia_metadata_file_path }}" + when: not standalone_mode | bool + +- name: Set build host vars (standalone mode) + when: standalone_mode | bool + ansible.builtin.set_fact: + oim_shared_path: "{{ standalone_config.build_host.shared_path }}" + oim_node_name: "{{ standalone_config.build_host.hostname }}" + domain_name: "{{ standalone_config.build_host.domain_name }}" + admin_nic_ip: "{{ standalone_config.build_host.admin_nic_ip }}" + cacheable: true + +# --- Step 3b: Load OS metadata from repo_status.yml --- +# OS type/version come from repo_status.yml (both Omnia and standalone modes) +# repo_status.yml is loaded in image_build_manager.yml Step 4 — these facts +# are set after include_vars of repo_status.yml: +# cluster_os_type: "{{ repo_status.cluster_os_type }}" +# cluster_os_version: "{{ repo_status.cluster_os_version }}" +# Downstream roles (fetch_build_packages) use these facts with fallback: +# rhel_tag: "{{ cluster_os_type | default(software_config.cluster_os_version) }}" + +# --- Step 4: Create build host inventory group --- +- name: Create build_host / oim group + ansible.builtin.add_host: + hostname: "{{ oim_node_name | default('localhost') }}" + ansible_host: "{{ oim_node_name | default('localhost') }}" + ansible_port: "{{ standalone_config.build_host.ssh_port | default(oim_host_port) | default('22') }}" + ansible_user: "{{ standalone_config.build_host.ssh_user | default(omit) }}" + groups: "{{ oim_host_group }}" + +# --- Step 5: Set guard facts --- +- name: Set image_build_manager guard facts + ansible.builtin.set_fact: + image_build_main_flow: true + skip_subscription_check: true + standalone_mode: "{{ standalone_mode }}" + omnia_run_tags: "{{ omnia_run_tags | default(['image_build_manager']) }}" + image_build_setup_done: true + cacheable: true +``` + +### 6.2 `ansible.cfg` — Standalone-Aware Logging + +```ini +# Current (Omnia-coupled): +log_path = /opt/omnia/image_build_manager/log/image_build_manager.log +remote_tmp = /opt/omnia/tmp/.ansible/tmp/ + +# Standalone-ready (use env var fallback): +# NOTE: ansible.cfg doesn't support Jinja — use environment variables instead +log_path = %(LOG_DIR)s/image_build_manager.log +remote_tmp = /tmp/.ansible/tmp/ +``` + +**Practical approach**: Keep `/opt/omnia/` in `ansible.cfg` for mono-repo compatibility. +Override via environment variable in standalone mode: + +```bash +export ANSIBLE_LOG_PATH=./log/image_build_manager.log +ansible-playbook image_build_manager.yml +``` + +### 6.3 `repo_status.yml` — Same Format, User-Provided + +In standalone mode, `repo_status.yml` is **user-provided** with the same format as +repo_manager output. It contains RPM repo URLs + `cluster_os_type`/`cluster_os_version`. + +The existing Step 4 in `image_build_manager.yml` loads `repo_status.yml` via `include_vars`. +In standalone mode, the path is configured in `config.yml` or defaults to `./repo_status.yml`. + +```yaml +# image_build_manager.yml Step 4 — load repo_status.yml (both modes) +- name: Load repo status + ansible.builtin.include_vars: + file: "{{ repo_status_path | default(playbook_dir + '/repo_status.yml') }}" + name: repo_status + +- name: Set OS facts from repo_status + ansible.builtin.set_fact: + cluster_os_type: "{{ repo_status.cluster_os_type }}" + cluster_os_version: "{{ repo_status.cluster_os_version }}" + cacheable: true + when: repo_status.cluster_os_type is defined +``` + +### 6.4 omnia.target — Conditional Skip + +```yaml +# prepare_image_build_manager.yml — add standalone guard +- name: Update omnia.target with services + when: not standalone_mode | default(false) | bool + block: + # ... existing omnia.target registration ... +``` + +--- + +## 7. `/opt/omnia/` Path Decoupling Plan + +### 7.1 Files With Hardcoded `/opt/omnia/` Paths + +| File | Occurrences | Decoupling Strategy | +|------|------------|---------------------| +| `ansible.cfg` | 2 (log_path, remote_tmp) | Override via `$ANSIBLE_LOG_PATH` env var | +| `playbooks/ansible.cfg` | 2 (same) | Same override | +| `image_build_setup/vars/main.yml` | 5 (input/output dirs, metadata, lock) | Already handled by mode detection | +| `cleanup_build_artifacts/vars/main.yml` | 4 (oim_shared, registry, output) | Use `hostvars['localhost']` facts (already does) | +| `deploy_minio/vars/main.yml` | 1 (oim_shared_path default) | Default fallback OK — overridden by facts | +| `deploy_registry/vars/main.yml` | 2 (oim_shared_path, registry_storage) | Default fallback OK | +| `build_os_images/vars/main.yml` | 1 | Default fallback OK | +| Other role vars | 8 | All use `hostvars['localhost']` with `/opt/omnia/` as default | + +### 7.2 Strategy: Facts-First, Defaults-Second + +Most role vars already follow the pattern: +```yaml +oim_shared_path: "{{ hostvars['localhost']['oim_shared_path'] | default('/opt/omnia') }}" +``` + +This means: +- **Omnia mode**: `image_build_setup` loads OIM metadata → sets `oim_shared_path` fact → roles use it +- **Standalone mode**: `image_build_setup` loads `config.yml` → sets `oim_shared_path` from `build_host.shared_path` → roles use it +- **Default `/opt/omnia/`**: Only used if neither mode sets the fact (defensive fallback) + +**No changes needed in downstream roles** — the setup role is the single point of configuration. + +--- + +## 8. Standalone Execution Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────────────┐ +│ STANDALONE EXECUTION FLOW │ +└─────────────────────────────────────────────────────────────────────────────────────┘ + + ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ User │ │ Setup │ │ Build │ │ Build │ │ Output │ + │ (shell) │ │ (config) │ │ Host │ │ Pipeline│ │ │ + └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ │ │ + │ 1. ansible- │ │ │ │ + │ playbook │ │ │ │ + │────────────────>│ │ │ │ + │ │ ┌───────────────────────────────┐ │ │ + │ │ │ 0. Validate tags │ │ │ + │ │ │ 0.5 Detect standalone mode │ │ │ + │ │ │ 1. Skip upgrade guard │ │ │ + │ │ │ 2. Load config.yml │ │ │ + │ │ │ 3. Set build_host facts │ │ │ + │ │ │ 4. Create host group │ │ │ + │ │ │ 5. Set guard facts │ │ │ + │ │ └───────────────────────────────┘ │ │ + │ │ │ │ │ + │ │ Validate config │ │ │ + │ │────────────────>│ │ │ + │ │ Collect creds │ │ │ + │ │────────────────>│ │ │ + │ │ Deploy MinIO │ │ │ + │ │────────────────>│ │ │ + │ │ Deploy Registry │ │ │ + │ │────────────────>│ │ │ + │ │ │ │ │ + │ │ Build images (x86_64 + aarch64) │ │ + │ │──────────────────────────────────>│ │ + │ │ │ │ │ + │ │ Write build_status.yml │ + │ │────────────────────────────────────────────────────>│ + ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ + │ User │ │ Setup │ │ Build │ │ Build │ │ Output │ + └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ +``` + +### Usage Commands + +```bash +# ═══════════════════════════════════════════════════════════ +# MODE A: Bare-metal (user has Ansible + Python installed) +# ═══════════════════════════════════════════════════════════ + +# Prerequisites check +python3 --version # 3.11+ +ansible --version # 2.16+ +podman --version # 4.0+ (for MinIO/registry containers) + +# Clone and configure +git clone https://github.com/dell/image-build-manager.git +cd image-build-manager +pip install -r requirements.txt +ansible-galaxy collection install -r requirements.yml +cp config.yml.sample config.yml +vim config.yml # Set your RPM repos, build host, OS version + +# Create input directory +mkdir -p input/my_project/image_build_manager +cp samples/image_build_config.yml input/my_project/image_build_manager/ + +# Run +ansible-playbook image_build_manager.yml # Full flow +ansible-playbook image_build_manager.yml --tags prepare # Deploy MinIO + Registry +ansible-playbook image_build_manager.yml --tags build # Build images only +ansible-playbook image_build_manager.yml --tags validate # Validate config only +ansible-playbook image_build_manager.yml --tags cleanup # Cleanup + +# ═══════════════════════════════════════════════════════════ +# MODE B: Per-domain container (zero local setup) +# ═══════════════════════════════════════════════════════════ + +# Build image +podman build -t image_build_runner:1.0 -f containers/Containerfile . + +# Start container (long-running — stays alive) +podman run -d --name image_build_mgr \ + --privileged \ + -p 2230:2230 \ + -v ./config.yml:/image_build_manager/config.yml:ro \ + -v ./repo_status.yml:/image_build_manager/repo_status.yml:ro \ + -v ./input:/image_build_manager/input:rw \ + -v ./output:/image_build_manager/output:rw \ + -v /run/podman/podman.sock:/run/podman/podman.sock \ + image_build_runner:1.0 + +# Run playbooks via podman exec (container stays alive between runs) +podman exec -it image_build_mgr ansible-playbook image_build_manager.yml --tags prepare +podman exec -it image_build_mgr ansible-playbook image_build_manager.yml --tags build +podman exec -it image_build_mgr ansible-playbook image_build_manager.yml --tags cleanup + +# Stop when done +podman stop image_build_mgr && podman rm image_build_mgr + +# ═══════════════════════════════════════════════════════════ +# MODE C: Inside Omnia mono-repo (existing behavior — unchanged) +# ═══════════════════════════════════════════════════════════ + +cd src/image_build_manager +ansible-playbook image_build_manager.yml +``` + +--- + +## 9. Repository Structure (Standalone) + +``` +image-build-manager/ # STANDALONE REPO +├── README.md # Getting started (3 modes documented) +├── config.yml.sample # Sample standalone config +├── requirements.txt # Python: ansible-core, jsonschema, PyYAML, jmespath +├── requirements.yml # Ansible collections: ansible.utils, community.general +├── Makefile # help, lint, test, build, clean targets +├── .gitignore # input/, output/, *.retry, *.log, .vault_pass +├── ansible.cfg # Domain config (fully local paths) +├── image_build_manager.yml # Top-level entrypoint +├── callback_plugins/ +│ └── omnia_default.py +├── library/ +│ ├── modules/ +│ │ ├── base_image_package_collector.py +│ │ ├── image_package_collector.py +│ │ ├── functional_group_parser.py +│ │ ├── generate_functional_groups.py +│ │ └── validate_image_build_config.py +│ └── module_utils/ +│ ├── build_image/ +│ │ ├── __init__.py +│ │ ├── common_functions.py +│ │ └── config.py +│ └── image_build_validation/ +│ ├── __init__.py +│ ├── image_build_validation_flow.py +│ └── schema/ +│ ├── image_build_config.json +│ ├── image_build_credentials.json +│ └── functional_groups_config.json +├── playbooks/ +│ ├── ansible.cfg +│ ├── prepare_image_build_manager.yml +│ ├── build_image_x86_64.yml +│ ├── build_image_aarch64.yml +│ ├── cleanup_image_build_manager.yml +│ ├── get_build_credentials.yml +│ ├── validate_image_build_config.yml +│ ├── upgrade_image_build_manager.yml +│ └── rollback_image_build_manager.yml +├── roles/ +│ ├── image_build_setup/ # Tri-mode bootstrap +│ ├── validate_image_build_input/ # L1 schema + L2 logic +│ ├── collect_build_credentials/ # Credential prompt + vault +│ ├── generate_functional_groups/ # CSV → functional_groups_config.yml +│ ├── validate_build_runtime/ # L3 runtime pre-checks +│ ├── deploy_minio/ # MinIO S3 container +│ ├── deploy_registry/ # OCI container registry +│ ├── fetch_build_packages/ # RPM package collection +│ ├── build_os_images/ # Image build (x86_64 + aarch64) +│ ├── prepare_aarch64_node/ # ARM build host setup +│ └── cleanup_build_artifacts/ # Cleanup all artifacts +├── containers/ +│ ├── Containerfile # Per-domain runner container (Mode B) +│ ├── build_images.sh # Container image builder script +│ └── image_builder/ # Builder container (for OS image builds) +├── vars/ +│ ├── image_vars.yml # S3 bucket constants +│ └── openchami_image_cmd.yml # OpenCHAMI build commands +├── samples/ +│ ├── config.yml # Sample standalone config +│ ├── image_build_config.yml # Sample domain config +│ ├── build_status.yml # Sample output +│ └── repo_status.yml # Sample repo_manager output +├── input/ # User input (gitignored) +├── output/ # Build output (gitignored) +├── log/ # Logs (gitignored) +├── test/ # Unit + integration tests +│ ├── test_validate_config.py +│ └── test_functional_groups.py +├── IMAGE_BUILDER_DESIGN.md +├── INPUT_CONTRACT.md +├── OUTPUT_CONTRACT.md +└── STANDALONE_REPO_DESIGN.md # This file +``` + +--- + +## 10. Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|-----------| +| Ansible version mismatch (bare-metal) | Module incompatibility | Pin in `requirements.txt`; test matrix in CI | +| Missing Python deps on bare-metal host | Import failures | `requirements.txt` + pre-flight check in setup role | +| RPM repo URL format differs from Pulp | Build failures | Validate URLs in `validate_build_runtime` role | +| No NFS shared path in standalone | Artifacts inaccessible | Support `build_host.hostname: localhost` for local-only builds | +| Credentials on non-containerized host | Vault key exposure | Same Ansible Vault encryption — identical security model | +| Container runtime differences | MinIO/registry deploy fails | Document: Podman 4+ or Docker 24+; test both | +| Standalone container needs podman-in-podman | Nested container execution | Mount host podman socket; document rootless vs rootful | +| Air-gap standalone | Can't pull container images | Document pre-pull + `podman save/load` workflow | + +--- + +## 11. Prerequisites + +### System Requirements + +| Requirement | Minimum | Notes | +|------------|---------|-------| +| OS | RHEL 9.x / 10.x, Rocky 9.x, Fedora 40+ | | +| Python | 3.11+ | System or venv | +| Ansible | ansible-core 2.16+ | `pip install ansible-core` | +| Container runtime | Podman 4.0+ or Docker 24.0+ | For MinIO + registry containers | +| Disk space | 50 GB free | For OS image builds | +| RAM | 8 GB minimum | 16 GB recommended | +| Network | Access to RPM repos (direct or mirrored) | Air-gap: use local mirror URLs | + +### Python Dependencies (`requirements.txt`) + +``` +ansible-core>=2.16,<2.18 +jsonschema>=4.17 +PyYAML>=6.0 +jmespath>=1.0 +``` + +### Ansible Collections (`requirements.yml`) + +```yaml +collections: + - name: ansible.utils + version: ">=2.0" + - name: community.general + version: ">=7.0" +``` + +--- + +## 12. Naming Convention + +| Component | Convention | Example | +|-----------|-----------|---------| +| **Playbook** | `verb_noun.yml` | `get_build_credentials.yml`, `build_image_x86_64.yml` | +| **Role** | `verb_noun` | `collect_build_credentials`, `deploy_minio` | +| **Data file** | `noun.yml` | `image_build_credentials.yml`, `build_status.yml` | +| **Task file** | `verb_noun.yml` | `prompt_credential_field.yml`, `cleanup_minio.yml` | +| **Container** | `image_build_runner` | Per-domain runner | +| **Config** | `config.yml` | Standalone all-in-one config | diff --git a/src/image_build_manager/docs/design/standalone-mode-a.md b/src/image_build_manager/docs/design/standalone-mode-a.md new file mode 100644 index 0000000000..563b4e10f5 --- /dev/null +++ b/src/image_build_manager/docs/design/standalone-mode-a.md @@ -0,0 +1,454 @@ +# Image Build Manager — Bare-Metal Design + +## Status: ACTIVE v1.1 + +This document describes how `image_build_manager` operates directly on a +RHEL bare-metal host using Ansible + Python, without any container or Omnia core dependency. +This is the only supported execution mode. + +--- + +## 1. Execution Model + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Mode A — Bare-Metal │ +│ │ +│ User runs ansible-playbook on the RHEL host directly. │ +│ All tasks execute locally (connection: local) or via SSH (aarch64). │ +│ No container runtime required for the playbook itself. │ +│ Podman is required for building OS images (OpenCHAMI) and deploying │ +│ MinIO + Registry via Quadlet. │ +│ │ +│ ┌──────────┐ ┌──────────────────┐ ┌──────────────┐ │ +│ │ config │────>| image_build │────>│ build_status │ │ +│ │ .yml │ │ _manager.yml │ │ .yml │ │ +│ └──────────┘ └──────────────────┘ └──────────────┘ │ +│ │ │ +│ ┌──────┴──────┐ │ +│ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ MinIO S3 │ │ Registry │ │ +│ │ (Quadlet)│ │ (Quadlet)│ │ +│ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Other Modes (NOT SUPPORTED) + +| Mode | Description | Status | +|------|-------------|--------| +| **B — Container** | Run inside a long-running domain container | Commented out. `image_build_runner` container kept as-is but not used by playbooks. | +| **C — Omnia mono-repo** | Run inside `omnia_core` container via NFS | Commented out. All `/opt/omnia/` Omnia coupling removed. | + +--- + +## 2. Directory Layout + +### Shared Path Convention + +Each Omnia domain gets its own directory under `/opt/omnia/`: + +``` +/opt/omnia/ +├── image_build_manager/ ← this domain's persistent data +│ ├── workdir/ ← OpenCHAMI image build working directory +│ ├── log/ ← build logs (base + compute images) +│ └── ... ← MinIO data, registry data +├── repo_manager/ +│ └── output/ +│ └── repo_status.yml ← upstream input (produced by repo_manager) +└── pulp/ + └── settings/ + └── certs/ + ├── pulp_webserver.crt ← Pulp TLS certificate (read as-is) + └── pulp_webserver.key ← Pulp TLS key +``` + +### Repository Layout + +``` +image-build-manager/ +├── config.yml # Host and project settings (edit this) +├── config.yml.sample # Sample config +├── requirements.txt # Python dependencies +├── requirements.yml # Ansible collections +├── Makefile # help, setup, lint, test, clean +├── docs/ # All documentation +│ ├── design/ # Architecture and design docs +│ ├── migration/ # Migration history +│ ├── contracts/ # Input/output contracts +│ ├── architecture.md # Architecture overview +│ ├── package-mapping-guide.md # Package customization guide +│ └── troubleshooting.md # Common issues +├── test/ # Unit tests +└── src/ + ├── ansible.cfg # Fully local paths + ├── image_build_manager.yml # Main playbook entry point + ├── roles/ # All Ansible roles + ├── playbooks/ # Sub-playbooks + ├── library/ # Custom Ansible modules + ├── callback_plugins/ # Output callback + ├── vars/ # Shared variables + ├── input/ # User input files + │ └── project_default/ + │ ├── image_build_config.yml # Build config + │ └── repo_manager_output/ # Local dev sample files + │ ├── repo_status.yml # Sample repo URLs + │ ├── functional_group_packages.yml + │ └── certs/ # Sample certs + ├── output/ # Build output (auto-created) + ├── samples/ # Reference samples + └── containers/ # Container build files (kept, not used by Mode A) +``` + +--- + +## 3. Configuration + +### `config.yml` + +```yaml +project_name: "project_default" + +host: + hostname: "myhost" + shared_path: "/opt/omnia/image_build_manager" + domain_name: "local" + admin_nic_ip: "10.20.0.1" +``` + +| Field | Description | +|-------|-------------| +| `project_name` | Maps to `input//` and `output//` directories | +| `host.hostname` | Short hostname (NOT FQDN) — domain_name appended automatically | +| `host.shared_path` | Persistent storage for MinIO data, registry data, workdir | +| `host.domain_name` | Domain suffix for registry naming | +| `host.admin_nic_ip` | Admin NIC IP — used for Pulp, S3, and registry endpoints | + +### `repo_status.yml` (from repo_manager) + +In production, repo_manager writes to `/opt/omnia/repo_manager/output/repo_status.yml`. +The `image_build_config.yml` references this path: + +```yaml +repo_manager_output_path: "/opt/omnia/repo_manager/output/project_default/repo_status.yml" +``` + +Certificate paths in `repo_status.yml` are **absolute** and read directly: + +```yaml +repo_manager: + port: 2225 + certificates: + server_crt: /opt/omnia/pulp/settings/certs/pulp_webserver.crt + server_key: /opt/omnia/pulp/settings/certs/pulp_webserver.key + certs_dir: /opt/omnia/pulp/settings/certs +``` + +The playbook validates the cert file exists at the specified path — no staging or copying needed. + +--- + +## 4. Ansible Installation + +### If Ansible is already installed + +Check the installed version: + +```bash +ansible --version +``` + +- **Required**: `ansible-core >= 2.20` +- If your system has an older version, you can install the required version in a virtual environment without affecting the system installation. + +### Fresh install (recommended) + +```bash +# Create a virtual environment (keeps your system Python clean) +python3 -m venv ~/.venvs/image-build +source ~/.venvs/image-build/bin/activate + +# Install dependencies +pip install -r requirements.txt +ansible-galaxy collection install -r requirements.yml +``` + +### Install alongside existing Ansible + +If you already have Ansible installed system-wide and don't want to disturb it: + +```bash +# Option 1: Use a virtual environment (RECOMMENDED) +python3 -m venv ~/.venvs/image-build +source ~/.venvs/image-build/bin/activate +pip install -r requirements.txt + +# Option 2: Use pipx for isolated installation +pipx install ansible-core +pipx inject ansible-core -r requirements.txt + +# Option 3: User-level install (no sudo needed) +pip install --user -r requirements.txt +``` + +### Verify installation + +```bash +ansible --version # Should show ansible-core 2.20+ +ansible-galaxy collection list | grep containers.podman +python3 -c "import yaml; print('PyYAML OK')" +``` + +--- + +## 5. Quick Start (Mode A) + +```bash +# 1. Clone and configure +git clone image-build-manager +cd image-build-manager +cp config.yml.sample config.yml +# Edit config.yml — set admin_nic_ip, shared_path, domain_name + +# 2. Install dependencies +python3 -m venv ~/.venvs/image-build +source ~/.venvs/image-build/bin/activate +pip install -r requirements.txt +ansible-galaxy collection install -r requirements.yml + +# 3. Ensure repo_manager output is available +# Production: repo_status.yml is at /opt/omnia/repo_manager/output/repo_status.yml +# Development: edit samples/repo_manager_output/repo_status.yml +# and update repo_manager_output_path in image_build_config.yml + +# 4. Configure functional groups +vi input/project_default/image_build_config.yml + +# 5. Run +cd src +ansible-playbook image_build_manager.yml --tags validate # Validate only +ansible-playbook image_build_manager.yml --tags prepare # Deploy MinIO + Registry +ansible-playbook image_build_manager.yml --tags build # Build OS images +ansible-playbook image_build_manager.yml --tags cleanup # Clean up everything +``` + +--- + +## 6. Data Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────────────┐ +│ IMAGE BUILD MANAGER - DATA FLOW (Mode A) │ +└─────────────────────────────────────────────────────────────────────────────────────┘ + + ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ │ │ │ │ │ │ │ │ │ + │ User │ │ Setup │ │ Validate │ │ Prepare │ │ Build │ + │ │ │ │ │ │ │ │ │ │ + └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ │ │ + │ config.yml │ │ │ │ + │────────────────>│ │ │ │ + │ │ │ │ │ + │ │ image_build │ │ │ + │ │ _config.yml │ │ │ + │ │────────────────>│ │ │ + │ │ │ │ │ + │ │ repo_status.yml │ │ │ + │ │────────────────>│ │ │ + │ │ │ │ │ + │ │ │ Credentials │ │ + │ │ │────────────────>│ │ + │ │ │ │ │ + │ │ │ │ MinIO + Registry│ + │ │ │ │────────────────>│ + │ │ │ │ │ + │ │ │ │ ┌─────────────────────────────┐ + │ │ │ │ │ OpenCHAMI image-build: │ + │ │ │ │ │ - Build base image │ + │ │ │ │ │ - Build compute images │ + │ │ │ │ │ - Upload to S3 │ + │ │ │ │ └─────────────────────────────┘ + │ │ │ │ │ + │ build_status │ │ │ │ + │<──────────────────────────────────────────────────────────────────────│ + │ │ │ │ │ + ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ + │ │ │ │ │ │ │ │ │ │ + │ User │ │ Setup │ │ Validate │ │ Prepare │ │ Build │ + │ │ │ │ │ │ │ │ │ │ + └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ + +Figure: Image Build Manager data flow in Mode A (bare-metal) +``` + +--- + +## 7. Dependency Inventory (Mode A) + +| # | Dependency | Resolution | +|---|-----------|------------| +| 1 | OIM metadata | Replaced by `config.yml → host` section | +| 2 | Project config | Replaced by `config.yml → project_name` | +| 3 | Upgrade lock | Commented out (Omnia mode only) | +| 4 | `repo_status.yml` | Read from `/opt/omnia/repo_manager/output/repo_status.yml` | +| 5 | `software_config.json` | Replaced by `functional_group_packages.yml` | +| 6 | Core container | Not required — runs on bare-metal | +| 7 | OIM host group | Created as localhost with `connection: local` | +| 8 | `omnia.target` | Skipped in Mode A | +| 9 | `/opt/omnia/` defaults | Config-driven via `host.shared_path` | +| 10 | Credential utility | Replaced by `collect_build_credentials` role | +| 11 | `common/callback_plugins/` | Local copy at `callback_plugins/omnia_default.py` | +| 12 | `common/library/modules/` | All modules local at `library/modules/` | +| 13 | `common/library/module_utils/` | Fully local at `library/module_utils/` | +| 14 | `common/vars/` | Inlined into `image_build_setup/vars/main.yml` | +| 15 | `playbooks/utils/` | Absorbed into domain roles | +| 16 | Pulp certificates | Read directly from absolute path in `repo_status.yml` | + +--- + +## 8. Upgrade & Rollback (Image Build Manager) + +### Tags + +```bash +ansible-playbook image_build_manager.yml --tags upgrade # Upgrade to new version +ansible-playbook image_build_manager.yml --tags rollback # Rollback to previous version +``` + +### Version File + +After successful deployment, the playbook writes: + +```yaml +# /.domain_version.yml +--- +domain: "image_build_manager" +version: "1.0.0" +installed_at: "2026-07-27T10:30:00Z" +ansible_version: "2.20.0" +python_version: "3.12.8" +config_hash: "sha256:abc123..." +previous_version: null +``` + +### Upgrade Flow (image_build_manager) + +``` + 1. Pre-upgrade validation + ├── Read .domain_version.yml → current version + ├── Verify target version is newer + ├── Check disk space (snapshot needs ~equal space to state_path) + └── Verify MinIO + Registry services are healthy + + 2. Snapshot current state + ├── Create /.upgrade_snapshot/ + ├── Copy MinIO data, registry data, Quadlet configs + └── Save current .domain_version.yml + + 3. Stop services + ├── systemctl stop minio + └── systemctl stop registry + + 4. Apply migrations + ├── Update Quadlet service files (if container image changed) + ├── Migrate config format (if schema changed) + └── Move/rename data directories (if path convention changed) + + 5. Restart services + ├── systemctl daemon-reload + ├── systemctl start minio + └── systemctl start registry + + 6. Post-upgrade verification + ├── MinIO health check (s3cmd ls) + ├── Registry health check (podman pull test) + └── Validate S3 buckets exist + + 7. Update version file + └── Write .domain_version.yml with new version +``` + +### Rollback Flow (image_build_manager) + +``` + 1. Validate snapshot + └── Check /.upgrade_snapshot/ exists + + 2. Stop services + ├── systemctl stop minio + └── systemctl stop registry + + 3. Restore from snapshot + ├── Remove current Quadlet files + ├── Restore Quadlet files from snapshot + ├── Restore MinIO data from snapshot + └── Restore registry data from snapshot + + 4. Restart services + ├── systemctl daemon-reload + ├── systemctl start minio + └── systemctl start registry + + 5. Verify rollback + ├── MinIO health check + ├── Registry health check + └── Restore .domain_version.yml from snapshot + + 6. Cleanup + └── Remove .upgrade_snapshot/ (optional — keep for safety) +``` + +### Playbook Structure + +``` +src/ +├── playbooks/ +│ ├── upgrade_image_build_manager.yml # Upgrade entry point +│ └── rollback_image_build_manager.yml # Rollback entry point +└── roles/ + └── image_build_upgrade/ # Upgrade/rollback role + ├── tasks/ + │ ├── main.yml # (empty — not called directly) + │ ├── pre_upgrade.yml # Version check, disk space, health + │ ├── snapshot.yml # Create state snapshot + │ ├── migrate.yml # Apply data/config migrations + │ ├── verify.yml # Post-upgrade/rollback verification + │ ├── validate_snapshot.yml # Check snapshot exists + │ └── restore.yml # Restore from snapshot + └── vars/ + └── main.yml # Error messages, version constants +``` + +### Invalid Tag Combinations + +Upgrade and rollback are mutually exclusive with all other operational tags: + +```yaml +# In image_build_setup/vars/main.yml +invalid_tag_combinations: + - [prepare, upgrade] + - [build, upgrade] + - [cleanup, upgrade] + - [prepare, rollback] + - [build, rollback] + - [cleanup, rollback] + - [upgrade, rollback] +``` + +--- + +## 9. Container Files (Kept, Not Required) + +The `src/containers/` directory contains: + +| Container | Purpose | Status | +|-----------|---------|--------| +| `image_builder/` | OpenCHAMI image-build container (ochami) | **USED** — pulled from Docker Hub for image builds | +| `image_build_runner/` | Domain runner container (sshd, long-running) | **KEPT** — not used by Mode A playbooks | +| `build_images.sh` | Build script for containers | **KEPT** — used for image_builder builds | + +The `image_build_runner` container and its Mode B integration are kept in the codebase +for future use but are not invoked by any Mode A playbook flow. diff --git a/src/image_build_manager/docs/package-mapping-guide.md b/src/image_build_manager/docs/package-mapping-guide.md new file mode 100644 index 0000000000..56852d0e75 --- /dev/null +++ b/src/image_build_manager/docs/package-mapping-guide.md @@ -0,0 +1,164 @@ +# Package Mapping Guide + +## Overview + +The `functional_group_packages.yml` file defines which RPM packages are installed +in each OS image variant. It is the **single source of truth** for package resolution. + +**Location** (runtime): `/opt/omnia/repo_manager/output//functional_group_packages.yml` + +**Sample**: `samples/repo_manager_output/functional_group_packages.yml` + +## File Structure + +```yaml +# Base packages — installed in EVERY image +base_packages: + - systemd + - kernel + - dracut + - nfs-utils + - NetworkManager + - ... + +# Functional group packages — ADDITIONAL packages per image variant +functional_groups: + os_x86_64: + packages: [] # Only base packages + + slurm_node_x86_64: + packages: + - munge # Slurm common + - slurm-slurmd # Slurm node daemon + - slurm-pam_slurm # Slurm PAM module + - openldap # LDAP authentication + - sssd # System Security Services + - ... + + slurm_control_node_x86_64: + packages: + - munge + - slurm-slurmctld # Slurm controller + - slurm-slurmdbd # Slurm database + - mariadb-server # Database backend + - ... +``` + +## How It Works + +``` +image_build_config.yml functional_group_packages.yml +┌──────────────────────────┐ ┌──────────────────────────────────┐ +│ functional_groups: │ │ base_packages: [...] │ +│ - name: slurm_node_x86│──┐ │ functional_groups: │ +│ - name: os_x86_64 │ └──────▶│ slurm_node_x86_64: │ +└──────────────────────────┘ │ packages: [munge, ...] │ + └──────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ + base_image_packages compute_images_dict + (flat RPM list) (dict per group) + │ │ + ▼ ▼ + Base OS image Compute image per group + (all base RPMs) (base + group RPMs) +``` + +1. `image_build_config.yml` lists which functional groups to build +2. `functional_group_packages.yml` maps each group to its RPM packages +3. `fetch_build_packages` role loads the mapping and creates: + - `base_image_packages` — flat list from `base_packages` + - `compute_images_dict` — dict keyed by group name, each with `packages` list +4. `build_os_images` role builds one base image + one compute image per group + +## Customization + +### Adding packages to a functional group + +Edit `functional_group_packages.yml`: + +```yaml +functional_groups: + slurm_node_x86_64: + packages: + - munge + - slurm-slurmd + - my-custom-package # <-- Add your package here +``` + +### Adding a new functional group + +1. Add the group to `functional_group_packages.yml`: + +```yaml +functional_groups: + my_custom_group_x86_64: + packages: + - package-a + - package-b +``` + +2. Enable it in `image_build_config.yml`: + +```yaml +functional_groups: + - name: "my_custom_group_x86_64" +``` + +3. The group name **must** end with `_x86_64` or `_aarch64`. + +### Adding base packages (all images) + +```yaml +base_packages: + - systemd + - kernel + - my-base-package # <-- Added to ALL images +``` + +## Package Name Rules + +- Package names must match RPM package names available in the Pulp repos + defined in `repo_status.yml` +- Use the **RPM package name** (not the binary name) +- Example: `slurm-slurmd` (not `slurmd`) +- Packages are resolved by `dnf` during image build — if a package is not + found in any configured repo, the build will fail + +## Valid Functional Group Names + +From `FUNCTIONAL_GROUP_LAYER_MAP` in `plugins/module_utils/build_image/config.py`: + +### x86_64 + +| Group Name | Layer | Description | +|------------|-------|-------------| +| `os_x86_64` | compute | Base OS only | +| `slurm_node_x86_64` | compute | Slurm compute node | +| `slurm_control_node_x86_64` | management | Slurm controller | +| `login_node_x86_64` | compute | Login/access node | +| `login_compiler_node_x86_64` | compute | Login node with compilers | +| `service_kube_control_plane_first_x86_64` | management | K8s first control plane | +| `service_kube_control_plane_x86_64` | management | K8s control plane | +| `service_kube_node_x86_64` | compute | K8s worker node | + +### aarch64 + +| Group Name | Layer | Description | +|------------|-------|-------------| +| `os_aarch64` | compute | Base OS only | +| `slurm_node_aarch64` | compute | Slurm compute node | +| `login_node_aarch64` | compute | Login/access node | +| `login_compiler_node_aarch64` | compute | Login node with compilers | + +## Historical Context (Omnia Mono-Repo) + +In the legacy Omnia mono-repo, package resolution used: + +1. `software_config.json` — listed enabled software modules +2. `config////*.json` — per-software RPM lists +3. `image_package_collector.py` — Python module that read both and merged +4. `base_image_package_collector.py` — collected base/admin/debug packages + +All of this is **replaced** by the single `functional_group_packages.yml` file. diff --git a/src/image_build_manager/docs/troubleshooting.md b/src/image_build_manager/docs/troubleshooting.md new file mode 100644 index 0000000000..df13439be7 --- /dev/null +++ b/src/image_build_manager/docs/troubleshooting.md @@ -0,0 +1,178 @@ +# Troubleshooting Guide + +## Common Issues + +### 1. "repo_status.yml not found" + +**Error**: +``` +repo_status.yml not found at .../repo_manager_output/repo_status.yml. +Copy repo_status.yml from your repo_manager output into .../repo_manager_output/. +``` + +**Fix**: Copy `repo_status.yml` from your repo_manager host to the runtime path: +```bash +mkdir -p /opt/omnia/repo_manager/output/project_default +cp src/image_build_manager/samples/repo_manager_output/repo_status.yml \ + /opt/omnia/repo_manager/output/project_default/ +``` +Edit the file with your actual Pulp server URLs and cert paths. + +--- + +### 2. "Unexpected property 'functional_groups'" + +**Error**: +``` +image_build_config.yml: Unexpected property 'functional_groups' +``` + +**Fix**: The JSON schema at `plugins/module_utils/image_build_validation/schema/image_build_config.json` +must include the `functional_groups` property. This was added in the standalone migration. +Verify the schema file has the `functional_groups` entry. + +--- + +### 3. "No x86_64 functional groups found" + +**Error**: +``` +No x86_64 functional groups found in functional_group_config. +Please ensure x86_64 functional groups are defined in image_build_config.yml. +``` + +**Fix**: Uncomment at least one `x86_64` functional group in +`input/project_default/image_build_config.yml`: + +```yaml +functional_groups: + - name: "os_x86_64" + - name: "slurm_node_x86_64" +``` + +--- + +### 4. "Destination directory /etc/containers/systemd does not exist" + +**Error** during MinIO or Registry deployment: +``` +fatal: Destination directory /etc/containers/systemd does not exist +``` + +**Fix**: The `deploy_minio` and `deploy_registry` roles create this directory +automatically. If you see this error, ensure the `prepare` tag ran on the +correct host (the `oim` host group). + +--- + +### 5. Pulp certificate not found + +**Error**: +``` +Pulp certificate not found at /opt/omnia/pulp/settings/certs/pulp_webserver.crt. +Ensure repo_manager has been run and the certificate exists on this host. +``` + +**Fix**: The playbook reads the cert path directly from `repo_status.yml`. +Ensure `repo_manager` has been run and the certificate exists at the path +specified in `repo_status.yml → repo_manager.certificates.server_crt`. + +--- + +### 6. Package not found during image build + +**Error** in OpenCHAMI build log: +``` +No match for argument: +``` + +**Fix**: The RPM package name in `functional_group_packages.yml` does not exist +in any of the Pulp repos defined in `repo_status.yml`. Either: +- Fix the package name in `functional_group_packages.yml` +- Add the missing repo to `repo_status.yml → rpm_repos` +- Sync the package in your Pulp server + +--- + +### 7. Validation log location + +Validation logs are written to: +``` +/opt/omnia/image_build_manager/log// +``` + +Ansible playbook logs are in: +``` +/opt/omnia/image_build_manager/log/playbooks/ +``` + +--- + +### 8. Registry TLS error ("http: server gave HTTP response to HTTPS client") + +**Error**: +``` +failed to list repositories for 10.x.x.x:5000: Get "https://10.x.x.x:5000/v2/_catalog": +http: server gave HTTP response to HTTPS client +``` + +**Fix**: The local container registry runs HTTP (not HTTPS). `regctl` defaults to +HTTPS for IP addresses. The playbook now auto-configures this, but if you see +this error, run: +```bash +/usr/local/bin/regctl registry set --tls disabled :5000 +``` + +--- + +### 9. DNS resolution failure for registry hostname + +**Error**: +``` +dial tcp: lookup .vm.cluster on :53: no such host +``` + +**Fix**: In standalone mode, the playbook uses `admin_nic_ip` (IP address) for +the registry host. If you see DNS errors, ensure `OMNIA_ADMIN_NIC_IP` is set +correctly in `omnia.env`. + +--- + +## Debug Tips + +### Check loaded variables + +```bash +cd src/image_build_manager/playbooks +ansible-playbook image_build_manager.yml --tags validate -vvv +``` + +### Verify functional groups were written + +```bash +cat /opt/omnia/image_build_manager/output/project_default/.data/functional_groups_config.yml +``` + +### Check which packages will be installed + +The `fetch_build_packages` role logs the count: +``` +Standalone: 2 functional groups, 35 base packages +``` + +Use `-v` to see the full debug output. + +### Run only validation (no side effects) + +```bash +cd src/image_build_manager/playbooks +ansible-playbook image_build_manager.yml --tags validate --check +``` + +### Check omnia-cli status + +```bash +./src/main/omnia-cli status +./src/main/omnia-cli repo-manager +./src/main/omnia-cli image-build +``` diff --git a/src/image_build_manager/galaxy.yml b/src/image_build_manager/galaxy.yml new file mode 100644 index 0000000000..e3b499e02e --- /dev/null +++ b/src/image_build_manager/galaxy.yml @@ -0,0 +1,40 @@ +--- +namespace: omnia +name: image_build +version: 2.3.0 +readme: README.md +authors: + - Dell Technologies +description: >- + Dell Omnia Image Build Manager — builds OS images (RHEL/Rocky x86_64 + aarch64) + for HPC cluster provisioning using OpenCHAMI. Deploys MinIO S3 + OCI Registry + and produces bare-metal OS images with per-functional-group package customization. +license: + - Apache-2.0 +tags: + - hpc + - image-build + - openchami + - minio + - dell + - omnia +dependencies: + community.general: ">=5.0.0" + containers.podman: ">=1.10.0" +repository: https://github.com/dell/omnia +documentation: https://github.com/dell/omnia/tree/main/docs +homepage: https://github.com/dell/omnia +issues: https://github.com/dell/omnia/issues +build_ignore: + - "*.tar.gz" + - "*.cfg" + - .gitignore + - Makefile + - __pycache__ + - "**/__pycache__" + - ansible_collections + - ansible_collections/* + - containers + - containers/* + - input + - input/* diff --git a/src/image_build_manager/image_build_manager.yml b/src/image_build_manager/image_build_manager.yml deleted file mode 100644 index a6ce714dfb..0000000000 --- a/src/image_build_manager/image_build_manager.yml +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. ---- -# image_build_manager.yml — Top-level entry point for the image_build_manager domain. -# Reads repo_status.yml from repo_manager output for Pulp URLs. -# Deploys MinIO + Registry, then builds OS images for x86_64 and aarch64. -# -# Usage (run ONE tag at a time — no comma-separated tags): -# cd src/image_build_manager -# ansible-playbook image_build_manager.yml # Default: prepare + build -# ansible-playbook image_build_manager.yml --tags prepare # Deploy MinIO + Registry only -# ansible-playbook image_build_manager.yml --tags build # Build images only (skip prepare) -# ansible-playbook image_build_manager.yml --tags validate # Validate config only (no credentials) -# ansible-playbook image_build_manager.yml --tags cleanup # Cleanup (no credentials) -# ansible-playbook image_build_manager.yml --tags upgrade # Upgrade flow (placeholder) -# ansible-playbook image_build_manager.yml --tags rollback # Rollback flow (placeholder) -# -# Tags (mutually exclusive — pick ONE): -# (none) — Default flow: always + prepare + build (with credentials) -# prepare — Deploy MinIO + Registry (with credentials) -# build — Build OS images (x86_64 + aarch64) + write build_status (with credentials) -# validate — Validate configuration only (skips credentials) -# cleanup — Remove MinIO, registry, build artifacts, omnia.target entries (skips credentials) -# upgrade — Upgrade flow (placeholder — Omnia 3.0) -# rollback — Rollback flow (placeholder — Omnia 3.0) -# -# Tag Validation: -# - Invalid tags will fail with error message -# - Invalid combinations (e.g., prepare + cleanup) will fail -# - cleanup and validate tags skip credential prompting -# -# Internal steps (always): -# Step 0: Tag validation + upgrade check -# Step 1: Input project directory -# Step 2: Credential collection (skipped for cleanup/validate tags) -# Step 3: Load image_build_config.yml -# Step 4: Pre-check repo_status.yml - -# Step 0: Self-contained setup — upgrade guard, input dir, oim group, guard facts -- name: Image build manager setup - hosts: localhost - connection: local - gather_facts: false - tags: always - roles: - - image_build_setup - -# Step 1: Image build input validation (domain-specific L1 schema + L2 logic) -- name: Validate image build configuration - ansible.builtin.import_playbook: playbooks/validate_image_build_config.yml - tags: - - always - - validate - -# Step 2: Credential collection (collect_build_credentials role) -# Skipped when cleanup or validate tags are used -- name: Get build credentials - ansible.builtin.import_playbook: playbooks/get_build_credentials.yml - when: not (skip_build_credentials | default(false) | bool) - tags: always - - -# Step 3: Load image_build_config.yml -- name: Load image_build_manager configuration - hosts: localhost - connection: local - gather_facts: false - tags: always - tasks: - - name: Load image_build_config.yml - ansible.builtin.include_vars: - file: "{{ input_project_dir }}/image_build_manager/image_build_config.yml" - - - name: Set s3_endpoint from config - ansible.builtin.set_fact: - s3_endpoint: >- - {{ s3_configurations.endpoint_url - if s3_configurations.endpoint_url | default('') | length > 0 - else ('http://' + admin_nic_ip + ':9000' - if s3_configurations.provider | default('minio') == 'minio' - else '') }} - cacheable: true - -# Step 4: PRE-CHECK — Read repo_status.yml from repo_manager output -# This loads rpm_repos, certificates, and content base URLs into facts. -- name: Pre-check repo_manager status and load RPM repo URLs - hosts: localhost - connection: local - gather_facts: false - tags: always - vars: - _repo_status_path: "{{ repo_manager_output_path | default('') | ternary(repo_manager_output_path, output_project_dir + '/repo_manager/repo_status.yml') }}" - tasks: - - name: Check if repo_status.yml exists - ansible.builtin.stat: - path: "{{ _repo_status_path }}" - register: repo_status_file - - - name: Load repo_status.yml from repo_manager output - ansible.builtin.include_vars: - file: "{{ _repo_status_path }}" - when: repo_status_file.stat.exists - - - name: Fail if repo_manager has not completed - ansible.builtin.fail: - msg: > - repo_status.yml not found at {{ _repo_status_path }} or overall_status != 'success'. - Run repo_manager first: cd ../repo_manager && ansible-playbook repo_manager.yml - when: > - repo_manager_output_path | default('') | length > 0 and - (not repo_status_file.stat.exists or overall_status | default('') != 'success') - - - name: Validate repo_status.yml has required repo_manager section - ansible.builtin.assert: - that: - - repo_manager is defined - - repo_manager.certificates is defined - - repo_manager.certificates.server_crt is defined - - repo_manager.certificates.server_crt | length > 0 - - repo_manager.port is defined - fail_msg: > - repo_status.yml is missing required 'repo_manager' section. - Ensure repo_manager produces repo_manager.certificates.server_crt - and repo_manager.port in {{ _repo_status_path }}. - quiet: true - when: repo_manager_output_path | default('') | length > 0 - - - name: Set repo_manager facts from repo_status.yml - ansible.builtin.set_fact: - pulp_port: "{{ repo_manager.port }}" - pulp_cert_oim_path: "{{ repo_manager.certificates.server_crt | regex_replace('^/opt/omnia/', oim_shared_path + '/omnia/') }}" - cacheable: true - when: - - repo_manager_output_path | default('') | length > 0 - - repo_manager is defined - - - name: Build repo lists from repo_manager output - ansible.builtin.set_fact: - repo_manager_repos_x86_64: [] - repo_manager_repos_aarch64: [] - rpm_repos_aarch64: "{{ rpm_repos.aarch64 | default({}) }}" - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined - - - name: Build x86_64 repo list from rpm_repos - ansible.builtin.set_fact: - repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" - loop: "{{ (rpm_repos.x86_64 | default({})) | dict2items }}" - loop_control: - loop_var: item - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined - - - name: Build x86_64 repo list from user_repos - ansible.builtin.set_fact: - repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" - loop: "{{ (user_repos.x86_64 | default({})) | dict2items }}" - loop_control: - loop_var: item - when: - - repo_manager_output_path | default('') | length > 0 - - user_repos is defined - - - name: Build aarch64 repo list from rpm_repos - ansible.builtin.set_fact: - repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" - loop: "{{ (rpm_repos.aarch64 | default({})) | dict2items }}" - loop_control: - loop_var: item - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined - - - name: Build aarch64 repo list from user_repos - ansible.builtin.set_fact: - repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" - loop: "{{ (user_repos.aarch64 | default({})) | dict2items }}" - loop_control: - loop_var: item - when: - - repo_manager_output_path | default('') | length > 0 - - user_repos is defined - - - name: Make repo lists cacheable - ansible.builtin.set_fact: - repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 }}" - repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 }}" - cacheable: true - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined - - - name: Display loaded repo_manager data for image_build_manager - ansible.builtin.debug: - msg: - - "Repo manager status: {{ overall_status }}" - - "Pulp cert: {{ pulp_cert_oim_path | default('not set') }}" - - "x86_64 repos: {{ repo_manager_repos_x86_64 | length }} loaded" - - "aarch64 repos: {{ repo_manager_repos_aarch64 | length }} loaded" - verbosity: 1 - when: repo_manager_output_path | default('') | length > 0 - - -# ========================================================================= -# FLOW: prepare — Deploy MinIO + Registry (idempotent) -# ========================================================================= -- name: Prepare build infrastructure (MinIO + Registry) - ansible.builtin.import_playbook: playbooks/prepare_image_build_manager.yml - tags: - - prepare - -# ========================================================================= -# FLOW: build — Build OS images and write build_status.yml -# ========================================================================= -# Step 6: Build x86_64 images -- name: Build x86_64 images - ansible.builtin.import_playbook: playbooks/build_image_x86_64.yml - tags: - - x86_64 - - build - -# Step 7: Build aarch64 images -# NOTE: The aarch64 playbook has its own internal checks for ARM host availability. -- name: Build aarch64 images - ansible.builtin.import_playbook: playbooks/build_image_aarch64.yml - when: hostvars['localhost']['aarch64_inventory_host_ip'] | default('') | length > 0 - tags: - - aarch64 - - build - -# Step 8: Write build_status.yml output -# Output: {{ output_project_dir }}/build_status.yml (NFS-shared) -# Includes per-functional-group image details with S3 paths for kernel, initrd, image. -- name: Write build_status.yml output - hosts: localhost - connection: local - gather_facts: true - tags: - - x86_64 - - aarch64 - - build - tasks: - - name: Write build status - ansible.builtin.include_role: - name: build_os_images - tasks_from: write_build_status.yml - -# ========================================================================= -# FLOW: cleanup — Remove MinIO, registry, build artifacts (opt-in only) -# ========================================================================= -- name: Cleanup image_build_manager - ansible.builtin.import_playbook: playbooks/cleanup_image_build_manager.yml - tags: - - never - - cleanup - -# ========================================================================= -# FLOW: upgrade — Upgrade flow placeholder (opt-in only) -# ========================================================================= -- name: Upgrade image_build_manager - ansible.builtin.import_playbook: playbooks/upgrade_image_build_manager.yml - tags: - - never - - upgrade - -# ========================================================================= -# FLOW: rollback — Rollback flow placeholder (opt-in only) -# ========================================================================= -- name: Rollback image_build_manager - ansible.builtin.import_playbook: playbooks/rollback_image_build_manager.yml - tags: - - never - - rollback diff --git a/src/image_build_manager/input/image_build_config.yml b/src/image_build_manager/input/project_default/image_build_config.yml similarity index 54% rename from src/image_build_manager/input/image_build_config.yml rename to src/image_build_manager/input/project_default/image_build_config.yml index 363a75444d..54187c4f4c 100644 --- a/src/image_build_manager/input/image_build_config.yml +++ b/src/image_build_manager/input/project_default/image_build_config.yml @@ -16,8 +16,8 @@ # # This file is the per-domain input configuration for image_build_manager. # -# Location (NFS): -# /opt/omnia/input/project_default/image_build_manager/image_build_config.yml +# Location: +# src/input/project_default/image_build_config.yml # --------------------------------------------------------------------------- # S3 Storage Backend @@ -34,16 +34,17 @@ s3_configurations: # --------------------------------------------------------------------------- # Repo Manager Output (upstream dependency) # --------------------------------------------------------------------------- -# image_build_manager reads the repo_manager output file to obtain Pulp RPM repo URLs, -# certificate paths, and content base URLs. Run repo_manager before image_build_manager. +# Full path to the repo_status.yml file produced by repo_manager. +# Supports multiple projects — each project can point to a different file. # -# The repo_status.yml provides: -# - rpm_repos.x86_64.baseos, appstream, codeready_builder, epel, ... -# - rpm_repos.aarch64.baseos, appstream, codeready_builder, epel, ... +# The repo_status.yml contains: +# - overall_status, cluster_os_type, cluster_os_version +# - rpm_repos (x86_64 + aarch64 repo URLs) +# - repo_manager.port, certificates (absolute paths) +# - repo_manager.package_list — path to functional_group_packages.yml # -# image_build_manager pre-check validates this file exists and overall_status == 'synced'. -# Leave empty to use default: /opt/omnia/output//repo_manager/repo_status.yml -repo_manager_output_path: "" +# Convention: /opt/omnia/repo_manager/output//repo_status.yml +repo_manager_output_path: "/opt/omnia/repo_manager/output/project_default/repo_status.yml" # --------------------------------------------------------------------------- # ARM Build Host (aarch64) @@ -56,6 +57,41 @@ aarch64_inventory_host_ip: "" # SSH user for connecting to the ARM build host (default: root) aarch64_ssh_user: "root" +# --------------------------------------------------------------------------- +# Functional Groups +# --------------------------------------------------------------------------- +# Functional groups are defined here. Each entry defines an image variant +# to build. The "name" must end with +# the target architecture suffix (_x86_64 or _aarch64). +# +# The RPM packages for each group are defined in: +# functional_group_packages.yml (path from repo_status.yml → repo_manager.package_list) +# Edit that file to add/remove packages per functional group. +# +# Valid functional group names (from FUNCTIONAL_GROUP_LAYER_MAP): +# x86_64: os_x86_64, slurm_node_x86_64, slurm_control_node_x86_64, +# service_kube_control_plane_first_x86_64, +# service_kube_control_plane_x86_64, service_kube_node_x86_64, +# login_node_x86_64, login_compiler_node_x86_64 +# aarch64: os_aarch64, slurm_node_aarch64, login_node_aarch64, +# login_compiler_node_aarch64 +# +# Uncomment the groups you need. At minimum, include "os_" for a +# base OS image build. +functional_groups: + # - name: "os_x86_64" + - name: "slurm_node_x86_64" + - name: "slurm_control_node_x86_64" + # - name: "service_kube_control_plane_first_x86_64" + # - name: "service_kube_control_plane_x86_64" + # - name: "service_kube_node_x86_64" + # - name: "login_node_x86_64" + # - name: "login_compiler_node_x86_64" + # - name: "os_aarch64" + # - name: "slurm_node_aarch64" + # - name: "login_node_aarch64" + # - name: "login_compiler_node_aarch64" + # --------------------------------------------------------------------------- # Build Image Settings # --------------------------------------------------------------------------- diff --git a/src/image_build_manager/library/MIGRATED_FROM_COMMON.md b/src/image_build_manager/library/MIGRATED_FROM_COMMON.md deleted file mode 100644 index aeb545ce54..0000000000 --- a/src/image_build_manager/library/MIGRATED_FROM_COMMON.md +++ /dev/null @@ -1,63 +0,0 @@ -# Files Migrated from src/common/ - -The following files were copied from `src/common/` to `src/image_build_manager/library/` -as part of the image_build_manager domain isolation. - -## Modules (copied to library/modules/) - -| Source | Target | Status | -|--------|--------|--------| -| `common/library/modules/base_image_package_collector.py` | `library/modules/base_image_package_collector.py` | **MOVED** — removed from common | -| `common/library/modules/image_package_collector.py` | `library/modules/image_package_collector.py` | **MOVED** — removed from common | -| `common/library/modules/functional_group_parser.py` | `library/modules/functional_group_parser.py` | **MOVED** — removed from common | - -## Module Utils (copied to library/module_utils/build_image/) - -| Source | Target | Status | -|--------|--------|--------| -| `common/library/module_utils/build_image/__init__.py` | `library/module_utils/build_image/__init__.py` | **COPIED** — common copy kept for orchestrator | -| `common/library/module_utils/build_image/common_functions.py` | `library/module_utils/build_image/common_functions.py` | **COPIED** — common copy kept for orchestrator | -| `common/library/module_utils/build_image/config.py` | `library/module_utils/build_image/config.py` | **COPIED** + `FUNCTIONAL_GROUP_LAYER_MAP` inlined from `input_validation/common_utils/config.py` | - -## Additional Modules (copied to library/modules/) - -| Source | Target | Status | -|--------|--------|--------| -| `common/library/modules/generate_functional_groups.py` | `library/modules/generate_functional_groups.py` | **COPIED** — import updated to use local `build_image.config` | - -## Callback Plugins (copied to callback_plugins/) - -| Source | Target | Status | -|--------|--------|--------| -| `common/callback_plugins/omnia_default.py` | `callback_plugins/omnia_default.py` | **COPIED** — common copy kept for other domains | - -## Schemas (copied to library/module_utils/image_build_validation/schema/) - -| Source | Target | Status | -|--------|--------|--------| -| `common/library/module_utils/input_validation/schema/image_build_config.json` | `library/module_utils/image_build_validation/schema/image_build_config.json` | Copied — keep in common for central validate_input.py | -| `common/library/module_utils/input_validation/schema/image_build_credentials.json` | `library/module_utils/image_build_validation/schema/image_build_credentials.json` | Copied — keep in common for central validate_input.py | -| `common/library/module_utils/input_validation/schema/functional_groups_config.json` | `library/module_utils/image_build_validation/schema/functional_groups_config.json` | Copied — keep in common for central validate_input.py | - -## New Files (created in library/) - -| File | Purpose | -|------|---------| -| `library/modules/validate_image_build_config.py` | Image build-specific L1+L2 validation module | -| `library/module_utils/image_build_validation/__init__.py` | Package init | -| `library/module_utils/image_build_validation/image_build_validation_flow.py` | L2 validation rules | -| `library/module_utils/image_build_validation/schema/__init__.py` | Package init | - -## Removal Status - -The following files have been **removed** from `src/common/`: - -1. ~~`src/common/library/modules/base_image_package_collector.py`~~ — **REMOVED** -2. ~~`src/common/library/modules/image_package_collector.py`~~ — **REMOVED** -3. ~~`src/common/library/modules/functional_group_parser.py`~~ — **REMOVED** - -**DO NOT remove** from common (still used by other domains): -- `module_utils/build_image/` — shared with orchestrator via `additional_images_collector.py` -- `module_utils/input_validation/` — used by central `validate_input.py` -- `callback_plugins/omnia_default.py` — used by all other domains -- `modules/generate_functional_groups.py` — used by orchestrator diff --git a/src/image_build_manager/meta/runtime.yml b/src/image_build_manager/meta/runtime.yml new file mode 100644 index 0000000000..ce6befd7ba --- /dev/null +++ b/src/image_build_manager/meta/runtime.yml @@ -0,0 +1,2 @@ +--- +requires_ansible: ">=2.14.0" diff --git a/src/image_build_manager/output/.gitkeep b/src/image_build_manager/output/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/image_build_manager/playbooks/ansible.cfg b/src/image_build_manager/playbooks/ansible.cfg index 34c24880b1..2b4c5012f4 100644 --- a/src/image_build_manager/playbooks/ansible.cfg +++ b/src/image_build_manager/playbooks/ansible.cfg @@ -1,15 +1,19 @@ -# ansible.cfg for standalone execution of image_build_manager sub-playbooks. +# ansible.cfg for image_build_manager playbooks. +# # Usage: # cd src/image_build_manager/playbooks -# ansible-playbook build_image_x86_64.yml -i -# ansible-playbook build_image_aarch64.yml -i +# ansible-playbook image_build_manager.yml --tags validate # -# This config allows sub-playbooks to run independently without -# the top-level image_build_manager.yml orchestrator. +# Role, module, and plugin paths are relative to THIS directory (playbooks/). +# Sub-directory playbooks (build/, cleanup/, etc.) have their own ansible.cfg. [defaults] -log_path = /opt/omnia/log/core/playbooks/image_build_manager.log -remote_tmp = /opt/omnia/tmp/.ansible/tmp/ +log_path = /opt/omnia/image_build_manager/log/playbooks/image_build_manager.log +roles_path = ../roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = ../plugins/modules +module_utils = ../plugins/module_utils +callback_plugins = ../plugins/callback +remote_tmp = /tmp/.ansible/tmp/ host_key_checking = false forks = 5 timeout = 180 @@ -18,10 +22,7 @@ interpreter_python = /usr/bin/python3 deprecation_warnings = false show_task_path_on_failure = false stdout_callback = omnia_default -roles_path = ../roles -callback_plugins = ../callback_plugins -library = ../library/modules -module_utils = ../library/module_utils +collections_paths = ..:~/.ansible/collections:/usr/share/ansible/collections [persistent_connection] command_timeout = 180 diff --git a/src/image_build_manager/playbooks/build/ansible.cfg b/src/image_build_manager/playbooks/build/ansible.cfg new file mode 100644 index 0000000000..c25eb9690a --- /dev/null +++ b/src/image_build_manager/playbooks/build/ansible.cfg @@ -0,0 +1,31 @@ +# ansible.cfg for image_build_manager/playbooks/build/ +# +# Usage: +# cd src/image_build_manager/playbooks/build +# ansible-playbook build_image_x86_64.yml +# ansible-playbook build_image_aarch64.yml + +[defaults] +log_path = /opt/omnia/image_build_manager/log/build/build_image.log +roles_path = ../../roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = ../../plugins/modules +module_utils = ../../plugins/module_utils +callback_plugins = ../../plugins/callback +remote_tmp = /tmp/.ansible/tmp/ +host_key_checking = false +forks = 5 +timeout = 180 +executable = /bin/bash +interpreter_python = /usr/bin/python3 +deprecation_warnings = false +show_task_path_on_failure = false +stdout_callback = omnia_default +collections_paths = ../..:~/.ansible/collections:/usr/share/ansible/collections + +[persistent_connection] +command_timeout = 180 +connect_timeout = 180 + +[ssh_connection] +retries = 3 +ssh_args = -o ControlMaster=auto -o ControlPersist=60 -o ConnectTimeout=60 diff --git a/src/image_build_manager/playbooks/build_image_aarch64.yml b/src/image_build_manager/playbooks/build/build_image_aarch64.yml similarity index 62% rename from src/image_build_manager/playbooks/build_image_aarch64.yml rename to src/image_build_manager/playbooks/build/build_image_aarch64.yml index 4874958046..78c968a22b 100644 --- a/src/image_build_manager/playbooks/build_image_aarch64.yml +++ b/src/image_build_manager/playbooks/build/build_image_aarch64.yml @@ -38,76 +38,8 @@ build_arch: "aarch64" cacheable: true -- name: Pre-check repo_manager status and load RPM repo URLs - hosts: localhost - connection: local - gather_facts: false - vars: - _repo_status_path: "{{ repo_manager_output_path | default('') | ternary(repo_manager_output_path, output_project_dir + '/repo_manager/repo_status.yml') }}" - tasks: - - name: Check if repo_status.yml exists - ansible.builtin.stat: - path: "{{ _repo_status_path }}" - register: repo_status_file - - - name: Load repo_status.yml from repo_manager output - ansible.builtin.include_vars: - file: "{{ _repo_status_path }}" - when: repo_status_file.stat.exists - - - name: Fail if repo_manager has not completed - ansible.builtin.fail: - msg: > - repo_status.yml not found at {{ _repo_status_path }} or overall_status != 'success'. - Run repo_manager first: cd ../repo_manager && ansible-playbook repo_manager.yml - when: > - repo_manager_output_path | default('') | length > 0 and - (not repo_status_file.stat.exists or overall_status | default('') != 'success') - - - name: Set repo_manager facts from repo_status.yml - ansible.builtin.set_fact: - pulp_port: "{{ repo_manager.port | default(2225) }}" - pulp_cert_oim_path: "{{ repo_manager.certificates.server_crt | default('') | regex_replace('^/opt/omnia/', oim_shared_path + '/omnia/') }}" - cacheable: true - when: - - repo_manager_output_path | default('') | length > 0 - - repo_manager is defined - - - name: Build repo lists from repo_manager output - ansible.builtin.set_fact: - repo_manager_repos_aarch64: [] - rpm_repos_aarch64: "{{ rpm_repos.aarch64 | default({}) }}" - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined - - - name: Build aarch64 repo list from rpm_repos - ansible.builtin.set_fact: - repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" - loop: "{{ (rpm_repos.aarch64 | default({})) | dict2items }}" - loop_control: - loop_var: item - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined - - - name: Build aarch64 repo list from user_repos - ansible.builtin.set_fact: - repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" - loop: "{{ (user_repos.aarch64 | default({})) | dict2items }}" - loop_control: - loop_var: item - when: - - repo_manager_output_path | default('') | length > 0 - - user_repos is defined - - - name: Make repo list cacheable - ansible.builtin.set_fact: - repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 }}" - cacheable: true - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined +# NOTE: repo_status.yml and repo lists are already loaded by image_build_setup role. +# This playbook only needs to reload image_build_config.yml for build_stream vars. - name: Load image_build_manager configuration for aarch64 hosts: localhost @@ -116,7 +48,7 @@ tasks: - name: Load image_build_config.yml ansible.builtin.include_vars: - file: "{{ input_project_dir }}/image_build_manager/image_build_config.yml" + file: "{{ input_project_dir }}/image_build_config.yml" - name: Set build_stream variables from extra_vars ansible.builtin.set_fact: @@ -169,7 +101,7 @@ aarch64_inventory_host_ip is not set in image_build_config.yml. Cannot proceed with aarch64 image build. Set aarch64_inventory_host_ip in: - {{ input_project_dir }}/image_build_manager/image_build_config.yml + {{ input_project_dir }}/image_build_config.yml when: not (aarch64_build_enabled | default(false) | bool) - name: Gather OIM data @@ -187,10 +119,23 @@ connection: local gather_facts: false tasks: - - name: Generate functional groups from mapping.csv - ansible.builtin.include_role: - name: generate_functional_groups - when: not (enable_build_stream | default(false) | bool) + # --- Standalone mode: write functional groups from image_build_config.yml --- + - name: Write functional_groups_config.yml from image_build_config.yml (standalone) + when: + - standalone_mode | default(false) | bool + - not (enable_build_stream | default(false) | bool) + block: + - name: Ensure functional groups data directory exists + ansible.builtin.file: + path: "{{ functional_groups_config_path | dirname }}" + state: directory + mode: "0755" + + - name: Write functional groups config from image_build_config + ansible.builtin.copy: + content: "{{ {'functional_groups': functional_groups} | to_nice_yaml }}" + dest: "{{ functional_groups_config_path }}" + mode: "0644" - name: Fetch aarch64 functional_groups ansible.builtin.include_role: diff --git a/src/image_build_manager/playbooks/build_image_x86_64.yml b/src/image_build_manager/playbooks/build/build_image_x86_64.yml similarity index 56% rename from src/image_build_manager/playbooks/build_image_x86_64.yml rename to src/image_build_manager/playbooks/build/build_image_x86_64.yml index e0525e57c8..c4d60ed09a 100644 --- a/src/image_build_manager/playbooks/build_image_x86_64.yml +++ b/src/image_build_manager/playbooks/build/build_image_x86_64.yml @@ -38,75 +38,8 @@ build_arch: "x86_64" cacheable: true -- name: Pre-check repo_manager status and load RPM repo URLs - hosts: localhost - connection: local - gather_facts: false - vars: - _repo_status_path: "{{ repo_manager_output_path | default('') | ternary(repo_manager_output_path, output_project_dir + '/repo_manager/repo_status.yml') }}" - tasks: - - name: Check if repo_status.yml exists - ansible.builtin.stat: - path: "{{ _repo_status_path }}" - register: repo_status_file - - - name: Load repo_status.yml from repo_manager output - ansible.builtin.include_vars: - file: "{{ _repo_status_path }}" - when: repo_status_file.stat.exists - - - name: Fail if repo_manager has not completed - ansible.builtin.fail: - msg: > - repo_status.yml not found at {{ _repo_status_path }} or overall_status != 'success'. - Run repo_manager first: cd ../repo_manager && ansible-playbook repo_manager.yml - when: > - repo_manager_output_path | default('') | length > 0 and - (not repo_status_file.stat.exists or overall_status | default('') != 'success') - - - name: Set repo_manager facts from repo_status.yml - ansible.builtin.set_fact: - pulp_port: "{{ repo_manager.port | default(2225) }}" - pulp_cert_oim_path: "{{ repo_manager.certificates.server_crt | default('') | regex_replace('^/opt/omnia/', oim_shared_path + '/omnia/') }}" - cacheable: true - when: - - repo_manager_output_path | default('') | length > 0 - - repo_manager is defined - - - name: Build repo lists from repo_manager output - ansible.builtin.set_fact: - repo_manager_repos_x86_64: [] - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined - - - name: Build x86_64 repo list from rpm_repos - ansible.builtin.set_fact: - repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" - loop: "{{ (rpm_repos.x86_64 | default({})) | dict2items }}" - loop_control: - loop_var: item - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined - - - name: Build x86_64 repo list from user_repos - ansible.builtin.set_fact: - repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" - loop: "{{ (user_repos.x86_64 | default({})) | dict2items }}" - loop_control: - loop_var: item - when: - - repo_manager_output_path | default('') | length > 0 - - user_repos is defined - - - name: Make repo list cacheable - ansible.builtin.set_fact: - repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 }}" - cacheable: true - when: - - repo_manager_output_path | default('') | length > 0 - - rpm_repos is defined +# NOTE: repo_status.yml and repo lists are already loaded by image_build_setup role. +# This playbook only needs to reload image_build_config.yml for build_stream vars. - name: Load image_build_manager configuration for x86_64 hosts: localhost @@ -115,7 +48,7 @@ tasks: - name: Load image_build_config.yml ansible.builtin.include_vars: - file: "{{ input_project_dir }}/image_build_manager/image_build_config.yml" + file: "{{ input_project_dir }}/image_build_config.yml" - name: Set build_stream variables from extra_vars ansible.builtin.set_fact: @@ -157,10 +90,23 @@ connection: local gather_facts: false tasks: - - name: Generate functional groups from mapping.csv - ansible.builtin.include_role: - name: generate_functional_groups - when: not (enable_build_stream | default(false) | bool) + # --- Standalone mode: write functional groups from image_build_config.yml --- + - name: Write functional_groups_config.yml from image_build_config.yml (standalone) + when: + - standalone_mode | default(false) | bool + - not (enable_build_stream | default(false) | bool) + block: + - name: Ensure functional groups data directory exists + ansible.builtin.file: + path: "{{ functional_groups_config_path | dirname }}" + state: directory + mode: "0755" + + - name: Write functional groups config from image_build_config + ansible.builtin.copy: + content: "{{ {'functional_groups': functional_groups} | to_nice_yaml }}" + dest: "{{ functional_groups_config_path }}" + mode: "0644" - name: Fetch x86_64 functional_groups ansible.builtin.include_role: @@ -176,8 +122,8 @@ - fetch_build_packages - name: OpenCHAMI build image for x86_64 - hosts: oim - connection: ssh + hosts: localhost + connection: local gather_facts: false roles: - role: build_os_images diff --git a/src/image_build_manager/playbooks/cleanup/ansible.cfg b/src/image_build_manager/playbooks/cleanup/ansible.cfg new file mode 100644 index 0000000000..04abdbede9 --- /dev/null +++ b/src/image_build_manager/playbooks/cleanup/ansible.cfg @@ -0,0 +1,30 @@ +# ansible.cfg for image_build_manager/playbooks/cleanup/ +# +# Usage: +# cd src/image_build_manager/playbooks/cleanup +# ansible-playbook cleanup_image_build_manager.yml + +[defaults] +log_path = /opt/omnia/image_build_manager/log/playbooks/cleanup.log +roles_path = ../../roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = ../../plugins/modules +module_utils = ../../plugins/module_utils +callback_plugins = ../../plugins/callback +remote_tmp = /tmp/.ansible/tmp/ +host_key_checking = false +forks = 5 +timeout = 180 +executable = /bin/bash +interpreter_python = /usr/bin/python3 +deprecation_warnings = false +show_task_path_on_failure = false +stdout_callback = omnia_default +collections_paths = ../..:~/.ansible/collections:/usr/share/ansible/collections + +[persistent_connection] +command_timeout = 180 +connect_timeout = 180 + +[ssh_connection] +retries = 3 +ssh_args = -o ControlMaster=auto -o ControlPersist=60 -o ConnectTimeout=60 diff --git a/src/image_build_manager/playbooks/cleanup_image_build_manager.yml b/src/image_build_manager/playbooks/cleanup/cleanup_image_build_manager.yml similarity index 90% rename from src/image_build_manager/playbooks/cleanup_image_build_manager.yml rename to src/image_build_manager/playbooks/cleanup/cleanup_image_build_manager.yml index 3220203330..32f4c128b7 100644 --- a/src/image_build_manager/playbooks/cleanup_image_build_manager.yml +++ b/src/image_build_manager/playbooks/cleanup/cleanup_image_build_manager.yml @@ -15,7 +15,7 @@ # cleanup_image_build_manager.yml — Cleanup MinIO, registry, and build artifacts. # # Usage: -# cd src/image_build_manager/playbooks +# cd src/image_build_manager/playbooks/cleanup # ansible-playbook cleanup_image_build_manager.yml # Full cleanup (includes credentials) # ansible-playbook cleanup_image_build_manager.yml --tags minio # MinIO only # ansible-playbook cleanup_image_build_manager.yml --tags registry # Registry only @@ -44,21 +44,21 @@ tasks: - name: Load image_build_config.yml ansible.builtin.include_vars: - file: "{{ input_project_dir }}/image_build_manager/image_build_config.yml" + file: "{{ input_project_dir }}/image_build_config.yml" failed_when: false -# Execute cleanup on OIM host (includes credential removal by default) +# Execute cleanup on localhost - name: Cleanup image_build_manager services # noqa:role-name[path] - hosts: oim - connection: ssh + hosts: localhost + connection: local gather_facts: false roles: - cleanup_build_artifacts # Display post-cleanup notes - name: Show cleanup instructions - hosts: oim - connection: ssh + hosts: localhost + connection: local gather_facts: false tasks: - name: Display image_build_manager post-cleanup notes diff --git a/src/image_build_manager/playbooks/credentials/ansible.cfg b/src/image_build_manager/playbooks/credentials/ansible.cfg new file mode 100644 index 0000000000..8261cc5104 --- /dev/null +++ b/src/image_build_manager/playbooks/credentials/ansible.cfg @@ -0,0 +1,30 @@ +# ansible.cfg for image_build_manager/playbooks/credentials/ +# +# Usage: +# cd src/image_build_manager/playbooks/credentials +# ansible-playbook get_build_credentials.yml + +[defaults] +log_path = /opt/omnia/image_build_manager/log/playbooks/credentials.log +roles_path = ../../roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = ../../plugins/modules +module_utils = ../../plugins/module_utils +callback_plugins = ../../plugins/callback +remote_tmp = /tmp/.ansible/tmp/ +host_key_checking = false +forks = 5 +timeout = 180 +executable = /bin/bash +interpreter_python = /usr/bin/python3 +deprecation_warnings = false +show_task_path_on_failure = false +stdout_callback = omnia_default +collections_paths = ../..:~/.ansible/collections:/usr/share/ansible/collections + +[persistent_connection] +command_timeout = 180 +connect_timeout = 180 + +[ssh_connection] +retries = 3 +ssh_args = -o ControlMaster=auto -o ControlPersist=60 -o ConnectTimeout=60 diff --git a/src/image_build_manager/playbooks/get_build_credentials.yml b/src/image_build_manager/playbooks/credentials/get_build_credentials.yml similarity index 88% rename from src/image_build_manager/playbooks/get_build_credentials.yml rename to src/image_build_manager/playbooks/credentials/get_build_credentials.yml index 0beae469f2..108532ca80 100644 --- a/src/image_build_manager/playbooks/get_build_credentials.yml +++ b/src/image_build_manager/playbooks/credentials/get_build_credentials.yml @@ -21,15 +21,15 @@ # Data: image_build_credentials.yml (noun — the output artifact) # # Usage: -# cd src/image_build_manager -# ansible-playbook playbooks/get_build_credentials.yml +# cd src/image_build_manager/playbooks/credentials +# ansible-playbook get_build_credentials.yml # # This playbook: # 1. Loads the input project directory # 2. Loads image_build_config.yml (for s3_configurations) # 3. Validates, creates, and prompts for image build credentials # 4. Credentials are stored encrypted at: -# {input_project_dir}/image_build_manager/image_build_credentials.yml +# {input_project_dir}/image_build_credentials.yml - name: Standalone setup for credentials hosts: localhost @@ -48,7 +48,7 @@ tasks: - name: Load image_build_config.yml for storage provider info ansible.builtin.include_vars: - file: "{{ input_project_dir }}/image_build_manager/image_build_config.yml" + file: "{{ input_project_dir }}/image_build_config.yml" failed_when: false - name: Run collect_build_credentials role diff --git a/src/image_build_manager/playbooks/image_build_manager.yml b/src/image_build_manager/playbooks/image_build_manager.yml new file mode 100644 index 0000000000..7708418053 --- /dev/null +++ b/src/image_build_manager/playbooks/image_build_manager.yml @@ -0,0 +1,151 @@ +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +# image_build_manager.yml — Top-level entry point for the image_build_manager domain. +# Reads repo_status.yml from repo_manager output for Pulp URLs. +# Deploys MinIO + Registry, then builds OS images for x86_64 and aarch64. +# +# Usage (run ONE tag at a time — no comma-separated tags): +# cd src/image_build_manager/playbooks +# ansible-playbook image_build_manager.yml # Default: prepare + build +# ansible-playbook image_build_manager.yml --tags prepare # Deploy MinIO + Registry only +# ansible-playbook image_build_manager.yml --tags build # Build images only (skip prepare) +# ansible-playbook image_build_manager.yml --tags validate # Validate config only (no credentials) +# ansible-playbook image_build_manager.yml --tags cleanup # Cleanup (no credentials) +# ansible-playbook image_build_manager.yml --tags upgrade # Upgrade flow (placeholder) +# ansible-playbook image_build_manager.yml --tags rollback # Rollback flow (placeholder) +# +# Or run sub-playbooks directly from their directory: +# cd validate && ansible-playbook validate_image_build_config.yml +# cd prepare && ansible-playbook prepare_image_build_manager.yml +# cd build && ansible-playbook build_image_x86_64.yml +# cd cleanup && ansible-playbook cleanup_image_build_manager.yml +# +# Tags (mutually exclusive — pick ONE): +# (none) — Default flow: always + prepare + build (with credentials) +# prepare — Deploy MinIO + Registry (with credentials) +# build — Build OS images (x86_64 + aarch64) + write build_status (with credentials) +# validate — Validate configuration only (skips credentials) +# cleanup — Remove MinIO, registry, build artifacts (skips credentials) +# upgrade — Upgrade flow (placeholder — future release) +# rollback — Rollback flow (placeholder — future release) +# +# Tag Validation: +# - Invalid tags will fail with error message +# - Invalid combinations (e.g., prepare + cleanup) will fail +# - cleanup and validate tags skip credential prompting +# +# Internal steps (always): +# Step 0: Setup — load env vars (omnia.env), validate host, check prereqs, load repo_status.yml +# Step 1: Input validation (schema + logic) +# Step 2: Credential collection (skipped for cleanup/validate tags) + +# ========================================================================= +# Step 0: Setup — load config, validate host, check prereqs, load repos +# ========================================================================= +- name: Image build manager setup + hosts: localhost + connection: local + gather_facts: false + tags: always + roles: + - image_build_setup + +# ========================================================================= +# Step 1: Input validation (schema + logic) +# ========================================================================= +- name: Validate image build configuration + ansible.builtin.import_playbook: validate/validate_image_build_config.yml + tags: + - always + - validate + +# ========================================================================= +# Step 2: Credential collection (skipped for cleanup/validate) +# ========================================================================= +- name: Get build credentials + ansible.builtin.import_playbook: credentials/get_build_credentials.yml + when: not (skip_build_credentials | default(false) | bool) + tags: always + +# ========================================================================= +# FLOW: prepare — Deploy MinIO + Registry (idempotent) +# ========================================================================= +- name: Prepare build infrastructure (MinIO + Registry) + ansible.builtin.import_playbook: prepare/prepare_image_build_manager.yml + tags: + - prepare + +# ========================================================================= +# FLOW: build — Build OS images and write build_status.yml +# ========================================================================= +# Step 6: Build x86_64 images +- name: Build x86_64 images + ansible.builtin.import_playbook: build/build_image_x86_64.yml + tags: + - x86_64 + - build + +# Step 7: Build aarch64 images +# NOTE: The aarch64 playbook has its own internal checks for ARM host availability. +- name: Build aarch64 images + ansible.builtin.import_playbook: build/build_image_aarch64.yml + when: hostvars['localhost']['aarch64_inventory_host_ip'] | default('') | length > 0 + tags: + - aarch64 + - build + +# Step 8: Write build_status.yml output +# Output: {{ output_project_dir }}/build_status.yml (NFS-shared) +# Includes per-functional-group image details with S3 paths for kernel, initrd, image. +- name: Write build_status.yml output + hosts: localhost + connection: local + gather_facts: true + tags: + - x86_64 + - aarch64 + - build + tasks: + - name: Write build status + ansible.builtin.include_role: + name: build_os_images + tasks_from: write_build_status.yml + +# ========================================================================= +# FLOW: cleanup — Remove MinIO, registry, build artifacts (opt-in only) +# ========================================================================= +- name: Cleanup image_build_manager + ansible.builtin.import_playbook: cleanup/cleanup_image_build_manager.yml + tags: + - never + - cleanup + +# ========================================================================= +# FLOW: upgrade — Upgrade flow placeholder (opt-in only) +# ========================================================================= +- name: Upgrade image_build_manager + ansible.builtin.import_playbook: upgrade/upgrade_image_build_manager.yml + tags: + - never + - upgrade + +# ========================================================================= +# FLOW: rollback — Rollback flow placeholder (opt-in only) +# ========================================================================= +- name: Rollback image_build_manager + ansible.builtin.import_playbook: rollback/rollback_image_build_manager.yml + tags: + - never + - rollback diff --git a/src/image_build_manager/playbooks/prepare/ansible.cfg b/src/image_build_manager/playbooks/prepare/ansible.cfg new file mode 100644 index 0000000000..bd5ff55145 --- /dev/null +++ b/src/image_build_manager/playbooks/prepare/ansible.cfg @@ -0,0 +1,30 @@ +# ansible.cfg for image_build_manager/playbooks/prepare/ +# +# Usage: +# cd src/image_build_manager/playbooks/prepare +# ansible-playbook prepare_image_build_manager.yml + +[defaults] +log_path = /opt/omnia/image_build_manager/log/playbooks/prepare.log +roles_path = ../../roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = ../../plugins/modules +module_utils = ../../plugins/module_utils +callback_plugins = ../../plugins/callback +remote_tmp = /tmp/.ansible/tmp/ +host_key_checking = false +forks = 5 +timeout = 180 +executable = /bin/bash +interpreter_python = /usr/bin/python3 +deprecation_warnings = false +show_task_path_on_failure = false +stdout_callback = omnia_default +collections_paths = ../..:~/.ansible/collections:/usr/share/ansible/collections + +[persistent_connection] +command_timeout = 180 +connect_timeout = 180 + +[ssh_connection] +retries = 3 +ssh_args = -o ControlMaster=auto -o ControlPersist=60 -o ConnectTimeout=60 diff --git a/src/image_build_manager/playbooks/prepare_image_build_manager.yml b/src/image_build_manager/playbooks/prepare/prepare_image_build_manager.yml similarity index 52% rename from src/image_build_manager/playbooks/prepare_image_build_manager.yml rename to src/image_build_manager/playbooks/prepare/prepare_image_build_manager.yml index b2db3f413e..9915e006d6 100644 --- a/src/image_build_manager/playbooks/prepare_image_build_manager.yml +++ b/src/image_build_manager/playbooks/prepare/prepare_image_build_manager.yml @@ -16,7 +16,7 @@ # Idempotent — safe to run multiple times. # # Standalone usage: -# cd src/image_build_manager/playbooks +# cd src/image_build_manager/playbooks/prepare # ansible-playbook prepare_image_build_manager.yml # # When run standalone, image_build_setup handles upgrade guard, input dir, oim group. @@ -43,9 +43,9 @@ name: validate_image_build_input when: not (image_build_setup_done | default(false) | bool) -- name: Deploy MinIO and Registry on OIM - hosts: oim - connection: ssh +- name: Deploy MinIO and Registry + hosts: localhost + connection: local gather_facts: true tasks: - name: Deploy MinIO S3 storage @@ -61,47 +61,3 @@ name: build_os_images tasks_from: preflight_selinux_check.yml - - name: Update omnia.target with image_build_manager services - block: - - name: Check if omnia.target exists - ansible.builtin.stat: - path: /etc/systemd/system/omnia.target - register: omnia_target_file - - - name: Read existing omnia.target content - ansible.builtin.slurp: - src: /etc/systemd/system/omnia.target - register: omnia_target_content - when: omnia_target_file.stat.exists - - - name: Parse existing Requires line - ansible.builtin.set_fact: - omnia_target_requires: "{{ (omnia_target_content.content | b64decode | regex_search('Requires=(.+)', '\\1'))[0] | default('') }}" - when: omnia_target_file.stat.exists - - - name: Append registry.service to omnia.target if missing - ansible.builtin.replace: - path: /etc/systemd/system/omnia.target - regexp: '^(Requires=.*)$' - replace: '\1 registry.service' - when: - - omnia_target_file.stat.exists - - "'registry.service' not in omnia_target_requires" - notify: Reload systemd for omnia target - - - name: Append minio.service to omnia.target if missing (minio provider only) - ansible.builtin.replace: - path: /etc/systemd/system/omnia.target - regexp: '^(Requires=.*)$' - replace: '\1 minio.service' - when: - - omnia_target_file.stat.exists - - "'minio.service' not in omnia_target_requires" - - hostvars['localhost']['s3_configurations']['provider'] | default('minio') != 'powerscale' - notify: Reload systemd for omnia target - - handlers: - - name: Reload systemd for omnia target - ansible.builtin.systemd: - daemon_reload: true - listen: Reload systemd for omnia target diff --git a/src/image_build_manager/playbooks/rollback/ansible.cfg b/src/image_build_manager/playbooks/rollback/ansible.cfg new file mode 100644 index 0000000000..a6c21f79fb --- /dev/null +++ b/src/image_build_manager/playbooks/rollback/ansible.cfg @@ -0,0 +1,30 @@ +# ansible.cfg for image_build_manager/playbooks/rollback/ +# +# Usage: +# cd src/image_build_manager/playbooks/rollback +# ansible-playbook rollback_image_build_manager.yml + +[defaults] +log_path = /opt/omnia/image_build_manager/log/playbooks/rollback.log +roles_path = ../../roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = ../../plugins/modules +module_utils = ../../plugins/module_utils +callback_plugins = ../../plugins/callback +remote_tmp = /tmp/.ansible/tmp/ +host_key_checking = false +forks = 5 +timeout = 180 +executable = /bin/bash +interpreter_python = /usr/bin/python3 +deprecation_warnings = false +show_task_path_on_failure = false +stdout_callback = omnia_default +collections_paths = ../..:~/.ansible/collections:/usr/share/ansible/collections + +[persistent_connection] +command_timeout = 180 +connect_timeout = 180 + +[ssh_connection] +retries = 3 +ssh_args = -o ControlMaster=auto -o ControlPersist=60 -o ConnectTimeout=60 diff --git a/src/image_build_manager/playbooks/rollback_image_build_manager.yml b/src/image_build_manager/playbooks/rollback/rollback_image_build_manager.yml similarity index 91% rename from src/image_build_manager/playbooks/rollback_image_build_manager.yml rename to src/image_build_manager/playbooks/rollback/rollback_image_build_manager.yml index 7177372ecd..eb4841dc54 100644 --- a/src/image_build_manager/playbooks/rollback_image_build_manager.yml +++ b/src/image_build_manager/playbooks/rollback/rollback_image_build_manager.yml @@ -13,7 +13,7 @@ # limitations under the License. --- # rollback_image_build_manager.yml — Placeholder for image_build_manager rollback flow. -# Full implementation deferred to Omnia 3.0. +# Full implementation deferred to a future release. - name: Image build manager rollback placeholder hosts: localhost @@ -22,4 +22,4 @@ tasks: - name: Rollback not implemented ansible.builtin.debug: - msg: "image_build_manager rollback flow is not implemented yet — reserved for Omnia 3.0" + msg: "image_build_manager rollback flow is not implemented yet — reserved for future release" diff --git a/src/image_build_manager/playbooks/upgrade/ansible.cfg b/src/image_build_manager/playbooks/upgrade/ansible.cfg new file mode 100644 index 0000000000..9927ed8e9f --- /dev/null +++ b/src/image_build_manager/playbooks/upgrade/ansible.cfg @@ -0,0 +1,30 @@ +# ansible.cfg for image_build_manager/playbooks/upgrade/ +# +# Usage: +# cd src/image_build_manager/playbooks/upgrade +# ansible-playbook upgrade_image_build_manager.yml + +[defaults] +log_path = /opt/omnia/image_build_manager/log/playbooks/upgrade.log +roles_path = ../../roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = ../../plugins/modules +module_utils = ../../plugins/module_utils +callback_plugins = ../../plugins/callback +remote_tmp = /tmp/.ansible/tmp/ +host_key_checking = false +forks = 5 +timeout = 180 +executable = /bin/bash +interpreter_python = /usr/bin/python3 +deprecation_warnings = false +show_task_path_on_failure = false +stdout_callback = omnia_default +collections_paths = ../..:~/.ansible/collections:/usr/share/ansible/collections + +[persistent_connection] +command_timeout = 180 +connect_timeout = 180 + +[ssh_connection] +retries = 3 +ssh_args = -o ControlMaster=auto -o ControlPersist=60 -o ConnectTimeout=60 diff --git a/src/image_build_manager/playbooks/upgrade_image_build_manager.yml b/src/image_build_manager/playbooks/upgrade/upgrade_image_build_manager.yml similarity index 91% rename from src/image_build_manager/playbooks/upgrade_image_build_manager.yml rename to src/image_build_manager/playbooks/upgrade/upgrade_image_build_manager.yml index dec6dbdf0a..96b5bfeb72 100644 --- a/src/image_build_manager/playbooks/upgrade_image_build_manager.yml +++ b/src/image_build_manager/playbooks/upgrade/upgrade_image_build_manager.yml @@ -13,7 +13,7 @@ # limitations under the License. --- # upgrade_image_build_manager.yml — Placeholder for image_build_manager upgrade flow. -# Full implementation deferred to Omnia 3.0. +# Full implementation deferred to a future release. - name: Image build manager upgrade placeholder hosts: localhost @@ -22,4 +22,4 @@ tasks: - name: Upgrade not implemented ansible.builtin.debug: - msg: "image_build_manager upgrade flow is not implemented yet — reserved for Omnia 3.0" + msg: "image_build_manager upgrade flow is not implemented yet — reserved for future release" diff --git a/src/image_build_manager/playbooks/validate/ansible.cfg b/src/image_build_manager/playbooks/validate/ansible.cfg new file mode 100644 index 0000000000..c96ec9674c --- /dev/null +++ b/src/image_build_manager/playbooks/validate/ansible.cfg @@ -0,0 +1,30 @@ +# ansible.cfg for image_build_manager/playbooks/validate/ +# +# Usage: +# cd src/image_build_manager/playbooks/validate +# ansible-playbook validate_image_build_config.yml + +[defaults] +log_path = /opt/omnia/image_build_manager/log/playbooks/validate.log +roles_path = ../../roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +library = ../../plugins/modules +module_utils = ../../plugins/module_utils +callback_plugins = ../../plugins/callback +remote_tmp = /tmp/.ansible/tmp/ +host_key_checking = false +forks = 5 +timeout = 180 +executable = /bin/bash +interpreter_python = /usr/bin/python3 +deprecation_warnings = false +show_task_path_on_failure = false +stdout_callback = omnia_default +collections_paths = ../..:~/.ansible/collections:/usr/share/ansible/collections + +[persistent_connection] +command_timeout = 180 +connect_timeout = 180 + +[ssh_connection] +retries = 3 +ssh_args = -o ControlMaster=auto -o ControlPersist=60 -o ConnectTimeout=60 diff --git a/src/image_build_manager/playbooks/validate_image_build_config.yml b/src/image_build_manager/playbooks/validate/validate_image_build_config.yml similarity index 93% rename from src/image_build_manager/playbooks/validate_image_build_config.yml rename to src/image_build_manager/playbooks/validate/validate_image_build_config.yml index 5402c883cd..169ae67160 100644 --- a/src/image_build_manager/playbooks/validate_image_build_config.yml +++ b/src/image_build_manager/playbooks/validate/validate_image_build_config.yml @@ -19,8 +19,8 @@ # - image_build_credentials.yml (if present and decrypted) # # Usage: -# cd src/image_build_manager -# ansible-playbook playbooks/validate_image_build_config.yml +# cd src/image_build_manager/playbooks/validate +# ansible-playbook validate_image_build_config.yml # # When run standalone, image_build_setup handles upgrade guard, input dir, oim group. # When imported from image_build_manager.yml (image_build_setup_done=true), setup is skipped. diff --git a/src/image_build_manager/callback_plugins/omnia_default.py b/src/image_build_manager/plugins/callback/omnia_default.py similarity index 100% rename from src/image_build_manager/callback_plugins/omnia_default.py rename to src/image_build_manager/plugins/callback/omnia_default.py diff --git a/src/image_build_manager/library/module_utils/build_image/__init__.py b/src/image_build_manager/plugins/module_utils/build_image/__init__.py similarity index 100% rename from src/image_build_manager/library/module_utils/build_image/__init__.py rename to src/image_build_manager/plugins/module_utils/build_image/__init__.py diff --git a/src/image_build_manager/library/module_utils/build_image/common_functions.py b/src/image_build_manager/plugins/module_utils/build_image/common_functions.py similarity index 100% rename from src/image_build_manager/library/module_utils/build_image/common_functions.py rename to src/image_build_manager/plugins/module_utils/build_image/common_functions.py diff --git a/src/image_build_manager/library/module_utils/build_image/config.py b/src/image_build_manager/plugins/module_utils/build_image/config.py similarity index 93% rename from src/image_build_manager/library/module_utils/build_image/config.py rename to src/image_build_manager/plugins/module_utils/build_image/config.py index c6d309a16e..a0008291df 100644 --- a/src/image_build_manager/library/module_utils/build_image/config.py +++ b/src/image_build_manager/plugins/module_utils/build_image/config.py @@ -43,8 +43,7 @@ # ---------------------------- # Functional group → layer mapping -# Used by generate_functional_groups.py for YAML output -# Copied from common/library/module_utils/input_validation/common_utils/config.py +# Maps functional group names to their image layer type. # ---------------------------- FUNCTIONAL_GROUP_LAYER_MAP = { "service_kube_control_plane_first_x86_64": "management", diff --git a/src/image_build_manager/library/module_utils/image_build_validation/__init__.py b/src/image_build_manager/plugins/module_utils/image_build_validation/__init__.py similarity index 100% rename from src/image_build_manager/library/module_utils/image_build_validation/__init__.py rename to src/image_build_manager/plugins/module_utils/image_build_validation/__init__.py diff --git a/src/image_build_manager/library/module_utils/image_build_validation/image_build_validation_flow.py b/src/image_build_manager/plugins/module_utils/image_build_validation/image_build_validation_flow.py similarity index 88% rename from src/image_build_manager/library/module_utils/image_build_validation/image_build_validation_flow.py rename to src/image_build_manager/plugins/module_utils/image_build_validation/image_build_validation_flow.py index 785ea7854d..59843290b6 100644 --- a/src/image_build_manager/library/module_utils/image_build_validation/image_build_validation_flow.py +++ b/src/image_build_manager/plugins/module_utils/image_build_validation/image_build_validation_flow.py @@ -98,7 +98,8 @@ def validate_credentials_logic(cred_data, config_data, errors, logger=None): Rules: - If s3 provider is 'powerscale', s3_access_id must be present and non-empty. - - s3_secret_key and provision_password are always required (enforced by L1 schema). + - s3_secret_key is always required (enforced by L1 schema). + - aarch64_ssh_password is required only when aarch64_inventory_host_ip is set. """ s3 = config_data.get("s3_configurations", {}) provider = s3.get("provider", "") @@ -112,6 +113,16 @@ def validate_credentials_logic(cred_data, config_data, errors, logger=None): if logger: logger.error(msg) + aarch64_ip = config_data.get("aarch64_inventory_host_ip", "") + if aarch64_ip and aarch64_ip.strip(): + aarch64_pw = cred_data.get("aarch64_ssh_password", "") + if not aarch64_pw or not aarch64_pw.strip(): + msg = ("image_build_credentials: aarch64_ssh_password is required when " + "aarch64_inventory_host_ip is set.") + errors.append(msg) + if logger: + logger.error(msg) + def validate_image_build_config(config_data, logger=None): """ diff --git a/src/image_build_manager/library/module_utils/image_build_validation/schema/__init__.py b/src/image_build_manager/plugins/module_utils/image_build_validation/schema/__init__.py similarity index 100% rename from src/image_build_manager/library/module_utils/image_build_validation/schema/__init__.py rename to src/image_build_manager/plugins/module_utils/image_build_validation/schema/__init__.py diff --git a/src/image_build_manager/plugins/module_utils/image_build_validation/schema/config.schema.json b/src/image_build_manager/plugins/module_utils/image_build_validation/schema/config.schema.json new file mode 100644 index 0000000000..95b0e45e19 --- /dev/null +++ b/src/image_build_manager/plugins/module_utils/image_build_validation/schema/config.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Image Build Manager Configuration Schema", + "description": "Validates config.yml structure for image_build_manager", + "type": "object", + "required": ["project_name", "host"], + "properties": { + "project_name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 64, + "description": "Project name (alphanumeric, underscore, hyphen only)" + }, + "host": { + "type": "object", + "required": ["hostname", "shared_path", "domain_name", "admin_nic_ip"], + "properties": { + "hostname": { + "type": "string", + "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$", + "minLength": 1, + "maxLength": 63, + "description": "Short hostname (NOT FQDN)" + }, + "shared_path": { + "type": "string", + "pattern": "^/[a-zA-Z0-9/_-]*$", + "minLength": 1, + "description": "Absolute path to shared storage" + }, + "domain_name": { + "type": "string", + "minLength": 1, + "maxLength": 253, + "description": "Domain suffix" + }, + "admin_nic_ip": { + "type": "string", + "format": "ipv4", + "description": "Admin NIC IPv4 address" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/src/image_build_manager/library/module_utils/image_build_validation/schema/functional_groups_config.json b/src/image_build_manager/plugins/module_utils/image_build_validation/schema/functional_groups_config.json similarity index 100% rename from src/image_build_manager/library/module_utils/image_build_validation/schema/functional_groups_config.json rename to src/image_build_manager/plugins/module_utils/image_build_validation/schema/functional_groups_config.json diff --git a/src/image_build_manager/library/module_utils/image_build_validation/schema/image_build_config.json b/src/image_build_manager/plugins/module_utils/image_build_validation/schema/image_build_config.json similarity index 74% rename from src/image_build_manager/library/module_utils/image_build_validation/schema/image_build_config.json rename to src/image_build_manager/plugins/module_utils/image_build_validation/schema/image_build_config.json index ff49a6b312..9f90a70482 100644 --- a/src/image_build_manager/library/module_utils/image_build_validation/schema/image_build_config.json +++ b/src/image_build_manager/plugins/module_utils/image_build_validation/schema/image_build_config.json @@ -35,7 +35,24 @@ }, "repo_manager_output_path": { "type": "string", - "description": "Path to repo_manager output file (repo_status.yml). Default: {{ output_project_dir }}/repo_manager/repo_status.yml" + "description": "Full path to repo_status.yml produced by repo_manager. Supports multiple projects." + }, + "functional_groups": { + "type": "array", + "description": "Functional groups defining image variants to build. Each group must end with _x86_64 or _aarch64.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Functional group name (e.g., os_x86_64, slurm_node_x86_64).", + "pattern": "^(os|slurm_node|slurm_control_node|login_node|login_compiler_node|service_kube_control_plane_first|service_kube_control_plane|service_kube_node)_(x86_64|aarch64)$" + } + }, + "required": ["name"], + "additionalProperties": false + }, + "minItems": 1 }, "build_image": { "type": "object", diff --git a/src/image_build_manager/library/module_utils/image_build_validation/schema/image_build_credentials.json b/src/image_build_manager/plugins/module_utils/image_build_validation/schema/image_build_credentials.json similarity index 77% rename from src/image_build_manager/library/module_utils/image_build_validation/schema/image_build_credentials.json rename to src/image_build_manager/plugins/module_utils/image_build_validation/schema/image_build_credentials.json index a54295215c..3ff9ce2ad8 100644 --- a/src/image_build_manager/library/module_utils/image_build_validation/schema/image_build_credentials.json +++ b/src/image_build_manager/plugins/module_utils/image_build_validation/schema/image_build_credentials.json @@ -16,13 +16,13 @@ "minLength": 8, "maxLength": 128 }, - "provision_password": { + "aarch64_ssh_password": { "type": "string", - "description": "Password for SSH to ARM build host for aarch64 builds.", - "minLength": 8, - "maxLength": 32 + "description": "SSH password for ARM build host (only required when aarch64_inventory_host_ip is set).", + "minLength": 0, + "maxLength": 128 } }, - "required": ["s3_secret_key", "provision_password"], + "required": ["s3_secret_key"], "additionalProperties": false } diff --git a/src/image_build_manager/library/modules/base_image_package_collector.py b/src/image_build_manager/plugins/modules/base_image_package_collector.py similarity index 100% rename from src/image_build_manager/library/modules/base_image_package_collector.py rename to src/image_build_manager/plugins/modules/base_image_package_collector.py diff --git a/src/image_build_manager/library/modules/functional_group_parser.py b/src/image_build_manager/plugins/modules/functional_group_parser.py similarity index 100% rename from src/image_build_manager/library/modules/functional_group_parser.py rename to src/image_build_manager/plugins/modules/functional_group_parser.py diff --git a/src/image_build_manager/library/modules/generate_functional_groups.py b/src/image_build_manager/plugins/modules/generate_functional_groups.py similarity index 100% rename from src/image_build_manager/library/modules/generate_functional_groups.py rename to src/image_build_manager/plugins/modules/generate_functional_groups.py diff --git a/src/image_build_manager/library/modules/image_package_collector.py b/src/image_build_manager/plugins/modules/image_package_collector.py similarity index 100% rename from src/image_build_manager/library/modules/image_package_collector.py rename to src/image_build_manager/plugins/modules/image_package_collector.py diff --git a/src/image_build_manager/library/modules/validate_image_build_config.py b/src/image_build_manager/plugins/modules/validate_image_build_config.py similarity index 94% rename from src/image_build_manager/library/modules/validate_image_build_config.py rename to src/image_build_manager/plugins/modules/validate_image_build_config.py index c6e487c818..6e8650b5b9 100644 --- a/src/image_build_manager/library/modules/validate_image_build_config.py +++ b/src/image_build_manager/plugins/modules/validate_image_build_config.py @@ -27,7 +27,7 @@ - name: Validate image build configuration validate_image_build_config: input_project_dir: "{{ input_project_dir }}" - schema_dir: "{{ role_path }}/../../../library/module_utils/image_build_validation/schema" + schema_dir: "{{ role_path }}/../../../plugins/module_utils/image_build_validation/schema" """ import json @@ -42,17 +42,17 @@ ) -VALIDATION_LOG_PATH = "/opt/omnia/log/core/playbooks/" +VALIDATION_LOG_PATH = "/opt/omnia/image_build_manager/log/" # Default — overridden by log_dir param # Files to validate and their corresponding schema names VALIDATION_FILES = [ { - "config_file": "image_build_manager/image_build_config.yml", + "config_file": "image_build_config.yml", "schema_file": "image_build_config.json", "required": True, }, { - "config_file": "image_build_manager/image_build_credentials.yml", + "config_file": "image_build_credentials.yml", "schema_file": "image_build_credentials.json", "required": False, }, @@ -160,14 +160,20 @@ def run_module(): module_args = dict( input_project_dir=dict(type="str", required=True), schema_dir=dict(type="str", required=True), + log_dir=dict(type="str", required=False, default=""), ) module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) input_project_dir = module.params["input_project_dir"] schema_dir = module.params["schema_dir"] + log_dir = module.params.get("log_dir", "") project_name = os.path.basename(input_project_dir) + # Use provided log_dir or fall back to default + if log_dir: + global VALIDATION_LOG_PATH + VALIDATION_LOG_PATH = log_dir logger, log_file = create_logger(project_name) logger.info("=== Image Build Manager Validation Start ===") diff --git a/src/image_build_manager/plugins/modules/validate_yaml_schema.py b/src/image_build_manager/plugins/modules/validate_yaml_schema.py new file mode 100644 index 0000000000..1927c4f311 --- /dev/null +++ b/src/image_build_manager/plugins/modules/validate_yaml_schema.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Ansible module to validate YAML files against JSON Schema.""" + +import json +from typing import Any, Dict + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +try: + import jsonschema + from jsonschema import Draft7Validator + HAS_JSONSCHEMA = True +except ImportError: + HAS_JSONSCHEMA = False + +from ansible.module_utils.basic import AnsibleModule + + +DOCUMENTATION = r''' +--- +module: validate_yaml_schema +short_description: Validate YAML file against JSON Schema +description: + - Validates a YAML file against a JSON Schema definition + - Returns validation errors if schema validation fails +options: + yaml_file: + description: Path to YAML file to validate + required: true + type: str + schema_file: + description: Path to JSON Schema file + required: true + type: str +''' + +EXAMPLES = r''' +- name: Validate config.yml against schema + validate_yaml_schema: + yaml_file: /path/to/config.yml + schema_file: /path/to/config.schema.json +''' + + +def load_yaml_file(file_path: str) -> Dict[str, Any]: + """Load YAML file and return parsed content.""" + with open(file_path, 'r', encoding='utf-8') as f: + return yaml.safe_load(f) + + +def load_json_schema(file_path: str) -> Dict[str, Any]: + """Load JSON Schema file and return parsed content.""" + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + + +def validate_against_schema(data: Dict[str, Any], schema: Dict[str, Any]) -> tuple: + """Validate data against schema. + + Returns: + tuple: (is_valid: bool, errors: list) + """ + validator = Draft7Validator(schema) + errors = sorted(validator.iter_errors(data), key=lambda e: e.path) + + if errors: + error_messages = [] + for error in errors: + path = '.'.join(str(p) for p in error.path) if error.path else 'root' + error_messages.append(f"{path}: {error.message}") + return False, error_messages + + return True, [] + + +def main(): + """Main module entry point.""" + module = AnsibleModule( + argument_spec=dict( + yaml_file=dict(type='str', required=True), + schema_file=dict(type='str', required=True), + ), + supports_check_mode=True, + ) + + if not HAS_YAML: + module.fail_json(msg="PyYAML library is required for this module") + + if not HAS_JSONSCHEMA: + module.fail_json(msg="jsonschema library is required for this module") + + yaml_file = module.params['yaml_file'] + schema_file = module.params['schema_file'] + + try: + # Load YAML data + yaml_data = load_yaml_file(yaml_file) + + # Load JSON Schema + schema = load_json_schema(schema_file) + + # Validate + is_valid, errors = validate_against_schema(yaml_data, schema) + + if is_valid: + module.exit_json( + changed=False, + msg="YAML file is valid against schema", + valid=True + ) + else: + module.fail_json( + msg=f"YAML validation failed: {'; '.join(errors)}", + errors=errors, + valid=False + ) + + except FileNotFoundError as e: + module.fail_json(msg=f"File not found: {e}") + except yaml.YAMLError as e: + module.fail_json(msg=f"YAML parsing error: {e}") + except json.JSONDecodeError as e: + module.fail_json(msg=f"JSON schema parsing error: {e}") + except Exception as e: + module.fail_json(msg=f"Unexpected error: {e}") + + +if __name__ == '__main__': + main() diff --git a/src/image_build_manager/requirements.txt b/src/image_build_manager/requirements.txt new file mode 100644 index 0000000000..1174f608bf --- /dev/null +++ b/src/image_build_manager/requirements.txt @@ -0,0 +1,19 @@ +# ============================================================================= +# Image Build Manager — Python Requirements +# ============================================================================= +# Install into the shared Omnia venv: +# ./src/main/omnia.sh --setup-venv +# Or manually: +# pip install -r requirements.txt +# ============================================================================= + +# --- Core Ansible --- +ansible-core>=2.20 + +# --- Python Libraries --- +PyYAML>=6.0.3 +jmespath>=1.0.1 +jsonschema>=4.17 +cryptography>=48.0.0 +netaddr>=1.3.0 +requests>=2.32.5 diff --git a/src/image_build_manager/requirements.yml b/src/image_build_manager/requirements.yml new file mode 100644 index 0000000000..d2ba7ece2b --- /dev/null +++ b/src/image_build_manager/requirements.yml @@ -0,0 +1,20 @@ +# ============================================================================= +# Image Build Manager — Ansible Galaxy Collections +# ============================================================================= +# Install into the shared Omnia venv: +# ./src/main/omnia.sh --setup-venv +# Or manually: +# ansible-galaxy collection install -r requirements.yml +# ============================================================================= +--- +collections: + - name: ansible.posix + version: "2.0.0" + - name: containers.podman + version: "1.16.2" + - name: ansible.utils + version: "5.1.1" + - name: community.crypto + version: "2.23.0" + - name: community.general + version: "10.3.0" diff --git a/src/image_build_manager/roles/build_os_images/tasks/build_base_image_aarch64.yml b/src/image_build_manager/roles/build_os_images/tasks/build_base_image_aarch64.yml index 2e1830ebbd..e783235389 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/build_base_image_aarch64.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/build_base_image_aarch64.yml @@ -82,9 +82,14 @@ retries: "{{ job_retry }}" delay: "{{ job_delay }}" + - name: Configure regctl to use HTTP for local registry + ansible.builtin.command: "/usr/local/bin/regctl registry set --tls disabled {{ registry_host }}:5000" + changed_when: false + failed_when: false + - name: Verify the aarch64 base osimage in registry ansible.builtin.command: - cmd: "/usr/local/bin/regctl repo ls --limit 500 {{ oim_node_name }}.{{ domain_name }}:5000" + cmd: "/usr/local/bin/regctl repo ls --limit 500 {{ registry_host }}:5000" delegate_to: "{{ aarch64_build_host }}" connection: ssh changed_when: false @@ -92,8 +97,8 @@ - name: Fail if aarch64 base osimage not created ansible.builtin.fail: - msg: "Failed to build base osimage {{ oim_node_name }}/{{ rhel_arch_base_image_name }}" - when: (oim_node_name + '/' + rhel_arch_base_image_name) not in verify_base_osimage.stdout_lines + msg: "Failed to build base osimage {{ host_name }}/{{ rhel_arch_base_image_name }}" + when: (host_name + '/' + rhel_arch_base_image_name) not in verify_base_osimage.stdout_lines - name: Verify aarch64 base osimage output ansible.builtin.debug: @@ -104,19 +109,19 @@ ansible.builtin.fail: msg: "{{ base_image_failure_msg }}" - always: - - name: Set openchami SELinux context for Local flow - ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ oim_shared_path }}/omnia/openchami" - changed_when: true - delegate_to: oim - connection: ssh - failed_when: false - when: omnia_share_option == 'Local' + # always: + # - name: Set openchami SELinux context for Local flow + # ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ oim_shared_path }}/omnia/openchami" + # changed_when: true + # delegate_to: oim + # connection: ssh + # failed_when: false + # when: omnia_share_option == 'Local' - - name: Set openchami SELinux context for NFS internal flow - ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ nfs_server_share_path }}/omnia/openchami" - changed_when: true - delegate_to: oim - connection: ssh - failed_when: false - when: omnia_share_option == 'NFS' and nfs_type | default('') == 'internal' + # - name: Set openchami SELinux context for NFS internal flow + # ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ nfs_server_share_path }}/omnia/openchami" + # changed_when: true + # delegate_to: oim + # connection: ssh + # failed_when: false + # when: omnia_share_option == 'NFS' and nfs_type | default('') == 'internal' diff --git a/src/image_build_manager/roles/build_os_images/tasks/build_base_image_x86_64.yml b/src/image_build_manager/roles/build_os_images/tasks/build_base_image_x86_64.yml index f185b0c12c..80a663c94d 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/build_base_image_x86_64.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/build_base_image_x86_64.yml @@ -70,16 +70,21 @@ retries: "{{ job_retry }}" delay: "{{ job_delay }}" + - name: Configure regctl to use HTTP for local registry + ansible.builtin.command: "/usr/local/bin/regctl registry set --tls disabled {{ registry_host }}:5000" + changed_when: false + failed_when: false + - name: Verify the x86_64 base osimage in registry ansible.builtin.command: - cmd: "/usr/local/bin/regctl repo ls --limit 500 {{ oim_node_name }}.{{ domain_name }}:5000" + cmd: "/usr/local/bin/regctl repo ls --limit 500 {{ registry_host }}:5000" changed_when: false register: verify_base_osimage - name: Fail if x86_64 base osimage not created ansible.builtin.fail: - msg: "Failed to build base osimage {{ oim_node_name }}/{{ rhel_arch_base_image_name }}" - when: (oim_node_name + '/' + rhel_arch_base_image_name) not in verify_base_osimage.stdout_lines + msg: "Failed to build base osimage {{ host_name }}/{{ rhel_arch_base_image_name }}" + when: (host_name + '/' + rhel_arch_base_image_name) not in verify_base_osimage.stdout_lines - name: Verify x86_64 base osimage output ansible.builtin.debug: @@ -90,19 +95,19 @@ ansible.builtin.fail: msg: "{{ base_image_failure_msg }}" - always: - - name: Set openchami SELinux context for Local flow - ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ hostvars['localhost']['oim_shared_path'] }}/omnia/openchami" - changed_when: true - delegate_to: oim - connection: ssh - failed_when: false - when: hostvars['localhost']['omnia_share_option'] == 'Local' + # always: + # - name: Set openchami SELinux context for Local flow + # ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ hostvars['localhost']['oim_shared_path'] }}/omnia/openchami" + # changed_when: true + # delegate_to: oim + # connection: ssh + # failed_when: false + # when: hostvars['localhost']['omnia_share_option'] == 'Local' - - name: Set openchami SELinux context for NFS internal flow - ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ hostvars['localhost']['nfs_server_share_path'] }}/omnia/openchami" - changed_when: true - delegate_to: oim - connection: ssh - failed_when: false - when: (hostvars['localhost']['omnia_share_option'] == 'NFS' and hostvars['localhost']['nfs_type'] | default('') == 'internal') + # - name: Set openchami SELinux context for NFS internal flow + # ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ hostvars['localhost']['nfs_server_share_path'] }}/omnia/openchami" + # changed_when: true + # delegate_to: oim + # connection: ssh + # failed_when: false + # when: (hostvars['localhost']['omnia_share_option'] == 'NFS' and hostvars['localhost']['nfs_type'] | default('') == 'internal') diff --git a/src/image_build_manager/roles/build_os_images/tasks/build_compute_image_aarch64.yml b/src/image_build_manager/roles/build_os_images/tasks/build_compute_image_aarch64.yml index 4d8427fb39..5cebddda53 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/build_compute_image_aarch64.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/build_compute_image_aarch64.yml @@ -21,6 +21,14 @@ - (build_stream_job_id | default('') | length) > 0 - (image_key | default('') | length) > 0 +- name: Ensure workdir images directory exists + ansible.builtin.file: + path: "{{ openchami_work_dir }}/images" + state: directory + mode: "{{ dir_permissions_755 }}" + delegate_to: "{{ aarch64_build_host }}" + connection: ssh + - name: Ensure log directory exists ansible.builtin.file: path: "{{ image_build_log_dir }}" @@ -100,9 +108,14 @@ loop_control: label: "Building: {{ item.item.key }}" + - name: Configure regctl to use HTTP for local registry + ansible.builtin.command: "/usr/local/bin/regctl registry set --tls disabled {{ registry_host }}:5000" + changed_when: false + failed_when: false + - name: Verify aarch64 compute osimages in registry ansible.builtin.command: - cmd: "/usr/local/bin/regctl repo ls --limit 500 {{ oim_node_name }}.{{ domain_name }}:5000" + cmd: "/usr/local/bin/regctl repo ls --limit 500 {{ registry_host }}:5000" delegate_to: "{{ aarch64_build_host }}" connection: ssh changed_when: false @@ -127,7 +140,7 @@ ansible.builtin.set_fact: failure_msg_list: - "aarch64 compute image build job did not complete successfully." - - "Check logs at {{ image_build_log_dir }} on OIM host for respective functional group for more details." + - "Check logs at {{ image_build_log_dir }} on localhost for respective functional group for more details." - "" - "Failed images:" @@ -138,7 +151,7 @@ - name: Add log paths section to message ansible.builtin.set_fact: - failure_msg_list: "{{ failure_msg_list + ['', 'Check logs at ' + image_build_log_dir + ' on OIM host for details:'] }}" + failure_msg_list: "{{ failure_msg_list + ['', 'Check logs at ' + image_build_log_dir + ' on localhost for details:'] }}" - name: Add log file paths to message ansible.builtin.set_fact: @@ -155,19 +168,19 @@ ansible.builtin.fail: msg: "aarch64 compute image build failed. See details above." - always: - - name: Set openchami SELinux context for Local flow - ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ oim_shared_path }}/omnia/openchami" - changed_when: true - delegate_to: oim - connection: ssh - failed_when: false - when: omnia_share_option == 'Local' - - - name: Set openchami SELinux context for NFS internal flow - ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ nfs_server_share_path }}/omnia/openchami" - changed_when: true - delegate_to: oim - connection: ssh - failed_when: false - when: omnia_share_option == 'NFS' and nfs_type | default('') == 'internal' + # always: + # - name: Set openchami SELinux context for Local flow + # ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ oim_shared_path }}/omnia/openchami" + # changed_when: true + # delegate_to: oim + # connection: ssh + # failed_when: false + # when: omnia_share_option == 'Local' + + # - name: Set openchami SELinux context for NFS internal flow + # ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ nfs_server_share_path }}/omnia/openchami" + # changed_when: true + # delegate_to: oim + # connection: ssh + # failed_when: false + # when: omnia_share_option == 'NFS' and nfs_type | default('') == 'internal' diff --git a/src/image_build_manager/roles/build_os_images/tasks/build_compute_image_x86_64.yml b/src/image_build_manager/roles/build_os_images/tasks/build_compute_image_x86_64.yml index 9af4125c53..efabed8343 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/build_compute_image_x86_64.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/build_compute_image_x86_64.yml @@ -21,6 +21,12 @@ - (build_stream_job_id | default('') | length) > 0 - (image_key | default('') | length) > 0 +- name: Ensure workdir images directory exists + ansible.builtin.file: + path: "{{ openchami_work_dir }}/images" + state: directory + mode: "{{ dir_permissions_755 }}" + - name: Ensure log directory exists ansible.builtin.file: path: "{{ image_build_log_dir }}" @@ -92,9 +98,14 @@ loop_control: label: "Building: {{ item.item.key }}" + - name: Configure regctl to use HTTP for local registry + ansible.builtin.command: "/usr/local/bin/regctl registry set --tls disabled {{ registry_host }}:5000" + changed_when: false + failed_when: false + - name: Verify x86_64 compute osimages in registry ansible.builtin.command: - cmd: "/usr/local/bin/regctl repo ls --limit 500 {{ oim_node_name }}.{{ domain_name }}:5000" + cmd: "/usr/local/bin/regctl repo ls --limit 500 {{ registry_host }}:5000" changed_when: false register: verify_compute_osimages @@ -117,7 +128,7 @@ ansible.builtin.set_fact: failure_msg_list: - "x86_64 compute image build job did not complete successfully." - - "Check logs at {{ image_build_log_dir }} on OIM host for respective functional group for more details." + - "Check logs at {{ image_build_log_dir }} on localhost for respective functional group for more details." - "" - "Failed images:" @@ -128,7 +139,7 @@ - name: Add log paths section to message ansible.builtin.set_fact: - failure_msg_list: "{{ failure_msg_list + ['', 'Check logs at ' + image_build_log_dir + ' on OIM host for details:'] }}" + failure_msg_list: "{{ failure_msg_list + ['', 'Check logs at ' + image_build_log_dir + ' on localhost for details:'] }}" - name: Add log file paths to message ansible.builtin.set_fact: @@ -145,19 +156,19 @@ ansible.builtin.fail: msg: "x86_64 compute image build failed. See details above." - always: - - name: Set openchami SELinux context for Local flow - ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ hostvars['localhost']['oim_shared_path'] }}/omnia/openchami" - changed_when: true - delegate_to: oim - connection: ssh - failed_when: false - when: hostvars['localhost']['omnia_share_option'] == 'Local' - - - name: Set openchami SELinux context for NFS internal flow - ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ hostvars['localhost']['nfs_server_share_path'] }}/omnia/openchami" - changed_when: true - delegate_to: oim - connection: ssh - failed_when: false - when: (hostvars['localhost']['omnia_share_option'] == 'NFS' and hostvars['localhost']['nfs_type'] | default('') == 'internal') + # always: + # - name: Set openchami SELinux context for Local flow + # ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ hostvars['localhost']['oim_shared_path'] }}/omnia/openchami" + # changed_when: true + # delegate_to: oim + # connection: ssh + # failed_when: false + # when: hostvars['localhost']['omnia_share_option'] == 'Local' + + # - name: Set openchami SELinux context for NFS internal flow + # ansible.builtin.command: chcon -R system_u:object_r:container_file_t:s0 "{{ hostvars['localhost']['nfs_server_share_path'] }}/omnia/openchami" + # changed_when: true + # delegate_to: oim + # connection: ssh + # failed_when: false + # when: (hostvars['localhost']['omnia_share_option'] == 'NFS' and hostvars['localhost']['nfs_type'] | default('') == 'internal') diff --git a/src/image_build_manager/roles/build_os_images/tasks/build_image_common_aarch64.yml b/src/image_build_manager/roles/build_os_images/tasks/build_image_common_aarch64.yml index ba171144c7..1d57655989 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/build_image_common_aarch64.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/build_image_common_aarch64.yml @@ -21,12 +21,15 @@ - name: Set facts for aarch64 image templates vars ansible.builtin.set_fact: rhel_tag: "{{ hostvars['localhost']['rhel_tag'] }}" - oim_node_name: "{{ hostvars['localhost']['oim_node_name'] }}" + host_name: "{{ hostvars['localhost']['host_name'] }}" domain_name: "{{ hostvars['localhost']['domain_name'] }}" rhel_arch_repos: "{{ hostvars['localhost']['rhel_arch_repos'] }}" base_image_packages: "{{ hostvars['localhost']['base_image_packages'] }}" compute_images_dict: "{{ hostvars['localhost']['compute_images_dict'] }}" +- name: Configure registry host and regctl TLS + ansible.builtin.include_tasks: configure_registry.yml + - name: Normalize build stream inputs for base image ansible.builtin.set_fact: enable_build_stream: "{{ enable_build_stream | default(false) | bool }}" diff --git a/src/image_build_manager/roles/build_os_images/tasks/build_image_common_x86_64.yml b/src/image_build_manager/roles/build_os_images/tasks/build_image_common_x86_64.yml index e500783ff8..07ffe833cb 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/build_image_common_x86_64.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/build_image_common_x86_64.yml @@ -21,12 +21,15 @@ - name: Set facts for x86_64 image templates vars ansible.builtin.set_fact: rhel_tag: "{{ hostvars['localhost']['rhel_tag'] }}" - oim_node_name: "{{ hostvars['localhost']['oim_node_name'] }}" + host_name: "{{ hostvars['localhost']['host_name'] }}" domain_name: "{{ hostvars['localhost']['domain_name'] }}" rhel_arch_repos: "{{ hostvars['localhost']['rhel_arch_repos'] }}" base_image_packages: "{{ hostvars['localhost']['base_image_packages'] }}" compute_images_dict: "{{ hostvars['localhost']['compute_images_dict'] }}" +- name: Configure registry host and regctl TLS + ansible.builtin.include_tasks: configure_registry.yml + - name: Normalize build stream inputs for base image ansible.builtin.set_fact: enable_build_stream: "{{ hostvars['localhost']['enable_build_stream'] | default(false) | bool }}" diff --git a/src/image_build_manager/roles/build_os_images/tasks/configure_registry.yml b/src/image_build_manager/roles/build_os_images/tasks/configure_registry.yml new file mode 100644 index 0000000000..d52a3f0ee0 --- /dev/null +++ b/src/image_build_manager/roles/build_os_images/tasks/configure_registry.yml @@ -0,0 +1,35 @@ +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +# configure_registry.yml — Shared task to compute registry host and configure regctl TLS. +# +# Inputs (must be set before including): +# host_name — OIM short hostname +# domain_name — cluster domain +# +# Outputs (set as facts): +# registry_host — IP (standalone) or FQDN for the local OCI registry + +- name: Compute registry host (use admin_nic_ip for standalone mode) + vars: + _node: "{{ host_name }}" + _domain: "{{ domain_name }}" + _fqdn: "{{ _node if _node.endswith('.' + _domain) else _node + '.' + _domain }}" + ansible.builtin.set_fact: + registry_host: "{{ hostvars['localhost']['admin_nic_ip'] if hostvars['localhost']['standalone_mode'] | default(false) else _fqdn }}" + +- name: Configure regctl to use HTTP for local registry + ansible.builtin.command: "/usr/local/bin/regctl registry set --tls disabled {{ registry_host }}:5000" + changed_when: false + failed_when: false diff --git a/src/image_build_manager/roles/build_os_images/tasks/main.yml b/src/image_build_manager/roles/build_os_images/tasks/main.yml index 2613b65d3f..53b945475f 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/main.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/main.yml @@ -17,11 +17,6 @@ ansible.builtin.include_tasks: prepare_pulp_image.yml when: build_arch == 'x86_64' -- name: Include metadata vars - ansible.builtin.include_vars: "{{ omnia_metadata_file }}" - register: include_metadata - no_log: true - - name: Include global variables from common folder ansible.builtin.include_vars: "{{ role_path }}/../../vars/openchami_image_cmd.yml" register: ochami_image_global_vars diff --git a/src/image_build_manager/roles/build_os_images/tasks/set_s3_acl.yml b/src/image_build_manager/roles/build_os_images/tasks/set_s3_acl.yml index 6eb54cb502..0e6295be0f 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/set_s3_acl.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/set_s3_acl.yml @@ -22,20 +22,16 @@ # Context: PowerScale S3 requires object-level ACLs (--recursive) for anonymous # GetObject access. Bucket-level ACL only grants listing permission. # -# Runs on: OIM host (where s3cmd is configured with /root/.s3cfg) +# Runs on: localhost (where s3cmd is configured with /root/.s3cfg) # ═══════════════════════════════════════════════════════════════════════════ - name: Set ACL to public for 'efi' bucket and all objects ansible.builtin.command: s3cmd setacl s3://efi --acl-public --recursive changed_when: true - delegate_to: oim - connection: ssh - name: Set ACL to public for 'boot-images' bucket and all objects ansible.builtin.command: s3cmd setacl s3://boot-images --acl-public --recursive changed_when: true - delegate_to: oim - connection: ssh - name: Verify S3 bucket ACLs are set ansible.builtin.debug: diff --git a/src/image_build_manager/roles/build_os_images/tasks/write_build_status.yml b/src/image_build_manager/roles/build_os_images/tasks/write_build_status.yml index f545dc3173..08c9d4aa3f 100644 --- a/src/image_build_manager/roles/build_os_images/tasks/write_build_status.yml +++ b/src/image_build_manager/roles/build_os_images/tasks/write_build_status.yml @@ -28,8 +28,8 @@ - name: Set output paths ansible.builtin.set_fact: - _output_base: "{{ output_project_dir | default('/opt/omnia/output/project_default') }}" - _output_domain: "{{ output_project_dir | default('/opt/omnia/output/project_default') }}/image_build_manager" + _output_base: "{{ output_project_dir }}" + _output_domain: "{{ output_project_dir }}" _omnia_version: "{{ omnia_version | default('2.2.0.0') }}" - name: Ensure output directories exist @@ -47,7 +47,7 @@ dest: "{{ _output_base }}/build_status.yml" mode: "0644" vars: - build_completed_images: "{{ hostvars['oim']['build_completed_images'] | default({}) }}" + build_completed_images: "{{ hostvars['localhost']['build_completed_images'] | default({}) }}" - name: Write versioned build_status copy ansible.builtin.template: @@ -55,7 +55,7 @@ dest: "{{ _output_domain }}/build_status_{{ _omnia_version }}_{{ ansible_date_time.date | regex_replace('-', '') }}_{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}.yml" mode: "0644" vars: - build_completed_images: "{{ hostvars['oim']['build_completed_images'] | default({}) }}" + build_completed_images: "{{ hostvars['localhost']['build_completed_images'] | default({}) }}" - name: Display build status output locations ansible.builtin.debug: diff --git a/src/image_build_manager/roles/build_os_images/templates/images/rhel-base-config.yaml.j2 b/src/image_build_manager/roles/build_os_images/templates/images/rhel-base-config.yaml.j2 index e819bc8b3c..176acc118c 100644 --- a/src/image_build_manager/roles/build_os_images/templates/images/rhel-base-config.yaml.j2 +++ b/src/image_build_manager/roles/build_os_images/templates/images/rhel-base-config.yaml.j2 @@ -4,9 +4,9 @@ options: publish_tags: '{{ rhel_tag }}' pkg_manager: 'dnf' parent: 'scratch' - publish_registry: '{{ oim_node_name }}.{{ domain_name }}:5000/{{ oim_node_name }}' + publish_registry: '{{ registry_host }}:5000/{{ oim_node_name }}' registry_opts_push: - - '--tls-verify=false' + - '--tls-verify={{ registry_tls_verify | default(false) | lower }}' repos: {% for repo in rhel_arch_repos %} diff --git a/src/image_build_manager/roles/build_os_images/templates/images/rhel-compute-config.yaml.j2 b/src/image_build_manager/roles/build_os_images/templates/images/rhel-compute-config.yaml.j2 index 8abe505451..b7bfcc43f0 100644 --- a/src/image_build_manager/roles/build_os_images/templates/images/rhel-compute-config.yaml.j2 +++ b/src/image_build_manager/roles/build_os_images/templates/images/rhel-compute-config.yaml.j2 @@ -3,15 +3,15 @@ options: name: '{{ rhel_base_compute_image_name }}' publish_tags: '{{ rhel_tag }}' pkg_manager: dnf - parent: '{{ oim_node_name }}.{{ domain_name }}:5000/{{ oim_node_name }}/{{ rhel_arch_base_image_name }}:{{ rhel_tag }}' + parent: '{{ registry_host }}:5000/{{ oim_node_name }}/{{ rhel_arch_base_image_name }}:{{ rhel_tag }}' registry_opts_pull: - - '--tls-verify=false' + - '--tls-verify={{ registry_tls_verify | default(false) | lower }}' publish_s3: '{{ s3_endpoint }}' s3_prefix: '{{ group_name }}/{{ rhel_base_compute_image_name }}/' s3_bucket: 'boot-images' - publish_registry: '{{ oim_node_name }}.{{ domain_name }}:5000/{{ oim_node_name }}' + publish_registry: '{{ registry_host }}:5000/{{ oim_node_name }}' registry_opts_push: - - '--tls-verify=false' + - '--tls-verify={{ registry_tls_verify | default(false) | lower }}' repos: {% for repo in rhel_arch_repos %} diff --git a/src/image_build_manager/roles/build_os_images/vars/main.yml b/src/image_build_manager/roles/build_os_images/vars/main.yml index 633fae59e2..5e78032fe0 100644 --- a/src/image_build_manager/roles/build_os_images/vars/main.yml +++ b/src/image_build_manager/roles/build_os_images/vars/main.yml @@ -22,16 +22,15 @@ pull_image_retries: "5" pull_image_delay: "10" omnia_version: "{{ hostvars['localhost']['omnia_version'] | default('2.2.0.0') }}" input_project_dir: "{{ hostvars['localhost']['input_project_dir'] }}" -omnia_metadata_file: "/opt/omnia/.data/oim_metadata.yml" dir_permissions_644: "0644" dir_permissions_755: "0755" -oim_shared_path: "{{ hostvars['localhost']['oim_shared_path'] }}" -pulp_cert_host_path: "{{ hostvars['localhost']['pulp_cert_oim_path'] | default(oim_shared_path + '/omnia/pulp/settings/certs/pulp_webserver.crt') }}" +shared_path: "{{ hostvars['localhost']['shared_path'] }}" +pulp_cert_host_path: "{{ hostvars['localhost']['pulp_cert_path'] | default('/opt/omnia/pulp/settings/certs/pulp_webserver.crt') }}" job_retry: "240" job_delay: "30" job_async: "7200" anchors_path: "/etc/pki/ca-trust/source/anchors/pulp_webserver.crt" -openchami_work_dir: "{{ oim_shared_path }}/omnia/image_build_manager/workdir" +openchami_work_dir: "{{ shared_path }}/workdir" ochami_mounts: - --user 0 --privileged - -v {{ pulp_cert_host_path }}:/etc/pki/ca-trust/source/anchors/pulp_webserver.crt:z @@ -48,16 +47,16 @@ ochami_base_command: - -c 'update-ca-trust extract && image-build --config /home/builder/config.yaml --log-level DEBUG' # build_base_image.yml -image_build_log_dir: "{{ oim_shared_path }}/omnia/image_build_manager/log" -image_build__base_image_log_path: "{{ oim_shared_path }}/omnia/image_build_manager/log/{{ build_arch }}_base_image.log" +image_build_log_dir: "{{ hostvars['localhost']['log_dir'] | default(shared_path + '/log') }}" +image_build__base_image_log_path: "{{ image_build_log_dir }}/{{ build_arch }}_base_image.log" # build_base_image.yml - image-build config template openchami_base_image_config_template: "{{ role_path }}/templates/images/rhel-base-config.yaml.j2" base_image_failure_msg: | Base {{ build_arch }} image build job failed or timed out. - Check logs at path {{ image_build__base_image_log_path }} on OIM host for details. + Check logs at path {{ image_build__base_image_log_path }} for details. compute_image_failure_msg: | {{ build_arch }} compute image build job did not complete successfully. - Check logs at {{ image_build_log_dir }} on OIM host for respective functional group for more details. + Check logs at {{ image_build_log_dir }} for respective functional group for more details. # build_compute_image.yml - image-build config template openchami_compute_image_config_template: "{{ role_path }}/templates/images/rhel-compute-config.yaml.j2" diff --git a/src/image_build_manager/roles/cleanup_build_artifacts/tasks/cleanup_omnia_target.yml b/src/image_build_manager/roles/cleanup_build_artifacts/tasks/cleanup_omnia_target.yml deleted file mode 100644 index f242aaa7fb..0000000000 --- a/src/image_build_manager/roles/cleanup_build_artifacts/tasks/cleanup_omnia_target.yml +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. ---- -# cleanup_omnia_target.yml — Remove minio.service and registry.service from omnia.target. - -- name: Check if omnia.target exists - ansible.builtin.stat: - path: "{{ omnia_target_path }}" - register: omnia_target_check - -- name: Remove image_build_manager services from omnia.target - when: omnia_target_check.stat.exists - block: - - name: Remove minio.service from omnia.target Requires line - ansible.builtin.replace: - path: "{{ omnia_target_path }}" - regexp: '\s*minio\.service' - replace: '' - register: minio_target_removed - - - name: Remove registry.service from omnia.target Requires line - ansible.builtin.replace: - path: "{{ omnia_target_path }}" - regexp: '\s*registry\.service' - replace: '' - register: registry_target_removed - - - name: Reload systemd after target update - ansible.builtin.systemd: - daemon_reload: true - when: minio_target_removed.changed or registry_target_removed.changed diff --git a/src/image_build_manager/roles/cleanup_build_artifacts/tasks/main.yml b/src/image_build_manager/roles/cleanup_build_artifacts/tasks/main.yml index a945e18f03..56ea44aa61 100644 --- a/src/image_build_manager/roles/cleanup_build_artifacts/tasks/main.yml +++ b/src/image_build_manager/roles/cleanup_build_artifacts/tasks/main.yml @@ -30,10 +30,6 @@ ansible.builtin.include_tasks: cleanup_build_output.yml tags: output -- name: Remove image_build_manager entries from omnia.target - ansible.builtin.include_tasks: cleanup_omnia_target.yml - tags: omnia_target - - name: Cleanup s3cmd configuration ansible.builtin.include_tasks: cleanup_s3cmd.yml tags: s3cmd diff --git a/src/image_build_manager/roles/cleanup_build_artifacts/vars/main.yml b/src/image_build_manager/roles/cleanup_build_artifacts/vars/main.yml index 8e68819d30..d703b20df2 100644 --- a/src/image_build_manager/roles/cleanup_build_artifacts/vars/main.yml +++ b/src/image_build_manager/roles/cleanup_build_artifacts/vars/main.yml @@ -15,8 +15,8 @@ # cleanup_image_build_manager/vars/main.yml — Variables for image_build_manager cleanup. # OIM metadata (loaded on localhost via include_input_dir with omnia_metadata_support) -oim_shared_path: "{{ hostvars['localhost']['oim_shared_path'] | default('/opt/omnia') }}" -omnia_nfs_share: "{{ oim_shared_path }}/omnia" +shared_path: "{{ hostvars['localhost']['shared_path'] }}" +omnia_nfs_share: "{{ shared_path }}" # S3 configuration from image_build_config.yml (loaded on localhost) s3_configurations: "{{ hostvars['localhost']['s3_configurations'] | default({}) }}" @@ -27,8 +27,8 @@ s3_provider: "{{ s3_configurations.provider | default('minio') }}" # --------------------------------------------------------------------------- minio_service_name: "minio" minio_quadlet_file: "/etc/containers/systemd/minio.container" -minio_s3_data_dir: "{{ oim_shared_path }}/omnia/image_build_manager/s3/data" -minio_s3_work_dir: "{{ oim_shared_path }}/omnia/image_build_manager/s3" +minio_s3_data_dir: "{{ shared_path }}/s3/data" +minio_s3_work_dir: "{{ shared_path }}/s3" minio_cleanup_directories: - "{{ minio_s3_data_dir }}" - "{{ minio_s3_work_dir }}" @@ -41,8 +41,8 @@ minio_firewall_ports: # --------------------------------------------------------------------------- registry_service_name: "registry" registry_quadlet_file: "/etc/containers/systemd/registry.container" -registry_storage_dir: "/opt/omnia/registry/data" -registry_oci_data_dir: "{{ oim_shared_path }}/omnia/image_build_manager/oci/data" +registry_storage_dir: "{{ shared_path }}/registry/data" +registry_oci_data_dir: "{{ shared_path }}/oci/data" registry_cleanup_directories: - "{{ registry_storage_dir }}" - "{{ registry_oci_data_dir }}" @@ -52,21 +52,21 @@ registry_firewall_ports: # --------------------------------------------------------------------------- # OpenCHAMI image artifacts cleanup # --------------------------------------------------------------------------- -image_build_work_dir: "{{ oim_shared_path }}/omnia/image_build_manager" +image_build_work_dir: "{{ shared_path }}/workdir" # --------------------------------------------------------------------------- # Output files cleanup # --------------------------------------------------------------------------- -output_project_dir: "{{ hostvars['localhost']['output_project_dir'] | default('/opt/omnia/output/project_default') }}" +output_project_dir: "{{ hostvars['localhost']['output_project_dir'] }}" build_status_latest: "{{ output_project_dir }}/build_status.yml" -build_status_versioned_dir: "{{ output_project_dir }}/image_build_manager" +build_status_versioned_dir: "{{ output_project_dir }}" # --------------------------------------------------------------------------- # Credential cleanup # --------------------------------------------------------------------------- -input_project_dir: "{{ hostvars['localhost']['input_project_dir'] | default('/opt/omnia/input/project_default') }}" -image_build_credentials_file: "{{ input_project_dir }}/image_build_manager/image_build_credentials.yml" -image_build_credentials_key: "{{ input_project_dir }}/image_build_manager/.image_build_credentials_key" +input_project_dir: "{{ hostvars['localhost']['input_project_dir'] }}" +image_build_credentials_file: "{{ input_project_dir }}/image_build_credentials.yml" +image_build_credentials_key: "{{ input_project_dir }}/.image_build_credentials_key" # --------------------------------------------------------------------------- # omnia.target service entries to remove diff --git a/src/image_build_manager/roles/collect_build_credentials/templates/image_build_credential.j2 b/src/image_build_manager/roles/collect_build_credentials/templates/image_build_credential.j2 index 15dbb97f0b..c77ac1cd3f 100644 --- a/src/image_build_manager/roles/collect_build_credentials/templates/image_build_credential.j2 +++ b/src/image_build_manager/roles/collect_build_credentials/templates/image_build_credential.j2 @@ -17,5 +17,5 @@ s3_access_id: "" s3_secret_key: "" -# Provision password (SSH to ARM build host for aarch64 builds) -provision_password: "" +# SSH password for ARM build host (only needed when aarch64_inventory_host_ip is set) +aarch64_ssh_password: "" diff --git a/src/image_build_manager/roles/collect_build_credentials/vars/main.yml b/src/image_build_manager/roles/collect_build_credentials/vars/main.yml index 02eaa68233..a4ac84d0dc 100644 --- a/src/image_build_manager/roles/collect_build_credentials/vars/main.yml +++ b/src/image_build_manager/roles/collect_build_credentials/vars/main.yml @@ -19,18 +19,19 @@ image_build_credential_file: credential_type: "Image Build" template: "{{ role_path }}/templates/image_build_credential.j2" - file_path: "{{ input_project_dir }}/image_build_manager/image_build_credentials.yml" + file_path: "{{ input_project_dir }}/image_build_credentials.yml" file_mode: "0600" - vault_path: "{{ input_project_dir }}/image_build_manager/.image_build_credentials_key" + vault_path: "{{ input_project_dir }}/.image_build_credentials_key" # Credential prompt definitions image_build_cred_config: mandatory: - password: s3_secret_key - - password: provision_password conditional_mandatory: - username: s3_access_id condition: "{{ s3_configurations.provider | default('minio') == 'powerscale' }}" + - password: aarch64_ssh_password + condition: "{{ aarch64_inventory_host_ip | default('') | length > 0 }}" # Messages image_build_mandatory_warning_msg: "WARNING: The following are mandatory image build credentials and cannot be left empty." diff --git a/src/image_build_manager/roles/deploy_minio/tasks/main.yml b/src/image_build_manager/roles/deploy_minio/tasks/main.yml index ce56f1bebc..c9fbdc09bb 100644 --- a/src/image_build_manager/roles/deploy_minio/tasks/main.yml +++ b/src/image_build_manager/roles/deploy_minio/tasks/main.yml @@ -16,11 +16,25 @@ # Extracted from prepare_oim/roles/deploy_containers/openchami/tasks/configs/minio.yml # Skipped when s3_configurations.provider == 'powerscale' (external S3). +- name: Set S3 credentials from loaded image_build_credentials.yml + ansible.builtin.set_fact: + s3_access_id: "{{ hostvars['localhost']['s3_access_id'] | default('admin') if (hostvars['localhost']['s3_access_id'] | default('')) | length > 0 else 'admin' }}" + s3_secret_key: "{{ hostvars['localhost']['s3_secret_key'] | default('') }}" + no_log: true + - name: Skip MinIO deployment for PowerScale provider ansible.builtin.debug: msg: "S3 provider is '{{ s3_configurations.provider | default('minio') }}' — skipping MinIO deployment. Using external PowerScale S3 endpoint." when: s3_configurations.provider | default('minio') == 'powerscale' +- name: Validate S3 secret key is not empty + ansible.builtin.assert: + that: + - s3_secret_key | default('') | length > 0 + fail_msg: "{{ s3_secret_key_empty_fail_msg }}" + quiet: true + when: s3_configurations.provider | default('minio') != 'powerscale' + - name: Deploy MinIO S3 storage backend when: s3_configurations.provider | default('minio') != 'powerscale' block: diff --git a/src/image_build_manager/roles/deploy_minio/templates/s3/s3cfg.j2 b/src/image_build_manager/roles/deploy_minio/templates/s3/s3cfg.j2 index 126dc3c7db..7653e0dca1 100644 --- a/src/image_build_manager/roles/deploy_minio/templates/s3/s3cfg.j2 +++ b/src/image_build_manager/roles/deploy_minio/templates/s3/s3cfg.j2 @@ -8,8 +8,8 @@ use_https = {{ 'True' if s3_configurations.endpoint_url.startswith('https') else check_ssl_certificate = False check_ssl_hostname = False {% else %} -host_base = {{ cluster_name }}.{{ cluster_domain }}:9000 -host_bucket = {{ cluster_name }}.{{ cluster_domain }}:9000 +host_base = {{ admin_nic_ip }}:9000 +host_bucket = {{ admin_nic_ip }}:9000 use_https = False check_ssl_certificate = False check_ssl_hostname = False diff --git a/src/image_build_manager/roles/deploy_minio/vars/main.yml b/src/image_build_manager/roles/deploy_minio/vars/main.yml index a7e8641c56..e135925a83 100644 --- a/src/image_build_manager/roles/deploy_minio/vars/main.yml +++ b/src/image_build_manager/roles/deploy_minio/vars/main.yml @@ -13,25 +13,25 @@ # limitations under the License. --- # OIM metadata (loaded on localhost via include_input_dir with omnia_metadata_support) -oim_shared_path: "{{ hostvars['localhost']['oim_shared_path'] | default('/opt/omnia') }}" -oim_node_name: "{{ hostvars['localhost']['oim_node_name'] | default(ansible_hostname) }}" +shared_path: "{{ hostvars['localhost']['shared_path'] }}" +host_name: "{{ hostvars['localhost']['host_name'] | default(ansible_hostname) }}" domain_name: "{{ hostvars['localhost']['domain_name'] | default(ansible_domain) }}" admin_nic_ip: "{{ hostvars['localhost']['admin_nic_ip'] | default(ansible_default_ipv4.address) }}" # Derived cluster variables (same as prepare_oim/deploy_openchami) -cluster_name: "{{ oim_node_name }}" +cluster_name: "{{ host_name }}" cluster_domain: "{{ domain_name }}" cluster_boot_ip: "{{ admin_nic_ip }}" -# Storage backend (set by validate_build_config on localhost) -storage_backend: "{{ hostvars['localhost']['storage_backend'] | default('minio') }}" - # S3 configuration from image_build_config.yml (loaded on localhost) s3_configurations: "{{ hostvars['localhost']['s3_configurations'] | default({}) }}" +# Storage backend (derived from s3_configurations.provider) +storage_backend: "{{ s3_configurations.provider | default('minio') | lower }}" + # Directory paths (image_build_manager domain-specific) -data_s3_dir: "{{ oim_shared_path }}/omnia/image_build_manager/s3/data" -s3_work_dir: "{{ oim_shared_path }}/omnia/image_build_manager/s3" +data_s3_dir: "{{ shared_path }}/s3/data" +s3_work_dir: "{{ shared_path }}/s3" # File permissions file_perm_rw: "0644" @@ -42,12 +42,11 @@ minio_console_port: 9001 minio_image: "quay.io/minio/minio:latest" minio_release_tag: "latest" -# S3 credentials from image_build_credentials.yml (loaded on localhost) -# For minio: access_id defaults to 'admin' if not provided in credentials -s3_access_id: "{{ hostvars['localhost']['image_build_creds']['s3_access_id'] | default(hostvars['localhost']['s3_access_id'], true) | default('admin', true) }}" -s3_secret_key: "{{ hostvars['localhost']['image_build_creds']['s3_secret_key'] | default(hostvars['localhost']['s3_secret_key'], true) | default('') }}" -minio_access_key: "{{ s3_access_id }}" -minio_secret_key: "{{ s3_secret_key }}" +# Validation messages +s3_secret_key_empty_fail_msg: >- + S3 secret key (s3_secret_key) is empty. This credential is mandatory. + Re-run the playbook to be prompted for credentials, or edit + {{ hostvars['localhost']['input_project_dir'] }}/image_build_credentials.yml directly. # EPEL repository for s3cmd installation epel10_url: https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm diff --git a/src/image_build_manager/roles/deploy_registry/vars/main.yml b/src/image_build_manager/roles/deploy_registry/vars/main.yml index 21d8a10a89..03d095fc6e 100644 --- a/src/image_build_manager/roles/deploy_registry/vars/main.yml +++ b/src/image_build_manager/roles/deploy_registry/vars/main.yml @@ -13,14 +13,14 @@ # limitations under the License. --- # OIM metadata (loaded on localhost via include_input_dir with omnia_metadata_support) -oim_shared_path: "{{ hostvars['localhost']['oim_shared_path'] | default('/opt/omnia') }}" +shared_path: "{{ hostvars['localhost']['shared_path'] }}" # Directory paths (image_build_manager domain-specific) -data_oci_dir: "{{ oim_shared_path }}/omnia/image_build_manager/oci/data" +data_oci_dir: "{{ shared_path }}/oci/data" # Registry container settings registry_port: 5000 -registry_storage_dir: "/opt/omnia/registry/data" +registry_storage_dir: "{{ shared_path }}/registry/data" registry_image: "docker.io/library/registry:2" registry_tag: "2" diff --git a/src/image_build_manager/roles/fetch_build_packages/tasks/main.yml b/src/image_build_manager/roles/fetch_build_packages/tasks/main.yml index 4d9b2fae1a..3493f34a2e 100644 --- a/src/image_build_manager/roles/fetch_build_packages/tasks/main.yml +++ b/src/image_build_manager/roles/fetch_build_packages/tasks/main.yml @@ -12,50 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. --- -- name: Check local_repo.yml execution - block: - - name: Check if metadata file exists - ansible.builtin.stat: - path: "{{ metadata_file_path }}" - register: metadata_file_status - - - name: Fail if metadata file is not present - ansible.builtin.fail: - msg: "{{ local_repo_check_msg }}" - when: not metadata_file_status.stat.exists - -- name: Initialize fg_present as false - ansible.builtin.set_fact: - fg_present: "{{ fg_present | default(false) }}" - when: enable_build_stream | default(false) - -- name: Include functional groups config - ansible.builtin.include_vars: - file: "{{ functional_groups_file_path }}" - name: functional_groups_config - when: not (enable_build_stream | default(false) | bool) +# ========================================================================== +# Load packages from functional_group_packages.yml +# ========================================================================== -- name: Set functional_groups_file_path for build_stream disabled flow - ansible.builtin.set_fact: - functional_groups_file_path: "{{ functional_groups_file_path }}" - when: not (enable_build_stream | default(false) | bool) - -- name: Include software config - ansible.builtin.include_vars: - file: "{{ software_config_file_path }}" - name: software_config - -- name: Set cluster OS facts - ansible.builtin.set_fact: - rhel_tag: "{{ software_config.cluster_os_version }}" - default_json_path: "{{ input_project_dir }}/config/{{ build_arch }}/{{ software_config.cluster_os_type }}/{{ software_config.cluster_os_version }}/default_packages.json" # noqa: yaml[line-length] - additional_json_path: "{{ input_project_dir }}/config/{{ build_arch }}/{{ software_config.cluster_os_type }}/{{ software_config.cluster_os_version }}/additional_packages.json" # noqa: yaml[line-length] - admin_debug_json_path: "{{ input_project_dir }}/config/{{ build_arch }}/{{ software_config.cluster_os_type }}/{{ software_config.cluster_os_version }}/admin_debug_packages.json" # noqa: yaml[line-length] +- name: Load functional group package mapping + block: + - name: Load functional_group_packages.yml + ansible.builtin.include_vars: + file: "{{ package_list_path }}" + name: _fg_pkg_mapping + + - name: Set base_image_packages from mapping + ansible.builtin.set_fact: + base_image_packages: "{{ _fg_pkg_mapping.base_packages | default([]) }}" + cacheable: true + + - name: Build compute_images_dict from mapping + enabled functional_groups + ansible.builtin.set_fact: + compute_images_dict: >- + {% set result = {} -%} + {% for fg in functional_groups -%} + {% set fg_name = fg.name if fg is mapping else fg -%} + {% if fg_name.endswith('_' + build_arch) -%} + {% set pkgs = (_fg_pkg_mapping.functional_groups[fg_name].packages | default([])) -%} + {% set _ = result.update({fg_name: {'functional_group': fg_name, 'packages': pkgs}}) -%} + {% endif -%} + {% endfor -%} + {{ result }} + cacheable: true + + - name: Set cluster OS version from repo_status + ansible.builtin.set_fact: + rhel_tag: "{{ hostvars['localhost']['cluster_os_version'] | default(cluster_os_version | default('10.0')) }}" + + - name: Set service_k8s_version (empty unless overridden) + ansible.builtin.set_fact: + service_k8s_version: "" + cacheable: true + + - name: Debug package resolution + ansible.builtin.debug: + msg: "Resolved {{ compute_images_dict | length }} functional groups, {{ base_image_packages | length }} base packages" + verbosity: 1 - name: Fetch pulp endpoint repos ansible.builtin.include_tasks: fetch_pulp_repos.yml - when: fg_present or (enable_build_stream | default(false) | bool) - -- name: Fetch packages for base and compute image softwares - ansible.builtin.include_tasks: fetch_packages.yml - when: fg_present or (enable_build_stream | default(false) | bool) diff --git a/src/image_build_manager/roles/fetch_build_packages/vars/main.yml b/src/image_build_manager/roles/fetch_build_packages/vars/main.yml index 3b1e1efddc..1309c8dfbf 100644 --- a/src/image_build_manager/roles/fetch_build_packages/vars/main.yml +++ b/src/image_build_manager/roles/fetch_build_packages/vars/main.yml @@ -14,13 +14,8 @@ --- -metadata_file_path: "/opt/omnia/offline_repo/.data/localrepo_metadata.yml" -local_repo_check_msg: | - Failure: metadata file path {{ metadata_file_path }} is not present inside omnia_core container. - Please make sure that local_repo.yml playbook is executed successfully. input_project_dir: "{{ hostvars['localhost']['input_project_dir'] }}" -functional_groups_file_path: "{{ hostvars['localhost']['functional_groups_config_path'] | default('/opt/omnia/.data/functional_groups_config.yml') }}" -software_config_file_path: "{{ input_project_dir }}/software_config.json" +functional_groups_file_path: "{{ hostvars['localhost']['functional_groups_config_path'] }}" # Architecture-agnostic variables — build_arch is set by the calling playbook build_image_completion_msg: | @@ -28,8 +23,8 @@ build_image_completion_msg: | To boot {{ build_arch }} nodes execute provision/provision.yml playbook. functional_group_absent_msg: | - Failure: No {{ build_arch }} functional groups found in functional_group_config.yml input file inside omnia_core container. - Please make sure {{ build_arch }} functional_group should be present in input file functional_group_config.yml + Failure: No {{ build_arch }} functional groups found in functional_group_config. + Please ensure {{ build_arch }} functional groups are defined in image_build_config.yml to execute build_image_{{ build_arch }}.yml successfully. build_stream_prerequisite_fail_msg: | Build Stream mode is enabled. Manual execution is not supported. diff --git a/src/image_build_manager/roles/generate_functional_groups/tasks/main.yml b/src/image_build_manager/roles/generate_functional_groups/tasks/main.yml deleted file mode 100644 index 7095209814..0000000000 --- a/src/image_build_manager/roles/generate_functional_groups/tasks/main.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. ---- -# image_build_functional_groups — Generate functional_groups_config.yml from mapping.csv. -# Replaces: ../playbooks/utils/generate_functional_groups.yml -# Only runs when enable_build_stream is false. - -- name: Include vars from provision_config.yml - ansible.builtin.include_vars: - file: "{{ input_project_dir }}/{{ provision_config_file }}" - -- name: Set fact for functional groups file path - ansible.builtin.set_fact: - functional_groups_config_path: "{{ functional_groups_config_path }}" - -- name: Generate functional groups from mapping.csv - generate_functional_groups: - mapping_file_path: "{{ pxe_mapping_file_path }}" - functional_groups_file_path: "{{ functional_groups_config_path }}" - omnia_config_path: "{{ omnia_config_dir }}/{{ omnia_config_file }}" - -- name: Check the functional_groups_config.yml file is created in /opt/omnia/.data - ansible.builtin.stat: - path: "{{ functional_groups_config_path }}" - register: functional_groups_file - -- name: Fail if functional groups file is not created - ansible.builtin.fail: - msg: "Failed to create functional groups file at {{ functional_groups_config_path }}" - when: not functional_groups_file.stat.exists diff --git a/src/image_build_manager/roles/generate_functional_groups/vars/main.yml b/src/image_build_manager/roles/generate_functional_groups/vars/main.yml deleted file mode 100644 index f1d05fe0cb..0000000000 --- a/src/image_build_manager/roles/generate_functional_groups/vars/main.yml +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. ---- - -provision_config_file: "{{ orchestrator_config_file | default('provision_config.yml') }}" -omnia_config_file: "omnia_config.yml" -omnia_config_dir: "{{ hostvars['localhost']['omnia_project_input_dir'] | default(input_project_dir) }}" -functional_groups_config_path: "/opt/omnia/.data/functional_groups_config.yml" diff --git a/src/image_build_manager/roles/image_build_setup/tasks/load_config.yml b/src/image_build_manager/roles/image_build_setup/tasks/load_config.yml new file mode 100644 index 0000000000..8ee462fd8c --- /dev/null +++ b/src/image_build_manager/roles/image_build_setup/tasks/load_config.yml @@ -0,0 +1,159 @@ +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +# Step 2: Load configuration from environment variables +# Step 3: Set project dirs and host vars +# +# Reads omnia.env environment variables instead of config.yml. +# Required env vars: OMNIA_DATA_PATH, OMNIA_ADMIN_NIC_IP, OMNIA_HOSTNAME, OMNIA_DOMAIN_NAME +# Optional env vars: OMNIA_PROJECT_NAME + +# --- Resolve domain root directory (works from any playbook location) --- +- name: Set domain root directory + ansible.builtin.set_fact: + _domain_root_dir: "{{ playbook_dir | regex_replace('/playbooks.*$', '') }}" + cacheable: true + +# --- Source omnia.env if present --- +- name: Check for omnia.env + ansible.builtin.stat: + path: "{{ _domain_root_dir }}/../main/omnia.env" + register: _omnia_env_file + +- name: Source omnia.env defaults (if present and env vars not already set) + ansible.builtin.shell: | + set -a + . "{{ _domain_root_dir }}/../main/omnia.env" + set +a + echo "OMNIA_DATA_PATH=${OMNIA_DATA_PATH}" + echo "OMNIA_PROJECT_NAME=${OMNIA_PROJECT_NAME}" + echo "OMNIA_ADMIN_NIC_IP=${OMNIA_ADMIN_NIC_IP}" + echo "OMNIA_HOSTNAME=${OMNIA_HOSTNAME}" + echo "OMNIA_DOMAIN_NAME=${OMNIA_DOMAIN_NAME}" + echo "OMNIA_VENV_PATH=${OMNIA_VENV_PATH}" + register: _env_output + changed_when: false + when: _omnia_env_file.stat.exists + +- name: Parse sourced environment variables + ansible.builtin.set_fact: + _env_vars: "{{ _env_vars | default({}) | combine({item.split('=', 1)[0]: item.split('=', 1)[1]}) }}" + loop: "{{ (_env_output.stdout_lines | default([])) }}" + when: "'=' in item" + no_log: true + +# --- Resolve and validate configuration (single set_fact) --- +- name: Resolve configuration from environment + ansible.builtin.set_fact: + omnia_data_path: "{{ lookup('env', 'OMNIA_DATA_PATH') | default(_env_vars.OMNIA_DATA_PATH | default('/opt/omnia'), true) }}" + project_name: "{{ lookup('env', 'OMNIA_PROJECT_NAME') | default(_env_vars.OMNIA_PROJECT_NAME | default('project_default'), true) }}" + admin_nic_ip: "{{ lookup('env', 'OMNIA_ADMIN_NIC_IP') | default(_env_vars.OMNIA_ADMIN_NIC_IP | default('172.16.107.254'), true) }}" + host_name: "{{ lookup('env', 'OMNIA_HOSTNAME') | default(_env_vars.OMNIA_HOSTNAME | default('oim'), true) }}" + domain_name: "{{ lookup('env', 'OMNIA_DOMAIN_NAME') | default(_env_vars.OMNIA_DOMAIN_NAME | default('omnia.cluster'), true) }}" + cacheable: true + +# --- Validate required variables --- +- name: Fail if OMNIA_ADMIN_NIC_IP is not set + ansible.builtin.fail: + msg: >- + OMNIA_ADMIN_NIC_IP is not set. Set it in omnia.env or export it: + export OMNIA_ADMIN_NIC_IP= + when: admin_nic_ip | length == 0 + +- name: Fail if OMNIA_HOSTNAME is not set + ansible.builtin.fail: + msg: >- + OMNIA_HOSTNAME is not set. Set it in omnia.env or export it: + export OMNIA_HOSTNAME=oim + when: host_name | length == 0 + +- name: Fail if OMNIA_DOMAIN_NAME is not set + ansible.builtin.fail: + msg: >- + OMNIA_DOMAIN_NAME is not set. Set it in omnia.env or export it: + export OMNIA_DOMAIN_NAME=omnia.cluster + when: domain_name | length == 0 + +- name: Validate configured environment + ansible.builtin.debug: + msg: + - "OMNIA_HOSTNAME: {{ host_name }}" + - "OMNIA_DOMAIN_NAME: {{ domain_name }}" + - "OMNIA_ADMIN_NIC_IP: {{ admin_nic_ip }}" + - "OMNIA_DATA_PATH: {{ omnia_data_path }}" + - "OMNIA_PROJECT_NAME: {{ project_name }}" + +# --- Set all derived paths in a single set_fact --- +- name: Set project dirs and derived paths + vars: + _ibm_path: "{{ lookup('env', 'IMAGE_BUILD_MANAGER_DATA_PATH') | default(omnia_data_path + '/image_build_manager', true) }}" + ansible.builtin.set_fact: + input_project_dir: "{{ _domain_root_dir }}/input/{{ project_name }}" + output_project_dir: "{{ _ibm_path }}/output/{{ project_name }}" + log_dir: "{{ _ibm_path }}/log/{{ project_name }}" + shared_path: "{{ _ibm_path }}" + oim_node_name: "{{ host_name }}" + functional_groups_config_path: "{{ _ibm_path }}/output/{{ project_name }}/.data/functional_groups_config.yml" + standalone_mode: true + cacheable: true + +- name: Verify project input directory exists + ansible.builtin.stat: + path: "{{ input_project_dir }}" + register: _project_dir_check + +- name: Fail if project input directory does not exist + ansible.builtin.fail: + msg: "{{ project_dir_not_exist_fail_msg }}" + when: not _project_dir_check.stat.exists + +# --- Create runtime directories --- +- name: Ensure base /opt/omnia directory exists + ansible.builtin.file: + path: "{{ omnia_data_path }}" + state: directory + mode: '0755' + become: true + +- name: Ensure domain-specific directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ shared_path }}" + - "{{ output_project_dir }}" + - "{{ log_dir }}" + - "{{ shared_path }}/log/playbooks" + - "{{ shared_path }}/s3" + - "{{ shared_path }}/registry" + - "{{ shared_path }}/oci" + - "{{ shared_path }}/workdir" + become: true + +- name: Validate domain directories were created + ansible.builtin.stat: + path: "{{ item }}" + loop: + - "{{ omnia_data_path }}" + - "{{ shared_path }}" + - "{{ output_project_dir }}" + - "{{ log_dir }}" + register: _dir_checks + +- name: Fail if any required directory is missing + ansible.builtin.fail: + msg: "Required directory does not exist: {{ item.item }}" + when: not item.stat.exists + loop: "{{ _dir_checks.results }}" diff --git a/src/image_build_manager/roles/image_build_setup/tasks/load_repo_status.yml b/src/image_build_manager/roles/image_build_setup/tasks/load_repo_status.yml new file mode 100644 index 0000000000..c39680f20e --- /dev/null +++ b/src/image_build_manager/roles/image_build_setup/tasks/load_repo_status.yml @@ -0,0 +1,133 @@ +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +# Step 5: Load repo_status.yml and validate +# Step 6: Set pulp facts and build repo lists +# +# Loads repo_status.yml, validates required sections, checks the pulp +# certificate exists, sets pulp facts, and builds architecture-specific +# repo lists for downstream build playbooks. + +# --- Load and validate repo_status.yml --- +- name: Load repo_status.yml + ansible.builtin.include_vars: + file: "{{ repo_manager_output_path }}" + +- name: Set OS facts from repo_status.yml + ansible.builtin.set_fact: + cluster_os_type: "{{ cluster_os_type | default('rhel') }}" + cluster_os_version: "{{ cluster_os_version | default('10.0') }}" + cacheable: true + +- name: Validate repo_status.yml has required sections + ansible.builtin.assert: + that: + - repo_manager is defined + - repo_manager.certificates is defined + - repo_manager.certificates.server_crt is defined + - repo_manager.certificates.server_crt | length > 0 + - repo_manager.port is defined + fail_msg: "{{ repo_status_missing_section_fail_msg }}" + quiet: true + +# --- Resolve functional_group_packages.yml path from package_list --- +- name: Set package_list_path from repo_status.yml + ansible.builtin.set_fact: + package_list_path: >- + {{ repo_manager.package_list | default( + (repo_manager_output_path | dirname) + '/functional_group_packages.yml' + ) }} + cacheable: true + +- name: Validate functional_group_packages.yml exists + ansible.builtin.stat: + path: "{{ package_list_path }}" + register: _pkg_list_check + +- name: Fail if functional_group_packages.yml not found + ansible.builtin.fail: + msg: "{{ functional_group_packages_not_found_fail_msg }}" + when: not _pkg_list_check.stat.exists + +# --- Validate pulp certificate --- +- name: Validate pulp certificate exists + ansible.builtin.stat: + path: "{{ repo_manager.certificates.server_crt }}" + register: _pulp_cert_check + +- name: Fail if pulp certificate not found + ansible.builtin.fail: + msg: "{{ pulp_cert_not_found_fail_msg }}" + when: not _pulp_cert_check.stat.exists + +# --- Set pulp and S3 facts --- +- name: Set pulp and S3 facts from repo_status.yml + ansible.builtin.set_fact: + pulp_port: "{{ repo_manager.port }}" + pulp_cert_path: "{{ repo_manager.certificates.server_crt }}" + s3_endpoint: >- + {{ s3_configurations.endpoint_url + if s3_configurations.endpoint_url | default('') | length > 0 + else ('http://' + admin_nic_ip + ':9000' + if s3_configurations.provider | default('minio') == 'minio' + else '') }} + cacheable: true + +# --- Build repo lists --- +- name: Build repo lists from repo_status.yml + when: rpm_repos is defined + block: + - name: Initialize repo lists + ansible.builtin.set_fact: + repo_manager_repos_x86_64: [] + repo_manager_repos_aarch64: [] + rpm_repos_aarch64: "{{ rpm_repos.aarch64 | default({}) }}" + + - name: Build x86_64 repo list from rpm_repos + ansible.builtin.set_fact: + repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" + loop: "{{ (rpm_repos.x86_64 | default({})) | dict2items }}" + loop_control: + loop_var: item + + - name: Build aarch64 repo list from rpm_repos + ansible.builtin.set_fact: + repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" + loop: "{{ (rpm_repos.aarch64 | default({})) | dict2items }}" + loop_control: + loop_var: item + + - name: Build x86_64 repo list from user_repos + ansible.builtin.set_fact: + repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" + loop: "{{ (user_repos.x86_64 | default({})) | dict2items }}" + loop_control: + loop_var: item + when: user_repos is defined + + - name: Build aarch64 repo list from user_repos + ansible.builtin.set_fact: + repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 + [{'name': item.key, 'base_url': item.value, 'gpg': ''}] }}" + loop: "{{ (user_repos.aarch64 | default({})) | dict2items }}" + loop_control: + loop_var: item + when: user_repos is defined + + - name: Make repo lists cacheable + ansible.builtin.set_fact: + repo_manager_repos_x86_64: "{{ repo_manager_repos_x86_64 }}" + repo_manager_repos_aarch64: "{{ repo_manager_repos_aarch64 }}" + cacheable: true + # NOTE: repo list loop set_facts above cannot be further consolidated + # because Ansible loops append to the list incrementally. diff --git a/src/image_build_manager/roles/image_build_setup/tasks/main.yml b/src/image_build_manager/roles/image_build_setup/tasks/main.yml index 82e39592d5..92be703ccb 100644 --- a/src/image_build_manager/roles/image_build_setup/tasks/main.yml +++ b/src/image_build_manager/roles/image_build_setup/tasks/main.yml @@ -13,111 +13,49 @@ # limitations under the License. --- # image_build_setup — Self-contained setup for image_build_manager. -# Replaces: upgrade_checkup.yml + include_input_dir.yml + create_container_group.yml # -# What it does: -# 0. Validate tags and tag combinations -# 1. Guard: block if upgrade is in progress -# 2. Load omnia project config (input/output dirs) -# 3. Load OIM metadata -# 4. Create oim inventory group -# 5. Set image_build_main_flow and skip_subscription_check facts +# Task files: +# validate_tags.yml — Step 1: Validate tags and tag combinations +# load_config.yml — Step 2-3: Load config.yml, validate host, set dirs +# validate_prereqs.yml — Step 4: Validate all prerequisite files (fail-fast) +# load_repo_status.yml — Step 5-6: Load repo_status.yml, build repo lists +# main.yml (this file) — Step 7: Set guard facts + +- name: Include tag validation + ansible.builtin.include_tasks: validate_tags.yml + +- name: Include config loading and host validation + ansible.builtin.include_tasks: load_config.yml + +- name: Include prerequisite file validation + ansible.builtin.include_tasks: validate_prereqs.yml + when: "'cleanup' not in (provided_tags | default([]))" + +- name: Include repo_status loading and repo list building + ansible.builtin.include_tasks: load_repo_status.yml + when: "'cleanup' not in (provided_tags | default([]))" + +# ========================================================================= +# Step 7: Set guard facts +# ========================================================================= +- name: Display loaded configuration + ansible.builtin.debug: + msg: + - "Project: {{ project_name }}" + - "Host: {{ host_name }}.{{ domain_name }} ({{ admin_nic_ip }})" + - "Shared path: {{ shared_path }}" + - "Output dir: {{ output_project_dir }}" + - "Log dir: {{ log_dir }}" + - "Repo manager output: {{ repo_manager_output_path | default('(skipped for cleanup)') }}" + - "Package list: {{ package_list_path | default('(skipped for cleanup)') }}" + - "Pulp cert: {{ pulp_cert_path | default('(skipped for cleanup)') }}" + - "x86_64 repos: {{ repo_manager_repos_x86_64 | default([]) | length }}" + - "aarch64 repos: {{ repo_manager_repos_aarch64 | default([]) | length }}" + verbosity: 1 -# --- Step 0: Tag validation --- -- name: Get provided tags (excluding 'always' and 'all') - ansible.builtin.set_fact: - provided_tags: "{{ ansible_run_tags | difference(['always', 'all']) }}" - -- name: Identify invalid tags - ansible.builtin.set_fact: - invalid_tags: "{{ provided_tags | difference(supported_tags) }}" - when: provided_tags | length > 0 - -- name: Fail if invalid tags are provided - ansible.builtin.fail: - msg: "{{ tag_validation_fail_msg }}" - when: - - provided_tags | length > 0 - - invalid_tags | default([]) | length > 0 - -- name: Check for invalid tag combinations - ansible.builtin.set_fact: - has_invalid_combination: true - when: - - provided_tags | length > 1 - - item | difference(provided_tags) | length == 0 - loop: "{{ invalid_tag_combinations }}" - -- name: Fail if invalid tag combination is detected - ansible.builtin.fail: - msg: "{{ tag_combination_fail_msg }}" - when: has_invalid_combination | default(false) | bool - -- name: Set skip_credentials flag for cleanup/validate tags - ansible.builtin.set_fact: - skip_build_credentials: true - cacheable: true - when: - - provided_tags | length > 0 - - (provided_tags | intersect(skip_credential_tags)) | length > 0 - -# --- Step 1: Upgrade guard --- -- name: Check upgrade lock file - ansible.builtin.stat: - path: "{{ upgrade_lock_path }}" - register: upgrade_lock - -- name: Block playbook while upgrade is in progress - ansible.builtin.fail: - msg: "{{ upgrade_in_progress_msg }}" - when: - - upgrade_lock.stat.exists - - not (upgrade_mode | default(false) | bool) - -# --- Step 2: Load omnia project config --- -- name: Load omnia project config file - block: - - name: Include omnia project config file - ansible.builtin.include_vars: "{{ omnia_input_config_file }}" - register: include_omnia_config - rescue: - - name: Failed to include omnia project config file - ansible.builtin.fail: - msg: "{{ omnia_input_config_syntax_fail_msg }} Error: {{ include_omnia_config.message }}" - -- name: Set input_project_dir and output_project_dir - ansible.builtin.set_fact: - input_project_dir: "{{ omnia_input_dir }}/{{ project_name }}" - output_project_dir: "{{ omnia_output_dir }}/{{ project_name }}" - cacheable: true - -- name: Verify the project directory exists - ansible.builtin.stat: - path: "{{ input_project_dir }}" - register: verify_project_dir - -- name: Fail if project directory does not exist - ansible.builtin.fail: - msg: "{{ project_dir_not_exist_fail_msg }}" - when: not verify_project_dir.stat - -# --- Step 3: Load OIM metadata --- -- name: Include oim metadata vars - ansible.builtin.include_vars: "{{ omnia_metadata_file_path }}" - -# --- Step 4: Create oim inventory group --- -- name: Create {{ oim_host_group }} group # noqa: name[template] - ansible.builtin.add_host: - hostname: "{{ oim_host_group }}" - ansible_host: "localhost" - ansible_port: "{{ oim_host_port }}" - groups: "{{ oim_host_group }}" - -# --- Step 5: Set image build guard facts --- - name: Set image_build_manager guard facts ansible.builtin.set_fact: image_build_main_flow: true skip_subscription_check: true - omnia_run_tags: "{{ omnia_run_tags | default(['image_build_manager']) }}" image_build_setup_done: true cacheable: true diff --git a/src/image_build_manager/roles/image_build_setup/tasks/validate_prereqs.yml b/src/image_build_manager/roles/image_build_setup/tasks/validate_prereqs.yml new file mode 100644 index 0000000000..e1b19d021e --- /dev/null +++ b/src/image_build_manager/roles/image_build_setup/tasks/validate_prereqs.yml @@ -0,0 +1,42 @@ +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +# Step 4: Validate all prerequisite files (fail-fast) +# +# Loads image_build_config.yml to discover repo_manager_output_path, +# then checks that the repo_status.yml file exists. +# functional_group_packages.yml path is resolved from repo_manager.package_list +# inside repo_status.yml (validated in load_repo_status.yml). + +- name: Load image_build_config.yml to get repo_manager_output_path + ansible.builtin.include_vars: + file: "{{ input_project_dir }}/image_build_config.yml" + +- name: Stat prerequisite files + ansible.builtin.stat: + path: "{{ item }}" + loop: + - "{{ input_project_dir }}/image_build_config.yml" + - "{{ repo_manager_output_path }}" + register: _prereq_checks + +- name: Fail if image_build_config.yml not found + ansible.builtin.fail: + msg: "{{ image_build_config_not_found_fail_msg }}" + when: not _prereq_checks.results[0].stat.exists + +- name: Fail if repo_status.yml not found + ansible.builtin.fail: + msg: "{{ repo_status_not_found_fail_msg }}" + when: not _prereq_checks.results[1].stat.exists diff --git a/src/image_build_manager/roles/image_build_setup/tasks/validate_tags.yml b/src/image_build_manager/roles/image_build_setup/tasks/validate_tags.yml new file mode 100644 index 0000000000..0709ee844a --- /dev/null +++ b/src/image_build_manager/roles/image_build_setup/tasks/validate_tags.yml @@ -0,0 +1,53 @@ +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +# Step 1: Tag validation +# Validates provided Ansible tags against supported tags and invalid combinations. + +- name: Get provided tags (excluding 'always' and 'all') + ansible.builtin.set_fact: + provided_tags: "{{ ansible_run_tags | difference(['always', 'all']) }}" + +- name: Identify invalid tags + ansible.builtin.set_fact: + invalid_tags: "{{ provided_tags | difference(supported_tags) }}" + when: provided_tags | length > 0 + +- name: Fail if invalid tags are provided + ansible.builtin.fail: + msg: "{{ tag_validation_fail_msg }}" + when: + - provided_tags | length > 0 + - invalid_tags | default([]) | length > 0 + +- name: Check for invalid tag combinations + ansible.builtin.set_fact: + has_invalid_combination: true + when: + - provided_tags | length > 1 + - item | difference(provided_tags) | length == 0 + loop: "{{ invalid_tag_combinations }}" + +- name: Fail if invalid tag combination is detected + ansible.builtin.fail: + msg: "{{ tag_combination_fail_msg }}" + when: has_invalid_combination | default(false) | bool + +- name: Set skip_credentials flag for cleanup/validate tags + ansible.builtin.set_fact: + skip_build_credentials: true + cacheable: true + when: + - provided_tags | length > 0 + - (provided_tags | intersect(skip_credential_tags)) | length > 0 diff --git a/src/image_build_manager/roles/image_build_setup/vars/main.yml b/src/image_build_manager/roles/image_build_setup/vars/main.yml index 33c90e2a80..a85f16469a 100644 --- a/src/image_build_manager/roles/image_build_setup/vars/main.yml +++ b/src/image_build_manager/roles/image_build_setup/vars/main.yml @@ -13,26 +13,11 @@ # limitations under the License. --- -# --- Upgrade guard --- -upgrade_lock_path: /opt/omnia/.data/upgrade_in_progress.lock -upgrade_in_progress_msg: >- - Upgrade is not completed fully. - Please run upgrade.yml to complete upgrade before running any other playbook using the below command: - "ansible-playbook /omnia/src/playbooks/upgrade/upgrade.yml" - If you don't require input files to be migrated, reconfigure the default input files, remove the lock file using the following command - "rm /opt/omnia/.data/upgrade_in_progress.lock" and then proceed. - -# --- Input project directory --- -omnia_input_dir: /opt/omnia/input -omnia_output_dir: /opt/omnia/output -omnia_input_config_file: "{{ omnia_input_dir }}/default.yml" -omnia_metadata_file_path: "/opt/omnia/.data/oim_metadata.yml" -omnia_input_config_syntax_fail_msg: >- - Unable to load the Omnia project configuration file at {{ omnia_input_config_file }}. - Please ensure the file exists and has valid YAML syntax before re-running the playbook. -project_dir_not_exist_fail_msg: >- - The project directory at {{ input_project_dir }} does not exist. - Please ensure the directory exists before re-running the playbook. +# ============================================================================ +# image_build_setup — Role Variables +# ============================================================================ +# All user-facing error messages are defined here (not inline in tasks). +# See docs/code-style/ for naming and message conventions. # --- Common vars (inlined from common/vars/common_vars.yml) --- clone_retry: "5" @@ -47,12 +32,7 @@ file_permissions_400: "0400" job_retry: "240" job_delay: "30" -# --- OIM group --- -oim_host_port: "22" -oim_host_group: "oim" - # --- Tag validation --- -# Supported tags for image_build_manager.yml supported_tags: - prepare - build @@ -61,12 +41,10 @@ supported_tags: - upgrade - rollback -# Tags that skip credential prompting skip_credential_tags: - cleanup - validate -# Invalid tag combinations (mutually exclusive) invalid_tag_combinations: - [prepare, cleanup] - [build, cleanup] @@ -78,22 +56,55 @@ invalid_tag_combinations: - [cleanup, rollback] - [upgrade, rollback] -# Tag validation messages +# --- Tag validation messages --- tag_validation_fail_msg: | Invalid tag usage detected. - Supported tags: {{ supported_tags | join(', ') }} - Invalid tags provided: {{ invalid_tags | join(', ') }} - Please use only supported tags. tag_combination_fail_msg: | Invalid tag combination detected: {{ ansible_run_tags | join(', ') }} - The following tag combinations are not allowed: {% for combo in invalid_tag_combinations %} - {{ combo | join(' + ') }} {% endfor %} - Please run with only ONE tag at a time (or no tags for default flow). + +# --- Config validation messages --- +env_admin_nic_ip_missing_fail_msg: >- + OMNIA_ADMIN_NIC_IP is not set. Set it in omnia.env or export it: + export OMNIA_ADMIN_NIC_IP= + + +# --- Prerequisite validation messages --- +project_dir_not_exist_fail_msg: >- + The project directory at {{ input_project_dir }} does not exist. + Please ensure the directory exists before re-running the playbook. + +repo_status_not_found_fail_msg: >- + repo_status.yml not found at {{ repo_manager_output_path }}. + Run repo_manager first, or copy the sample file: + mkdir -p {{ repo_manager_output_path | dirname }} + cp input/project_default/repo_manager_output/repo_status.yml {{ repo_manager_output_path }} + Then verify: ./src/main/omnia-cli repo-manager + +functional_group_packages_not_found_fail_msg: >- + functional_group_packages.yml not found at {{ package_list_path | default('unknown') }}. + This file defines the RPM packages for each functional group. + Check repo_manager.package_list in {{ repo_manager_output_path }} + or copy the sample file to {{ repo_manager_output_path | dirname }}/. + +pulp_cert_not_found_fail_msg: >- + Pulp certificate not found at {{ repo_manager.certificates.server_crt | default('unknown') }}. + Ensure repo_manager has been run and the certificate exists on this host. + +repo_status_missing_section_fail_msg: >- + repo_status.yml is missing required 'repo_manager' section. + Ensure repo_manager.certificates.server_crt, repo_manager.port, + and repo_manager.package_list are present in {{ repo_manager_output_path }}. + +image_build_config_not_found_fail_msg: >- + image_build_config.yml not found at {{ input_project_dir }}/image_build_config.yml. + This file is required. See docs/contracts/input-contract.md for the expected format. + diff --git a/src/image_build_manager/roles/prepare_aarch64_node/tasks/gather_oim_data.yml b/src/image_build_manager/roles/prepare_aarch64_node/tasks/gather_oim_data.yml index 164f4f0c26..b45c3f71de 100644 --- a/src/image_build_manager/roles/prepare_aarch64_node/tasks/gather_oim_data.yml +++ b/src/image_build_manager/roles/prepare_aarch64_node/tasks/gather_oim_data.yml @@ -29,20 +29,24 @@ msg: "{{ admin_aarch64_count_error_msg }}" when: groups['admin_aarch64'] | length != 1 -# Validate share option -- name: Set share option fact - ansible.builtin.set_fact: - omnia_share_option: "{{ hostvars['localhost']['omnia_share_option'] }}" +# # Validate share option +# - name: Set share option fact +# ansible.builtin.set_fact: +# omnia_share_option: "{{ hostvars['localhost']['omnia_share_option'] }}" -- name: Fail if share option is not NFS - ansible.builtin.fail: - msg: "{{ nfs_not_configured_msg }}" - when: omnia_share_option != "NFS" +# - name: Fail if share option is not NFS +# ansible.builtin.fail: +# msg: "{{ nfs_not_configured_msg }}" +# when: omnia_share_option != "NFS" -# Set OIM PXE IP from admin_nic_ip -- name: Set PXE IP fact +# Set OIM PXE IP and hostname from localhost facts +- name: Set OIM network facts ansible.builtin.set_fact: oim_pxe_ip: "{{ hostvars['localhost']['admin_nic_ip'] }}" + oim_hostname: >- + {{ hostvars['localhost']['host_name'] + if hostvars['localhost']['host_name'].endswith('.' + hostvars['localhost']['domain_name']) + else hostvars['localhost']['host_name'] + '.' + hostvars['localhost']['domain_name'] }} cacheable: true - name: Create aarch64 directory if not exists diff --git a/src/image_build_manager/roles/prepare_aarch64_node/tasks/main.yml b/src/image_build_manager/roles/prepare_aarch64_node/tasks/main.yml index 3f330c4c80..18e0f42a5f 100644 --- a/src/image_build_manager/roles/prepare_aarch64_node/tasks/main.yml +++ b/src/image_build_manager/roles/prepare_aarch64_node/tasks/main.yml @@ -32,7 +32,7 @@ ansible.builtin.expect: command: "ssh-copy-id -i /root/.ssh/id_rsa.pub root@{{ inventory_hostname }}" responses: - "password:": "{{ hostvars['localhost']['provision_password'] }}" + "password:": "{{ hostvars['localhost']['aarch64_ssh_password'] }}" when: ssh_check.failed delegate_to: localhost no_log: true @@ -100,7 +100,7 @@ nfs_info: server_ip: "{{ hostvars['localhost']['nfs_server_ip'] }}" server_share_path: "{{ hostvars['localhost']['nfs_server_share_path'] }}" - shared_path: "{{ hostvars['localhost']['oim_shared_path'] }}" + shared_path: "{{ hostvars['localhost']['shared_path'] }}" - name: Ensure NFS mount point directory exists ansible.builtin.file: @@ -219,8 +219,15 @@ mode: "{{ hostvars['localhost']['dir_permissions_755'] }}" become: true -- name: Set registry TLS option using regctl - ansible.builtin.command: "{{ regctl_bin_path }} registry set --tls disabled {{ hostvars['localhost']['oim_hostname'] }}:5000" +- name: Compute aarch64 registry host + ansible.builtin.set_fact: + _aarch64_registry_host: >- + {{ hostvars['localhost']['admin_nic_ip'] + if hostvars['localhost']['standalone_mode'] | default(false) | bool + else oim_hostname }} + +- name: Configure regctl to use HTTP for local registry + ansible.builtin.command: "{{ regctl_bin_path }} registry set --tls disabled {{ _aarch64_registry_host }}:5000" register: regctl_result changed_when: regctl_result.rc == 0 become: true diff --git a/src/image_build_manager/roles/prepare_aarch64_node/vars/main.yml b/src/image_build_manager/roles/prepare_aarch64_node/vars/main.yml index 2d824cc2a4..90aea43a77 100644 --- a/src/image_build_manager/roles/prepare_aarch64_node/vars/main.yml +++ b/src/image_build_manager/roles/prepare_aarch64_node/vars/main.yml @@ -19,9 +19,9 @@ pulp_image_name: "dellhpcomniaaisolution/image-build-aarch64:1.1" local_tag: "aarch64-image-builder/ochami" pull_image_retries: "5" pull_image_delay: "10" -ochami_aarch_64_dir: "/opt/omnia/openchami/aarch64" +ochami_aarch_64_dir: "{{ hostvars['localhost']['shared_path'] }}/omnia/openchami/aarch64" pulp_repo_file_path: "/etc/yum.repos.d/pulp.repo" -pulp_cert_host_path: "{{ hostvars['localhost']['pulp_cert_oim_path'] | default('/opt/omnia/pulp/settings/certs/pulp_webserver.crt') }}" +pulp_cert_host_path: "{{ hostvars['localhost']['pulp_cert_path'] | default('/opt/omnia/pulp/settings/certs/pulp_webserver.crt') }}" anchors_path: "/etc/pki/ca-trust/source/anchors/pulp_webserver.crt" regctl_bin_path: "/usr/local/bin/regctl" @@ -37,8 +37,8 @@ nfs_not_configured_msg: > Please run oim_cleanup.yml and reinstall the omnia_core container with the NFS option. aarch64_image_fail_msg: > Unable to pull the Ochami aarch64 image builder image. - Make sure you have added the default package for aarch64 in the software_config.json file and ran local_repo.yml. - If not, add that package and rerun local_repo.yml. + Make sure the aarch64 image builder image is available in your container registry. + Verify image_build_config.yml and functional_group_packages.yml are correctly configured. regctl_not_found_msg: > regctl binary not found at {{ ochami_aarch_64_dir }}/regctl. Please run prepare_oim.yml playbook to download the regctl binary. diff --git a/src/image_build_manager/roles/validate_build_runtime/tasks/main.yml b/src/image_build_manager/roles/validate_build_runtime/tasks/main.yml index 413b3c9877..73cbe5741b 100644 --- a/src/image_build_manager/roles/validate_build_runtime/tasks/main.yml +++ b/src/image_build_manager/roles/validate_build_runtime/tasks/main.yml @@ -33,31 +33,6 @@ - name: Run PowerScale S3 endpoint validation ansible.builtin.include_tasks: powerscale_check.yml -- name: Validate software_config.json exists - ansible.builtin.stat: - path: "{{ software_config_file_path }}" - register: sw_config_file - -- name: Fail if software_config.json not found - ansible.builtin.fail: - msg: "{{ validate_software_config_fail_msg }}" - when: not sw_config_file.stat.exists - -- name: Validate JSON syntax of software_config.json - ansible.builtin.command: "jq . {{ software_config_file_path }}" - register: json_check - changed_when: false - failed_when: false - -- name: Fail if software_config.json has invalid JSON - ansible.builtin.fail: - msg: | - JSON validation failed for software_config.json. - File: {{ software_config_file_path }} - {{ json_check.stderr | default('Unknown jq error') }} - Please fix the JSON syntax. - when: json_check.rc | default(1) != 0 - - name: Validate functional groups config exists ansible.builtin.stat: path: "{{ functional_groups_file_path }}" @@ -81,7 +56,7 @@ - name: Validate repo_status.yml was loaded (pre-check passed) ansible.builtin.assert: that: - - overall_status | default('') == 'synced' + - overall_status | default('') == 'success' fail_msg: "{{ validate_repo_status_fail_msg }}" quiet: true when: validate_repo_precheck | default(false) | bool diff --git a/src/image_build_manager/roles/validate_build_runtime/tasks/powerscale_check.yml b/src/image_build_manager/roles/validate_build_runtime/tasks/powerscale_check.yml index dfd367949f..8f4134ab47 100644 --- a/src/image_build_manager/roles/validate_build_runtime/tasks/powerscale_check.yml +++ b/src/image_build_manager/roles/validate_build_runtime/tasks/powerscale_check.yml @@ -15,11 +15,6 @@ # === PowerScale endpoint validation (S3 backend only) === -- name: Set storage backend fact - ansible.builtin.set_fact: - storage_backend: "{{ s3_configurations.provider | lower }}" - cacheable: true - - name: Validate PowerScale S3 endpoint configuration ansible.builtin.assert: that: @@ -31,7 +26,7 @@ - s3_secret_key is defined - s3_secret_key | length > 0 fail_msg: "{{ powerscale_validation_fail_msg }}" - when: storage_backend == 'powerscale' + when: s3_configurations.provider | lower == 'powerscale' - name: Test PowerScale endpoint reachability ansible.builtin.uri: @@ -43,13 +38,13 @@ register: ps_endpoint_check failed_when: false ignore_errors: true - when: storage_backend == 'powerscale' + when: s3_configurations.provider | lower == 'powerscale' - name: Fail if PowerScale unreachable ansible.builtin.fail: msg: "{{ powerscale_unreachable_msg }}" when: - - storage_backend == 'powerscale' + - s3_configurations.provider | lower == 'powerscale' - ps_endpoint_check is defined - ps_endpoint_check.status is not defined or ps_endpoint_check.status | int not in [200, 403, 404, 405] diff --git a/src/image_build_manager/roles/validate_build_runtime/tasks/validate_aarch64_host.yml b/src/image_build_manager/roles/validate_build_runtime/tasks/validate_aarch64_host.yml index 6ab561938d..349b6f5b9a 100644 --- a/src/image_build_manager/roles/validate_build_runtime/tasks/validate_aarch64_host.yml +++ b/src/image_build_manager/roles/validate_build_runtime/tasks/validate_aarch64_host.yml @@ -86,7 +86,7 @@ ansible.builtin.debug: msg: > SSH passwordless access to {{ aarch64_inventory_host_ip }} is not yet configured. - The prepare_arm_node role will attempt ssh-copy-id using provision_password. + The prepare_aarch64_node role will attempt ssh-copy-id using aarch64_ssh_password. # --- 6. Dynamically create admin_aarch64 inventory group --- - name: Add aarch64 host to admin_aarch64 group dynamically diff --git a/src/image_build_manager/roles/validate_build_runtime/vars/main.yml b/src/image_build_manager/roles/validate_build_runtime/vars/main.yml index d5814abeb7..335fb702cd 100644 --- a/src/image_build_manager/roles/validate_build_runtime/vars/main.yml +++ b/src/image_build_manager/roles/validate_build_runtime/vars/main.yml @@ -14,10 +14,8 @@ --- # Variables and error messages for image_build_manager input validation -# File paths used by validation tasks -input_project_dir: "{{ hostvars['localhost']['input_project_dir'] | default('/opt/omnia/input/project_default') }}" -software_config_file_path: "{{ input_project_dir }}/software_config.json" -functional_groups_file_path: "{{ hostvars['localhost']['functional_groups_config_path'] | default('/opt/omnia/.data/functional_groups_config.yml') }}" +# File paths used by validation tasks (already set by image_build_setup) +functional_groups_file_path: "{{ functional_groups_config_path }}" # Fixed S3 bucket names — NOT user-modifiable s3_boot_images_bucket: "boot-images" @@ -29,20 +27,16 @@ validate_s3_provider_fail_msg: > Invalid or missing s3_configurations.provider in image_build_config.yml. Allowed values: 'minio' or 'powerscale'. Please set s3_configurations.provider in - {{ input_project_dir }}/image_build_manager/image_build_config.yml + {{ input_project_dir }}/image_build_config.yml validate_s3_bucket_fail_msg: > S3 bucket name is invalid. NOTE: bucket names are FIXED constants (boot-images and efi-images) and cannot be changed by the user. -validate_software_config_fail_msg: > - software_config.json not found at {{ software_config_file_path }}. - This file is required for build_image. Ensure it exists in the input directory. - validate_fg_config_fail_msg: > functional_groups_config.yml not found at {{ functional_groups_file_path }}. - This file is required for image building. Ensure it is generated by - running the generate_functional_groups utility or providing it in input/. + This file is auto-generated from functional_groups in image_build_config.yml. + Ensure image_build_config.yml has at least one functional group enabled. validate_aarch64_host_fail_msg: > aarch64_inventory_host_ip is empty but aarch64 build was requested. diff --git a/src/image_build_manager/roles/validate_image_build_input/tasks/main.yml b/src/image_build_manager/roles/validate_image_build_input/tasks/main.yml index c67829e45d..befc9a5413 100644 --- a/src/image_build_manager/roles/validate_image_build_input/tasks/main.yml +++ b/src/image_build_manager/roles/validate_image_build_input/tasks/main.yml @@ -27,7 +27,7 @@ # Pre-check: ensure image_build_config.yml exists - name: Check image_build_config.yml exists ansible.builtin.stat: - path: "{{ input_project_dir }}/image_build_manager/image_build_config.yml" + path: "{{ input_project_dir }}/image_build_config.yml" register: image_build_config_stat - name: Fail if image_build_config.yml is missing @@ -40,6 +40,7 @@ validate_image_build_config: input_project_dir: "{{ input_project_dir }}" schema_dir: "{{ image_build_schema_dir }}" + log_dir: "{{ log_dir }}" register: image_build_validation_result # Report results diff --git a/src/image_build_manager/roles/validate_image_build_input/vars/main.yml b/src/image_build_manager/roles/validate_image_build_input/vars/main.yml index a3572b269d..2f91b482a3 100644 --- a/src/image_build_manager/roles/validate_image_build_input/vars/main.yml +++ b/src/image_build_manager/roles/validate_image_build_input/vars/main.yml @@ -14,7 +14,7 @@ --- # Path to the image_build-specific JSON schemas (domain-level library) -image_build_schema_dir: "{{ playbook_dir }}/../library/module_utils/image_build_validation/schema" +image_build_schema_dir: "{{ _domain_root_dir }}/plugins/module_utils/image_build_validation/schema" # Validation messages image_build_validation_start_msg: "Starting image_build_manager input validation (L1 schema + L2 logic)." @@ -23,5 +23,5 @@ image_build_validation_fail_msg: >- Image build configuration validation failed. Review the errors below and fix the configuration files before re-running. image_build_config_missing_msg: >- - image_build_config.yml not found at {{ input_project_dir }}/image_build_manager/image_build_config.yml. + image_build_config.yml not found at {{ input_project_dir }}/image_build_config.yml. Please create this file before running image_build_manager. diff --git a/src/image_build_manager/samples/README.md b/src/image_build_manager/samples/README.md deleted file mode 100644 index 2b64e1a40f..0000000000 --- a/src/image_build_manager/samples/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# image_build_manager Sample Files - -This directory contains sample configuration files that document the input and output contracts for the image_build_manager domain. - -## Files - -### repo_status.yml - -**Purpose**: Input contract produced by the `repo_manager` domain and consumed by `image_build_manager`. - -**Location**: `/opt/omnia/output/repo_manager/repo_status.yml` - -**Producer**: `repo_manager.yml` (repo_manager domain) - -**Consumer**: `image_build_manager.yml` (Step 4 - Pre-check) - -**Structure**: -```yaml -overall_status: "success" # or "failed" - -# Base URLs for other content types -tarball_base_url: "http://{{ admin_nic_ip }}:2225/pulp/content/tarballs/" -manifest_base_url: "http://{{ admin_nic_ip }}:2225/pulp/content/manifests/" -pip_base_url: "http://{{ admin_nic_ip }}:2225/pulp/content/pip/" -git_base_url: "http://{{ admin_nic_ip }}:2225/pulp/content/git/" - -repo_manager: - port: 2225 - certificates: - server_crt: "/opt/omnia/pulp/settings/certs/pulp_webserver.crt" - server_key: "/opt/omnia/pulp/settings/certs/pulp_webserver.key" - certs_dir: "/opt/omnia/pulp/settings/certs" - -# RPM Repository Base URLs -rpm_repos: - x86_64: - baseos: "http://{{ admin_nic_ip }}:2225/pulp/content/rhel/baseos/x86_64/" - appstream: "http://{{ admin_nic_ip }}:2225/pulp/content/rhel/appstream/x86_64/" - codeready_builder: "http://{{ admin_nic_ip }}:2225/pulp/content/rhel/codeready-builder/x86_64/" - epel: "http://{{ admin_nic_ip }}:2225/pulp/content/epel/x86_64/" - slurm_custom: "http://{{ admin_nic_ip }}:2225/pulp/content/slurm_custom/x86_64/" - doca: "http://{{ admin_nic_ip }}:2225/pulp/content/doca/x86_64/" - aarch64: - baseos: "http://{{ admin_nic_ip }}:2225/pulp/content/rhel/baseos/aarch64/" - appstream: "http://{{ admin_nic_ip }}:2225/pulp/content/rhel/appstream/aarch64/" - codeready_builder: "http://{{ admin_nic_ip }}:2225/pulp/content/rhel/codeready-builder/aarch64/" - epel: "http://{{ admin_nic_ip }}:2225/pulp/content/epel/aarch64/" - slurm_custom: "http://{{ admin_nic_ip }}:2225/pulp/content/slurm_custom/aarch64/" - doca: "http://{{ admin_nic_ip }}:2225/pulp/content/doca/aarch64/" - -# User-Defined Repositories (from local_repo_config.yml) -user_repos: - x86_64: - custom_repo_1: "http://{{ admin_nic_ip }}:2225/pulp/content/custom_repo_1/x86_64/" - custom_repo_2: "http://{{ admin_nic_ip }}:2225/pulp/content/custom_repo_2/x86_64/" - aarch64: - custom_repo_1: "http://{{ admin_nic_ip }}:2225/pulp/content/custom_repo_1/aarch64/" - custom_repo_2: "http://{{ admin_nic_ip }}:2225/pulp/content/custom_repo_2/aarch64/" -``` - -**Validation**: -- `overall_status` must be `"success"` -- `repo_manager` section must exist with `certificates.server_crt` and `port` -- `rpm_repos` must contain valid URLs for target architectures - -**Notes**: -- The `{{ admin_nic_ip }}` template variable is resolved by repo_manager at runtime -- `rpm_repos.aarch64` is used by `prepare_aarch64_node` to dynamically generate `pulp.repo` on ARM build hosts -- `aarch64_repo_store_path` was removed in Omnia 2.2 — AArch64 repo is now generated dynamically - ---- - -### build_status.yml - -**Purpose**: Output contract produced by `image_build_manager` and consumed by the `provision` domain for BSS template rendering. - -**Location**: -- Latest: `/opt/omnia/output/project_default/build_status.yml` -- Versioned: `/opt/omnia/output/project_default/image_build_manager/build_status__.yml` - -**Producer**: `image_build_manager.yml` (Step 8 - Write build_status) - -**Consumer**: -- `provision/roles/provision_validations/tasks/validate_image.yml` -- `provision/roles/configure_ochami/tasks/configure_bss_group.yml` - -**Structure**: -```yaml -overall_status: "success" # or "failed" - -s3_configurations: - endpoint_url: "http://10.20.0.1:9000" # or http://:9000 for MinIO - bucket: "boot-images" - -functional_group_images: - - x86_64: - - functional_group: "slurm_control_node_x86_64" - kernel: "boot-images/efi-images/slurm_control_node_x86_64/rhel-slurm_control_node_x86_64_omnia_2.2.0.0/vmlinuz" - initrd: "boot-images/efi-images/slurm_control_node_x86_64/rhel-slurm_control_node_x86_64_omnia_2.2.0.0/initramfs.img" - image: "boot-images/slurm_control_node_x86_64/rhel-slurm_control_node_x86_64_omnia_2.2.0.0" - - aarch64: - - functional_group: "slurm_node_aarch64" - kernel: "boot-images/efi-images/slurm_node_aarch64/rhel-slurm_node_aarch64_omnia_2.2.0.0/vmlinuz" - initrd: "boot-images/efi-images/slurm_node_aarch64/rhel-slurm_node_aarch64_omnia_2.2.0.0/initramfs.img" - image: "boot-images/slurm_node_aarch64/rhel-slurm_node_aarch64_omnia_2.2.0.0" -``` - -**Key Changes in Omnia 2.2**: -- `overall_status` changed from `"completed"` to `"success"` -- `s3_endpoint_url` replaced with `s3_configurations` block containing `endpoint_url` and `bucket` -- `functional_group_images` is now grouped by architecture (x86_64, aarch64) instead of a flat list -- Image entries now include `kernel`, `initrd`, and `image` paths instead of `s3_prefix` -- `build_arch_list` removed — architecture grouping is implicit in `functional_group_images` - -**S3 Endpoint Behavior**: -- **MinIO**: Auto-detected as `http://:9000` when `endpoint_url` is empty -- **PowerScale**: Uses configured `endpoint_url` as-is - -**Notes**: -- Functional group names (e.g., `slurm_control_node_x86_64`) already include architecture suffix -- Kernel/initrd paths use `efi-images/` subdirectory for UEFI boot -- Image path is the full S3 key for the rootfs image diff --git a/src/image_build_manager/samples/build_status.yml b/src/image_build_manager/samples/build_status.yml deleted file mode 100644 index 0e7dd1718f..0000000000 --- a/src/image_build_manager/samples/build_status.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. ---- -# ============================================================================ -# SAMPLE: build_status.yml — image_build_manager output contract -# ============================================================================ -# Location (NFS): -# /opt/omnia/output/project_default/build_status.yml (latest) -# /opt/omnia/output/project_default/image_build_manager/ -# build_status_2.2.0.0_20260720_1015.yml (versioned) -# -# Consumed by: -# - provision domain (BSS template rendering) -# - provision/roles/provision_validations/tasks/validate_image.yml -# - provision/roles/configure_ochami/tasks/configure_bss_group.yml -# ============================================================================ - -overall_status: "success" - -s3_configurations: - endpoint_url: "http://10.20.0.1:9000" - bucket: "boot-images" - -functional_group_images: - - x86_64: - - functional_group: "slurm_control_node_x86_64" - kernel: "boot-images/efi-images/slurm_control_node_x86_64/rhel-slurm_control_node_x86_64_omnia_2.2.0.0/vmlinuz" - initrd: "boot-images/efi-images/slurm_control_node_x86_64/rhel-slurm_control_node_x86_64_omnia_2.2.0.0/initramfs.img" - image: "boot-images/slurm_control_node_x86_64/rhel-slurm_control_node_x86_64_omnia_2.2.0.0" - - aarch64: - - functional_group: "slurm_node_aarch64" - kernel: "boot-images/efi-images/slurm_node_aarch64/rhel-slurm_node_aarch64_omnia_2.2.0.0/vmlinuz" - initrd: "boot-images/efi-images/slurm_node_aarch64/rhel-slurm_node_aarch64_omnia_2.2.0.0/initramfs.img" - image: "boot-images/slurm_node_aarch64/rhel-slurm_node_aarch64_omnia_2.2.0.0" - diff --git a/src/image_build_manager/samples/repo_manager_output/functional_group_packages.yml b/src/image_build_manager/samples/repo_manager_output/functional_group_packages.yml new file mode 100644 index 0000000000..2541c81167 --- /dev/null +++ b/src/image_build_manager/samples/repo_manager_output/functional_group_packages.yml @@ -0,0 +1,258 @@ +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +# ============================================================================ +# functional_group_packages.yml — Functional Group → RPM Package Mapping +# ============================================================================ +# This file is the single source of truth for which RPM packages belong to +# each functional group. +# +# Producer: repo_manager domain (local_repo.yml) or manual creation +# Consumer: image_build_manager (fetch_build_packages role) +# +# This is a REFERENCE TEMPLATE. In production, repo_manager generates this file. +# +# Runtime location (created by repo_manager or copied manually): +# /opt/omnia/repo_manager/output//functional_group_packages.yml +# +# Manual setup: see repo_status.yml header for instructions. +# +# Structure: +# base_packages — RPMs installed in ALL images (the base OS layer) +# functional_groups — dict keyed by functional group name +# Each group has: +# packages: list of RPM package names for that group's image layer +# +# To customize: add/remove RPM names under the appropriate group. +# Package names must match what's available in the Pulp RPM repos. +# ============================================================================ + +# --- Base OS packages (installed in every image) --- +base_packages: + - systemd + - systemd-udev + - kernel + - dracut + - dracut-live + - dracut-network + - squashfs-tools + - nfs-utils + - nfs4-acl-tools + - NetworkManager + - nm-connection-editor + - iproute + - iputils + - curl + - bash + - coreutils + - grep + - sed + - gawk + - findutils + - util-linux + - kbd + - lsof + - cryptsetup + - lvm2 + - device-mapper + - rsyslog + - chrony + - sudo + - gzip + - wget + - cloud-init + - glibc-langpack-en + - gedit + +# --- Functional group → RPM package mapping --- +# Each key is a functional group name (must match image_build_config.yml entries). +# Packages listed here are IN ADDITION to base_packages above. +functional_groups: + + # ---- x86_64 groups ---- + + os_x86_64: + packages: [] + + slurm_node_x86_64: + packages: + # slurm_custom common + - munge + - firewalld + - python3-firewall + - pmix + - apptainer + - iscsi-initiator-utils + - device-mapper-multipath + - sg3_utils + - lsscsi + # slurm_node role-specific + - slurm-slurmd + - slurm-pam_slurm + - kernel-devel + - kernel-headers + # openldap + - openldap + - openldap-clients + - sssd + - sssd-ldap + - oddjob + - oddjob-mkhomedir + + slurm_control_node_x86_64: + packages: + # slurm_custom common + - munge + - firewalld + - python3-firewall + - pmix + - apptainer + - iscsi-initiator-utils + - device-mapper-multipath + - sg3_utils + - lsscsi + # slurm_control_node role-specific + - slurm-slurmctld + - slurm-slurmdbd + - python3-PyMySQL + - mariadb-server + # openldap + - openldap + - openldap-clients + - sssd + - sssd-ldap + - oddjob + - oddjob-mkhomedir + + login_node_x86_64: + packages: + # slurm_custom common + - munge + - firewalld + - python3-firewall + - pmix + - apptainer + - iscsi-initiator-utils + - device-mapper-multipath + - sg3_utils + - lsscsi + # login_node role-specific + - slurm-slurmd + - slurm + # openldap + - openldap + - openldap-clients + - sssd + - sssd-ldap + - oddjob + - oddjob-mkhomedir + + login_compiler_node_x86_64: + packages: + # slurm_custom common + - munge + - firewalld + - python3-firewall + - pmix + - apptainer + - iscsi-initiator-utils + - device-mapper-multipath + - sg3_utils + - lsscsi + # login_compiler_node role-specific + - slurm + - slurm-slurmd + # openldap + - openldap + - openldap-clients + - sssd + - sssd-ldap + - oddjob + - oddjob-mkhomedir + + service_kube_control_plane_first_x86_64: + packages: [] + + service_kube_control_plane_x86_64: + packages: [] + + service_kube_node_x86_64: + packages: [] + + # ---- aarch64 groups ---- + + os_aarch64: + packages: [] + + slurm_node_aarch64: + packages: + - munge + - firewalld + - python3-firewall + - pmix + - apptainer + - iscsi-initiator-utils + - device-mapper-multipath + - sg3_utils + - lsscsi + - slurm-slurmd + - slurm-pam_slurm + - kernel-devel + - kernel-headers + - openldap + - openldap-clients + - sssd + - sssd-ldap + - oddjob + - oddjob-mkhomedir + + login_node_aarch64: + packages: + - munge + - firewalld + - python3-firewall + - pmix + - apptainer + - iscsi-initiator-utils + - device-mapper-multipath + - sg3_utils + - lsscsi + - slurm-slurmd + - slurm + - openldap + - openldap-clients + - sssd + - sssd-ldap + - oddjob + - oddjob-mkhomedir + + login_compiler_node_aarch64: + packages: + - munge + - firewalld + - python3-firewall + - pmix + - apptainer + - iscsi-initiator-utils + - device-mapper-multipath + - sg3_utils + - lsscsi + - slurm + - slurm-slurmd + - openldap + - openldap-clients + - sssd + - sssd-ldap + - oddjob + - oddjob-mkhomedir diff --git a/src/image_build_manager/samples/repo_status.yml b/src/image_build_manager/samples/repo_manager_output/repo_status.yml similarity index 80% rename from src/image_build_manager/samples/repo_status.yml rename to src/image_build_manager/samples/repo_manager_output/repo_status.yml index 714e92e346..3bcaf4b241 100644 --- a/src/image_build_manager/samples/repo_status.yml +++ b/src/image_build_manager/samples/repo_manager_output/repo_status.yml @@ -15,20 +15,39 @@ # ============================================================================ # SAMPLE: repo_status.yml — repo_manager output contract # ============================================================================ -# Location (NFS): -# /opt/omnia/output/repo_manager/repo_status.yml +# +# This is a REFERENCE TEMPLATE. In production, repo_manager generates this file. +# +# Runtime location (created by repo_manager or copied manually): +# /opt/omnia/repo_manager/output//repo_status.yml # # Producer: repo_manager domain (repo_manager.yml) # Consumer: image_build_manager domain (image_build_manager.yml Step 4) # +# Manual setup (if repo_manager was NOT run): +# 1. Copy this file to the runtime location: +# mkdir -p /opt/omnia/repo_manager/output/project_default +# cp repo_status.yml /opt/omnia/repo_manager/output/project_default/ +# 2. Edit the copied file with your actual Pulp URLs and cert paths +# 3. Verify: ./src/main/omnia-cli repo-manager +# # image_build_manager pre-check validates: -# 1. File exists at repo_manager_output path +# 1. File exists at repo_manager_output_path # 2. overall_status == "success" # 3. rpm_repos contains valid URLs for the target architectures # 4. repo_manager section contains certificate and port # ============================================================================ + overall_status: "success" +# OS metadata — tells consumers what OS the repos were synced for +# IMPORTANT: Keys must NOT have quotes in the key name. +# Correct: cluster_os_type: "rhel" +# Wrong: cluster_os_type": "rhel" ← extra quote breaks YAML +cluster_os_type: "rhel" +cluster_os_version: "10.0" +repo_config: "partial" + # Base URLs for other content types tarball_base_url: "https://{{ admin_nic_ip }}:2225/pulp/content/opt/omnia/offline_repo/cluster/tarballs/" manifest_base_url: "https://{{ admin_nic_ip }}:2225/pulp/content/opt/omnia/offline_repo/cluster/manifests/" @@ -38,9 +57,10 @@ git_base_url: "https://{{ admin_nic_ip }}:2225/pulp/content/opt/omnia/offline_re repo_manager: port: 2225 certificates: - server_crt: "/opt/omnia/pulp/settings/certs/pulp_webserver.crt" - server_key: "/opt/omnia/pulp/settings/certs/pulp_webserver.key" - certs_dir: "/opt/omnia/pulp/settings/certs" + server_crt: /opt/omnia/pulp/settings/certs/pulp_webserver.crt + server_key: /opt/omnia/pulp/settings/certs/pulp_webserver.key + certs_dir: /opt/omnia/pulp/settings/certs + package_list: /opt/omnia/repo_manager/output/project_default/functional_group_packages.yml # RPM Repository Base URLs rpm_repos: @@ -76,3 +96,7 @@ user_repos: slurm_custom: "https://{{ admin_nic_ip }}:2225/pulp/content/opt/omnia/offline_repo/cluster/x86_64/rhel/10.0/rpms/x86_64_rhel_10.0_slurm_custom/" aarch64: slurm_custom: "https://{{ admin_nic_ip }}:2225/pulp/content/opt/omnia/offline_repo/cluster/aarch64/rhel/10.0/rpms/aarch64_rhel_10.0_slurm_custom/" + +file_repos: + x86_64: {} + aarch64: {} \ No newline at end of file diff --git a/src/image_build_manager/test/.gitkeep b/src/image_build_manager/test/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/image_build_manager/vars/image_vars.yml b/src/image_build_manager/vars/image_vars.yml index a1a3df4cf5..e3fcba721e 100644 --- a/src/image_build_manager/vars/image_vars.yml +++ b/src/image_build_manager/vars/image_vars.yml @@ -20,3 +20,7 @@ # image_build_manager-specific S3 constants (fixed bucket names — NOT user-modifiable) s3_boot_images_bucket: "boot-images" s3_efi_images_bucket: "efi-images" + +# Container registry TLS verification (internal constant — NOT user-modifiable) +# The local OCI registry deployed by image_build_manager runs plain HTTP. +registry_tls_verify: false diff --git a/src/main/containers/README.md b/src/main/containers/README.md deleted file mode 100644 index eadd5c1cfc..0000000000 --- a/src/main/containers/README.md +++ /dev/null @@ -1,208 +0,0 @@ -# Omnia Container Images - -Container build infrastructure for all Omnia container images. -Each container has its own subdirectory with a `build.sh` and `Containerfile`. - -## Quick Start - -```bash -# Build OIM group (core + auth + image-builder) — default -./build_images.sh oim - -# Build a single container with custom tag -./build_images.sh core core_tag=2.2 - -# Build all containers -./build_images.sh all -``` - -**Prerequisite:** Podman (default) or Docker must be installed. - ---- - -## Directory Layout - -``` -src/main/containers/ -├── build_images.sh # Entry point — CLI dispatch + build summary -├── _common.sh # Shared helpers: colors, container_build(), print_build_summary() -├── README.md -│ -├── omnia_core/ # Core Omnia container -│ ├── build.sh # build_omnia_core() -│ ├── Containerfile # Fedora 42 · Python 3.13 · Ansible · Go · Git LFS -│ ├── cert-copy.sh -│ ├── entrypoint.sh -│ ├── pyproject.toml -│ └── uv.lock -│ -├── omnia_auth/ # OpenLDAP authentication -│ ├── build.sh # build_omnia_auth() -│ └── Containerfile # Fedora 42 · OpenLDAP -│ -├── omnia_build_stream/ # BuildStream API service -│ ├── build.sh # build_omnia_build_stream() -│ ├── Containerfile # Fedora 42 · FastAPI · uv · s3cmd -│ ├── init_s3cfg.sh -│ ├── pyproject.toml -│ └── uv.lock -│ -├── image_builder/ # OS image builder (OpenCHAMI) — x86_64 + aarch64 -│ ├── build.sh # build_image_builder() → auto-detects arch -│ ├── Containerfile.el10 # AlmaLinux 10 · Buildah · Go · Python -│ └── requirements.txt -│ -├── ldms/ # OVIS LDMS telemetry sampler -│ ├── build.sh # build_ldms() -│ ├── Containerfile.bld_n_run.ubuntu26.04 # Multi-stage build -│ └── configure.aggregator.sh -│ -├── kafkapump/ # iDRAC telemetry → Kafka -│ └── build.sh # build_kafkapump() -│ -├── victoriapump/ # iDRAC telemetry → VictoriaMetrics -│ └── build.sh # build_victoriapump() -│ -└── telemetry_receiver/ # iDRAC telemetry collector - └── build.sh # build_telemetry_receiver() -``` - ---- - -## Containers - -| Container | CLI Name | Default Tag | Base | Description | -|-----------|----------|-------------|------|-------------| -| omnia_core | `core` | 2.2 | Fedora 42 | Core container — Ansible, Python 3.13, SSH, Go, Git LFS | -| omnia_auth | `auth` | 1.1 | Fedora 42 | OpenLDAP authentication service | -| omnia_build_stream | `build-stream` | 1.1 | Fedora 42 | FastAPI build automation + S3 integration | -| image_builder | `image-builder` | 1.1 | AlmaLinux 10 | OpenCHAMI image builder — Buildah, Go, Python | -| ldms | `ldms` | 1.1 | Ubuntu 26.04 | OVIS LDMS monitoring (multi-stage build) | -| kafkapump | `kafkapump` | 1.3 | — | iDRAC telemetry → Kafka bridge | -| victoriapump | `victoriapump` | 1.3 | — | iDRAC telemetry → VictoriaMetrics bridge | -| telemetry_receiver | `telemetry-receiver` | 1.3 | — | iDRAC telemetry collector | - -### Build Groups - -| Group | Containers | Use Case | -|-------|-----------|----------| -| `oim` | core, auth, image-builder | OIM deployment (default) | -| `all` | All 8 containers | Full rebuild | -| `pipeline` | core, auth, ldms, kafkapump, victoriapump, telemetry-receiver, image-builder | CI/CD pipeline | -| `telemetry` | kafkapump, victoriapump, telemetry-receiver | Telemetry stack only | - ---- - -## Architecture Support (x86_64 / aarch64) - -### Image Builder — Dual Architecture - -The `image_builder` container automatically detects the host architecture and produces -the correct image name: - -| Host Arch | Image Name | Platform | -|-----------|-----------|----------| -| x86_64 | `image-build-el10` | `linux/amd64` | -| aarch64 | `image-build-aarch64` | `linux/arm64` | - -The `Containerfile.el10` is multi-arch — it downloads the correct Go toolchain -for the detected architecture. No separate Containerfile is needed. - -```bash -# On x86_64 host → produces image-build-el10:1.1 -./build_images.sh image-builder - -# On aarch64 host → produces image-build-aarch64:1.1 -./build_images.sh image-builder - -# With Docker — uses docker info to detect platform -./build_images.sh image-builder build_tool=docker -``` - -### Other Containers - -- **omnia_core** — x86_64 only (Fedora 42 base) -- **ldms** — architecture set by `--arch` in build script -- **RPM build** — see `src/rpm_build/README.md` for LDMS RPM builds on both architectures - ---- - -## Build Commands - -```bash -# Single container -./build_images.sh core core_tag=2.2 - -# Comma-separated list -./build_images.sh core,auth core_tag=2.2 auth_tag=1.1 - -# Telemetry group -./build_images.sh telemetry - -# Push to registry (requires Docker) -./build_images.sh core core_tag=2.2 build_tool=docker build_action=push -``` - ---- - -## Parameters - -### Global - -| Parameter | Values | Default | Description | -|-----------|--------|---------|-------------| -| `build_tool` | `podman`, `docker` | `podman` | Container build tool | -| `build_action` | `load`, `push` | `load` | Load locally or push to registry | - -### Per-Container Tags - -| Parameter | Default | Container | -|-----------|---------|-----------| -| `core_tag` | `2.2` | omnia_core | -| `auth_tag` | `1.1` | omnia_auth | -| `build_stream_tag` | `1.1` | omnia_build_stream | -| `ldms_tag` | `1.1` | ldms | -| `image_builder_tag` | `1.1` | image_builder | -| `kafkapump_tag` | `1.3` | kafkapump | -| `victoriapump_tag` | `1.3` | victoriapump | -| `telemetry_receiver_tag` | `1.3` | telemetry_receiver | - -Parameter validation is built in — the script rejects unknown or mismatched parameters. - ---- - -## Docker vs Podman - -| Feature | Podman (default) | Docker | -|---------|-----------------|--------| -| Daemon | Not required | Required | -| Rootless | Default | Requires config | -| Push to registry | Not supported via script | Supported (`build_action=push`) | -| Multi-platform | Via `--arch` flag | Via `buildx` | - -```bash -# Docker setup (if needed) -sudo systemctl start docker -docker buildx create --name mybuilder --driver docker-container --use -docker buildx inspect --bootstrap -``` - ---- - -## Updating Python Dependencies - -For containers using **uv** (omnia_core, omnia_build_stream): - -1. Install uv: `pip install uv` -2. Edit `pyproject.toml` in the container directory -3. Run `uv lock` to regenerate the lock file - ---- - -## Troubleshooting - -| Issue | Solution | -|-------|---------| -| Build fails | Verify Podman/Docker is running and internet is accessible | -| Permission errors (Podman) | Run as non-root; configure subuid/subgid if needed | -| Image-builder wrong arch | Check `uname -m`; use `build_tool=docker` for cross-platform | \ No newline at end of file diff --git a/src/main/containers/build_images.sh b/src/main/containers/build_images.sh deleted file mode 100755 index 8485947a42..0000000000 --- a/src/main/containers/build_images.sh +++ /dev/null @@ -1,264 +0,0 @@ -#!/bin/bash - -# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# ============================================================================= -# build_images.sh — OIM Core Container Build (Self-Contained) -# ============================================================================= -# Builds the core OIM containers: omnia_core and omnia_auth. -# -# Usage: -# ./build_images.sh # Build core + auth (default) -# ./build_images.sh core # Core only -# ./build_images.sh auth # Auth only -# ./build_images.sh core core_tag=2.3 # Custom tag -# ./build_images.sh core,auth build_tool=docker # Use docker -# ./build_images.sh all build_tool=docker build_action=push # Push to registry -# -# Available containers: -# all, core, auth -# -# Other domain containers have their own build scripts: -# Telemetry: src/telemetry/containers/build_images.sh -# BuildStream: src/build_stream/containers/build_images.sh -# Image Builder: src/image_build_manager/containers/build_images.sh -# -# Parameters: -# build_tool= podman | docker (default: podman) -# build_action= load | push (default: load) -# registry= Registry URL (default: docker.io/dellhpcomniaaisolution) -# core_tag= omnia_core tag (default: 2.2) -# auth_tag= omnia_auth tag (default: 1.1) -# ============================================================================= - -set -euo pipefail - -# ── Resolve paths ── -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" - -# ── Color codes ── -RED='\033[0;31m' -GREEN='\033[0;32m' -BLUE='\033[34m' -YELLOW='\033[1;33m' -MAGENTA='\033[0;35m' -NC='\033[0m' - -# ── Build status tracking ── -SUCCESSFUL_BUILDS=() -FAILED_BUILDS=() - -# ============================================================================= -# Show help -# ============================================================================= -show_help() { - echo -e "${GREEN}OIM Core Container Build Script${NC}" - echo -e "${GREEN}======================================${NC}" - echo "" - echo -e "${BLUE}USAGE:${NC}" - echo " ./build_images.sh [container] [parameters]" - echo "" - echo -e "${BLUE}CONTAINERS:${NC}" - echo " all Build core + auth (default)" - echo " core Build omnia_core container" - echo " auth Build omnia_auth container" - echo " Multiple containers can be comma-separated: core,auth" - echo "" - echo -e "${BLUE}OTHER DOMAIN CONTAINERS:${NC}" - echo " Telemetry: src/telemetry/containers/build_images.sh" - echo " BuildStream: src/build_stream/containers/build_images.sh" - echo " Image Builder: src/image_build_manager/containers/build_images.sh" - echo "" - echo -e "${BLUE}PARAMETERS (key=value format):${NC}" - echo " build_tool= podman | docker (default: podman)" - echo " build_action= load | push (default: load)" - echo " registry= Registry URL (default: docker.io/dellhpcomniaaisolution)" - echo " core_tag= omnia_core tag (default: 2.2)" - echo " auth_tag= omnia_auth tag (default: 1.1)" - echo "" - echo -e "${BLUE}EXAMPLES:${NC}" - echo " ./build_images.sh" - echo " ./build_images.sh core" - echo " ./build_images.sh core core_tag=2.3" - echo " ./build_images.sh core,auth build_tool=docker" - echo " ./build_images.sh all build_tool=docker build_action=push" - echo "" - echo -e "${BLUE}NOTES:${NC}" - echo " - build_action=push requires build_tool=docker" - echo " - Default registry: docker.io/dellhpcomniaaisolution" - echo " - After build, run: ./omnia.sh --install" - exit 0 -} - -# ============================================================================= -# Default parameterized values -# ============================================================================= -BUILD_TOOL="podman" -BUILD_ACTION="load" -OMNIA_DOCKER_REGISTERY="docker.io/dellhpcomniaaisolution" - -CORE_TAG="2.2" -AUTH_TAG="1.1" - -# ============================================================================= -# Parse command-line parameters -# ============================================================================= -CONTAINER_ARG="${1:-all}" -shift 2>/dev/null || true - -for arg in "$@"; do - case "$arg" in - build_tool=*) BUILD_TOOL="${arg#*=}" ;; - build_action=*) BUILD_ACTION="${arg#*=}" ;; - registry=*) OMNIA_DOCKER_REGISTERY="${arg#*=}" ;; - core_tag=*) CORE_TAG="${arg#*=}" ;; - auth_tag=*) AUTH_TAG="${arg#*=}" ;; - -h|--help) - show_help - ;; - *) - echo -e "${RED}Error: Unknown parameter '$arg'${NC}" - echo -e "${YELLOW}Valid: build_tool, build_action, registry, core_tag, auth_tag${NC}" - exit 1 - ;; - esac -done - -# ============================================================================= -# Validate inputs -# ============================================================================= -if [[ "$BUILD_TOOL" != "podman" && "$BUILD_TOOL" != "docker" ]]; then - echo -e "${RED}Error: Invalid build_tool '${BUILD_TOOL}'. Valid: podman, docker${NC}" - exit 1 -fi - -if [[ "$BUILD_ACTION" != "load" && "$BUILD_ACTION" != "push" ]]; then - echo -e "${RED}Error: Invalid build_action '${BUILD_ACTION}'. Valid: load, push${NC}" - exit 1 -fi - -if [[ "$BUILD_ACTION" == "push" && "$BUILD_TOOL" != "docker" ]]; then - echo -e "${RED}Error: build_action=push requires build_tool=docker${NC}" - exit 1 -fi - -# ============================================================================= -# Container build function (self-contained) -# ============================================================================= -container_build() { - local image_name="$1" - local image_tag="$2" - local build_dir="$3" - local containerfile="$4" - local extra_args="${5:-}" - local platform="${6:-linux/amd64}" - - cd "$build_dir" || exit 1 - - local BUILD_RESULT - - if [ "$BUILD_TOOL" = "podman" ]; then - # shellcheck disable=SC2086 - podman build ${extra_args} -t "${image_name}:${image_tag}" -f "${containerfile}" . - BUILD_RESULT=$? - elif [ "$BUILD_TOOL" = "docker" ]; then - if [ "$BUILD_ACTION" = "load" ]; then - # shellcheck disable=SC2086 - docker buildx build --no-cache ${extra_args} -t "${image_name}:${image_tag}" \ - --file "${containerfile}" --platform "${platform}" --load . - BUILD_RESULT=$? - elif [ "$BUILD_ACTION" = "push" ]; then - # shellcheck disable=SC2086 - docker buildx build --no-cache ${extra_args} \ - -t "${OMNIA_DOCKER_REGISTERY}/${image_name}:${image_tag}" \ - --file "${containerfile}" --platform "${platform}" \ - --provenance=true --sbom=true --push . - BUILD_RESULT=$? - fi - fi - - cd - > /dev/null || exit 1 - - if [ $BUILD_RESULT -eq 0 ]; then - echo -e "${GREEN}${image_name}:${image_tag} built successfully.${NC}" - SUCCESSFUL_BUILDS+=("${image_name}") - else - echo -e "${RED}${image_name}:${image_tag} build FAILED.${NC}" - FAILED_BUILDS+=("${image_name}") - fi -} - -# ============================================================================= -# Build functions -# ============================================================================= -build_omnia_core() { - echo -e "${BLUE}Building omnia_core (tag: ${CORE_TAG})...${NC}" - container_build "omnia_core" "${CORE_TAG}" \ - "${REPO_ROOT}" "src/main/containers/omnia_core/Containerfile" -} - -build_omnia_auth() { - echo -e "${BLUE}Building omnia_auth (tag: ${AUTH_TAG})...${NC}" - container_build "omnia_auth" "${AUTH_TAG}" \ - "${SCRIPT_DIR}/omnia_auth" "Containerfile" -} - -# ============================================================================= -# Build summary -# ============================================================================= -print_build_summary() { - echo -e "\n${BLUE}=== OIM BUILD SUMMARY ===${NC}" - if [ ${#SUCCESSFUL_BUILDS[@]} -ne 0 ]; then - echo -e "${GREEN}Successfully built: ${YELLOW}${SUCCESSFUL_BUILDS[*]}${NC}" - fi - if [ ${#FAILED_BUILDS[@]} -ne 0 ]; then - echo -e "${RED}Failed: ${MAGENTA}${FAILED_BUILDS[*]}${NC}" - exit 1 - fi - if [ ${#SUCCESSFUL_BUILDS[@]} -ne 0 ]; then - echo -e "\n${GREEN}Next step: ./omnia.sh --install${NC}" - fi -} - -# ============================================================================= -# Main — Dispatch -# ============================================================================= -echo -e "${GREEN}=======================================${NC}" -echo -e "${GREEN} OIM Core — Container Build ${NC}" -echo -e "${GREEN}=======================================${NC}" -echo -e "${BLUE}Build tool: ${BUILD_TOOL}${NC}" -echo -e "${BLUE}Build action: ${BUILD_ACTION}${NC}" -echo "" - -IFS=',' read -r -a containers <<< "$CONTAINER_ARG" - -for container in "${containers[@]}"; do - case "$container" in - all) build_omnia_core; build_omnia_auth ;; - core) build_omnia_core ;; - auth) build_omnia_auth ;; - *) - echo -e "${RED}Error: Unknown container '$container'. Valid: all, core, auth${NC}" - echo -e "${YELLOW}For other containers use domain-specific build scripts:${NC}" - echo -e " Telemetry: src/telemetry/containers/build_images.sh" - echo -e " BuildStream: src/build_stream/containers/build_images.sh" - echo -e " Image Builder: src/image_build_manager/containers/build_images.sh" - exit 1 - ;; - esac -done - -print_build_summary diff --git a/src/main/containers/omnia_auth/Containerfile b/src/main/containers/omnia_auth/Containerfile deleted file mode 100644 index dcd8114e64..0000000000 --- a/src/main/containers/omnia_auth/Containerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM fedora:42 - -# Update system packages to fix CVEs: openssl, libssh, curl, libxml2, systemd, vim, etc. -RUN dnf -y update && \ - dnf -y install openldap-servers openldap-clients && \ - dnf clean all - - -RUN mkdir -p /var/lib/openldap/openldap-data && \ - chown -R ldap:ldap /var/lib/openldap -EXPOSE 389 636 -CMD ["/bin/sh", "-c", "\ - mkdir -p /var/lib/openldap/openldap-data && \ - chown ldap:ldap /var/lib/openldap/openldap-data; \ - if [ ! -f /var/lib/openldap/openldap-data/__initialized ]; then \ - slapadd -f /etc/openldap/slapd.conf -l /container-init/bootstrap.ldif && \ - chown -R ldap:ldap /var/lib/openldap && \ - touch /var/lib/openldap/openldap-data/__initialized; \ - fi; \ - cp /etc/openldap/certs/ldapserver.crt /etc/pki/ca-trust/source/anchors/ && \ - update-ca-trust; \ - exec /usr/sbin/slapd -f /etc/openldap/slapd.conf -h 'ldap:/// ldaps:///' -d 0 \ -"] diff --git a/src/main/containers/omnia_core/Containerfile b/src/main/containers/omnia_core/Containerfile deleted file mode 100644 index f8d173b331..0000000000 --- a/src/main/containers/omnia_core/Containerfile +++ /dev/null @@ -1,184 +0,0 @@ -# Use Fedora 42 as the base image -FROM fedora:42 - -# Update the system and install Python 3.13 (Fedora 42 default; ansible-core 2.20 requires 3.12+) -RUN dnf -y update && \ - dnf -y install python3 python3-pip git openssh-server sshpass \ - iproute systemd passwd openssl jq createrepo yum-utils \ - wget unzip tar dnf-plugins-core rsync iputils nano && \ - dnf clean all - -# Install latest Go version with specific crypto version -RUN curl -LO https://go.dev/dl/go1.25.10.linux-amd64.tar.gz && \ - tar -C /usr/local -xzf go1.25.10.linux-amd64.tar.gz && \ - rm go1.25.10.linux-amd64.tar.gz -ENV PATH="/usr/local/go/bin:${PATH}" -ENV GOPATH="/go" -ENV GOCACHE="/go/cache" - -# Create persistent project with patched golang.org/x/crypto@v0.52.0 and golang.org/x/net@v0.54.0 -# IMPORTANT: x/net must be v0.54.0+ to avoid Go module resolver downgrading x/crypto. -# Scanners detect this from go.mod/go.sum files and module cache. -RUN mkdir -p /opt/go-crypto-patch && \ - cd /opt/go-crypto-patch && \ - go mod init crypto-patch && \ - go get golang.org/x/net@v0.54.0 && \ - go get golang.org/x/crypto@v0.52.0 && \ - go mod edit -require=golang.org/x/crypto@v0.52.0 && \ - go mod edit -require=golang.org/x/net@v0.54.0 && \ - printf 'package main\nimport _ "golang.org/x/crypto/ssh"\nimport _ "golang.org/x/net/html"\nfunc main() {}\n' > main.go && \ - go mod tidy && \ - echo "=== Verifying resolved versions ===" && \ - go list -m golang.org/x/crypto && \ - go list -m golang.org/x/net && \ - go list -m golang.org/x/crypto | grep -q "v0.52.0" || (echo "FATAL: x/crypto downgraded below v0.52.0 — CVE-2025-58181 and CVE-2025-47914 NOT fixed" && exit 1) && \ - go list -m golang.org/x/net | grep -q "v0.54.0" || (echo "FATAL: x/net is not v0.54.0" && exit 1) && \ - go build -o crypto-patch . && \ - echo "Crypto-patch built with Go $(go version)" && \ - cd / && \ - chmod -R 755 /opt/go-crypto-patch && \ - chmod -R 777 /go - -# Build Git LFS from source with patched Go 1.25.10, golang.org/x/crypto@v0.52.0, and golang.org/x/net@v0.54.0 -RUN git clone https://github.com/git-lfs/git-lfs.git /tmp/git-lfs && \ - cd /tmp/git-lfs && \ - git checkout v3.7.1 && \ - go mod edit -replace golang.org/x/crypto=golang.org/x/crypto@v0.52.0 && \ - go mod edit -replace golang.org/x/net=golang.org/x/net@v0.54.0 && \ - go mod tidy && \ - echo "=== Git LFS: Verifying resolved module versions ===" && \ - go list -m golang.org/x/crypto && \ - go list -m golang.org/x/net && \ - go list -m golang.org/x/crypto | grep -q "v0.52.0" || (echo "FATAL: git-lfs x/crypto downgraded below v0.52.0 — CVE-2025-58181 and CVE-2025-47914 NOT fixed" && exit 1) && \ - go list -m golang.org/x/net | grep -q "v0.54.0" || (echo "FATAL: git-lfs x/net is not v0.54.0" && exit 1) && \ - go build -o /usr/local/bin/git-lfs . && \ - git lfs install && \ - cd / && rm -rf /tmp/git-lfs - -# Verify Git LFS version and that it was built with patched Go -RUN git lfs version && \ - echo "Git LFS built with Go $(go version)" - -RUN dnf install --setopt=install_weak_deps=False ipcalc -y - -# Verify Python version -RUN python3 --version - -# Upgrade pip to fix CVE-2025-8869 and CVE-2025-68121 (system pip vulnerability) -# Use dynamic site-packages detection (Fedora 42 ships Python 3.13) -RUN SITE_PKGS=$(python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))") && \ - python3 -m pip install --upgrade pip==26.0 --target "$SITE_PKGS" --force-reinstall && \ - find "$SITE_PKGS" -name "pip-23.*" -exec rm -rf {} + 2>/dev/null || true - -# Install/sync python packages using pyproject.toml (includes all security-patched packages) -RUN pip3 install uv==0.11.15 -COPY src/main/containers/omnia_core/pyproject.toml src/main/containers/omnia_core/uv.lock ./ -ENV UV_PROJECT_ENVIRONMENT="/usr" -RUN uv lock && uv sync && \ - rm -rf /root/.cache/uv /root/.cache/pip - -# Allow root login by enabling it in the SSH configuration -RUN sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \ - sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config - -# Expose port 22 for SSH -RUN sed -i 's/^#Port 22/Port 2222/' /etc/ssh/sshd_config -EXPOSE 2222 - -# Generate host keys if they don't exist -RUN ssh-keygen -A - -# Configure workdir for ssh -RUN echo "cd /omnia" >> /root/.bashrc - -# Create the /tmp/omnia_core directory -RUN mkdir -p /tmp/omnia_core - -# Create the pulp config directory -RUN mkdir -p /root/.config/pulp - -# Dynamically create the Ansible Galaxy requirements file -RUN echo "collections:" > /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: ansible.utils" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 5.1.1" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: community.crypto" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 2.23.0" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: community.docker" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 3.12.1" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: community.general" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 10.3.0" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: community.grafana" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 2.1.0" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: community.mysql" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 3.10.3" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: dellemc.os10" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 1.1.1" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: dellemc.openmanage" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 10.0.2" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: ansible.posix" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 2.0.0" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: containers.podman" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 1.16.2" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: community.postgresql" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 3.10.2" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " - name: kubernetes.core" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " version: 5.2.0" >> /tmp/omnia_core/galaxy_requirements.yml && \ - echo " # community.kubernetes removed — deprecated, use kubernetes.core instead" >> /tmp/omnia_core/galaxy_requirements.yml - -# Install ansible-galaxy collections from galaxy_requirements.yml -# Note: ansible-core is installed via pyproject.toml -RUN ansible-galaxy collection install -r /tmp/omnia_core/galaxy_requirements.yml - -# Additional security: Clean up any cached vulnerable packages -RUN dnf -y autoremove && \ - rm -rf /var/cache/dnf/* /tmp/* /var/tmp/* && \ - # Secure permissions on sensitive directories - chmod 600 /root/.ssh/* 2>/dev/null || true && \ - chmod 700 /root/.ssh 2>/dev/null || true - -# Copy src/ directory from the monorepo into the container -RUN mkdir -p /omnia -COPY src/ /omnia/ - -# Remove build_stream app/ — deployed to NFS at runtime by deploy_bsm role -RUN rm -rf /omnia/build_stream/app - -# Copy the entrypoint script to / and make it executable -COPY src/main/containers/omnia_core/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Configure workdir for podman -WORKDIR /omnia - -# Copy pulp certificate to core container's trust -COPY src/main/containers/omnia_core/cert-copy.sh /cert-copy.sh -RUN chmod +x /cert-copy.sh - -# Document security patches applied -LABEL security.go.version="1.25.10" \ - security.go.cves="CVE-2025-61729-fixed,CVE-2025-61727-fixed,CVE-2026-32282-fixed,CVE-2026-32288-fixed,CVE-2026-32283-fixed,CVE-2026-42501-fixed,CVE-2026-33811-fixed" \ - security.golang.crypto.version="0.52.0" \ - security.golang.crypto.cves="CVE-2025-47913-fixed,CVE-2025-58181-fixed,CVE-2025-47914-fixed" \ - security.golang.net.version="0.54.0" \ - security.golang.net.cves="CVE-2025-47911-fixed,CVE-2025-58190-fixed" \ - security.git-lfs.version="3.7.1-patched" \ - security.git-lfs.note="Built from source with Go 1.25.10, golang.org/x/crypto v0.52.0, and golang.org/x/net v0.54.0; build-time verified" \ - security.urllib3.version="2.7.0" \ - security.urllib3.cves="CVE-2026-21441-fixed,CVE-2025-66471-fixed,CVE-2025-66418-fixed,GHSA-2xpw-w6gg-jr37-fixed,GHSA-gm62-xv2j-4w53-fixed" \ - security.pyasn1.version="0.6.3" \ - security.pyasn1.cves="CVE-2026-23490-fixed,CVE-2026-30922-fixed,GHSA-63vm-454h-vhhq-fixed,GHSA-jr27-m4p2-rc6r-fixed" \ - security.filelock.version="3.20.3" \ - security.filelock.cves="CVE-2025-68146-fixed,CVE-2026-22701-fixed" \ - security.cryptography.version="48.0.0" \ - security.cryptography.cves="CVE-2026-26007-fixed,GHSA-r6ph-v2qm-q3c2-fixed" \ - security.gitpython.version="3.1.50" \ - security.gitpython.cves="GHSA-x2qx-6953-8485-fixed,GHSA-rpm5-65cw-6hj4-fixed" \ - security.pip.version="26.0" \ - security.pip.cves="CVE-2025-8869-fixed,CVE-2025-68121-fixed" \ - security.wheel.version="0.46.2" \ - security.wheel.note="Upgraded wheel and cleared uv cache to remove vendored vulnerable version" \ - security.ansible.version="2.20.7" \ - security.ansible.note="Upgraded from EOL ansible-core 2.16 to stable 2.20.7 (supported through ~May 2027)" - -# Set the entrypoint script as the entrypoint -ENTRYPOINT ["/entrypoint.sh"] diff --git a/src/main/containers/omnia_core/cert-copy.sh b/src/main/containers/omnia_core/cert-copy.sh deleted file mode 100755 index b77b1e5321..0000000000 --- a/src/main/containers/omnia_core/cert-copy.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -CERT_PATH="/opt/omnia/pulp/settings/certs/pulp_webserver.crt" -ANCHOR_PATH="/etc/pki/ca-trust/source/anchors" -LOG_DIR="/opt/omnia/log/core" -LOG_FILE="${LOG_DIR}/pulp_cert_log" - -mkdir -p "$LOG_DIR" -: > "$LOG_FILE" - -echo "$(date '+%Y-%m-%d %H:%M:%S'): Container started" >> "$LOG_FILE" - -if [ -f "$CERT_PATH" ]; then - echo "$(date '+%Y-%m-%d %H:%M:%S'): Cert found at $CERT_PATH" >> "$LOG_FILE" - - cp -f "$CERT_PATH" "$ANCHOR_PATH/" - echo "$(date '+%Y-%m-%d %H:%M:%S'): Copied cert to $ANCHOR_PATH" >> "$LOG_FILE" - - update-ca-trust extract - echo "$(date '+%Y-%m-%d %H:%M:%S'): Ran update-ca-trust" >> "$LOG_FILE" -else - echo "$(date '+%Y-%m-%d %H:%M:%S'): Cert not found at $CERT_PATH" >> "$LOG_FILE" -fi - -exec "$@" diff --git a/src/main/containers/omnia_core/entrypoint.sh b/src/main/containers/omnia_core/entrypoint.sh deleted file mode 100755 index d50e068104..0000000000 --- a/src/main/containers/omnia_core/entrypoint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Call script to copy pulp cert -/cert-copy.sh - -# Grab the hashed password -omnia_core_hashed_passwd=$(grep omnia_core_hashed_passwd /opt/omnia/.data/oim_metadata.yml | awk -F: '{print $2}' | tr -d ' ') - -echo "root:$omnia_core_hashed_passwd" | chpasswd -e - -# Start SSH daemon -/usr/sbin/sshd -D diff --git a/src/main/containers/omnia_core/pyproject.toml b/src/main/containers/omnia_core/pyproject.toml deleted file mode 100644 index 1f3b16bf04..0000000000 --- a/src/main/containers/omnia_core/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[project] -name = "omnia_core" -version = "0.1.0" -description = "Python environment for the Omnia Core container." -requires-python = ">= 3.12" -dependencies = [ - "psycopg2-binary==2.9.11", - "requests>=2.33.0", - "pyarrow==22.0.0", - "pandas==2.3.3", - "passlib==1.7.4", - "pulp-cli==0.40.1", - "pygments>=2.20.0", - "netaddr==1.3.0", - "pexpect==4.9.0", - "psutil==7.1.3", - "pyyaml==6.0.3", - "PyMySQL==1.1.2", - "omsdk==1.2.518", - "pysnmp==7.1.22", - "kubernetes==32.0.1", - "cryptography==48.0.0", - "logger==1.4", - "prettytable==3.14.0", - "toml==0.10.2", - "http_client==0.1.22", - "gitPython==3.1.50", - "ansible-lint==25.6.1", - "jmespath==1.0.1", - "urllib3==2.7.0", - "pyasn1==0.6.3", - "filelock==3.20.3", - "wheel==0.46.2", - "ansible-core==2.20.7", - "jaraco.context>=6.1.0" -] - - -[tool.uv] -override-dependencies = [ - "requests>=2.33.0", - "click>=8.4.1", -] diff --git a/src/main/containers/omnia_core/uv.lock b/src/main/containers/omnia_core/uv.lock deleted file mode 100644 index 423c8f43f4..0000000000 --- a/src/main/containers/omnia_core/uv.lock +++ /dev/null @@ -1,1549 +0,0 @@ -version = 1 -revision = 4 -requires-python = ">=3.12" - -[manifest] -overrides = [{ name = "requests", specifier = ">=2.33.0" }, { name = "click", specifier = ">=8.4.1" }] - -[[package]] -name = "ansible-compat" -version = "25.12.1" -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/c7/8f/14e8566b9a0cd5393150719db218a7b332308456b003b289f21e5812000a/ansible_compat-25.12.1.tar.gz", hash = "sha256:509a42fbfc65acad90b4e62c311ec0b37447e497b9b2a19cc38a625e458f2afd", size = 214925, upload-time = "2026-02-25T15:47:02.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/05/5f889ab2b104426630e40a13c121594756047c06ebcaa5bf046883550e09/ansible_compat-25.12.1-py3-none-any.whl", hash = "sha256:9246bef1b8d89bdd18cc1f07d2711dcc6e09daf51f6e15880415dd4312d788f1", size = 27773, upload-time = "2026-02-25T15:47:00.249Z" }, -] - -[[package]] -name = "ansible-core" -version = "2.20.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/73/39/f41e3432e5cfbf039ee7c1288661d679cb4b25440f06712f9ee3fe4dd31b/ansible_core-2.20.7.tar.gz", hash = "sha256:84d4097a28e816e8bf8066307088f833837db9efdc9dc6716b43253de3b14106", size = 3338904, upload-time = "2026-06-18T19:36:23.660454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/9e/07afc00c1789d6ab40eb5e38ca5b258ff5ecded657ccf100a874e22d3b12/ansible_core-2.20.7-py3-none-any.whl", hash = "sha256:f4be63a9a8b6bb5f4fc68d41f8582f5b289b1ee1db0600caab8b3a660b9d7ddc", size = 2427099, upload-time = "2026-06-18T19:36:22.129746Z" }, -] - -[[package]] -name = "ansible-lint" -version = "25.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ansible-compat" }, - { name = "ansible-core" }, - { name = "black" }, - { name = "filelock" }, - { name = "importlib-metadata" }, - { name = "jsonschema" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "referencing" }, - { name = "ruamel-yaml" }, - { name = "subprocess-tee" }, - { name = "wcmatch" }, - { name = "yamllint" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/27/f04e69b710efbfde20cfdcdb8eec3b3b1d4b6d480a80afa803386304e829/ansible_lint-25.6.1.tar.gz", hash = "sha256:6a1dd2b7a9f3f202c9e92a6c80296ff33ca863348c3acf978f80fb0d4536dce4", size = 559349, upload-time = "2025-06-19T07:34:23.206Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/1d/8e4972bc37bc6a2ac87a687076775e631350dd05c3d0517f88e0c46176a6/ansible_lint-25.6.1-py3-none-any.whl", hash = "sha256:69fe294a3cc30d8819b5a30625a7e25225f48558cadb83ad3d4dec597c1b8c2c", size = 316902, upload-time = "2025-06-19T07:34:21.1Z" }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "black" -version = "26.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/5f/25b7b149b8b7d3b958efa4faa56446560408c0f2651108a517526de0320a/black-26.3.0.tar.gz", hash = "sha256:4d438dfdba1c807c6c7c63c4f15794dda0820d2222e7c4105042ac9ddfc5dd0b", size = 664127, upload-time = "2026-03-06T17:42:33.7Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/76/b21711045b7f4c4f1774048d0b34dd10a265c42255658b251ce3303ae3c7/black-26.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2b1e5eec220b419e3591a0aaa6351bd3a9c01fe6291fbaf76d84308eb7a2ede", size = 1895944, upload-time = "2026-03-06T17:46:24.841Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c3/8c56e73283326bc92a36101c660228fff09a2403a57a03cacf3f7f84cf62/black-26.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1bab64de70bccc992432bee56cdffbe004ceeaa07352127c386faa87e81f9261", size = 1718669, upload-time = "2026-03-06T17:46:26.639Z" }, - { url = "https://files.pythonhosted.org/packages/7b/8b/712a3ae8f17c1f3cd6f9ac2fffb167a27192f5c7aba68724e8c4ab8474ad/black-26.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b6c5f734290803b7b26493ffd734b02b72e6c90d82d45ac4d5b862b9bdf7720", size = 1794844, upload-time = "2026-03-06T17:46:28.334Z" }, - { url = "https://files.pythonhosted.org/packages/ba/5b/ee955040e446df86473287dd24dc69c80dd05e02cc358bca90e22059f7b1/black-26.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c767396af15b54e1a6aae99ddf241ae97e589f666b1d22c4b6618282a04e4ca", size = 1420461, upload-time = "2026-03-06T17:46:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/12/77/40b8bd44f032bb34c9ebf47ffc5bb47a2520d29e0a4b8a780ab515223b5a/black-26.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:765fd6ddd00f35c55250fdc6b790c272d54ac3f44da719cc42df428269b45980", size = 1229667, upload-time = "2026-03-06T17:46:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/28/c3/21a834ce3de02c64221243f2adac63fa3c3f441efdb3adbf4136b33dfeb0/black-26.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:59754fd8f43ef457be190594c07a52c999e22cb1534dc5344bff1d46fdf1027d", size = 1895195, upload-time = "2026-03-06T17:46:33.12Z" }, - { url = "https://files.pythonhosted.org/packages/1c/f9/212d9697dd78362dadb778d4616b74c8c2cf7f2e4a55aac2adeb0576f2e9/black-26.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1fd94cfee67b8d336761a0b08629a25938e4a491c440951ce517a7209c99b5ff", size = 1718472, upload-time = "2026-03-06T17:46:34.576Z" }, - { url = "https://files.pythonhosted.org/packages/a2/dd/da980b2f512441375b73cb511f38a2c3db4be83ccaa1302b8d39c9fa2dff/black-26.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b3e653a90ca1ef4e821c20f8edaee80b649c38d2532ed2e9073a9534b14a7", size = 1793741, upload-time = "2026-03-06T17:46:36.261Z" }, - { url = "https://files.pythonhosted.org/packages/93/11/cd69ae8826fe3bc6eaf525c8c557266d522b258154a2968eb46d6d25fac7/black-26.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:f8fb9d7c2496adc83614856e1f6e55a9ce4b7ae7fc7f45b46af9189ddb493464", size = 1422522, upload-time = "2026-03-06T17:46:37.607Z" }, - { url = "https://files.pythonhosted.org/packages/75/f5/647cf50255203eb286be197925e86eedc101d5409147505db3e463229228/black-26.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:e8618c1d06838f56afbcb3ffa1aa16436cec62b86b38c7b32ca86f53948ffb91", size = 1231807, upload-time = "2026-03-06T17:46:39.072Z" }, - { url = "https://files.pythonhosted.org/packages/ff/77/b197e701f15fd694d20d8ee0001efa2e29eba917aa7c3610ff7b10ae0f88/black-26.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d0c6f64ead44f4369c66f1339ecf68e99b40f2e44253c257f7807c5a3ef0ca32", size = 1889209, upload-time = "2026-03-06T17:46:40.453Z" }, - { url = "https://files.pythonhosted.org/packages/93/85/b4d4924ac898adc2e39fc7a923bed99797535bc16dea4bc63944c3903c2b/black-26.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed6f0809134e51ec4a7509e069cdfa42bf996bd0fd1df6d3146b907f36e28893", size = 1720830, upload-time = "2026-03-06T17:46:42.009Z" }, - { url = "https://files.pythonhosted.org/packages/00/b1/5c0bf29fe5b43fcc6f3e8480c6566d21a02d4e702b3846944e7daa06dea9/black-26.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc6ac0ea5dd5fa6311ca82edfa3620cba0ed0426022d10d2d5d39aedbf3e1958", size = 1787676, upload-time = "2026-03-06T17:46:43.382Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ce/cc8cf14806c144d6a16512272c537d5450f50675d3e8c038705430e90fd9/black-26.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:884bc0aefa96adabcba0b77b10e9775fd52d4b766e88c44dc6f41f7c82787fc8", size = 1445406, upload-time = "2026-03-06T17:46:44.948Z" }, - { url = "https://files.pythonhosted.org/packages/cf/bb/049ea0fad9f8bdec7b647948adcf74bb720bd71dcb213decd553e05b2699/black-26.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:be3bd02aab5c4ab03703172f5530ddc8fc8b5b7bb8786230e84c9e011cee9ca1", size = 1257945, upload-time = "2026-03-06T17:46:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/39/d7/7360654ba4f8b41afcaeb5aca973cfea5591da75aff79b0a8ae0bb8883f6/black-26.3.0-py3-none-any.whl", hash = "sha256:e825d6b121910dff6f04d7691f826d2449327e8e71c26254c030c4f3d2311985", size = 206848, upload-time = "2026-03-06T17:42:31.133Z" }, -] - -[[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 = "certifi" -version = "2026.2.25" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, -] - -[[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/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { 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 = "charset-normalizer" -version = "3.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, - { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, - { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, - { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, - { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, - { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, - { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, - { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, - { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, - { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, - { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, - { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, - { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, - { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, - { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, - { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, - { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, - { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, - { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, - { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, - { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, - { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, - { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, - { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, - { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, - { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, - { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, - { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, - { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, - { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, - { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, -] - -[[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 = "durationpy" -version = "0.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, -] - -[[package]] -name = "filelock" -version = "3.20.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, -] - -[[package]] -name = "future" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, -] - -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.50" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, -] - -[[package]] -name = "google-auth" -version = "2.49.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyasn1-modules" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/59/7371175bfd949abfb1170aa076352131d7281bd9449c0f978604fc4431c3/google_auth-2.49.0.tar.gz", hash = "sha256:9cc2d9259d3700d7a257681f81052db6737495a1a46b610597f4b8bafe5286ae", size = 333444, upload-time = "2026-03-06T21:53:06.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/45/de64b823b639103de4b63dd193480dce99526bd36be6530c2dba85bf7817/google_auth-2.49.0-py3-none-any.whl", hash = "sha256:f893ef7307f19cf53700b7e2f61b5a6affe3aa0edf9943b13788920ab92d8d87", size = 240676, upload-time = "2026-03-06T21:52:38.304Z" }, -] - -[[package]] -name = "http-client" -version = "0.1.22" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycurl" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/e5/2e8134353629a91854c50a228f1624e21985aca1abc9d0ef0ce25bf5f389/http_client-0.1.22.tar.gz", hash = "sha256:f33d77e4e08b70659e1497eeb3d2121a3c6e8a4c252b3c6a56e87a16bb442f91", size = 1582, upload-time = "2017-08-28T09:50:08.55Z" } - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - -[[package]] -name = "ipaddress" -version = "1.0.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/9a/3e9da40ea28b8210dd6504d3fe9fe7e013b62bf45902b458d1cdc3c34ed9/ipaddress-1.0.23.tar.gz", hash = "sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2", size = 32958, upload-time = "2019-10-18T01:30:24.58Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/f8/49697181b1651d8347d24c095ce46c7346c37335ddc7d255833e7cde674d/ipaddress-1.0.23-py2.py3-none-any.whl", hash = "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc", size = 18159, upload-time = "2019-10-18T01:30:27.002Z" }, -] - -[[package]] -name = "jaraco-context" -version = "6.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, -] - -[[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 = "jmespath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, -] - -[[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 = "kubernetes" -version = "32.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "durationpy" }, - { name = "google-auth" }, - { name = "oauthlib" }, - { name = "python-dateutil" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "requests-oauthlib" }, - { name = "six" }, - { name = "urllib3" }, - { name = "websocket-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/e8/0598f0e8b4af37cd9b10d8b87386cf3173cb8045d834ab5f6ec347a758b3/kubernetes-32.0.1.tar.gz", hash = "sha256:42f43d49abd437ada79a79a16bd48a604d3471a117a8347e87db693f2ba0ba28", size = 946691, upload-time = "2025-02-18T21:06:34.148Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/10/9f8af3e6f569685ce3af7faab51c8dd9d93b9c38eba339ca31c746119447/kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998", size = 1988070, upload-time = "2025-02-18T21:06:31.391Z" }, -] - -[[package]] -name = "logger" -version = "1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/2f/b0d28eaa1e2c1cf64129f8da3fe701888d152677fec708cd0f13e8309e1e/logger-1.4.tar.gz", hash = "sha256:4ecac57133c6376fa215f0fe6b4dc4d60e4d1ad8be005cab4e8a702df682f8b3", size = 1187, upload-time = "2013-10-22T16:49:01.48Z" } - -[[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/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { 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 = "multidict" -version = "6.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183, upload-time = "2025-05-19T14:16:37.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293, upload-time = "2025-05-19T14:14:44.724Z" }, - { url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096, upload-time = "2025-05-19T14:14:45.95Z" }, - { url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214, upload-time = "2025-05-19T14:14:47.158Z" }, - { url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686, upload-time = "2025-05-19T14:14:48.366Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061, upload-time = "2025-05-19T14:14:49.952Z" }, - { url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412, upload-time = "2025-05-19T14:14:51.812Z" }, - { url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563, upload-time = "2025-05-19T14:14:53.262Z" }, - { url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811, upload-time = "2025-05-19T14:14:55.232Z" }, - { url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524, upload-time = "2025-05-19T14:14:57.226Z" }, - { url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012, upload-time = "2025-05-19T14:14:58.597Z" }, - { url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765, upload-time = "2025-05-19T14:15:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888, upload-time = "2025-05-19T14:15:01.568Z" }, - { url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041, upload-time = "2025-05-19T14:15:03.759Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046, upload-time = "2025-05-19T14:15:05.698Z" }, - { url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106, upload-time = "2025-05-19T14:15:07.124Z" }, - { url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351, upload-time = "2025-05-19T14:15:08.556Z" }, - { url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791, upload-time = "2025-05-19T14:15:09.825Z" }, - { url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123, upload-time = "2025-05-19T14:15:11.044Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049, upload-time = "2025-05-19T14:15:12.902Z" }, - { url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078, upload-time = "2025-05-19T14:15:14.282Z" }, - { url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097, upload-time = "2025-05-19T14:15:15.566Z" }, - { url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768, upload-time = "2025-05-19T14:15:17.308Z" }, - { url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331, upload-time = "2025-05-19T14:15:18.73Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169, upload-time = "2025-05-19T14:15:20.179Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947, upload-time = "2025-05-19T14:15:21.714Z" }, - { url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761, upload-time = "2025-05-19T14:15:23.242Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605, upload-time = "2025-05-19T14:15:24.763Z" }, - { url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144, upload-time = "2025-05-19T14:15:26.249Z" }, - { url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100, upload-time = "2025-05-19T14:15:28.303Z" }, - { url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731, upload-time = "2025-05-19T14:15:30.263Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637, upload-time = "2025-05-19T14:15:33.337Z" }, - { url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594, upload-time = "2025-05-19T14:15:34.832Z" }, - { url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359, upload-time = "2025-05-19T14:15:36.246Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903, upload-time = "2025-05-19T14:15:37.507Z" }, - { url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895, upload-time = "2025-05-19T14:15:38.856Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183, upload-time = "2025-05-19T14:15:40.197Z" }, - { url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592, upload-time = "2025-05-19T14:15:41.508Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071, upload-time = "2025-05-19T14:15:42.877Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597, upload-time = "2025-05-19T14:15:44.412Z" }, - { url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253, upload-time = "2025-05-19T14:15:46.474Z" }, - { url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146, upload-time = "2025-05-19T14:15:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585, upload-time = "2025-05-19T14:15:49.546Z" }, - { url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080, upload-time = "2025-05-19T14:15:51.151Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558, upload-time = "2025-05-19T14:15:52.665Z" }, - { url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168, upload-time = "2025-05-19T14:15:55.279Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970, upload-time = "2025-05-19T14:15:56.806Z" }, - { url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980, upload-time = "2025-05-19T14:15:58.313Z" }, - { url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641, upload-time = "2025-05-19T14:15:59.866Z" }, - { url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728, upload-time = "2025-05-19T14:16:01.535Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913, upload-time = "2025-05-19T14:16:03.199Z" }, - { url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112, upload-time = "2025-05-19T14:16:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481, upload-time = "2025-05-19T14:16:36.024Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - -[[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 = "numpy" -version = "2.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, - { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, - { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, - { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, - { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, - { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, - { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, - { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, - { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, -] - -[[package]] -name = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, -] - -[[package]] -name = "omnia-core" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "ansible-core" }, - { name = "ansible-lint" }, - { name = "cryptography" }, - { name = "filelock" }, - { name = "gitpython" }, - { name = "http-client" }, - { name = "jaraco-context" }, - { name = "jmespath" }, - { name = "kubernetes" }, - { name = "logger" }, - { name = "netaddr" }, - { name = "omsdk" }, - { name = "pandas" }, - { name = "passlib" }, - { name = "pexpect" }, - { name = "prettytable" }, - { name = "psutil" }, - { name = "psycopg2-binary" }, - { name = "pulp-cli" }, - { name = "pyarrow" }, - { name = "pyasn1" }, - { name = "pygments" }, - { name = "pymysql" }, - { name = "pysnmp" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "toml" }, - { name = "urllib3" }, - { name = "wheel" }, -] - -[package.metadata] -requires-dist = [ - { name = "ansible-core", specifier = "==2.20.7" }, - { name = "ansible-lint", specifier = "==25.6.1" }, - { name = "cryptography", specifier = "==48.0.0" }, - { name = "filelock", specifier = "==3.20.3" }, - { name = "gitpython", specifier = "==3.1.50" }, - { name = "http-client", specifier = "==0.1.22" }, - { name = "jaraco-context", specifier = ">=6.1.0" }, - { name = "jmespath", specifier = "==1.0.1" }, - { name = "kubernetes", specifier = "==32.0.1" }, - { name = "logger", specifier = "==1.4" }, - { name = "netaddr", specifier = "==1.3.0" }, - { name = "omsdk", specifier = "==1.2.518" }, - { name = "pandas", specifier = "==2.3.3" }, - { name = "passlib", specifier = "==1.7.4" }, - { name = "pexpect", specifier = "==4.9.0" }, - { name = "prettytable", specifier = "==3.14.0" }, - { name = "psutil", specifier = "==7.1.3" }, - { name = "psycopg2-binary", specifier = "==2.9.11" }, - { name = "pulp-cli", specifier = "==0.33.0" }, - { name = "pyarrow", specifier = "==22.0.0" }, - { name = "pyasn1", specifier = "==0.6.3" }, - { name = "pygments", specifier = ">=2.20.0" }, - { name = "pymysql", specifier = "==1.1.2" }, - { name = "pysnmp", specifier = "==7.1.22" }, - { name = "pyyaml", specifier = "==6.0.3" }, - { name = "requests", specifier = ">=2.33.0" }, - { name = "toml", specifier = "==0.10.2" }, - { name = "urllib3", specifier = "==2.7.0" }, - { name = "wheel", specifier = "==0.46.2" }, -] - -[[package]] -name = "omsdk" -version = "1.2.518" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "future" }, - { name = "ipaddress" }, - { name = "pysnmp-mibs" }, - { name = "pyyaml" }, - { name = "requests" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/04/4321810d43e37ca07f46680f2622072af1eb15dcf33b33ab5ab09268a0e6/omsdk-1.2.518-py2.py3-none-any.whl", hash = "sha256:d0ae3ada1182ca2b03acdd5acb3c73d730bf7e26262bb4ed5cffad7289d1a571", size = 830131, upload-time = "2024-08-29T05:47:54.594Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "pandas" -version = "2.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, -] - -[[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 = "pathspec" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.9.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, -] - -[[package]] -name = "prettytable" -version = "3.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/7b/18bb59d7c3a4ac9ac7d986cfe49dd3c2e5f5ae3e65ca3db8816764e0c1df/prettytable-3.14.0.tar.gz", hash = "sha256:b804b8d51db23959b96b329094debdbbdf10c8c3aa75958c5988cfd7f78501dd", size = 61747, upload-time = "2025-01-31T14:58:20.079Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a2/fa0679e7a64b564074a8aa680c664ba8e6770166034f035a22ce74e55ae5/prettytable-3.14.0-py3-none-any.whl", hash = "sha256:61d5c68f04a94acc73c7aac64f0f380f5bed4d2959d59edc6e4cbb7a0e7b55c4", size = 31894, upload-time = "2025-01-31T14:58:18.69Z" }, -] - -[[package]] -name = "psutil" -version = "7.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, - { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, - { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, - { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, - { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, - { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, - { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, - { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, - { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, - { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, - { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, -] - -[[package]] -name = "psycopg2-binary" -version = "2.9.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, - { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, - { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, - { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, - { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, - { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, - { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, - { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, - { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, - { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, - { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, -] - -[[package]] -name = "pulp-cli" -version = "0.33.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "pulp-glue" }, - { name = "pyyaml" }, - { name = "schema" }, - { name = "tomli-w" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/4b/34e980065d3a2ea75618c1d0765d4547a91afff234708bf2ffb416e36da0/pulp-cli-0.33.0.tar.gz", hash = "sha256:3046170504c158e7238a27ac6bff81ea0c04d732b366f80db6f7a806491b5fca", size = 76337, upload-time = "2025-06-02T16:44:13.496Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/27/70e45fcb84fa2af1c8580e0a95df7b9590ca1a6121eeb20cd47a955e4463/pulp_cli-0.33.0-py3-none-any.whl", hash = "sha256:2552b850dac4d304c326f29dc567483bf952262f7ed166ce984fdef9211288d9", size = 110842, upload-time = "2025-06-02T16:44:11.885Z" }, -] - -[[package]] -name = "pulp-glue" -version = "0.33.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "multidict" }, - { name = "packaging" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/68/000464fd2edff33974488410cfbaf44000473d9a5eb86b1be230489f4843/pulp-glue-0.33.0.tar.gz", hash = "sha256:4d8f9b8e6c412a63974972aedb7146c49ae20aec44e5641bf71f8ca620ffde45", size = 39265, upload-time = "2025-06-02T16:44:10.543Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/f2/95637d2f954ad0b2025127052efdd593146c23bd36786c3b0bbe4c6da976/pulp_glue-0.33.0-py3-none-any.whl", hash = "sha256:bd81a64059adb5fd3fde17b5cecfd4ee4daa7c10e428cd380f2eb22ffa21574a", size = 47201, upload-time = "2025-06-02T16:44:08.962Z" }, -] - -[[package]] -name = "pyarrow" -version = "22.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, - { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, - { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, - { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, - { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, - { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, - { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, - { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, - { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, - { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, - { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, - { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, - { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, - { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, - { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, -] - -[[package]] -name = "pyasn1" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - -[[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 = "pycurl" -version = "7.45.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/3d/01255f1cde24401f54bb3727d0e5d3396b67fc04964f287d5d473155f176/pycurl-7.45.7.tar.gz", hash = "sha256:9d43013002eab2fd6d0dcc671cd1e9149e2fc1c56d5e796fad94d076d6cb69ef", size = 241098, upload-time = "2025-09-24T13:37:17.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/d3/77cf5cef99e83cf9e088e4a4539b933cdccf2bfabbb32f55d0273f9efd11/pycurl-7.45.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd69340f5a49d6e4f67723db6ea4459c554d2210c3dd4da99950d6d405ddafb4", size = 3795471, upload-time = "2025-09-24T13:36:17.608Z" }, - { url = "https://files.pythonhosted.org/packages/18/f9/42137fcace6403a1670dc203a16fce443f53da7b14cdc28f05053008e56b/pycurl-7.45.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:752a5bbbd1b148b61d69be718e2c1c89a1b16de27a21e6382d28c8d2a33e9bf1", size = 3903182, upload-time = "2025-09-24T13:36:19.353Z" }, - { url = "https://files.pythonhosted.org/packages/9f/db/46134f8e822527a39fd4ed0e5150992274f1368a4fe8402818cc45470ffe/pycurl-7.45.7-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fa5afd86d86e156051c7d7d6de34f6c03a7a64e4794ac0d72379bee8fcafafd", size = 5188809, upload-time = "2025-09-24T13:36:21.444Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f8/9a6148258b056202d5872fcd5e4d512ad9ee487c0f7b5dddd631004432b4/pycurl-7.45.7-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f463a7f7c12b322b4f72af908f863b4da1f71dae344dd75f50d30c33ff0f197a", size = 4893965, upload-time = "2025-09-24T13:36:23.108Z" }, - { url = "https://files.pythonhosted.org/packages/41/16/bbf5da7e0108dafddb70347f299f6de54f1364079a66653759c9e9fb099d/pycurl-7.45.7-cp312-cp312-win32.whl", hash = "sha256:83c217e0c7aea28bbd9512e27fb6f9ef9b9f398dff57ebe223ecaa043bcd08fa", size = 2736232, upload-time = "2025-09-24T13:36:25.072Z" }, - { url = "https://files.pythonhosted.org/packages/a8/16/ab7d73ed3d2a31a9d9bb8a44710b8f95f6ea24bd260967f07ae67c36e8c4/pycurl-7.45.7-cp312-cp312-win_amd64.whl", hash = "sha256:150b1d2e98bcf760878b3891e6f2f937a4b2a90c559ffc6add65aeb5844bc66a", size = 3296191, upload-time = "2025-09-24T13:36:27.242Z" }, - { url = "https://files.pythonhosted.org/packages/1a/0c/b6442a204c13d68e339d18c7f16a30b0178ad1efed644fe411f8a404934c/pycurl-7.45.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d39cc233c7c3303072758b088ad7ccde0a5dcb30ce752f2f8a31428a07faac1", size = 3795397, upload-time = "2025-09-24T13:36:29.362Z" }, - { url = "https://files.pythonhosted.org/packages/e5/23/dae7f5bf59f31908fd10797e03e9af6ad70ee0fe8a71783bcced68ca7db4/pycurl-7.45.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:824a785d01500da47fe6935dbadc4b0a14577405c25d01327a94717578dd2b1d", size = 3903225, upload-time = "2025-09-24T13:36:31.393Z" }, - { url = "https://files.pythonhosted.org/packages/33/93/016d37e76b172d508706828319389a08be4cb669daab8cdac6947b699f80/pycurl-7.45.7-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:998335e6b69691c097e3a74214901c71773e0a7a8d3d0cc87c60d6969eb8b584", size = 5189262, upload-time = "2025-09-24T13:36:33.752Z" }, - { url = "https://files.pythonhosted.org/packages/24/e3/039ed3b136fb008354047881a3f36e5647d67d97d7b7620123fa8b9c515f/pycurl-7.45.7-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:00824c3c64d5e935d0325e6a000bb0242622cd763d1e202c1a8df455bc58da58", size = 4895224, upload-time = "2025-09-24T13:36:35.449Z" }, - { url = "https://files.pythonhosted.org/packages/9a/52/f2326e0ddb2c6ab74a46c90aff0aac565cc4f804f03ac6a454cf7c75cb37/pycurl-7.45.7-cp313-cp313-win32.whl", hash = "sha256:bfe49668ba0a7fba183cf0e189bb8da515793da8f383d8d157ac8647143dc516", size = 2736217, upload-time = "2025-09-24T13:36:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/96/d5/d7214c92b97fe8541efcdf42a35e5b0852e6c57340f9266e4f4acccfa492/pycurl-7.45.7-cp313-cp313-win_amd64.whl", hash = "sha256:3d9d718c366983bc65b149f0947f016929b4001f604f2523e6de5a49907f4474", size = 3296259, upload-time = "2025-09-24T13:36:38.886Z" }, - { url = "https://files.pythonhosted.org/packages/85/bb/18c4e53ef9a8d52451336aae1e44768dfb639ba46dd02f331c5dc9220bc3/pycurl-7.45.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6157ac0daf30ffc28446c54ebbd073c8c11b45cba9c45256619e218a07a31312", size = 3794861, upload-time = "2025-09-24T13:36:40.509Z" }, - { url = "https://files.pythonhosted.org/packages/ce/23/ea963df437b8b0fd4e2ff5909dbfd800364231a95588de2b6446ffbdd1c8/pycurl-7.45.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:09ac9a855273a77f7c6a72f05be430646c47a298d403a75876c2188ea5d77534", size = 3903002, upload-time = "2025-09-24T13:36:42.765Z" }, - { url = "https://files.pythonhosted.org/packages/4c/33/6432144d3961ff862c1ac933f4851828f8947d615aa46b50b91162224af9/pycurl-7.45.7-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0263e94d2ea3cd25b5fcf96527f521dc29b8a73a9b19e71223b6e2452f5cb35a", size = 5186571, upload-time = "2025-09-24T13:36:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/635beb1e8c5a8b2a031fb09bd64e5c2abd332cebb9211739539ab385e731/pycurl-7.45.7-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:54cffa56a5ead14915f1f0d3e8fabf6fcea3056a8e5f971fc1caebb9715bd9b1", size = 4891741, upload-time = "2025-09-24T13:36:46.7Z" }, - { url = "https://files.pythonhosted.org/packages/77/bd/51ffbf6c4a3c5bcdad2f3175c5b5980fff4dd668e2556593530030430fcf/pycurl-7.45.7-cp314-cp314-win32.whl", hash = "sha256:09e426cf5e61e5b37c30903734204f75023baa6e4bfe574e037f12e53ec3f0ba", size = 2817391, upload-time = "2025-09-24T13:36:48.766Z" }, - { url = "https://files.pythonhosted.org/packages/f1/68/c7c7f4b2372568eff3a12046364fdd74e382778563ee26727a1b016702bc/pycurl-7.45.7-cp314-cp314-win_amd64.whl", hash = "sha256:8eab3a83670d83966c7a0df4fa02cf2272499966066bfaa810444c427cb653b0", size = 3403530, upload-time = "2025-09-24T13:36:50.825Z" }, - { url = "https://files.pythonhosted.org/packages/e1/66/b65ae067433f37fd4c2b89aec9b0ab31b35cd3846e77854e7ec47bfb9d13/pycurl-7.45.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:69f16d6709f3305da1043ee9edb5e1715212b75011779d58c4bd8fc9ad9fc88a", size = 3798020, upload-time = "2025-09-24T13:36:53.259Z" }, - { url = "https://files.pythonhosted.org/packages/5d/31/af6edb607b7d359b54df63ee561650fe7f94347f86e7c600e0042af595cc/pycurl-7.45.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:435f140afa59eb0cece57e6364348977af768add452d49a8547a26fc90dc2bfa", size = 3905273, upload-time = "2025-09-24T13:36:55.386Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/4825905afb55037db15d2d2c9a45253ca8951559ef5f62afd277c98d1ec2/pycurl-7.45.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ff50a3d787c3d059f330d7cb7325b40416bcb0f43f5006b396a6e61871df0ebf", size = 5247322, upload-time = "2025-09-24T13:36:57.568Z" }, - { url = "https://files.pythonhosted.org/packages/28/fc/716a3e9d335aa12f09471842e49e3db9eb792a06ddc6203a6fb1b90b7f7a/pycurl-7.45.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5ed0cab4e05558ba112111585ae2a85627ab48d6185547fc4ba8ec8e2cfecd0d", size = 4947252, upload-time = "2025-09-24T13:36:59.26Z" }, - { url = "https://files.pythonhosted.org/packages/43/05/b8274ad3740b76f029031921b1831f9a7f9fdc419333c85488b25dcde87a/pycurl-7.45.7-cp314-cp314t-win32.whl", hash = "sha256:e79f90c0af413933dedc79a1717f49076f08b32ff4a42776d6c38ba37125717e", size = 2824929, upload-time = "2025-09-24T13:37:01.665Z" }, - { url = "https://files.pythonhosted.org/packages/70/c1/3982528967a42eff566d7e94fce0c8a0cf293c398a38465f87709440f260/pycurl-7.45.7-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb72c53445b1c09315940c81a14a3635dfde6710aea703976ce9617209830e7", size = 3412632, upload-time = "2025-09-24T13:37:03.852Z" }, -] - -[[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 = "pymysql" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, -] - -[[package]] -name = "pysnmp" -version = "7.1.22" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/f7/63a4833b675f3f85296d85f2fddaed93d76799f29a813a2d5ca2bbe7fc50/pysnmp-7.1.22.tar.gz", hash = "sha256:37ac595c7f0c1c00514505939b4dcf5b4fd5a9ffe51b0349f60bb640c11b0f77", size = 259230, upload-time = "2025-10-26T22:09:56.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/da/9b6bc9517f6a69d03b70f43c4cfd1cca6289fdccabcf648204f2b0c6bc9b/pysnmp-7.1.22-py3-none-any.whl", hash = "sha256:57e704a6ba2bbf571d16cd5dc08b89bb3fa0ebeb5f4f26b87fececad3b3de7a6", size = 343380, upload-time = "2025-10-26T22:09:54.816Z" }, -] - -[[package]] -name = "pysnmp-mibs" -version = "0.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pysnmp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/7c/99ab192af934ed5d41ceef92a1b949b41652f29b46241b902ffec55642f4/pysnmp-mibs-0.1.6.tar.gz", hash = "sha256:00fa321ca598e8a14c0ad98702bf10fb5e9d57b7d06bb99dc818fea07683720c", size = 2113334, upload-time = "2016-02-13T23:55:21.888Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/33/4ce8b8cc60c871348ccd3d784415cb6b357d91ca158b63ef5c3e6f96bfe0/pysnmp_mibs-0.1.6-py2.py3-none-any.whl", hash = "sha256:5e153ebe8e767c07940cea435f866c623ff6b2376155c7da75085b08d3774d48", size = 2316964, upload-time = "2016-02-13T23:54:59.491Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "pytokens" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, - { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, - { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, - { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, - { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, - { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, - { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, - { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, - { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, -] - -[[package]] -name = "pytz" -version = "2026.1.post1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, -] - -[[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/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { 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" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -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 = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, -] - -[[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 = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, -] - -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - -[[package]] -name = "schema" -version = "0.7.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/2e/8da627b65577a8f130fe9dfa88ce94fcb24b1f8b59e0fc763ee61abef8b8/schema-0.7.8.tar.gz", hash = "sha256:e86cc08edd6fe6e2522648f4e47e3a31920a76e82cce8937535422e310862ab5", size = 45540, upload-time = "2025-10-11T13:15:40.281Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/75/aad85817266ac5285c93391711d231ca63e9ae7d42cd3ca37549e24ebe52/schema-0.7.8-py2.py3-none-any.whl", hash = "sha256:00bd977fadc7d9521bf289850cd8a8aa5f4948f575476b8daaa5c1b57af2dce1", size = 19108, upload-time = "2025-10-11T17:13:07.323Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "smmap" -version = "5.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, -] - -[[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 = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, -] - -[[package]] -name = "tomli-w" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "tzdata" -version = "2025.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] - -[[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" }, -] - -[[package]] -name = "wcwidth" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, -] - -[[package]] -name = "websocket-client" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, -] - -[[package]] -name = "wheel" -version = "0.46.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/92/3a64fa9639b8e290fe8630d8067a66f7c5510845c6d73686ad880c9b04d9/wheel-0.46.2.tar.gz", hash = "sha256:3d79e48fde9847618a5a181f3cc35764c349c752e2fe911e65fa17faab9809b0", size = 60274, upload-time = "2026-01-21T23:55:25.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/2c/5e079cefe955ae58e5a052fe037c850ce493eb7269dedeb960237e78fb0f/wheel-0.46.2-py3-none-any.whl", hash = "sha256:33ae60725d69eaa249bc1982e739943c23b34b58d51f1cb6253453773aca6e65", size = 29971, upload-time = "2026-01-21T23:55:24.447Z" }, -] - -[[package]] -name = "yamllint" -version = "1.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pathspec" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/a0/8fc2d68e132cf918f18273fdc8a1b8432b60d75ac12fdae4b0ef5c9d2e8d/yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d", size = 142446, upload-time = "2026-01-13T07:47:53.276Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, -] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] diff --git a/src/main/omnia-cli b/src/main/omnia-cli new file mode 100644 index 0000000000..5584cdc750 --- /dev/null +++ b/src/main/omnia-cli @@ -0,0 +1,714 @@ +#!/bin/bash + +# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ============================================================================= +# omnia-cli — Omnia Domain Status & Diagnostics CLI +# ============================================================================= +# +# Separated from omnia.sh to keep the main entrypoint clean. +# omnia.sh handles setup (venv, env). This CLI handles status & diagnostics. +# +# Usage: +# ./omnia-cli status [--project ] +# ./omnia-cli repo-manager [--project ] +# ./omnia-cli image-build [--project ] +# ./omnia-cli [--project ] +# ./omnia-cli version +# ./omnia-cli help [] +# ============================================================================= + +set -euo pipefail + +# ───────────────────────────────────────────────────────────────────────────── +# Constants +# ───────────────────────────────────────────────────────────────────────────── +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly ENV_FILE="$SCRIPT_DIR/omnia.env" + +# Color definitions (ANSI-C quoting for real escape characters) +readonly RED=$'\033[0;31m' +readonly GREEN=$'\033[0;32m' +readonly BLUE=$'\033[0;34m' +readonly YELLOW=$'\033[0;33m' +readonly CYAN=$'\033[0;36m' +readonly BOLD=$'\033[1m' +readonly DIM=$'\033[2m' +readonly NC=$'\033[0m' + +# Status symbols +readonly PASS="${GREEN}✔${NC}" +readonly FAIL="${RED}✗${NC}" +readonly WARN="${YELLOW}⚠${NC}" +readonly SKIP="${DIM}–${NC}" + +# Omnia release metadata +readonly OMNIA_RELEASE="2.3.0.0" + +# Known domains with their output status file patterns +declare -A DOMAIN_STATUS_FILES=( + [repo_manager]="repo_status.yml" + [image_build_manager]="build_status.yml" + [orchestrator]="orchestrator_status.yml" + [discovery]="discovery_status.yml" + [telemetry]="telemetry_status.yml" + [build_stream]="build_stream_status.yml" +) + +declare -A DOMAIN_DESCRIPTIONS=( + [repo_manager]="RPM repository synchronization (Pulp)" + [image_build_manager]="OS image building (MinIO + Registry + OpenCHAMI)" + [orchestrator]="Cluster orchestration and provisioning" + [discovery]="Hardware discovery and inventory" + [telemetry]="Monitoring and telemetry stack" + [build_stream]="CI/CD build pipeline (GitLab)" +) + +readonly DOMAIN_ORDER=( + repo_manager + image_build_manager + orchestrator + discovery + telemetry + build_stream +) + +# ───────────────────────────────────────────────────────────────────────────── +# Environment Loading +# ───────────────────────────────────────────────────────────────────────────── +load_env() { + if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + . "$ENV_FILE" + set +a + fi + + # Apply defaults + OMNIA_DATA_PATH="${OMNIA_DATA_PATH:-/opt/omnia}" + OMNIA_PROJECT_NAME="${OMNIA_PROJECT_NAME:-project_default}" + OMNIA_VENV_PATH="${OMNIA_VENV_PATH:-$OMNIA_DATA_PATH/venv}" + OMNIA_HOSTNAME="${OMNIA_HOSTNAME:-oim}" + OMNIA_DOMAIN_NAME="${OMNIA_DOMAIN_NAME:-omnia.cluster}" + OMNIA_ADMIN_NIC_IP="${OMNIA_ADMIN_NIC_IP:-172.16.107.254}" + + export OMNIA_DATA_PATH OMNIA_PROJECT_NAME OMNIA_VENV_PATH + export OMNIA_HOSTNAME OMNIA_DOMAIN_NAME OMNIA_ADMIN_NIC_IP +} + +# ───────────────────────────────────────────────────────────────────────────── +# Utility: Parse overall_status from a YAML file +# ───────────────────────────────────────────────────────────────────────────── +_parse_status() { + local file="$1" + grep -E '^\s*overall_status:' "$file" 2>/dev/null \ + | head -1 \ + | sed 's/.*: *"\?\([^"]*\)"\?/\1/' \ + | tr -d '[:space:]' +} + +_parse_yaml_key() { + local file="$1" key="$2" + grep -E "^\s*${key}:" "$file" 2>/dev/null \ + | head -1 \ + | sed 's/.*: *"\?\([^"]*\)"\?/\1/' \ + | tr -d '[:space:]' +} + +_resolve_output_dir() { + local domain="$1" project="$2" + local base="${OMNIA_DATA_PATH:-/opt/omnia}" + + # repo_manager uses a different path convention + if [ "$domain" = "repo_manager" ]; then + echo "${base}/repo_manager/output/${project}" + else + echo "${base}/${domain}/output/${project}" + fi +} + +_last_modified() { + local dir="$1" + local newest + newest=$(find "$dir" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-) + if [ -n "$newest" ]; then + local mod_time + mod_time=$(stat -c '%y' "$newest" 2>/dev/null | cut -d. -f1) + echo "${mod_time} ($(basename "$newest"))" + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +# Command: status — Show all domain statuses for a project +# ───────────────────────────────────────────────────────────────────────────── +cmd_status() { + local project="$1" + + echo "" + echo -e "${BOLD}${BLUE}Omnia Domain Status${NC} ${DIM}(project: ${project})${NC}" + echo -e "${DIM}$(printf '%.0s─' {1..60})${NC}" + echo "" + + local completed=0 + local total=0 + + for domain in "${DOMAIN_ORDER[@]}"; do + total=$((total + 1)) + local output_dir + output_dir=$(_resolve_output_dir "$domain" "$project") + local status_file_name="${DOMAIN_STATUS_FILES[$domain]}" + local desc="${DOMAIN_DESCRIPTIONS[$domain]}" + + # Format domain name for display + local display_name + display_name=$(echo "$domain" | tr '_' ' ') + + if [ ! -d "$output_dir" ]; then + echo -e " ${SKIP} ${BOLD}${display_name}${NC}" + echo -e " ${DIM}${desc}${NC}" + echo -e " ${DIM}No output directory${NC}" + echo "" + continue + fi + + # Look for status file + local status_file="${output_dir}/${status_file_name}" + local overall_status="" + + if [ -f "$status_file" ]; then + overall_status=$(_parse_status "$status_file") + else + # Fallback: search for any *status*.yml + for sf in "${output_dir}"/*status*.yml "${output_dir}"/*status*.yaml; do + if [ -f "$sf" ]; then + status_file="$sf" + overall_status=$(_parse_status "$sf") + break + fi + done + fi + + if [ "$overall_status" = "success" ]; then + completed=$((completed + 1)) + echo -e " ${PASS} ${BOLD}${display_name}${NC} ${GREEN}completed${NC}" + elif [ -n "$overall_status" ]; then + echo -e " ${FAIL} ${BOLD}${display_name}${NC} ${RED}${overall_status}${NC}" + elif [ -d "$output_dir" ]; then + echo -e " ${WARN} ${BOLD}${display_name}${NC} ${YELLOW}output exists, no status${NC}" + else + echo -e " ${SKIP} ${BOLD}${display_name}${NC}" + fi + + echo -e " ${DIM}${desc}${NC}" + + # Show last modified + local last_mod + last_mod=$(_last_modified "$output_dir") + if [ -n "$last_mod" ]; then + echo -e " ${DIM}Last: ${last_mod}${NC}" + fi + echo "" + done + + echo -e "${DIM}$(printf '%.0s─' {1..60})${NC}" + echo -e " ${BOLD}${completed}/${total}${NC} domains completed for project ${CYAN}${project}${NC}" + echo "" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Command: repo-manager — Detailed repo_manager status +# ───────────────────────────────────────────────────────────────────────────── +cmd_repo_manager() { + local project="$1" + local repo_dir + repo_dir=$(_resolve_output_dir "repo_manager" "$project") + local status_file="${repo_dir}/repo_status.yml" + local pkg_file="${repo_dir}/functional_group_packages.yml" + + echo "" + echo -e "${BOLD}${BLUE}Repo Manager Status${NC} ${DIM}(project: ${project})${NC}" + echo -e "${DIM}$(printf '%.0s─' {1..60})${NC}" + echo "" + echo -e " Output dir: ${DIM}${repo_dir}${NC}" + echo "" + + # Check directory + if [ ! -d "$repo_dir" ]; then + echo -e " ${FAIL} Output directory not found" + echo "" + echo -e " ${YELLOW}Run repo_manager first or copy output files:${NC}" + echo -e " mkdir -p ${repo_dir}" + echo -e " cp repo_status.yml ${repo_dir}/" + echo -e " cp functional_group_packages.yml ${repo_dir}/" + echo "" + return 1 + fi + echo -e " ${PASS} Directory exists" + + # Check repo_status.yml + if [ ! -f "$status_file" ]; then + echo -e " ${FAIL} repo_status.yml missing" + echo "" + return 1 + fi + + # Parse overall_status + local overall_status + overall_status=$(_parse_status "$status_file") + if [ "$overall_status" = "success" ]; then + echo -e " ${PASS} repo_status.yml (overall_status: ${GREEN}${overall_status}${NC})" + elif [ -n "$overall_status" ]; then + echo -e " ${FAIL} repo_status.yml (overall_status: ${RED}${overall_status}${NC})" + else + echo -e " ${WARN} repo_status.yml (overall_status: ${YELLOW}unknown${NC})" + fi + + # Parse OS metadata + local os_type os_ver + os_type=$(_parse_yaml_key "$status_file" "cluster_os_type") + os_ver=$(_parse_yaml_key "$status_file" "cluster_os_version") + if [ -n "$os_type" ]; then + echo -e " ${PASS} OS: ${os_type} ${os_ver}" + fi + + # Check functional_group_packages.yml (from package_list or default) + local pkg_list_path + pkg_list_path=$(grep -E '^\s*package_list:' "$status_file" 2>/dev/null | head -1 | sed 's/.*: *//' | tr -d '[:space:]') + if [ -n "$pkg_list_path" ] && [ -f "$pkg_list_path" ]; then + echo -e " ${PASS} functional_group_packages (from package_list: ${DIM}${pkg_list_path}${NC})" + elif [ -f "$pkg_file" ]; then + echo -e " ${PASS} functional_group_packages (${DIM}${pkg_file}${NC})" + else + echo -e " ${FAIL} functional_group_packages ${RED}MISSING${NC}" + fi + + # Check certificates + local cert_path + cert_path=$(grep -E '^\s*server_crt:' "$status_file" 2>/dev/null | head -1 | sed 's/.*: *//' | tr -d '[:space:]') + if [ -n "$cert_path" ]; then + if [ -f "$cert_path" ]; then + echo -e " ${PASS} Pulp certificate (${DIM}${cert_path}${NC})" + else + echo -e " ${FAIL} Pulp certificate ${RED}MISSING${NC} (expected: ${cert_path})" + fi + fi + + # Count repos + local x86_count aarch64_count + x86_count=$(grep -c 'x86_64' "$status_file" 2>/dev/null || echo "0") + aarch64_count=$(grep -c 'aarch64' "$status_file" 2>/dev/null || echo "0") + echo "" + echo -e " RPM repos (x86_64 lines): ${x86_count}" + echo -e " RPM repos (aarch64 lines): ${aarch64_count}" + + echo "" + if [ "$overall_status" = "success" ]; then + echo -e " ${GREEN}Repo manager output is ready.${NC}" + return 0 + else + echo -e " ${YELLOW}Repo manager output may not be complete. Check overall_status.${NC}" + return 1 + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +# Command: image-build — Detailed image_build_manager status +# ───────────────────────────────────────────────────────────────────────────── +cmd_image_build() { + local project="$1" + local output_dir + output_dir=$(_resolve_output_dir "image_build_manager" "$project") + local status_file="${output_dir}/build_status.yml" + + echo "" + echo -e "${BOLD}${BLUE}Image Build Manager Status${NC} ${DIM}(project: ${project})${NC}" + echo -e "${DIM}$(printf '%.0s─' {1..60})${NC}" + echo "" + echo -e " Output dir: ${DIM}${output_dir}${NC}" + echo "" + + if [ ! -d "$output_dir" ]; then + echo -e " ${SKIP} No output directory — image_build_manager has not been run" + echo "" + return 1 + fi + + # Check build_status.yml + if [ ! -f "$status_file" ]; then + echo -e " ${WARN} build_status.yml not found" + # Check if there are any status files at all + local found_any=0 + for sf in "${output_dir}"/*status*.yml "${output_dir}"/*status*.yaml; do + if [ -f "$sf" ]; then + found_any=1 + local st + st=$(_parse_status "$sf") + echo -e " ${WARN} $(basename "$sf"): ${st:-unknown}" + fi + done + if [ "$found_any" -eq 0 ]; then + echo -e " ${SKIP} No status files found" + fi + echo "" + return 1 + fi + + local overall_status + overall_status=$(_parse_status "$status_file") + + if [ "$overall_status" = "success" ]; then + echo -e " ${PASS} build_status.yml (overall_status: ${GREEN}${overall_status}${NC})" + elif [ -n "$overall_status" ]; then + echo -e " ${FAIL} build_status.yml (overall_status: ${RED}${overall_status}${NC})" + else + echo -e " ${WARN} build_status.yml (overall_status: ${YELLOW}unknown${NC})" + fi + + # Count built images + local image_count + image_count=$(grep -c 'functional_group:' "$status_file" 2>/dev/null || echo "0") + echo -e " Images built: ${image_count}" + + # Show per-arch counts + local x86_count aarch64_count + x86_count=$(grep -c 'x86_64' "$status_file" 2>/dev/null || echo "0") + aarch64_count=$(grep -c 'aarch64' "$status_file" 2>/dev/null || echo "0") + echo -e " x86_64 entries: ${x86_count}" + echo -e " aarch64 entries: ${aarch64_count}" + + # Show log file if exists (logs are under shared_path/log/, not output_dir) + local ibm_base="${OMNIA_DATA_PATH:-/opt/omnia}/image_build_manager" + local log_dir="${ibm_base}/log/${project}" + if [ -d "$log_dir" ]; then + local latest_log + latest_log=$(find "$log_dir" -name "*.log" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-) + if [ -n "$latest_log" ]; then + echo "" + echo -e " Latest log: ${DIM}${latest_log}${NC}" + fi + fi + + # Last modified + local last_mod + last_mod=$(_last_modified "$output_dir") + if [ -n "$last_mod" ]; then + echo -e " Last modified: ${DIM}${last_mod}${NC}" + fi + + echo "" + if [ "$overall_status" = "success" ]; then + echo -e " ${GREEN}Image build completed successfully.${NC}" + return 0 + else + echo -e " ${YELLOW}Image build may not be complete.${NC}" + return 1 + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +# Command: — Status for any domain +# ───────────────────────────────────────────────────────────────────────────── +cmd_domain_status() { + local domain="$1" project="$2" + local output_dir + output_dir=$(_resolve_output_dir "$domain" "$project") + local status_file_name="${DOMAIN_STATUS_FILES[$domain]:-status.yml}" + local desc="${DOMAIN_DESCRIPTIONS[$domain]:-$domain domain}" + local display_name + display_name=$(echo "$domain" | tr '_' ' ') + + echo "" + echo -e "${BOLD}${BLUE}${display_name} Status${NC} ${DIM}(project: ${project})${NC}" + echo -e "${DIM}$(printf '%.0s─' {1..60})${NC}" + echo "" + echo -e " ${DIM}${desc}${NC}" + echo -e " Output dir: ${DIM}${output_dir}${NC}" + echo "" + + if [ ! -d "$output_dir" ]; then + echo -e " ${SKIP} No output directory — ${domain} has not been run" + echo "" + return 1 + fi + + # Find status files + local found=0 + local status_file="${output_dir}/${status_file_name}" + + if [ -f "$status_file" ]; then + found=1 + local overall_status + overall_status=$(_parse_status "$status_file") + if [ "$overall_status" = "success" ]; then + echo -e " ${PASS} ${status_file_name} (overall_status: ${GREEN}${overall_status}${NC})" + elif [ -n "$overall_status" ]; then + echo -e " ${FAIL} ${status_file_name} (overall_status: ${RED}${overall_status}${NC})" + else + echo -e " ${WARN} ${status_file_name} (overall_status: ${YELLOW}unknown${NC})" + fi + fi + + # Also check for any other status files + for sf in "${output_dir}"/*status*.yml "${output_dir}"/*status*.yaml; do + if [ -f "$sf" ] && [ "$sf" != "$status_file" ]; then + found=1 + local st + st=$(_parse_status "$sf") + local fname + fname=$(basename "$sf") + if [ "$st" = "success" ]; then + echo -e " ${PASS} ${fname}: ${GREEN}${st}${NC}" + elif [ -n "$st" ]; then + echo -e " ${FAIL} ${fname}: ${RED}${st}${NC}" + else + echo -e " ${WARN} ${fname}: ${YELLOW}(no overall_status)${NC}" + fi + fi + done + + if [ "$found" -eq 0 ]; then + echo -e " ${WARN} No status files found in output directory" + fi + + # Last modified + local last_mod + last_mod=$(_last_modified "$output_dir") + if [ -n "$last_mod" ]; then + echo "" + echo -e " Last modified: ${DIM}${last_mod}${NC}" + fi + echo "" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Command: version +# ───────────────────────────────────────────────────────────────────────────── +cmd_version() { + echo -e "${BOLD}Omnia Infrastructure Manager${NC}" + echo -e " Release: ${GREEN}${OMNIA_RELEASE}${NC}" + + # Try git tag + local git_root="$SCRIPT_DIR" + while [ "$git_root" != "/" ] && [ ! -d "$git_root/.git" ]; do + git_root="$(dirname "$git_root")" + done + if [ "$git_root" != "/" ] && [ -d "$git_root/.git" ]; then + local tag + tag=$(cd "$git_root" && git tag --points-at HEAD 2>/dev/null | head -n 1) + if [ -n "$tag" ]; then + echo -e " Git tag: ${GREEN}${tag}${NC}" + fi + fi + + echo -e " Data: ${OMNIA_DATA_PATH:-/opt/omnia}" + echo -e " Project: ${OMNIA_PROJECT_NAME:-project_default}" + + if [ -d "${OMNIA_VENV_PATH:-/opt/omnia/venv}" ]; then + echo -e " Venv: ${GREEN}${OMNIA_VENV_PATH:-/opt/omnia/venv}${NC}" + else + echo -e " Venv: ${YELLOW}(not set up)${NC}" + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +# Help — Domain-specific +# ───────────────────────────────────────────────────────────────────────────── +show_domain_help() { + local domain="$1" + local display_name + display_name=$(echo "$domain" | tr '_' '-') + + case "$domain" in + repo_manager) + cat <] + +CHECKS: + - Output directory exists at \${OMNIA_DATA_PATH}/repo_manager/output// + - repo_status.yml exists and overall_status == "success" + - functional_group_packages.yml found (from package_list or default) + - Pulp certificate file exists at path from repo_status.yml + - RPM repo URL counts for x86_64 and aarch64 + +ENVIRONMENT: + OMNIA_DATA_PATH Root data directory (default: /opt/omnia) + OMNIA_PROJECT_NAME Project name (default: project_default) +EOF + ;; + image_build_manager) + cat <] + +CHECKS: + - Output directory exists + - build_status.yml exists and overall_status == "success" + - Per-architecture image counts + - Latest build log location + +ENVIRONMENT: + OMNIA_DATA_PATH Root data directory (default: /opt/omnia) + OMNIA_PROJECT_NAME Project name (default: project_default) +EOF + ;; + *) + cat <] + +CHECKS: + - Output directory exists at \${OMNIA_DATA_PATH}/${domain}/output// + - Status file: ${DOMAIN_STATUS_FILES[$domain]:-_status.yml} + - overall_status validation +EOF + ;; + esac +} + +# ───────────────────────────────────────────────────────────────────────────── +# Help — Main +# ───────────────────────────────────────────────────────────────────────────── +show_help() { + cat < [--project ] + +COMMANDS: + ${BOLD}status${NC} Show all domain statuses for a project + ${BOLD}repo-manager${NC} Detailed repo_manager diagnostics + ${BOLD}image-build${NC} Detailed image_build_manager diagnostics + ${BOLD}orchestrator${NC} Orchestrator status + ${BOLD}discovery${NC} Discovery domain status + ${BOLD}telemetry${NC} Telemetry stack status + ${BOLD}build-stream${NC} Build stream (GitLab) status + ${BOLD}version${NC} Show Omnia version info + ${BOLD}help${NC} [] Show help (or domain-specific help) + +OPTIONS: + --project Project name (default: \$OMNIA_PROJECT_NAME or project_default) + +ENVIRONMENT: + All configuration via src/main/omnia.env: + OMNIA_DATA_PATH Root data directory (default: /opt/omnia) + OMNIA_PROJECT_NAME Project name (default: project_default) + OMNIA_ADMIN_NIC_IP Admin NIC IP (default: 172.16.107.254) + OMNIA_VENV_PATH Python venv path (default: /opt/omnia/venv) + +EXAMPLES: + omnia-cli status # All domains, default project + omnia-cli status --project my_cluster # All domains, custom project + omnia-cli repo-manager # Detailed repo check + omnia-cli image-build --project prod # Image build status for 'prod' + omnia-cli help repo-manager # Domain-specific help + +SEE ALSO: + omnia.sh --setup-venv Set up Python + Ansible venv + omnia.sh --help Main Omnia setup help +EOF +} + +# ───────────────────────────────────────────────────────────────────────────── +# Argument Parsing +# ───────────────────────────────────────────────────────────────────────────── +_domain_from_arg() { + case "$1" in + repo-manager|repo_manager) echo "repo_manager" ;; + image-build|image_build|image-build-manager|image_build_manager) echo "image_build_manager" ;; + orchestrator) echo "orchestrator" ;; + discovery) echo "discovery" ;; + telemetry) echo "telemetry" ;; + build-stream|build_stream) echo "build_stream" ;; + *) echo "" ;; + esac +} + +# ───────────────────────────────────────────────────────────────────────────── +# Main Dispatch +# ───────────────────────────────────────────────────────────────────────────── +main() { + load_env + + local command="${1:-help}" + shift || true + + # Parse --project flag from remaining args + local project="${OMNIA_PROJECT_NAME:-project_default}" + while [ $# -gt 0 ]; do + case "$1" in + --project|-p) + project="${2:-project_default}" + shift 2 || { echo -e "${RED}--project requires a value${NC}"; exit 1; } + ;; + *) + # Positional arg — treat as project for backward compat + project="$1" + shift + ;; + esac + done + + case "$command" in + status) + cmd_status "$project" + ;; + repo-manager|repo_manager) + cmd_repo_manager "$project" + ;; + image-build|image_build|image-build-manager|image_build_manager) + cmd_image_build "$project" + ;; + version|--version|-v) + cmd_version + ;; + help|--help|-h) + if [ -n "${1:-}" ]; then + local domain + domain=$(_domain_from_arg "$1") + if [ -n "$domain" ]; then + show_domain_help "$domain" + else + echo -e "${RED}Unknown domain: $1${NC}" + show_help + fi + else + show_help + fi + ;; + *) + # Try as a domain name + local domain + domain=$(_domain_from_arg "$command") + if [ -n "$domain" ]; then + cmd_domain_status "$domain" "$project" + else + echo -e "${RED}Unknown command: ${command}${NC}" + echo "" + show_help + exit 1 + fi + ;; + esac +} + +main "$@" diff --git a/src/main/omnia.env b/src/main/omnia.env new file mode 100644 index 0000000000..c6a473f1bc --- /dev/null +++ b/src/main/omnia.env @@ -0,0 +1,64 @@ +# ============================================================================= +# omnia.env — Omnia Environment Configuration +# ============================================================================= +# +# FILL THIS FILE BEFORE RUNNING omnia.sh OR ANY ANSIBLE PLAYBOOK. +# +# This file is the SINGLE source of configuration for the Omnia deployment. +# Uncomment and set the required variables below. Optional variables have +# sensible defaults — change them only if your setup differs. +# +# Usage: +# 1. Edit this file: vi src/main/omnia.env +# 2. Setup venv: ./omnia.sh --setup-venv +# -- OR -- +# Source manually: set -a; source src/main/omnia.env; set +a +# Run playbooks: cd src/ && ansible-playbook playbooks/.yml +# +# ============================================================================= + +# ┌───────────────────────────────────────────────────────────────────────────┐ +# │ REQUIRED — You MUST set these before running anything │ +# └───────────────────────────────────────────────────────────────────────────┘ + +# Admin NIC IP address of the OIM host. +# This is the IP used for Pulp, S3, container registry, and provisioning. +OMNIA_ADMIN_NIC_IP=172.16.107.254 + +# ┌───────────────────────────────────────────────────────────────────────────┐ +# │ OPTIONAL — Defaults work for most deployments │ +# └───────────────────────────────────────────────────────────────────────────┘ + +# Root data directory for all Omnia persistent data (NFS-mounted or local). +# All component data paths are derived from this root. +OMNIA_DATA_PATH=/opt/omnia + +# Project name — maps to input/output subdirectories. +# Override for multi-project deployments. +OMNIA_PROJECT_NAME=project_default + +# Short hostname of the OIM host (NOT FQDN). +OMNIA_HOSTNAME=oim + +# Domain name of the OIM host. +OMNIA_DOMAIN_NAME=omnia.cluster + +# Path to the shared Omnia Python virtual environment. +# Created by: ./omnia.sh --setup-venv +OMNIA_VENV_PATH=/opt/omnia/venv + +# Omnia release version. +# OMNIA_VERSION=2.2 + +# ┌───────────────────────────────────────────────────────────────────────────┐ +# │ COMPONENT OVERRIDES — Change only for non-standard layouts │ +# └───────────────────────────────────────────────────────────────────────────┘ +# Each component stores data under OMNIA_DATA_PATH//. +# Uncomment to override: + +# IMAGE_BUILD_MANAGER_DATA_PATH=${OMNIA_DATA_PATH}/image_build_manager +# REPO_MANAGER_DATA_PATH=${OMNIA_DATA_PATH}/repo_manager +# DISCOVERY_DATA_PATH=${OMNIA_DATA_PATH}/discovery +# ORCHESTRATOR_DATA_PATH=${OMNIA_DATA_PATH}/orchestrator +# TELEMETRY_DATA_PATH=${OMNIA_DATA_PATH}/telemetry +# BUILD_STREAM_DATA_PATH=${OMNIA_DATA_PATH}/build_stream diff --git a/src/main/omnia.sh b/src/main/omnia.sh index c9a017bb5e..ffe98a0bab 100755 --- a/src/main/omnia.sh +++ b/src/main/omnia.sh @@ -14,2957 +14,340 @@ # See the License for the specific language governing permissions and # limitations under the License. +# ============================================================================= +# omnia.sh — Omnia Infrastructure Manager (OIM) CLI +# ============================================================================= +# +# Prerequisites: +# 1. Edit src/main/omnia.env with your environment settings +# 2. Run: ./omnia.sh --setup-venv (one-time Python + Ansible setup) +# +# All configuration is via environment variables in omnia.env. +# No interactive prompts — fill omnia.env before running. +# ============================================================================= + +set -euo pipefail + +# ───────────────────────────────────────────────────────────────────────────── +# Constants +# ───────────────────────────────────────────────────────────────────────────── +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SRC_DIR="$(dirname "$SCRIPT_DIR")" +readonly REPO_ROOT="$(dirname "$SRC_DIR")" +readonly ENV_FILE="$SCRIPT_DIR/omnia.env" + +# Color definitions +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly BLUE='\033[0;34m' +readonly YELLOW='\033[0;33m' +readonly NC='\033[0m' + +# Omnia release metadata +readonly OMNIA_RELEASE="2.2.0.0" + +# Known domain directories (each may contain requirements.txt / requirements.yml) +readonly DOMAINS=( + "build_stream" + "discovery" + "image_build_manager" + "orchestrator" + "repo_manager" + "telemetry" + "utils" +) -# This script is used to generate the Omnia core docker image. -# The image is based on Fedora and uses systemd to start all of the necessary -# services. -# -# This script prompts the user for the Omnia shared path and the root -# password. It then checks if the Omnia shared path exists. -# -# The script checks if the ssh key file exists. If it does not exist, a new ssh - -# Color Definitions -RED='\033[0;31m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color -YELLOW='\033[0;33m' -WHITE='\033[0;37m' - -# Function to get version from git tag -get_version_from_git_tag() { - local tag_version - local script_dir - local git_root - - # First try to get script directory - if [ -L "${BASH_SOURCE[0]}" ]; then - # If script is a symlink, resolve it - script_dir="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" - else - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - fi - - # Find git repository by traversing up from script directory - git_root="$script_dir" - while [ "$git_root" != "/" ] && [ ! -d "$git_root/.git" ]; do - git_root="$(dirname "$git_root")" - done - - # If we found a git repository, run git command - if [ "$git_root" != "/" ] && [ -d "$git_root/.git" ]; then - tag_version=$(cd "$git_root" && git tag --points-at HEAD 2>/dev/null | head -n 1) - else - tag_version="" - fi - - if [ -z "$tag_version" ]; then - echo "" - return 1 - fi - - # If tag starts with 'v', strip it and return the rest - if [[ "$tag_version" =~ ^v(.+)$ ]]; then - echo "${BASH_REMATCH[1]}" - return 0 - fi - - # Tag doesn't start with 'v', return as-is - echo "$tag_version" - return 0 -} - -# Function to validate version string format -validate_version_string() { - local version="$1" - - # Check if version is empty - if [ -z "$version" ]; then - return 1 - fi - - # Basic version format validation: X.Y.Z.W or X.Y.Z.W-rcN or X.Y.Z.W-suffix - if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then - return 0 - fi - - return 1 -} - -# Function to get version for metadata (from git tag or default) -get_metadata_version() { - local default_version="${1:-$omnia_release}" - local git_tag_version - - git_tag_version=$(get_version_from_git_tag) - - if [ -n "$git_tag_version" ] && validate_version_string "$git_tag_version"; then - echo "$git_tag_version" - else - echo "$default_version" - fi -} - -omnia_release=2.2.0.0 -omnia_prev_release=2.1.0.0 - -core_container_status=false -omnia_path="" -hashed_passwd="" -domain_name="" - -is_local_ip() { - local ip_to_check="$1" - - # Get all local IP addresses (excluding loopback) - local local_ips - local_ips=$(hostname -I) - - # Check if the IP matches any local IP - if echo "$local_ips" | grep -qw "$ip_to_check"; then - return 0 # IP is local - else - return 1 # IP is not local - fi -} - -# Version configuration variables -OMNIA_CORE_CONTAINER_TAG="2.2" # Default container tag -OMNIA_VERSION="" # Will be read from metadata -TARGET_OMNIA_VERSION="" # Target version for upgrade -TARGET_CONTAINER_TAG="" # Target container tag for upgrade - -# Centralized version list (in chronological order) -# Note: Include RC milestones so upgrades from RC to RC/GA appear -ALL_OMNIA_VERSIONS=( - "2.1.0.0" - "2.2.0.0" -) - -# Container-side paths (used inside podman exec commands) -CONTAINER_INPUT_DIR="/opt/omnia/input" -CONTAINER_BACKUPS_DIR="/opt/omnia/backups" -CONTAINER_METADATA_FILE="/opt/omnia/.data/oim_metadata.yml" - -# Function to get available upgrade versions (higher than current) -get_available_upgrade_versions() { - local current_version="$1" - local available_versions=() - local version_descriptions=() - - # Find versions higher than current - local found_current=false - for version in "${ALL_OMNIA_VERSIONS[@]}"; do - if [ "$version" = "$current_version" ]; then - found_current=true - continue - fi - - if [ "$found_current" = true ]; then - # Skip RC targets; only offer GA paths - if [[ "$version" == *-rc* ]]; then - continue - fi - available_versions+=("$version") - - # Generate description based on upgrade type - local current_tag=$(get_container_tag_from_version "$current_version") - local target_tag=$(get_container_tag_from_version "$version") - - if [ "$current_tag" = "$target_tag" ]; then - version_descriptions+=("Patch upgrade to $version (container restart only)") - else - version_descriptions+=("Major upgrade to $version (container swap required)") - fi - fi - done - - # Return arrays - printf '%s\n' "${available_versions[@]}" - printf '%s\n' "${version_descriptions[@]}" -} - -# Function to get available rollback versions (lower than current) -get_available_rollback_versions() { - local current_version="$1" - local normalized_current_version="${current_version%%-rc*}" - if [ -z "$normalized_current_version" ]; then - normalized_current_version="$current_version" - fi - local available_versions=() - - # Find versions lower than current - for version in "${ALL_OMNIA_VERSIONS[@]}"; do - if [ "$version" = "$normalized_current_version" ]; then - break - fi - # Skip RC targets for rollback choices - if [[ "$version" == *-rc* ]]; then - continue - fi - available_versions+=("$version") - done - - # Return array (reverse order for rollback - newest first) - local reversed_versions=() - for ((i=${#available_versions[@]}-1; i>=0; i--)); do - reversed_versions+=("${available_versions[$i]}") - done - - printf '%s\n' "${reversed_versions[@]}" -} - -# Function to perform same-tag rollback (container restart only) -rollback_same_tag() { - local target_version="$1" - local current_version="$2" - local backup_path="$3" - - echo "[INFO] [ROLLBACK] Phase: Same-Tag Rollback" - echo "[INFO] [ROLLBACK] Rolling back to $target_version within same container tag" - - # Verify container is running - if ! podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo "[ERROR] [ROLLBACK] Container is not running for same-tag rollback" - return 1 - fi - - # Restore files from backup (input, metadata, container config) - if [ -n "$backup_path" ]; then - echo "[INFO] [ROLLBACK] Restoring files from backup: $backup_path" - if ! restore_from_backup "$backup_path"; then - echo "[ERROR] [ROLLBACK] Failed to restore files from backup" - return 1 - fi - fi - - echo "[INFO] [ROLLBACK] Restarting container to apply changes..." - - # Restart container to apply changes - if ! systemctl restart omnia_core.service; then - echo "[ERROR] [ROLLBACK] Failed to restart container service" - echo "[ERROR] [ROLLBACK] Rollback failed: Container restart failed" - return 1 - fi - - # Wait for container to be healthy after restart - echo "[INFO] [ROLLBACK] Waiting for container health check after restart (30s)" - local health_timeout=30 - local health_count=0 - - while [ $health_count -lt $health_timeout ]; do - if podman ps --format '{{.Names}} {{.Status}}' | grep -E "omnia_core.*Up" | grep -q "healthy\|Up"; then - echo "[INFO] [ROLLBACK] Container is healthy after restart" - break - fi - sleep 1 - health_count=$((health_count + 1)) - echo -n "." - done - - if [ $health_count -ge $health_timeout ]; then - echo "" - echo "[ERROR] [ROLLBACK] Container failed to become healthy within 30 seconds after restart" - echo "[ERROR] [ROLLBACK] Rollback failed: Container health check failed" - return 1 - fi - - # Verify version from restored metadata - local updated_version=$(get_current_omnia_version) - if [ "$updated_version" != "$target_version" ]; then - echo "[ERROR] [ROLLBACK] Version verification failed" - echo "[ERROR] [ROLLBACK] Expected: $target_version, Found: $updated_version" - return 1 - fi - - echo "[INFO] [ROLLBACK] Same-tag rollback completed successfully" - echo "[INFO] [ROLLBACK] Version rolled back to: $updated_version" - return 0 -} - -# Function to validate container image availability and show build instructions -validate_container_image() { - local target_version="$1" - local target_container_tag="$2" - local operation="${3:-upgrade}" - - echo -e "${BLUE}Validating target container image: omnia_core:$target_container_tag${NC}" - if ! podman inspect "omnia_core:$target_container_tag" >/dev/null 2>&1; then - echo "" - echo -e "${RED}================================================================================${NC}" - echo -e "${RED}ERROR: Target container image not found locally${NC}" - echo -e "${RED}================================================================================${NC}" - echo -e "${YELLOW}Required image:${NC} omnia_core:$target_container_tag" - echo "" - echo -e "${YELLOW}Omnia does not pull images from Docker Hub.${NC}" - echo -e "${YELLOW}You must build the container image locally before proceeding.${NC}" - echo "" - echo -e "${BLUE}Build the required image using:${NC}" - echo "" - echo -e " ${BLUE}./omnia.sh --build${NC}" - echo "" - echo -e "${BLUE}After the image is built successfully, re-run:${NC}" - echo -e " ${BLUE}./omnia.sh --$operation${NC}" - echo "" - echo -e "${RED}================================================================================${NC}" - return 1 - fi - - echo -e "${GREEN}✓ Target image available locally: omnia_core:$target_container_tag${NC}" - return 0 -} - -# Function to get container tag from omnia version -get_container_tag_from_version() { - local version="$1" - - case "$version" in - 2.1.*) - echo "2.1" - ;; - 2.2.*) - echo "2.2" - ;; - *) - # All other versions use major.minor as tag - echo "$(echo "$version" | awk -F. '{print $1"."$2}')" - ;; - esac -} - -# Function to read current omnia version from metadata -get_current_omnia_version() { - if podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - podman exec omnia_core cat /opt/omnia/.data/oim_metadata.yml 2>/dev/null | grep "^omnia_version:" | awk '{print $2}' | tr -d '"' - else - echo "" - fi -} - -# Update metadata with git tag version from inside container -update_metadata_with_git_tag() { - local default_version="${1:-$omnia_prev_release}" - - podman exec -u root omnia_core bash -c ' - set -e - - cd /omnia || exit 0 - git_tag_version=$(git tag --points-at HEAD 2>/dev/null | head -n 1 || true) - - if [[ "$git_tag_version" =~ ^v(.+)$ ]]; then - git_tag_version="${BASH_REMATCH[1]}" - fi - - if [[ "$git_tag_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then - metadata_version="$git_tag_version" - else - metadata_version="'"$default_version"'" - fi - - if [ -f '"'$CONTAINER_METADATA_FILE'"' ]; then - if grep -q "^omnia_version:" '"'$CONTAINER_METADATA_FILE'"'; then - sed -i "s/^omnia_version:.*/omnia_version: $metadata_version/" '"'$CONTAINER_METADATA_FILE'"' - else - echo "omnia_version: $metadata_version" >> '"'$CONTAINER_METADATA_FILE'"' - fi - echo "[INFO] Updated omnia_version to: $metadata_version" - fi - ' || true -} - -show_post_upgrade_instructions() { - local upgraded_version="$1" - - echo "" - echo -e "${YELLOW}================================================================================${NC}" - echo -e "${YELLOW} IMPORTANT POST-UPGRADE STEP${NC}" - echo -e "${YELLOW}================================================================================${NC}" - echo "" - echo -e "${BLUE}NEXT REQUIRED ACTION:${NC}" - echo -e "${YELLOW}You must now run the upgrade playbooks inside the omnia_core container:${NC}" - echo "" - echo -e "${GREEN}Step 1: Prepare upgrade (transform inputs, restore credentials)${NC}" - echo -e "${GREEN} ansible-playbook /omnia/upgrade/prepare_upgrade.yml${NC}" - echo "" - echo -e "${GREEN}Step 2: Execute upgrade${NC}" - echo -e "${GREEN} ansible-playbook /omnia/upgrade/upgrade.yml${NC}" - echo "" - echo -e "${YELLOW}Note: Run these commands after the container is fully healthy and stable${NC}" - echo -e "${YELLOW}================================================================================${NC}" - echo "" -} - -# Host-side paths (initialized dynamically after omnia_path is set) -OMNIA_INPUT_DIR="" -OMNIA_METADATA_DIR="" -OMNIA_METADATA_FILE="" - -update_metadata_upgrade_backup_dir() { - local backup_dir="$1" - - if ! podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo "[ERROR] [ORCHESTRATOR] omnia_core container is not running" - return 1 - fi - - podman exec -u root omnia_core bash -c " - set -e - if [ ! -f '$CONTAINER_METADATA_FILE' ]; then - echo '[ERROR] Metadata file not found inside container: $CONTAINER_METADATA_FILE' >&2 - exit 1 - fi - - # Get current omnia_version to store as previous_omnia_version - current_version=\$(grep '^omnia_version:' '$CONTAINER_METADATA_FILE' | cut -d':' -f2 | tr -d ' \t\n\r') - - # Update upgrade_backup_dir - if grep -q '^upgrade_backup_dir:' '$CONTAINER_METADATA_FILE'; then - sed -i 's|^upgrade_backup_dir:.*|upgrade_backup_dir: ${backup_dir}|' '$CONTAINER_METADATA_FILE' - else - echo 'upgrade_backup_dir: ${backup_dir}' >> '$CONTAINER_METADATA_FILE' - fi - - # Update previous_omnia_version - if [ -n \"\$current_version\" ]; then - if grep -q '^previous_omnia_version:' '$CONTAINER_METADATA_FILE'; then - sed -i \"s|^previous_omnia_version:.*|previous_omnia_version: \$current_version|\" '$CONTAINER_METADATA_FILE' - else - echo \"previous_omnia_version: \$current_version\" >> '$CONTAINER_METADATA_FILE' - fi - fi - " -} - -# Resolve the upgrade guard lock path (container or host shared path) -get_upgrade_guard_lock_path() { - local upgrade_guard_lock_container="/opt/omnia/.data/upgrade_in_progress.lock" - local upgrade_guard_lock_host - upgrade_guard_lock_host=$(podman exec -u root omnia_core grep '^oim_shared_path:' /opt/omnia/.data/oim_metadata.yml 2>/dev/null | cut -d':' -f2- | tr -d ' \t\n\r') - if [ -n "$upgrade_guard_lock_host" ]; then - upgrade_guard_lock_host="$upgrade_guard_lock_host/omnia/.data/upgrade_in_progress.lock" - else - upgrade_guard_lock_host="$upgrade_guard_lock_container" - fi - echo "$upgrade_guard_lock_host" -} - -check_internal_nfs_export() { - local nfs_server_ip="$1" nfs_server_share_path="$2" - local exports line export_path share_path export_path_norm share_path_norm - - if ! is_local_ip "$nfs_server_ip"; then - echo "The provided NFS server IP ($nfs_server_ip) is NOT the current system's IP." - exit 1 - fi - echo "The provided NFS server IP ($nfs_server_ip) belongs to the current system." - - if ! exports=$(showmount -e "$nfs_server_ip" 2>/dev/null); then - echo -e "${RED}ERROR: Unable to contact NFS server at $nfs_server_ip. Ensure NFS and rpcbind are running, and firewall allows access.${NC}" - exit 1 - fi - - # Normalize share path - share_path="${nfs_server_share_path#"${nfs_server_share_path%%[![:space:]]*}"}" - share_path="${share_path%"${share_path##*[![:space:]]}"}" - share_path_norm="${share_path%/}" - [[ -z "$share_path_norm" ]] && share_path_norm="/" - - # Check showmount exports - while IFS= read -r line; do - line="${line#"${line%%[![:space:]]*}"}" - [[ -z "$line" || "$line" == \#* || "$line" == Export\ list\ for* ]] && continue - export_path="${line%%[[:space:]]*}" - [[ "$export_path" != /* ]] && continue - export_path_norm="${export_path%/}" - [[ -z "$export_path_norm" ]] && export_path_norm="/" - - if [[ "$share_path_norm" == "$export_path_norm" || "$share_path_norm" == "$export_path_norm"/* ]]; then - echo -e "${GREEN}Path $nfs_server_share_path is covered by exported path $export_path_norm on $nfs_server_ip.${NC}" - return 0 - fi - done <<< "$exports" - - # Fallback: check /etc/exports if showmount didn't find a match - if [[ -f /etc/exports ]]; then - while IFS= read -r line; do - line="${line#"${line%%[![:space:]]*}"}" - [[ -z "$line" || "$line" == \#* ]] && continue - export_path="${line%%[[:space:]]*}" - [[ "$export_path" != /* ]] && continue - export_path_norm="${export_path%/}" - [[ -z "$export_path_norm" ]] && export_path_norm="/" - - if [[ "$share_path_norm" == "$export_path_norm" || "$share_path_norm" == "$export_path_norm"/* ]]; then - echo -e "${GREEN}Path $nfs_server_share_path is covered by exported path $export_path_norm on $nfs_server_ip.${NC}" - return 0 - fi - done < /etc/exports - fi - - echo -e "${RED}ERROR: Path $nfs_server_share_path is NOT exported by $nfs_server_ip.${NC}" - exit 1 -} - -display_supported_use_cases() { - # Color definitions - BLUE='\033[1;34m' - YELLOW='\033[1;33m' - GREEN='\033[1;32m' - NC='\033[0m' # No Color - - # Introductory Guidance - echo -e "${BLUE} ----------------- Omnia Shared Path Configuration ---------------- ${NC}" - echo -e "${BLUE} Please choose the type of Omnia shared path in Omnia Infrastructure Manager (OIM): ${NC}" - echo -e "${BLUE} It is recommended to use a external NFS share for the Omnia shared path. ${NC}" - echo -e "${BLUE} If you are not using NFS, make sure enough space is available on the disk. ${NC}" - echo -e "\nSupported Use Cases:\n" - - # Table content - { - echo -e "Share Option\tType\tDescription\tAdditional Info" - echo -e "${GREEN}NFS\tExternal\tExternal NFS server(outside OIM) created by user\tUsed only for flat provisioning. Mounts on OIM. ${NC}" - echo -e "NFS\tInternal\tNFS server created by user in OIM\tUsed only for flat provisioning. No mount performed." - echo -e "Local\tDisk\tDisk storage in OIM\tUsed only for flat provisioning. Directory to be created by user." - } | column -t -s $'\t' -} - - -# This function is responsible for initializing the Omnia core container -# It prompts the user for the Omnia shared path and the root password. -# It checks if the Omnia shared path exists. -setup_omnia_core() { - # Validate the system environment - validate_oim - - # Initialize the container configuration - init_container_config - - # Setup the container - setup_container - - # Post container setup configuration - post_setup_config - - remove_container_omnia_sh - - # Start the container - start_container_session -} - - -# This function is responsible for cleaning up the Omnia core container. -# It removes the container and performs the necessary cleanup steps. -cleanup_omnia_core() { - # Block if critical service containers exist - critical_running=$(podman ps --format '{{.Names}}' | grep -E '^pulp$|^omnia_auth$|^minio-server$|^registry$|^step-ca$|^postgres$|^hydra$|^opaal-idp$|^smd$|^opaal$|^bss$|^cloud-init-server$|^haproxy$|^coresmd$|^omnia_build_stream$|^omnia_postgres$') - if [ -n "$critical_running" ]; then - echo -e "${RED}Failed to intiatiate omnia_core container cleanup. There are other critical service containers still running:${NC}" - echo "$critical_running" - echo -e "${GREEN}Run oim_cleanup.yml first to cleanup all containers.${NC}" - exit 1 - fi - - echo -e "${RED} WARNING: This will remove Omnia core container and all files in Omnia Shared Path.${NC}" - echo -e "${GREEN} You can abort and take backup if you want.${NC}" - read -p " Are you sure you want to continue with the cleanup? (y/n): " confirm - if [ "$confirm" = "n" ] || [ "$confirm" = "N" ]; then - echo -e "${GREEN}Aborting.${NC}" - exit 0 - elif [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then - - # Fetch the configuration from the Omnia core container. - fetch_config - - # Clear upgrade guard lock if present (shared path visible to container and host) - local upgrade_guard_lock_path=$(get_upgrade_guard_lock_path) - rm -f "$upgrade_guard_lock_path" >/dev/null 2>&1 || true - echo "[INFO] [CLEANUP] Cleared upgrade guard lock (if present): $upgrade_guard_lock_path" - - # Remove the container - remove_container - - # Perform the necessary cleanup steps - cleanup_config - fi -} - - -# This function is responsible for cleaning up the Omnia core container configuration. -# It removes the public key from the authorized_keys file. -# It removes the private key. -# It removes the ssh key from the known_hosts file. -# It removes the Omnia core configuration. -# -cleanup_config(){ - - # Set the path to the ssh public key. - ssh_key_file="$HOME/.ssh/oim_rsa.pub" - - # Remove the public key from the authorized_keys file. - if [ -f "$ssh_key_file" ]; then - # Remove the line from the authorized_keys file. - sed -i "\|^$(cat $ssh_key_file)$|d" $HOME/.ssh/authorized_keys - echo -e "${GREEN} Public key has been removed from authorized_keys.${NC}" - else - echo -e "${RED} Public key file not found.${NC}" - fi - - # Remove the SSH key pair. - ssh_key_file="$HOME/.ssh/oim_rsa" - ssh_key_file_pub="${ssh_key_file}.pub" - if [ -f "$ssh_key_file" ] && [ -f "$ssh_key_file_pub" ]; then - rm -f "$ssh_key_file" "$ssh_key_file_pub" - echo -e "${GREEN} SSH key pair have been removed.${NC}" - else - echo -e "${RED} SSH key file not found.${NC}" - fi - - # Remove the ssh key from the known_hosts file. - echo -e "${BLUE} Removing ssh key from known_hosts file.${NC}" - ssh-keygen -R "[localhost]:2222" >/dev/null 2>&1 - - - # Remove the host entry from the config file in .ssh folder. - ssh_config_file="$HOME/.ssh/config" - if [ -f "$ssh_config_file" ]; then - sed -i '/Host omnia_core/,+5d' "$ssh_config_file" - echo -e "${GREEN} Host entry has been removed from config file.${NC}" - else - echo -e "${RED} Config file not found.${NC}" - fi - - # Remove the Omnia core configuration. - echo -e "${BLUE} Removing Omnia core configuration.${NC}" - rm -rf $omnia_path/omnia/{hosts,input,log,pulp,provision,pcs,ssh_config,tmp,.data} - - # Unmount the NFS shared path if the share option is NFS. - if [ "$share_option" = "NFS" ] && [ "$nfs_type" = "external" ]; then - umount "$omnia_path" - if [ $? -eq 0 ]; then - echo -e "${GREEN} NFS shared path has been unmounted.${NC}" - else - echo -e "${RED} Failed to unmount NFS shared path.${NC}" - fi - # Remove the entry from /etc/fstab - fstab_file="/etc/fstab" - if [ -f "$fstab_file" ]; then - # Create a backup of the fstab file. - cp "$fstab_file" "$fstab_file.bak" - - # Remove the line from the fstab file. - sed -i "\#$omnia_path#d" "$fstab_file" - if [ $? -ne 0 ]; then - echo -e "${RED} Failed to remove the entry from /etc/fstab.${NC}" - fi - fi - fi - - echo -e "${GREEN} Omnia core configuration has been cleaned up.${NC}" -} - -# This function is responsible for removing the Omnia core container. -# -# It removes the container using the 'podman rm -f' command. -# If the container is removed successfully, it prints a success message. -# Otherwise, it prints an error message. -remove_container() { - # Block if critical service containers exist - critical_running=$(podman ps --format '{{.Names}}' | grep -E '^pulp$|^omnia_auth$|^minio-server$|^registry$|^step-ca$|^postgres$|^hydra$|^opaal-idp$|^smd$|^opaal$|^bss$|^cloud-init-server$|^haproxy$|^coresmd$|^omnia_build_stream$|^omnia_postgres$') - if [ -n "$critical_running" ]; then - echo -e "${RED}Failed to intiatiate omnia_core container cleanup. There are other critical service containers still running:${NC}" - echo "$critical_running" - echo -e "${GREEN}Run oim_cleanup.yml first to cleanup all containers.${NC}" - exit 1 - fi - - # Remove the container. - echo -e "${BLUE} Removing the Omnia core container.${NC}" - if systemctl stop omnia_core.service; then - echo -e "${GREEN} Omnia core container has been removed.${NC}" - # Remove the systemd generator symlinks. - echo -e "${GREEN} Cleaning up systemd generator symlinks.${NC}" - rm -f /run/systemd/generator/omnia_core.service - rm -f /run/systemd/generator/multi-user.target.wants/omnia_core.service - rm -f /run/systemd/generator/default.target.wants/omnia_core.service - - echo -e "${GREEN} Cleaning up omnia_core.container.${NC}" - rm -f /etc/containers/systemd/omnia_core.container - - # Remove the omnia_core.service file. - rm -f /etc/systemd/system/omnia_core.service - systemctl daemon-reload - systemctl reset-failed omnia_core.service - # check if service is removed - if systemctl status omnia_core.service >/dev/null 2>&1; then - echo -e "${RED} Failed to remove Omnia core service.${NC}" - else - echo -e "${GREEN} Omnia core service has been removed.${NC}" - fi - else - echo -e "${RED} Failed to remove Omnia core container.${NC}" - fi - - # Remove the container image. - # if podman rmi omnia_core; then - # echo -e "${GREEN} Omnia core image has been removed.${NC}" - # else - # echo -e "${RED} Failed to remove Omnia core image.${NC}" - # fi -} - - -# This function is responsible for initializing the Omnia core container. -# -# It prompts the user for the Omnia shared path and the root -# password. It then checks if the Omnia shared path exists. -# -# The function generates the ssh key pair and copies the private -# key to the Omnia shared path. -# -# The function also copies the ssh public key to the -# authorized_keys file. -# -# The function creates the necessary log directories. -init_container_config() { - - share_option="" - # Display the supported use cases - display_supported_use_cases - - # Display the choices for the user - echo -e "${BLUE} Choose the type of Omnia shared path:${NC}" - options=( "NFS (recommended)" "Local" ) - - PS3="Select the option number: " - - select opt in "${options[@]}"; do - case $opt in - "NFS (recommended)") - share_option="NFS" - break - ;; - "Local") - share_option="Local" - break - ;; - *) - echo -e "${RED} Invalid option.${NC}" - continue - esac - done - - case $share_option in - "Local") - # Prompt the user for the Omnia shared path. - echo -e "${BLUE} Please provide Omnia shared path:${NC}" - read -p "Omnia shared path: " omnia_path - - # Check if the Omnia shared path is absolute path and path exists. - if [[ "$omnia_path" != /* ]] || [ ! -d "$omnia_path" ]; then - echo -e "${RED} Omnia shared path is not an absolute path or does not exist! Please re-run omnia.sh --install with valid Omnia shared path.${NC}" - exit 1 - fi - ;; - "NFS") - echo -e "${BLUE} Select NFS type:${NC}" - select nfs_type in "External (Recommended)" "Internal"; do - case $nfs_type in - "External (Recommended)") - echo -e "${BLUE} Please provide the external NFS server IP:${NC}" - read -p "External NFS server IP: " nfs_server_ip - - echo -e "${BLUE} Please provide the external NFS server share path:${NC}" - read -p "External NFS share path: " nfs_server_share_path - - echo -e "${BLUE} Please provide the directory to store Omnia cluster configuration files:${NC}" - read -p "Omnia config path: " omnia_path - - # Validate Omnia shared path is absolute - if [[ "$omnia_path" != /* ]]; then - echo -e "${RED}Omnia shared path must be an absolute path.${NC}" - exit 1 - fi - - nfs_type="external" - break - ;; - "Internal") - echo -e "${BLUE} Please provide the OIM server IP:${NC}" - read -p "OIM server IP: " nfs_server_ip - - echo -e "${BLUE} Please provide the OIM server share path:${NC}" - read -p "OIM server share path: " nfs_server_share_path - - echo -e "${BLUE} Checking if the OIM server share path is mounted${NC}" - check_internal_nfs_export "$nfs_server_ip" "$nfs_server_share_path" - - # Note: No mounting performed here - echo -e "${YELLOW}Note: Internal NFS does not support HA OIM or hierarchical cluster. Proceeding...${NC}" - nfs_type="internal" - omnia_path="$nfs_server_share_path" - break - ;; - *) - echo -e "${RED}Invalid option. Please choose 1 or 2.${NC}" - ;; - esac - done - ;; - esac - - # Prompt for admin NIC IP (primary non-loopback IP of the OIM host) - echo -e "${BLUE} Please provide the admin NIC IP address of the OIM host:${NC}" - read -p "Admin NIC IP: " admin_nic_ip - if [ -z "$admin_nic_ip" ]; then - echo -e "${RED} Admin NIC IP cannot be empty. Please re-run omnia.sh --install.${NC}" - exit 1 - fi - - # Prompt the user for the Omnia core root password. - echo -e "${BLUE} Please provide Omnia core root password for accessing container:${NC}" - - read -p " Enter: " -s passwd - - # Prompt the user for the Omnia core root password confirmation. - echo -e "\n${BLUE} Please confirm password:${NC}" - read -s -p " Enter: " cnf_passwd - - # Check if the provided passwords match. - if [ "$passwd" != "$cnf_passwd" ]; then - echo -e "${RED} Invalid Omnia core root password, passwords do not match!${NC}" - exit 1 - fi - - # Check if the password contains any of the invalid characters - invalid_chars='[\\|&;`"><*?!$(){}[\]]' - if [[ "$passwd" =~ $invalid_chars ]]; then - echo -e "${RED} Invalid password, passwords must not contain any of these special characters: [\\|&;\`\"><*?!$(){}[\]]${NC}" - exit 1 - fi - - # Install NFS client package if option NFS is selected - if [[ "$share_option" == "NFS" ]]; then - # Install NFS client package - echo -e "${BLUE} Installing NFS client package.${NC}" - dnf install -y nfs-utils nfs4-acl-tools - - # Create omnia_path directory if it does not exist - echo -e "${BLUE} Creating omnia shared path directory if it does not exist.${NC}" - mkdir -p $omnia_path - - # Mount NFS server share path in Omnia share path - if [[ "$nfs_type" == "external" ]]; then - - if is_local_ip "$nfs_server_ip"; then - echo -e "${RED} Error: NFS server $nfs_server_ip is a local IP.${NC}" - echo -e "${RED} Please provide an external NFS server IP or re-run omnia.sh --install with valid options.${NC}" - exit 1 - fi - - # Validate if NFS server is reachable - echo -e "${BLUE} Validating if NFS server is reachable.${NC}" - ping -c1 -W1 $nfs_server_ip > /dev/null - if [ $? -ne 0 ]; then - echo -e "${RED} NFS server $nfs_server_ip is not reachable.${NC}" - exit 1 - fi - - echo -e "${BLUE} Mounting NFS server share path in Omnia share path.${NC}" - mount -t nfs -o nosuid,rw,sync,hard,intr,timeo=30 "$nfs_server_ip:$nfs_server_share_path" "$omnia_path" - if [[ $? -ne 0 ]]; then - echo -e "${RED} Failed to mount NFS. Please check the IP and path.${NC}" - exit 1 - fi - # Validate if NFS server share path is mounted - echo -e "${BLUE} Validating if NFS server share path is mounted.${NC}" - # strip the trailing slash from nfs_server_share_path - nfs_server_share_path="${nfs_server_share_path%/}" - if grep -qs "$nfs_server_ip:$nfs_server_share_path" /proc/mounts; then - echo -e "${GREEN} NFS server share path is mounted.${NC}" - else - echo -e "${RED} NFS server share path is not mounted. Provide valid NFS server details. ${NC}" - exit 1 - fi - # Add NFS server share to /etc/fstab to mount on startup - echo "$nfs_server_ip:$nfs_server_share_path $omnia_path nfs nosuid,rw,sync,hard,intr" >> /etc/fstab - else - echo -e "${BLUE} Using internal NFS path without mounting.${NC}" - fi - - fi - - hashed_passwd=$(openssl passwd -1 $passwd) - ssh_key_file="/root/.ssh/oim_rsa" - ssh_port=2222 - - # Generate a new ssh key pair. - if [ -f "$ssh_key_file" ]; then - echo -e "\n${BLUE} Skipping generating new ssh key pair.${NC}" - else - echo -e "\n${GREEN} Generating a new ssh key pair.${NC}" - ssh-keygen -t rsa -b 4096 -C "omnia_oim" -q -N '' -f /root/.ssh/oim_rsa - { - echo "Host omnia_core" - echo " Hostname localhost" - echo " Port $ssh_port" - echo " User root" - echo " IdentityFile ~/.ssh/oim_rsa" - echo " IdentitiesOnly yes" - } >> $HOME/.ssh/config - fi - - # Create the ssh configuration directory if it does not exist. - echo -e "${GREEN} Creating the ssh configuration directory if it does not exist.${NC}" - mkdir -p "$omnia_path/omnia/ssh_config/.ssh" - - # Copy the omnia_core ssh config to the shared path. - echo -e "${GREEN} Copying the omnia_core ssh config to the omnia shared path.${NC}" - cp "$HOME/.ssh/config" "$omnia_path/omnia/ssh_config/.ssh/config" - - # Copy the oim_rsa ssh key to the shared path. - echo -e "${GREEN} Copying the oim_rsa ssh key to the omnia shared path.${NC}" - cp "$HOME/.ssh/oim_rsa" "$omnia_path/omnia/ssh_config/.ssh/oim_rsa" - - # Copy the ssh private key to the omnia shared path. - echo -e "${GREEN} Copying the ssh private key to the omnia shared path.${NC}" - cp $ssh_key_file "$omnia_path/omnia/ssh_config/.ssh/id_rsa" - - # Copy the ssh public key to the omnia shared path. - echo -e "${GREEN} Copying the ssh public key to the omnia shared path.${NC}" - cp $ssh_key_file.pub "$omnia_path/omnia/ssh_config/.ssh/id_rsa.pub" - - # Get the ssh public key. - ssh_public_key="$(cat /root/.ssh/oim_rsa.pub)" - - validate_nfs_server - - # Add ssh public key to the authorized_keys. - echo -e "${GREEN} Adding ssh public key to the authorized_keys.${NC}" - if grep -q "$ssh_public_key" $HOME/.ssh/authorized_keys; then - echo -e "${BLUE} Skipping adding ssh public key to the authorized_keys.${NC}" - else - echo "$ssh_public_key" >> $HOME/.ssh/authorized_keys - chmod 600 $HOME/.ssh/authorized_keys - fi - - # Add ssh public key to the authorized_keys in the ssh_config directory. - echo -e "${GREEN} Adding ssh public key to the authorized_keys in the Omnia ssh_config directory.${NC}" - if [ -f "$omnia_path/omnia/ssh_config/.ssh/authorized_keys" ] && grep -q "$ssh_public_key" "$omnia_path/omnia/ssh_config/.ssh/authorized_keys"; then - echo -e "${BLUE} Skipping adding ssh public key to the authorized_keys in the Omnia ssh_config directory.${NC}" - else - echo "$ssh_public_key" >> "$omnia_path/omnia/ssh_config/.ssh/authorized_keys" - chmod 600 "$omnia_path/omnia/ssh_config/.ssh/authorized_keys" - fi - - # Create the log directory if it does not exist. - echo -e "${GREEN} Creating the log directory if it does not exist.${NC}" - mkdir -p "$omnia_path/omnia/log/core/container" - mkdir -p "$omnia_path/omnia/log/core/playbooks" - - # Create the hosts file for cluster in $omnia_path/omnia/hosts - echo -e "${GREEN} Creating the hosts file for cluster.${NC}" - touch "$omnia_path/omnia/hosts" - - # Create the pulp_ha directory if it does not exist. - echo -e "${GREEN} Creating the pulp HA directory if it does not exist.${NC}" - mkdir -p "$omnia_path/omnia/pulp/pulp_ha" - - # Initialize host-side path variables based on user-provided omnia_path - OMNIA_INPUT_DIR="$omnia_path/omnia/input" - OMNIA_METADATA_DIR="$omnia_path/omnia/.data" - OMNIA_METADATA_FILE="$omnia_path/omnia/.data/oim_metadata.yml" -} - - -# This function is responsible for fetching the configuration from the Omnia core. -# It uses podman exec to run a command in the Omnia core container. -# The command retrieves the metadata from the oim_metadata.yml file. -# The metadata is then parsed and the required configuration is extracted. -fetch_config() { - - # Fetch the metadata from the oim_metadata.yml file. - echo -e "${GREEN} Fetching the metadata from the oim_metadata.yml file.${NC}" - core_config=$(podman exec omnia_core /bin/bash -c 'cat /opt/omnia/.data/oim_metadata.yml') - - # Split the metadata into separate lines. - IFS=$'\n' read -r -d '' -a config_lines <<<"$core_config" - - # Loop through the lines and extract the required configuration. - for line in "${config_lines[@]}"; do - # Extract the key and value from the line. - key=$(echo "$line" | awk -F ':' '{print $1}') - value=$(echo "$line" | awk -F ':' '{print $2}') - - # Check the key and assign the value to the corresponding variable. - case $key in - oim_shared_path) - # Assign the shared path. - omnia_path=$(echo "$value" | tr -d '[:space:]') - ;; - omnia_core_hashed_passwd) - # Assign the hashed password. - hashed_passwd=$(echo "$value" | tr -d '[:space:]') - ;; - nfs_server_ip) - # Assign the nfs server ip. - nfs_server_ip=$(echo "$value" | tr -d '[:space:]') - ;; - nfs_server_share_path) - # Assign the nfs server share path. - nfs_server_share_path=$(echo "$value" | tr -d '[:space:]') - ;; - omnia_share_option) - # Assign the share option. - share_option=$(echo "$value" | tr -d '[:space:]') - ;; - nfs_type) - # Assign the share option. - nfs_type=$(echo "$value" | tr -d '[:space:]') - ;; - admin_nic_ip) - # Assign the admin NIC IP. - admin_nic_ip=$(echo "$value" | tr -d '[:space:]') - ;; - esac - done - # Check if the required configuration is extracted successfully. - if [ -z "$omnia_path" ] || [ -z "$hashed_passwd" ]; then - echo -e "${RED} Failed to fetch data from metadata file.${NC}" - exit 1 - else - echo -e "${GREEN} Successfully fetched data from metadata file.${NC}" - fi - - # Initialize host-side path variables based on fetched omnia_path - OMNIA_INPUT_DIR="$omnia_path/omnia/input" - OMNIA_METADATA_DIR="$omnia_path/omnia/.data" - OMNIA_METADATA_FILE="$omnia_path/omnia/.data/oim_metadata.yml" -} - -# Validates the OIM (Omnia Infrastructure Manager) by checking if the hostname is -# configured with a domain name, checking if Podman is installed, enabling and -# starting the Podman socket. -validate_oim() { - # Check if the hostname is set - hostname_value=$(hostname) - if [[ -z "$hostname_value" ]]; then - echo -e "${RED}Hostname is not set!${NC}" - exit 1 - fi - - # Check if the hostname is static - static_hostname=$(hostnamectl --static) - current_hostname=$(hostname) - if [[ "$static_hostname" != "$current_hostname" ]]; then - echo -e "${RED}Static Hostname is unset. Current: '$current_hostname', Static: '$static_hostname'${NC}" - echo -e "${RED}Please set the static hostname and try again.${NC}" - echo -e "${BLUE}Command to set hostname: hostnamectl set-hostname ${NC}" - echo -e "${RED}Exiting...${NC}" - exit 1 - fi - - # Check if the hostname is configured with a domain name. - domain_name=$(hostname -d) - if [[ -n "$domain_name" ]]; then - echo -e "${BLUE}Hostname is configured with a domain name: $domain_name${NC}" - else - echo -e "${RED}Invalid hostname, hostname is not configured with a domain name!${NC}" - exit 1 - fi - - # Detect OIM timezone from systemd in a stable, case‑independent way - oim_timezone=$(timedatectl show -p Timezone --value 2>/dev/null) - - # Fallbacks if needed (non‑systemd or old timedatectl) - if [[ -z "$oim_timezone" ]]; then - if [[ -f /etc/timezone ]]; then - # Debian/Ubuntu style - oim_timezone=$(< /etc/timezone) - elif [[ -L /etc/localtime ]]; then - # Derive from /etc/localtime symlink - oim_timezone=$(readlink -f /etc/localtime | sed -n 's|^.*zoneinfo/||p') - fi - fi - - podman --version - - # Capture the exit status - if [ $? -eq 0 ]; then - echo -e "${BLUE} Podman is installed. Version: $(podman --version)${NC}" - else - echo -e "${RED} Podman is not installed.${NC}" - exit 1 - fi - - # Enable the podman socket to start at boot - echo -e "${BLUE} Enabling podman.socket...${NC}" - systemctl enable podman.socket - - # Start the podman socket now - echo -e "${BLUE} Starting podman.socket...${NC}" - systemctl start podman.socket - - # Print a success message after enabling and starting the podman socket - echo -e "${GREEN} Podman socket has been enabled and started.${NC}" -} - -# Checks if the required directories for Omnia are present. -# This function iterates over a list of required directories/files and checks if each one exists. -check_required_directories() { - required_paths=( - "$omnia_path/omnia" - "$omnia_path/omnia/ssh_config/.ssh" - "$omnia_path/omnia/log/core/container" - "$omnia_path/omnia/hosts" - "$omnia_path/omnia/pulp/pulp_ha" - ) - - missing_paths=() - - for path in "${required_paths[@]}"; do - if [ ! -e "$path" ]; then # Checks both files and directories - missing_paths+=("$path") - fi - done - - if [ "${#missing_paths[@]}" -ne 0 ]; then - echo -e "${RED}Error: The following required files or directories are missing:${NC}" - echo -e "${RED}${missing_paths[*]}${NC}" - echo -e "User can not Retain Existing configuration" - echo - echo -e "${YELLOW}Instructions:${NC}" - echo -e "${YELLOW}* Backup any existing files if required${NC}" - echo -e "${YELLOW}* Run ./omnia.sh --install and choose:${NC}" - echo -e "${YELLOW} Options:${NC}" - echo -e "${YELLOW} -> Reinstall the container${NC}" - echo -e "${YELLOW} -> Overwrite and create new configuration${NC}" - exit 1 - fi -} - -# Sets up the Omnia core container. -# This function pulls the Omnia core Podman image and runs the container. -# Creates a Quadlet service for the container and also creates a metadata file. -# It defines the container options and runs the container. -setup_container() { - container_name="omnia_core" - echo "==> Setting up $container_name container" - - # SELinux option handling - # For external NFS: disable SELinux labeling entirely because NFS-mounted - # files carry the 'nfs_t' type which containers (container_t) cannot access. - # The ':z' relabel also fails on NFS since NFS does not support xattrs. - # For local/internal: use ':z' to relabel volumes for container access. - selinux_option=":z" - selinux_disable="" - if [ "$share_option" = "NFS" ] && [ "$nfs_type" = "external" ]; then - selinux_option="" - selinux_disable="SecurityLabelDisable=true" - fi - - # --- Generate Quadlet container file --- - cat > /etc/containers/systemd/${container_name}.container <> "$oim_metadata_file" - if [ "$share_option" = "NFS" ]; then - { - echo "nfs_server_ip: $nfs_server_ip" - echo "nfs_server_share_path: $nfs_server_share_path" - echo "nfs_type: $nfs_type" - } >> "$oim_metadata_file" - fi - else - sed -i '/^upgrade_backup_dir:/d' "$oim_metadata_file" >/dev/null 2>&1 || true - if grep -q '^omnia_version:' "$oim_metadata_file"; then - sed -i "s/^omnia_version:.*/omnia_version: $metadata_version/" "$oim_metadata_file" >/dev/null 2>&1 || true - else - echo "omnia_version: $metadata_version" >> "$oim_metadata_file" - fi - # Only add admin_nic_ip if missing (do not refresh on re-runs) - if ! grep -q '^admin_nic_ip:' "$oim_metadata_file"; then - echo "admin_nic_ip: $admin_nic_ip" >> "$oim_metadata_file" - fi - fi - - # --- Remove old service if exists --- - if systemctl list-unit-files | grep -q "${container_name}.service"; then - systemctl stop ${container_name}.service - systemctl disable ${container_name}.service - rm -f /etc/systemd/system/${container_name}.service - fi - - # --- Reload systemd so Quadlet generates the service --- - systemctl daemon-reexec - systemctl daemon-reload - systemctl start ${container_name}.service - - # --- Start the container via Quadlet --- - echo "==> ${container_name} container deployed and starting via Quadlet" - - # --- Wait for container to be running --- - echo "Waiting for $container_name container to start..." - for i in {1..30}; do - if podman ps --format '{{.Names}}' | grep -qw "$container_name"; then - echo "$container_name container is running." - break - else - sleep 1 - fi - done - - if ! podman ps --format '{{.Names}}' | grep -qw "$container_name"; then - echo -e "${RED}Error: $container_name container failed to start.${NC}" - rm -rf "$OMNIA_METADATA_FILE" - exit 1 - fi - - systemctl start firewalld - systemctl enable firewalld - firewall-cmd --permanent --zone=public --add-port=2222/tcp - firewall-cmd --reload -} -# post_setup_config is a function that sets up the configuration for the Omnia core. -# It creates the necessary directories and files, copies input files from the Omnia container, -# and creates the oim_metadata.yml file. -post_setup_config() { - - # Create the ansible tmp directory if it does not exist. - mkdir -p "$omnia_path/omnia/tmp/.ansible/tmp" - chmod 757 "$omnia_path/omnia/tmp/.ansible/tmp" - # Create the input directory if it does not exist. - echo -e "${GREEN} Creating the input directory if it does not exist.${NC}" - mkdir -p "$OMNIA_INPUT_DIR/" - - # Create the default.yml file if it does not exist. - if [ ! -f "$OMNIA_INPUT_DIR/default.yml" ]; then - echo -e "${BLUE} Creating default.yml file.${NC}" - { - echo "# This file defines the project name." - echo "# The name of the project should be set in a directory under input." - echo "project_name: project_default" - } >> "$OMNIA_INPUT_DIR/default.yml" - fi - - # Copy input files from /omnia to /opt/omnia/project_default/ inside omnia_core container - podman exec -u root omnia_core bash -c "cd /omnia && git pull" - echo -e "${BLUE} Moving input files from /omnia dir to project_default folder.${NC}" - podman exec -u root omnia_core bash -c " - mkdir -p /opt/omnia/input/project_default - cp -r /omnia/input/* /opt/omnia/input/project_default - rm -rf /omnia/input - rm -rf /omnia/omnia.sh" - - # Copy image_build_manager input files to project_default/image_build_manager/ subdir - # NOTE: Credential files are NOT copied here — they are loaded dynamically - # by the credential utility at runtime. - echo -e "${BLUE} Copying image_build_manager input files to project_default/image_build_manager/.${NC}" - podman exec -u root omnia_core bash -c " - if [ -d /omnia/image_build_manager/input ]; then - mkdir -p /opt/omnia/input/project_default/image_build_manager - cp -r /omnia/image_build_manager/input/* /opt/omnia/input/project_default/image_build_manager/ - fi" - - # Copy telemetry input files to project_default/telemetry/ subdir - echo -e "${BLUE} Copying telemetry input files to project_default/telemetry/.${NC}" - podman exec -u root omnia_core bash -c " - if [ -d /omnia/telemetry/input ]; then - mkdir -p /opt/omnia/input/project_default/telemetry - cp -r /omnia/telemetry/input/* /opt/omnia/input/project_default/telemetry/ - fi" - - # Create the output directory for project_default - echo -e "${GREEN} Creating the output directory.${NC}" - podman exec -u root omnia_core bash -c " - mkdir -p /opt/omnia/output/project_default" - - # Copy build_stream input files to project_default/build_stream/ subdir - echo -e "${BLUE} Copying build_stream config to project_default/build_stream/.${NC}" - podman exec -u root omnia_core bash -c " - if [ -d /omnia/build_stream/input ]; then - mkdir -p /opt/omnia/input/project_default/build_stream - cp -r /omnia/build_stream/input/* /opt/omnia/input/project_default/build_stream/ - fi" -} - -validate_nfs_server() { - if [ "$share_option" = "NFS" ]; then - local temp_file="$omnia_path/temp_file" - touch "$temp_file" - if chown root:root "$temp_file"; then - rm -f "$temp_file" - else - echo "Error: Unable to chown file to root in $omnia_path. NFS server permission validation failed. Please ensure no_root_squash option is enabled in the NFS export configuration." - exit 1 - fi - - if [ "`ls -ld $omnia_path/omnia/ssh_config/.ssh/id_rsa | awk '{print $3 ":" $4}'`" != "root:root" ]; then - echo "Error: The $omnia_path/omnia/ssh_config/.ssh/id_rsa file should be owned by root:root. NFS server permission validation failed. Please verify the NFS export configuration." - exit 1 - fi - fi -} - -# Wait for the SSH daemon inside omnia_core to start accepting connections -# on port 2222. After a container swap (upgrade / rollback) the container -# process is "Up" but sshd may still be initialising. Without this wait, -# ssh-keyscan and the subsequent `ssh omnia_core` fail with -# "Connection refused". -wait_for_ssh_ready() { - local ssh_port=2222 - local max_wait=30 - local waited=0 - - echo -n "[INFO] Waiting for SSH daemon inside omnia_core to be ready" - while [ $waited -lt $max_wait ]; do - # Use ssh-keyscan as a lightweight probe — it exits 0 when it - # receives at least one host key line. - if ssh-keyscan -p "$ssh_port" localhost 2>/dev/null | grep -q .; then - echo " ready (${waited}s)" - return 0 - fi - sleep 1 - waited=$((waited + 1)) - echo -n "." - done - - echo "" - echo "[WARN] SSH daemon did not become ready within ${max_wait}s." - echo "[WARN] You can connect manually later with: ssh omnia_core" - return 1 -} - -init_ssh_config() { - local ssh_port=2222 - - mkdir -p "$HOME/.ssh" - touch "$HOME/.ssh/known_hosts" - ssh-keygen -R "[localhost]:$ssh_port" >/dev/null 2>&1 || true - ssh-keyscan -p "$ssh_port" localhost 2>/dev/null | grep -v "^#" >> "$HOME/.ssh/known_hosts" || true -} - -remove_container_omnia_sh() { - podman exec -u root omnia_core bash -c 'if [ -f /omnia/omnia.sh ]; then rm -f /omnia/omnia.sh; fi' >/dev/null 2>&1 || true - podman exec -u root omnia_core bash -c 'if [ -d /omnia/input ]; then rm -rf /omnia/input; fi' >/dev/null 2>&1 || true -} - -start_container_session() { - - echo -e "${GREEN} - ------------------------------------------------------------------------------------------------------------------------------------------ - Omnia Core container running successfully. - - Entering the container from Omnia Infrastructure Manager(OIM): - Through podman: - # podman exec -it -u root omnia_core bash - - Direct SSH: - # ssh omnia_core - - You are now in the Omnia environment. - - The following are the main directories available in the Omnia core container: - - - The shared directory, which is mapped to $omnia_path in OIM: /opt/omnia - - The input directory: /opt/omnia/input - - The Omnia source code directory: /omnia - - The Omnia playbooks logs directory: /opt/omnia/log/core/playbooks - - It's important to note: - - Files placed in the shared directory should not be manually deleted. - - Use the playbook /omnia/utils/oim_cleanup.yml to safely remove the shared directory and Omnia containers (except the core container). - - If you need to delete the core container, please run the omnia.sh script with --uninstall option. - - If you need to redeploy the core container with new input configs, please rerun the omnia.sh script with --install option. - - Provide any file paths (ISO, mapping files, etc.) that are mentioned in input files in the /opt/omnia directory. - - The domain name that will be used for Omnia is $domain_name, if you wish to change the domain name please cleanup Omnia, - change the Omnia Infrastructure Manager's domain name and rerun omnia.sh script with --install option. - - -------------------------------------------------------------------------------------------------------------------------------------------------- - ${NC}" - - init_ssh_config - - # Entering Omnia-core container - ssh omnia_core -} - -# Function to build Omnia core container image -build_omnia_core_image() { - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local build_script="$script_dir/containers/build_images.sh" - - if [ ! -f "$build_script" ]; then - echo -e "${RED}ERROR: Build script not found: $build_script${NC}" - exit 1 - fi - - echo -e "${BLUE}Building Omnia core container image...${NC}" - echo -e "${BLUE}Build script: $build_script${NC}" - echo "" - - # Execute the build script - bash "$build_script" core - local build_exit_code=$? - - if [ $build_exit_code -eq 0 ]; then - echo "" - echo -e "${GREEN}================================================================================${NC}" - echo -e "${GREEN} BUILD COMPLETED SUCCESSFULLY${NC}" - echo -e "${GREEN}================================================================================${NC}" - echo "" - echo -e "${GREEN}✓ Omnia core container image built successfully${NC}" - echo -e "${GREEN}✓ You can now run: ./omnia.sh --install${NC}" - echo "" - else - echo "" - echo -e "${RED}================================================================================${NC}" - echo -e "${RED} BUILD FAILED${NC}" - echo -e "${RED}================================================================================${NC}" - echo "" - echo -e "${RED}Build script exited with error code: $build_exit_code${NC}" - echo "" - exit $build_exit_code - fi -} - -show_help() { - echo "Usage: $0 [--install | --uninstall | --upgrade | --rollback | --build | --version | --help]" - echo " -i, --install Install and start the Omnia core container" - echo " -u, --uninstall Uninstall the Omnia core container and clean up configuration" - echo " --upgrade Upgrade the Omnia core container to newer version" - echo " --rollback Rollback the Omnia core container to previous version" - echo " -b, --build Build the Omnia core container image" - echo " -v, --version Display Omnia version information" - echo " -h, --help More information about usage" -} - -install_omnia_core() { - # Check for existing installation - if podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - # Read version from metadata inside container - current_version=$(podman exec -u root omnia_core grep '^omnia_version:' /opt/omnia/.data/oim_metadata.yml 2>/dev/null | cut -d':' -f2 | tr -d ' \t\n\r') - if [ "$current_version" = "2.1.0.0" ]; then - echo -e "${RED}ERROR: Existing Omnia 2.1 installation detected.${NC}" - echo -e "${YELLOW}To upgrade, run: $0 --upgrade${NC}" - echo -e "${YELLOW}For a fresh install, first run: $0 --uninstall${NC}" - exit 1 - fi - fi - - local omnia_core_tag="$OMNIA_CORE_CONTAINER_TAG" - local omnia_core_registry="" - - # Check if local omnia_core image exists using validate function - if ! validate_container_image "" "$omnia_core_tag" "install"; then - exit 1 - fi - echo -e "${GREEN}✓ Omnia core image (omnia_core:${omnia_core_tag}) found locally.${NC}" - - # Check if any other containers with 'omnia' in their name are running - other_containers=$(podman ps -a --format '{{.Names}}' | grep -E 'omnia' | grep -v 'omnia_core') - - # If there are any, exit - if [ -n "$other_containers" ]; then - echo -e "${RED} Failed to intiatiate omnia_core container cleanup. There are other omnia container running.${NC}" - echo -e "${GREEN} Execute oim_cleanup.yml first to cleanup all containers.${NC}" - ssh omnia_core - exit 1 - fi - - # Check if the omnia_core container is already running - running_containers=$(podman ps -a --format '{{.Names}} {{.State}}' | grep -E 'omnia_core') - - # If yes, set the variable to true - if [ -n "$running_containers" ]; then - core_container_status=true - fi - - # If core container is running - if [ "$core_container_status" = true ]; then - if [ -n "$(echo "$running_containers" | grep -E 'running')" ]; then - echo -e "${GREEN} Omnia core container is already running.${NC}" - echo -e "${GREEN} Do you want to:${NC}" - PS3="Select the option number: " - - select opt in "Enter omnia_core container" "Reinstall the container" "Exit"; do - case $opt in - "Enter omnia_core container") - choice=1 - break - ;; - "Reinstall the container") - choice=2 - break - ;; - "Exit") - echo "Exiting the script." - exit 0 - ;; - *) - echo "Invalid choice. Please try again." - continue - ;; - esac - done - - # If the user wants to enter omnia_core container - if [ "$choice" = "1" ]; then - start_container_session - fi - # If the user wants to reinstall, call the remove_container function, and then call the setup_omnia_core function - if [ "$choice" = "2" ]; then - # Block if critical service containers exist - critical_running=$(podman ps --format '{{.Names}}' | grep -E '^pulp$|^omnia_auth$|^minio-server$|^registry$|^step-ca$|^postgres$|^hydra$|^opaal-idp$|^smd$|^opaal$|^bss$|^cloud-init-server$|^haproxy$|^coresmd$|^omnia_build_stream$|^omnia_postgres$') - if [ -n "$critical_running" ]; then - echo -e "${RED}Failed to intiatiate omnia_core container cleanup. There are other critical service containers still running:${NC}" - echo "$critical_running" - echo -e "${GREEN}Run oim_cleanup.yml first to cleanup all containers.${NC}" - exit 1 - fi - echo -e "${GREEN} What configuration do you want to use for reinstallation:${NC}" - - PS3="Select the option number: " - - select opt in "Retain Existing configuration" "Overwrite and create new configuration" "Exit"; do - case $opt in - "Retain Existing configuration") - choice=1 - break - ;; - "Overwrite and create new configuration") - choice=2 - break - ;; - "Exit") - echo "Exiting the script." - exit 0 - ;; - *) - echo "Invalid choice. Please try again." - continue - ;; - esac - done - - # If the user wants to retain existing configuration, call the remove_container function - if [ "$choice" = "1" ]; then - fetch_config - check_required_directories - remove_container - setup_container - init_ssh_config - start_container_session - # If the user wants to overwrite and create new configuration, call the cleanup_omnia_core function - elif [ "$choice" = "2" ]; then - cleanup_omnia_core - setup_omnia_core - fi - fi - else - # If omnia_core container exists and is not running call the remove_container function - - echo -e "${RED} The Omnia Core container is present but not in running state.${NC}" - echo -e "${GREEN} Only the core container can be cleanup can be performed.${NC}" - echo -e "${GREEN} Container Configurations in the shared directory will not be cleaned up.${NC}" - echo -e "${GREEN} Do you want to perform cleanup:${NC}" - echo -e "${GREEN} 1. Yes.${NC}" - echo -e "${GREEN} 2. No. ${NC}" - read -p " Enter your choice (1 or 2): " choice - if [ "$choice" = "1" ]; then - remove_container - elif [ "$choice" = "2" ]; then - exit - fi - fi - - # If core container is not present - else - setup_omnia_core - fi -} - -# Check if Omnia core container is running -check_container_status() { - # Check if the Omnia core container is running - if ! podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo -e "${RED}ERROR: Omnia core container is not running.${NC}" - exit 1 - fi -} - -# Function to display version information -display_version() { - # Check if metadata file exists and Omnia core container is running - check_container_status - - # Fetch the metadata from the oim_metadata.yml file in the container - echo -e "${GREEN} Fetching metadata from omnia_core container...${NC}" - core_config=$(podman exec omnia_core /bin/bash -c 'cat /opt/omnia/.data/oim_metadata.yml') - - # Extract Omnia version from metadata file - omnia_version=$(echo "$core_config" | grep "^omnia_version:" | cut -d':' -f2 | tr -d ' \t\n\r') - - # Display version information - echo "Omnia version: $omnia_version" - - # Return exit code 0 on success - exit 0 -} - -phase1_validate() { - local current_image - local core_config - local previous_omnia_version - local shared_path - - echo "[INFO] [ORCHESTRATOR] Phase 1: Pre-Upgrade Validation" - - if ! podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo "[ERROR] [ORCHESTRATOR] Prerequisite failed: omnia_core container is not running" - display_cleanup_instructions - return 1 - fi - - core_config=$(podman exec omnia_core /bin/bash -c 'cat /opt/omnia/.data/oim_metadata.yml' 2>/dev/null) - if [ -z "$core_config" ]; then - echo "[ERROR] [ORCHESTRATOR] Unable to read oim_metadata.yml from omnia_core container" - display_cleanup_instructions - return 1 - fi - - previous_omnia_version=$(echo "$core_config" | grep "^omnia_version:" | cut -d':' -f2 | tr -d ' \t\n\r') - if [ -z "$previous_omnia_version" ]; then - echo "[ERROR] [ORCHESTRATOR] omnia_version not found in oim_metadata.yml" - display_cleanup_instructions - return 1 - fi - - shared_path=$(echo "$core_config" | grep "^oim_shared_path:" | cut -d':' -f2- | tr -d ' \t\n\r') - if [ -z "$shared_path" ]; then - echo "[ERROR] [ORCHESTRATOR] oim_shared_path not found in oim_metadata.yml" - return 1 - fi - - omnia_path="$shared_path" - - if [ ! -d "$omnia_path" ]; then - echo "[ERROR] [ORCHESTRATOR] Shared path from metadata does not exist on host: $omnia_path" - return 1 - fi - - if [ ! -w "$omnia_path" ]; then - echo "[ERROR] [ORCHESTRATOR] Permission denied: no write permission on shared path: $omnia_path" - return 1 - fi - - echo "[INFO] [ORCHESTRATOR] Phase 1: Validation passed" - return 0 -} - -phase2_approval() { - local backup_base default_backup_dir current_omnia_version - - echo "[INFO] [ORCHESTRATOR] Phase 2: Approval Gate" - echo "============================================" - echo "OMNIA UPGRADE SUMMARY" - echo "============================================" - echo "Current Container Tag: $OMNIA_CORE_CONTAINER_TAG" - echo "Target Container Tag: $TARGET_CONTAINER_TAG" - echo "Current Omnia Release: $OMNIA_VERSION" - echo "Target Omnia Release: $TARGET_OMNIA_VERSION" - - # Show upgrade type - if [ "$OMNIA_CORE_CONTAINER_TAG" = "$TARGET_CONTAINER_TAG" ]; then - echo "Upgrade Type: Same-tag upgrade (container restart)" - else - echo "Upgrade Type: Cross-tag upgrade (container swap)" - fi - - echo "============================================" - - current_omnia_version=$(podman exec -u root omnia_core /bin/bash -c "grep '^omnia_version:' '$CONTAINER_METADATA_FILE' | cut -d':' -f2 | tr -d ' \t\n\r'" 2>/dev/null) - if [ -z "$current_omnia_version" ]; then - echo "[ERROR] [ORCHESTRATOR] Failed to read omnia_version from metadata inside container" - return 1 - fi - - default_backup_dir="$CONTAINER_BACKUPS_DIR/upgrade/version_${current_omnia_version}" - backup_base="$default_backup_dir" - - echo "[INFO] [ORCHESTRATOR] Backup destination (inside omnia_core container): $backup_base" - - if ! update_metadata_upgrade_backup_dir "$backup_base"; then - echo "[ERROR] [ORCHESTRATOR] Failed to update upgrade backup directory in metadata" - return 1 - fi - - read -p "Proceed with upgrade? (y/N): " confirm - if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then - echo "[INFO] [ORCHESTRATOR] Upgrade cancelled by user" - return 1 - fi - - OMNIA_UPGRADE_BACKUP_PATH="$backup_base" - export OMNIA_UPGRADE_BACKUP_PATH - - echo "[INFO] [ORCHESTRATOR] Phase 2: Approval granted" - return 0 -} - -# ═══════════════════════════════════════════════════════════════════════════ -# validate_backup_disk_space: Pre-upgrade disk space validation -# Ensures sufficient space exists before backup creation to prevent partial -# backups due to disk full conditions. -# ═══════════════════════════════════════════════════════════════════════════ -validate_backup_disk_space() { - local backup_base="$1" - local safety_multiplier=2 # Require 2× the estimated backup size - - echo "[INFO] [ORCHESTRATOR] Validating disk space for backup..." - - if ! podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo "[ERROR] [ORCHESTRATOR] Cannot validate disk space: omnia_core container not running" - return 1 - fi - - # Calculate size of data to be backed up (in KB) - local input_size=0 - local openchami_size=0 - local metadata_size=0 - - # Get input directory size - input_size=$(podman exec -u root omnia_core bash -c " - if [ -d '$CONTAINER_INPUT_DIR' ]; then - du -sk '$CONTAINER_INPUT_DIR' 2>/dev/null | cut -f1 - else - echo 0 - fi - " 2>/dev/null || echo 0) - - # Get OpenCHAMI directory size - openchami_size=$(podman exec -u root omnia_core bash -c " - if [ -d '/opt/omnia/openchami' ]; then - du -sk '/opt/omnia/openchami' 2>/dev/null | cut -f1 - else - echo 0 - fi - " 2>/dev/null || echo 0) - - # Get metadata file size (small, but include for completeness) - metadata_size=$(podman exec -u root omnia_core bash -c " - if [ -f '$CONTAINER_METADATA_FILE' ]; then - du -sk '$CONTAINER_METADATA_FILE' 2>/dev/null | cut -f1 - else - echo 0 - fi - " 2>/dev/null || echo 0) - - # Ensure values are numeric - input_size=${input_size:-0} - openchami_size=${openchami_size:-0} - metadata_size=${metadata_size:-0} - - # Calculate total estimated backup size - local total_backup_size_kb=$((input_size + openchami_size + metadata_size)) - - # Add buffer for quadlet files from host (~100KB typical) - total_backup_size_kb=$((total_backup_size_kb + 100)) - - # Calculate required space with safety multiplier - local required_space_kb=$((total_backup_size_kb * safety_multiplier)) - - # Convert to human-readable for display - local total_backup_size_mb=$((total_backup_size_kb / 1024)) - local required_space_mb=$((required_space_kb / 1024)) - - echo "[INFO] [ORCHESTRATOR] Estimated backup size: ${total_backup_size_mb}MB" - echo "[INFO] [ORCHESTRATOR] Required space (${safety_multiplier}× safety margin): ${required_space_mb}MB" - - # Get available space on backup destination filesystem - # The backup path is inside the container, which maps to the omnia share - local backup_parent_dir - backup_parent_dir=$(dirname "$backup_base") - - local available_space_kb - available_space_kb=$(podman exec -u root omnia_core bash -c " - # Ensure parent directory exists for df check - mkdir -p '$backup_parent_dir' 2>/dev/null || true - df -k '$backup_parent_dir' 2>/dev/null | tail -1 | awk '{print \$4}' - " 2>/dev/null) - - if [ -z "$available_space_kb" ] || ! [[ "$available_space_kb" =~ ^[0-9]+$ ]]; then - echo "[WARN] [ORCHESTRATOR] Could not determine available disk space; proceeding with backup" - return 0 - fi - - local available_space_mb=$((available_space_kb / 1024)) - echo "[INFO] [ORCHESTRATOR] Available space on backup filesystem: ${available_space_mb}MB" - - # Check if sufficient space is available - if [ "$available_space_kb" -lt "$required_space_kb" ]; then - echo "" - echo -e "${RED}═══════════════════════════════════════════════════════════════════════════${NC}" - echo -e "${RED} INSUFFICIENT DISK SPACE FOR BACKUP${NC}" - echo -e "${RED}═══════════════════════════════════════════════════════════════════════════${NC}" - echo "" - echo -e "${RED}ERROR: Not enough disk space to safely create upgrade backup.${NC}" - echo "" - echo -e "${YELLOW}Disk Space Summary:${NC}" - echo -e "${YELLOW} - Estimated backup size: ${total_backup_size_mb}MB${NC}" - echo -e "${YELLOW} - Required space (${safety_multiplier}×): ${required_space_mb}MB${NC}" - echo -e "${YELLOW} - Available space: ${available_space_mb}MB${NC}" - echo -e "${YELLOW} - Shortfall: $((required_space_mb - available_space_mb))MB${NC}" - echo "" - echo -e "${YELLOW}Backup destination: $backup_base${NC}" - echo "" - echo -e "${GREEN}Required Action:${NC}" - echo -e "${GREEN} 1. Free up at least $((required_space_mb - available_space_mb))MB on the Omnia share${NC}" - echo -e "${GREEN} 2. Re-run 'omnia.sh --upgrade' after freeing space${NC}" - echo "" - return 1 - fi - - echo "[INFO] [ORCHESTRATOR] Disk space validation passed" - return 0 -} - -backup_openchami_data() { - local backup_base="$1" - - echo "[INFO] [ORCHESTRATOR] Backing up OpenCHAMI data..." - - # Check if /opt/omnia/openchami exists inside the container - if ! podman exec -u root omnia_core test -d /opt/omnia/openchami 2>/dev/null; then - echo "[INFO] [ORCHESTRATOR] No OpenCHAMI directory found at /opt/omnia/openchami — skipping" - return 0 - fi - - # Create openchami backup directory structure - if ! podman exec -u root omnia_core bash -c " - set -e - mkdir -p '${backup_base%/}/openchami/openchami_data' - cp -a /opt/omnia/openchami/. '${backup_base%/}/openchami/openchami_data/' 2>&1 - "; then - echo "[WARN] [ORCHESTRATOR] Failed to backup OpenCHAMI data — upgrade will continue" - return 0 - fi - - # Verify configs_vars.yaml was captured - if podman exec -u root omnia_core test -f "${backup_base%/}/openchami/openchami_data/configs_vars.yaml" 2>/dev/null; then - echo "[INFO] [ORCHESTRATOR] OpenCHAMI configs_vars.yaml backed up successfully" - else - echo "[WARN] [ORCHESTRATOR] configs_vars.yaml not found in OpenCHAMI backup" - fi - - # Backup openchami.target systemd unit from host (if it exists) - if [ -f "/etc/systemd/system/openchami.target" ]; then - podman cp "/etc/systemd/system/openchami.target" \ - "omnia_core:${backup_base%/}/openchami/openchami.target" >/dev/null 2>&1 || true - echo "[INFO] [ORCHESTRATOR] openchami.target backed up" - fi - - # Backup quadlet .container files from host (if they exist) - if ls /etc/containers/systemd/*.container >/dev/null 2>&1; then - podman exec -u root omnia_core mkdir -p "${backup_base%/}/openchami/quadlets" 2>/dev/null || true - for qfile in /etc/containers/systemd/*.container; do - podman cp "$qfile" \ - "omnia_core:${backup_base%/}/openchami/quadlets/$(basename "$qfile")" >/dev/null 2>&1 || true - done - echo "[INFO] [ORCHESTRATOR] Quadlet .container files backed up" +# ───────────────────────────────────────────────────────────────────────────── +# Environment Loading +# ───────────────────────────────────────────────────────────────────────────── +load_env() { + if [ ! -f "$ENV_FILE" ]; then + echo -e "${RED}ERROR: omnia.env not found at $ENV_FILE${NC}" + echo -e "${YELLOW}Configure it before running:${NC}" + echo -e " vi $ENV_FILE" + exit 1 fi - # Backup quadlet .network files from host (if they exist) - # These define Podman networks that enable DNS resolution between containers - if ls /etc/containers/systemd/*.network >/dev/null 2>&1; then - podman exec -u root omnia_core mkdir -p "${backup_base%/}/openchami/quadlets" 2>/dev/null || true - for nfile in /etc/containers/systemd/*.network; do - podman cp "$nfile" \ - "omnia_core:${backup_base%/}/openchami/quadlets/$(basename "$nfile")" >/dev/null 2>&1 || true - done - echo "[INFO] [ORCHESTRATOR] Quadlet .network files backed up" - fi + # Source env file (set -a exports all vars) + set -a + # shellcheck disable=SC1090 + . "$ENV_FILE" + set +a - # Normalize permissions on the openchami backup tree so rollback can - # read it even under NFS root_squash. cp -a preserves the source - # permissions which may be restrictive (container-created files). - podman exec -u root omnia_core bash -c " - find '${backup_base%/}/openchami' -type d -exec chmod 0755 {} + 2>/dev/null || true - find '${backup_base%/}/openchami' -type f -exec chmod 0644 {} + 2>/dev/null || true - " 2>/dev/null || true + # Apply defaults for optional variables + OMNIA_DATA_PATH="${OMNIA_DATA_PATH:-/opt/omnia}" + OMNIA_PROJECT_NAME="${OMNIA_PROJECT_NAME:-project_default}" + OMNIA_VENV_PATH="${OMNIA_VENV_PATH:-$OMNIA_DATA_PATH/venv}" + OMNIA_HOSTNAME="${OMNIA_HOSTNAME:-oim}" + OMNIA_DOMAIN_NAME="${OMNIA_DOMAIN_NAME:-omnia.cluster}" + OMNIA_ADMIN_NIC_IP="${OMNIA_ADMIN_NIC_IP:-172.16.107.254}" - echo "[INFO] [ORCHESTRATOR] OpenCHAMI data backup completed: ${backup_base}/openchami/" - return 0 + export OMNIA_DATA_PATH OMNIA_PROJECT_NAME OMNIA_VENV_PATH + export OMNIA_HOSTNAME OMNIA_DOMAIN_NAME OMNIA_ADMIN_NIC_IP } -phase3_backup_creation() { - local backup_base="$1" +validate_env() { + local errors=0 - echo "[INFO] [ORCHESTRATOR] Phase 3: Backup Creation" - - if ! podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo "[ERROR] [ORCHESTRATOR] Cannot create backup because omnia_core is not running" - return 1 + if [ -z "${OMNIA_ADMIN_NIC_IP:-}" ]; then + echo -e "${RED}ERROR: OMNIA_ADMIN_NIC_IP is not set in $ENV_FILE${NC}" + errors=$((errors + 1)) fi - if [ -z "$backup_base" ]; then - echo "[ERROR] [ORCHESTRATOR] Backup destination is empty" - return 1 + if [ -z "${OMNIA_HOSTNAME:-}" ]; then + echo -e "${RED}ERROR: OMNIA_HOSTNAME is not set in $ENV_FILE${NC}" + errors=$((errors + 1)) fi - if ! podman exec -u root omnia_core bash -c " - set -e - rm -rf '${backup_base%/}/input' '${backup_base%/}/metadata' '${backup_base%/}/configs' - mkdir -p '${backup_base%/}/input' '${backup_base%/}/metadata' '${backup_base%/}/configs' - chmod 0755 '${backup_base%/}' - chmod 0755 '${backup_base%/}/input' '${backup_base%/}/metadata' '${backup_base%/}/configs' - - if [ -f '$CONTAINER_INPUT_DIR/default.yml' ]; then - cp -a '$CONTAINER_INPUT_DIR/default.yml' '${backup_base%/}/input/' - chmod 0644 '${backup_base%/}/input/default.yml' - fi - - if [ -d '$CONTAINER_INPUT_DIR/project_default' ]; then - cp -a '$CONTAINER_INPUT_DIR/project_default' '${backup_base%/}/input/' - chmod -R 0644 '${backup_base%/}/input/project_default'/* - find '${backup_base%/}/input/project_default' -type d -exec chmod 0755 {} \; - fi - - if [ ! -f '$CONTAINER_METADATA_FILE' ]; then - echo '[ERROR] Metadata file not found inside container: $CONTAINER_METADATA_FILE' >&2 - exit 1 - fi - cp -a '$CONTAINER_METADATA_FILE' '${backup_base%/}/metadata/oim_metadata.yml' - chmod 0644 '${backup_base%/}/metadata/oim_metadata.yml' - "; then - echo "[ERROR] [ORCHESTRATOR] Backup failed; cleaning up partial backup" - podman exec -u root omnia_core bash -c "rm -rf '${backup_base%/}/input' '${backup_base%/}/metadata' '${backup_base%/}/configs'" >/dev/null 2>&1 || true - return 1 + if [ -z "${OMNIA_DOMAIN_NAME:-}" ]; then + echo -e "${RED}ERROR: OMNIA_DOMAIN_NAME is not set in $ENV_FILE${NC}" + errors=$((errors + 1)) fi - if [ -f "/etc/containers/systemd/omnia_core.container" ]; then - if ! podman cp "/etc/containers/systemd/omnia_core.container" "omnia_core:${backup_base%/}/configs/omnia_core.container" >/dev/null 2>&1; then - echo "[ERROR] [ORCHESTRATOR] Failed to backup quadlet container file" - podman exec -u root omnia_core bash -c "rm -rf '${backup_base%/}/input' '${backup_base%/}/metadata' '${backup_base%/}/configs'" >/dev/null 2>&1 || true - return 1 - fi - podman exec -u root omnia_core chmod 0644 "${backup_base%/}/configs/omnia_core.container" 2>/dev/null || true + if [ "$errors" -gt 0 ]; then + echo -e "${YELLOW}Edit $ENV_FILE and set all required variables before running omnia.sh${NC}" + exit 1 fi - echo "[INFO] [ORCHESTRATOR] Backup created at: $backup_base" - - # Backup OpenCHAMI data (workdir, configs_vars, deployment-recipes, quadlets) - backup_openchami_data "$backup_base" - - echo "[INFO] [ORCHESTRATOR] Phase 3: Backup completed" - return 0 + echo -e "${GREEN}Environment validated:${NC}" + echo -e " Hostname: ${OMNIA_HOSTNAME}" + echo -e " Domain: ${OMNIA_DOMAIN_NAME}" + echo -e " Admin IP: ${OMNIA_ADMIN_NIC_IP}" + echo -e " Data path: ${OMNIA_DATA_PATH}" + echo -e " Project: ${OMNIA_PROJECT_NAME}" } -phase4_same_tag_upgrade() { - local target_version="$1" - - echo "[INFO] [ORCHESTRATOR] Phase 4: Same-Tag Upgrade" - echo "[INFO] [ORCHESTRATOR] Upgrading to $target_version within same container tag" - - # Verify container is running - if ! podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo "[ERROR] [ORCHESTRATOR] Container is not running for same-tag upgrade" - return 1 - fi - - # Get version from git tag or use target version - local metadata_version=$(get_metadata_version "$target_version") - echo "[INFO] [ORCHESTRATOR] Updating metadata to version $metadata_version" - - # Update version metadata - if ! podman exec -u root omnia_core bash -c " - set -e - if [ ! -f '$CONTAINER_METADATA_FILE' ]; then - echo '[ERROR] Metadata file not found inside container: $CONTAINER_METADATA_FILE' >&2 - exit 1 - fi - if grep -q '^omnia_version:' '$CONTAINER_METADATA_FILE'; then - sed -i 's/^omnia_version:.*/omnia_version: $metadata_version/' '$CONTAINER_METADATA_FILE' - else - echo 'omnia_version: $metadata_version' >> '$CONTAINER_METADATA_FILE' - fi - "; then - echo "[ERROR] [ORCHESTRATOR] Failed to update metadata version" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: Could not update version metadata" - return 1 - fi - - echo "[INFO] [ORCHESTRATOR] Restarting container to apply changes..." - - # Restart container to apply changes - if ! systemctl restart omnia_core.service; then - echo "[ERROR] [ORCHESTRATOR] Failed to restart container service" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: Container restart failed" - return 1 - fi - - # Wait for container to be healthy after restart - echo "[INFO] [ORCHESTRATOR] Waiting for container health check after restart (30s)" - local health_timeout=30 - local health_count=0 - - while [ $health_count -lt $health_timeout ]; do - if podman ps --format '{{.Names}} {{.Status}}' | grep -E "omnia_core.*Up" | grep -q "healthy\|Up"; then - echo "[INFO] [ORCHESTRATOR] Container is healthy after restart" - break - fi - sleep 1 - health_count=$((health_count + 1)) - echo -n "." - done - - if [ $health_count -ge $health_timeout ]; then - echo "" - echo "[ERROR] [ORCHESTRATOR] Container failed to become healthy within 30 seconds after restart" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: Container health check failed" - return 1 - fi - - # Verify version update - local updated_version=$(get_current_omnia_version) - if [ "$updated_version" != "$metadata_version" ]; then - echo "[ERROR] [ORCHESTRATOR] Version update verification failed" - echo "[ERROR] [ORCHESTRATOR] Expected: $metadata_version, Found: $updated_version" - return 1 - fi - - echo "[INFO] [ORCHESTRATOR] Same-tag upgrade completed successfully" - echo "[INFO] [ORCHESTRATOR] Version updated to: $metadata_version" - - # Update metadata with git tag version from inside container - update_metadata_with_git_tag "$target_version" - - return 0 +# ───────────────────────────────────────────────────────────────────────────── +# Base Directory Creation +# ───────────────────────────────────────────────────────────────────────────── +create_base_dirs() { + echo -e "${BLUE}Creating base directory structure at ${OMNIA_DATA_PATH}...${NC}" + mkdir -p "${OMNIA_DATA_PATH}" + mkdir -p "${OMNIA_DATA_PATH}/log" + mkdir -p "${OMNIA_DATA_PATH}/input" + mkdir -p "${OMNIA_DATA_PATH}/.data" + echo -e "${GREEN}Base directories created. Domain directories will be created by respective playbooks.${NC}" } -phase4_container_swap() { - local quadlet_file="/etc/containers/systemd/omnia_core.container" - local i - - echo "[INFO] [ORCHESTRATOR] Phase 4: Container Swap" - - if [ ! -f "$quadlet_file" ]; then - echo "[ERROR] [ORCHESTRATOR] Phase 4.3 failed: Quadlet file not found: $quadlet_file" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: Quadlet configuration file missing" - display_cleanup_instructions - return 1 - fi - - echo "[INFO] [ORCHESTRATOR] Stopping omnia_core $OMNIA_CORE_CONTAINER_TAG container" - systemctl stop omnia_core.service >/dev/null 2>&1 || true - - if podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo "[WARN] [ORCHESTRATOR] omnia_core still running; forcing stop" - podman stop -t 30 omnia_core >/dev/null 2>&1 || true - fi - - if podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo "[ERROR] [ORCHESTRATOR] Failed to stop omnia_core container" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: Could not stop $OMNIA_CORE_CONTAINER_TAG container" - echo "[ERROR] [ORCHESTRATOR] Initiating rollback to restore container..." - rollback_omnia_core - return 1 - fi - - echo "[INFO] [ORCHESTRATOR] Starting omnia_core $TARGET_CONTAINER_TAG Quadlet unit" - if ! podman inspect "omnia_core:$TARGET_CONTAINER_TAG" >/dev/null 2>&1; then - echo "[ERROR] [ORCHESTRATOR] Target image missing locally: omnia_core:$TARGET_CONTAINER_TAG" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: $TARGET_CONTAINER_TAG image not available" - echo "[ERROR] [ORCHESTRATOR] Initiating rollback to restore container..." - rollback_omnia_core - return 1 - fi - - if ! sed -i "s/^Image=omnia_core:.*/Image=omnia_core:$TARGET_CONTAINER_TAG/" "$quadlet_file"; then - echo "[ERROR] [ORCHESTRATOR] Phase 4.3 failed: Failed to update Image to $TARGET_CONTAINER_TAG in quadlet file" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: Could not update container image tag" - echo "[ERROR] [ORCHESTRATOR] Initiating rollback to restore container..." - rollback_omnia_core - return 1 - fi - - systemctl daemon-reload || { - echo "[ERROR] [ORCHESTRATOR] Phase 4.3 failed: systemctl daemon-reload failed" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: System daemon reload failed" - echo "[ERROR] [ORCHESTRATOR] Initiating rollback to restore container..." - rollback_omnia_core - return 1 - } +# ───────────────────────────────────────────────────────────────────────────── +# Venv Setup +# ───────────────────────────────────────────────────────────────────────────── +setup_venv() { + load_env + validate_env + create_base_dirs - systemctl start omnia_core.service || { - echo "[ERROR] [ORCHESTRATOR] Phase 4.3 failed: Failed to start omnia_core.service" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: Could not start $TARGET_CONTAINER_TAG container" - echo "[ERROR] [ORCHESTRATOR] Initiating rollback to restore container..." - rollback_omnia_core - return 1 - } + echo -e "${BLUE}================================================================================${NC}" + echo -e "${BLUE} Omnia Virtual Environment Setup${NC}" + echo -e "${BLUE}================================================================================${NC}" + echo "" + echo -e " Venv path: ${GREEN}$OMNIA_VENV_PATH${NC}" + echo -e " Source dir: ${GREEN}$SRC_DIR${NC}" + echo "" - echo "[INFO] [ORCHESTRATOR] Waiting for omnia_core $TARGET_CONTAINER_TAG health check (60s)" - for i in $(seq 1 60); do - if podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then + # ── Find Python 3.11+ ── + local python_cmd="" + for candidate in python3.12 python3.11 python3; do + if command -v "$candidate" >/dev/null 2>&1; then + python_cmd="$candidate" break fi - sleep 1 done - if ! podman ps --format '{{.Names}} {{.Status}}' | grep -E "omnia_core.*Up" | grep -q "healthy\|Up"; then - echo "[ERROR] [ORCHESTRATOR] Phase 4.4 failed: Container failed health check after swap" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: $TARGET_CONTAINER_TAG container failed health check" - echo "[ERROR] [ORCHESTRATOR] Initiating rollback to restore container..." - rollback_omnia_core - return 1 - fi - - # Get version from git tag or use target version - local metadata_version=$(get_metadata_version "$TARGET_OMNIA_VERSION") - echo "[INFO] [ORCHESTRATOR] Updating metadata omnia_version to $metadata_version" - if ! podman exec -u root omnia_core bash -c " - set -e - if [ ! -f '$CONTAINER_METADATA_FILE' ]; then - echo '[ERROR] Metadata file not found inside container: $CONTAINER_METADATA_FILE' >&2 - exit 1 - fi - if grep -q '^omnia_version:' '$CONTAINER_METADATA_FILE'; then - sed -i 's/^omnia_version:.*/omnia_version: $metadata_version/' '$CONTAINER_METADATA_FILE' - else - echo 'omnia_version: $metadata_version' >> '$CONTAINER_METADATA_FILE' - fi - "; then - echo "[ERROR] [ORCHESTRATOR] Phase 4.5 failed: Failed to update metadata version" - echo "[ERROR] [ORCHESTRATOR] Upgrade failed: Could not update version metadata" - echo "[ERROR] [ORCHESTRATOR] Initiating rollback to restore container..." - rollback_omnia_core - return 1 + if [ -z "$python_cmd" ]; then + echo -e "${RED}ERROR: Python 3.11+ is required but not found.${NC}" + echo -e "${YELLOW}Install: dnf install -y python3.12${NC}" + exit 1 fi - echo "[INFO] [ORCHESTRATOR] Phase 4: Container swap completed" - # Update metadata with git tag version from inside container - update_metadata_with_git_tag "$TARGET_OMNIA_VERSION" - return 0 -} + local py_ver + py_ver=$($python_cmd --version 2>&1 | awk '{print $2}') + local py_major py_minor + py_major=$(echo "$py_ver" | cut -d. -f1) + py_minor=$(echo "$py_ver" | cut -d. -f2) -upgrade_omnia_core() { - # FIRST THING: Check if user has root privileges - if [ "$(id -u)" -ne 0 ]; then - echo -e "${RED}ERROR: Upgrade requires root or sudo privileges${NC}" - echo -e "${YELLOW}Please run this script with sudo or login as root user.${NC}" - echo -e "${YELLOW}Example: sudo $0 --upgrade${NC}" - exit 1 - fi - - echo -e "${BLUE}=================== Omnia Core Upgrade ====================${NC}" - echo -e "${BLUE}This script will upgrade Omnia core container.${NC}" - echo -e "${BLUE}Current version will be backed up and upgraded to target version.${NC}" - echo -e "${BLUE}=============================================================${NC}" - - # Read current version - OMNIA_VERSION=$(get_current_omnia_version) - if [ -z "$OMNIA_VERSION" ]; then - echo -e "${RED}ERROR: Could not determine current Omnia version${NC}" - echo -e "${YELLOW}Please ensure omnia_core container is running and metadata is accessible${NC}" - display_cleanup_instructions - exit 1 - fi - - # Get current container tag - OMNIA_CORE_CONTAINER_TAG=$(get_container_tag_from_version "$OMNIA_VERSION") - - echo -e "${GREEN}Current Omnia version: $OMNIA_VERSION${NC}" - echo -e "${GREEN}Current container tag: $OMNIA_CORE_CONTAINER_TAG${NC}" - - # Show available upgrade options - echo "" - echo "Available upgrade options:" - echo "=========================" - - # Get available upgrade versions dynamically - local upgrade_output - upgrade_output=$(get_available_upgrade_versions "$OMNIA_VERSION") - - # Parse output into versions and descriptions - local available_versions=() - local version_descriptions=() - local line_count=0 - local total_lines - - # Count total lines - total_lines=$(echo "$upgrade_output" | wc -l) - - # Split into versions and descriptions (first half = versions, second half = descriptions) - local mid_line=$((total_lines / 2)) - local line_num=0 - - while IFS= read -r line; do - line_num=$((line_num + 1)) - if [ $line_num -le $mid_line ]; then - available_versions+=("$line") - else - version_descriptions+=("$line") - fi - done <<< "$upgrade_output" - - # Check if any upgrade options are available - if [ ${#available_versions[@]} -eq 0 ]; then - echo -e "${GREEN}Already at latest version $OMNIA_VERSION${NC}" - echo "No upgrade options available." - exit 0 - fi - - # Display upgrade options - for i in "${!available_versions[@]}"; do - local target_version="${available_versions[$i]}" - local target_container_tag=$(get_container_tag_from_version "$target_version") - - # Check if target image exists locally - local image_status="✓ Available" - if ! podman inspect "omnia_core:$target_container_tag" >/dev/null 2>&1; then - image_status="✗ Missing (build required)" - fi - - echo "$((i+1)). Upgrade to $target_version (container tag: $target_container_tag) [$image_status]" - done - - # Prompt user to select upgrade version - echo -n "Select upgrade option (1-${#available_versions[@]}) or press Enter to cancel: " - read -r selection - - # Validate selection - if [ -z "$selection" ]; then - echo "Upgrade cancelled by user." - exit 0 - fi - - if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt ${#available_versions[@]} ]; then - echo -e "${RED}ERROR: Invalid selection.${NC}" - exit 1 - fi - - # Set target version based on user selection - TARGET_OMNIA_VERSION="${available_versions[$((selection-1))]}" - TARGET_CONTAINER_TAG=$(get_container_tag_from_version "$TARGET_OMNIA_VERSION") - - # Pre-validation: Check if target container image exists locally - if ! validate_container_image "$TARGET_OMNIA_VERSION" "$TARGET_CONTAINER_TAG" "upgrade"; then + if [ "$py_major" -lt 3 ] || { [ "$py_major" -eq 3 ] && [ "$py_minor" -lt 11 ]; }; then + echo -e "${RED}ERROR: Python >= 3.11 required. Found: $py_ver${NC}" exit 1 fi - - echo -e "${GREEN}Target Omnia version: $TARGET_OMNIA_VERSION${NC}" - echo -e "${GREEN}Target container tag: $TARGET_CONTAINER_TAG${NC}" - - # Check if container tag change is needed - if [ "$OMNIA_CORE_CONTAINER_TAG" = "$TARGET_CONTAINER_TAG" ]; then - echo -e "${BLUE}Upgrade within same container tag ($TARGET_CONTAINER_TAG)${NC}" - echo -e "${BLUE}Will restart container instead of swapping${NC}" - SAME_TAG_UPGRADE=true + echo -e "${GREEN}Using Python: $python_cmd ($py_ver)${NC}" + + # ── Create or update venv ── + if [ -d "$OMNIA_VENV_PATH" ]; then + echo -e "${YELLOW}Venv exists at $OMNIA_VENV_PATH — updating...${NC}" else - echo -e "${BLUE}Container tag change required ($OMNIA_CORE_CONTAINER_TAG -> $TARGET_CONTAINER_TAG)${NC}" - echo -e "${BLUE}Will perform full container swap${NC}" - SAME_TAG_UPGRADE=false - fi - - local lock_file="/tmp/omnia_upgrade.lock" - if [ -f "$lock_file" ]; then - echo -e "${RED}ERROR: Another upgrade process is already running${NC}" - echo -e "${YELLOW}If this is incorrect, remove the lock file: rm -f $lock_file${NC}" - exit 1 + echo -e "${GREEN}Creating venv at $OMNIA_VENV_PATH ...${NC}" + mkdir -p "$(dirname "$OMNIA_VENV_PATH")" + $python_cmd -m venv "$OMNIA_VENV_PATH" fi - touch "$lock_file" - trap 'rm -f "$lock_file"' EXIT - - # Create upgrade guard lock in shared path so other playbooks can block during upgrade - local upgrade_guard_lock_path - upgrade_guard_lock_path=$(get_upgrade_guard_lock_path) - mkdir -p "$(dirname "$upgrade_guard_lock_path")" 2>/dev/null || true - echo "Upgrade in progress. Run prepare_upgrade.yml + upgrade.yml or rollback to clear." > "$upgrade_guard_lock_path" || { - echo -e "${RED}ERROR: Failed to create upgrade guard lock: $upgrade_guard_lock_path${NC}" - exit 1 - } - - # Run upgrade phases - if ! phase1_validate; then - echo "[ERROR] [ORCHESTRATOR] Upgrade failed in Phase 1" - exit 1 - fi + # shellcheck disable=SC1091 + source "$OMNIA_VENV_PATH/bin/activate" - if ! phase2_approval; then - exit 0 - fi + echo -e "${BLUE}Upgrading pip...${NC}" + pip install --upgrade pip setuptools wheel --quiet - local backup_base="$OMNIA_UPGRADE_BACKUP_PATH" - if [ -z "$backup_base" ]; then - echo "[ERROR] [ORCHESTRATOR] Backup path is empty" - exit 1 - fi + # ── Discover and install per-domain pip requirements ── + local pip_installed=0 + for domain in "${DOMAINS[@]}"; do + local domain_pip="$SRC_DIR/$domain/requirements.txt" + if [ -f "$domain_pip" ]; then + echo -e "${BLUE}Installing pip packages for ${domain} ...${NC}" + if ! pip install -r "$domain_pip"; then + echo -e "${YELLOW}WARNING: pip install failed for $domain_pip — continuing${NC}" + else + pip_installed=$((pip_installed + 1)) + fi + fi + done - # Validate disk space before backup creation - if ! validate_backup_disk_space "$backup_base"; then - echo "[ERROR] [ORCHESTRATOR] Upgrade aborted: Insufficient disk space for backup" - exit 1 + if [ "$pip_installed" -eq 0 ]; then + echo -e "${YELLOW}WARNING: No requirements.txt found in any domain${NC}" fi - if ! phase3_backup_creation "$backup_base"; then - echo "[ERROR] [ORCHESTRATOR] Upgrade failed in Phase 3" + # ── Verify ansible is available ── + if ! command -v ansible >/dev/null 2>&1; then + echo -e "${RED}ERROR: ansible not found after pip install${NC}" + deactivate 2>/dev/null || true exit 1 fi - # Choose upgrade path based on container tag - if [ "$SAME_TAG_UPGRADE" = "true" ]; then - if ! phase4_same_tag_upgrade "$TARGET_OMNIA_VERSION"; then - echo "[ERROR] [ORCHESTRATOR] Upgrade failed in same-tag upgrade" - exit 1 - fi - else - if ! phase4_container_swap; then - echo "[ERROR] [ORCHESTRATOR] Upgrade failed in Phase 4" - exit 1 + # ── Discover and install per-domain Galaxy collections ── + for domain in "${DOMAINS[@]}"; do + local domain_galaxy="$SRC_DIR/$domain/requirements.yml" + if [ -f "$domain_galaxy" ]; then + echo -e "${BLUE}Installing Galaxy collections for ${domain} ...${NC}" + if ! ansible-galaxy collection install -r "$domain_galaxy" --force; then + echo -e "${YELLOW}WARNING: Galaxy install failed for $domain_galaxy — continuing${NC}" + fi fi - fi - - echo "[INFO] [ORCHESTRATOR] Upgrade completed successfully" - echo "[INFO] [ORCHESTRATOR] Backup location (inside omnia_core container): $backup_base" - - # Seed inputs and defaults after upgrade - post_setup_config + done + # ── Summary ── echo "" echo -e "${GREEN}================================================================================${NC}" - echo -e "${GREEN} UPGRADE COMPLETED SUCCESSFULLY${NC}" + echo -e "${GREEN} Omnia Venv Setup Complete${NC}" echo -e "${GREEN}================================================================================${NC}" echo "" - echo -e "${GREEN}✓ Omnia core has been upgraded to version $TARGET_OMNIA_VERSION${NC}" - echo -e "${GREEN}✓ Container is running and healthy${NC}" - echo -e "${GREEN}✓ Configuration backed up to: $backup_base${NC}" + echo -e " Venv: ${GREEN}$OMNIA_VENV_PATH${NC}" + echo -e " Python: ${GREEN}$(python3 --version)${NC}" + echo -e " Ansible: ${GREEN}$(ansible --version | head -1)${NC}" + echo "" + echo -e "${BLUE}Installed collections:${NC}" + ansible-galaxy collection list 2>/dev/null | grep -E "^(ansible\.|containers\.|community\.|kubernetes\.|omnia\.)" || true + echo "" + echo -e "${YELLOW}Activate in your shell:${NC}" + echo -e " ${GREEN}source $OMNIA_VENV_PATH/bin/activate${NC}" echo "" - show_post_upgrade_instructions "$TARGET_OMNIA_VERSION" - # Wait for sshd inside the new container before configuring keys / connecting - wait_for_ssh_ready - # Initialize SSH config and start container session - init_ssh_config - remove_container_omnia_sh - start_container_session - exit 0 + deactivate 2>/dev/null || true } -# Validate backup directory structure and files -validate_backup_directory() { - local backup_path="$1" - - echo "[INFO] [ROLLBACK] Validating backup directory: $backup_path" - - # Check if backup directory exists - if ! podman exec -u root omnia_core test -d "$backup_path"; then - echo "[ERROR] [ROLLBACK] Backup directory does not exist: $backup_path" - return 1 - fi - - # Check for required subdirectories - for subdir in input metadata configs; do - if ! podman exec -u root omnia_core test -d "$backup_path/$subdir"; then - echo "[ERROR] [ROLLBACK] Missing required subdirectory: $backup_path/$subdir" - return 1 - fi +# ───────────────────────────────────────────────────────────────────────────── +# Version +# ───────────────────────────────────────────────────────────────────────────── +get_version_from_git_tag() { + local git_root="$SCRIPT_DIR" + while [ "$git_root" != "/" ] && [ ! -d "$git_root/.git" ]; do + git_root="$(dirname "$git_root")" done - # Check for required files - if ! podman exec -u root omnia_core test -f "$backup_path/metadata/oim_metadata.yml"; then - echo "[ERROR] [ROLLBACK] Missing metadata file: $backup_path/metadata/oim_metadata.yml" - return 1 - fi - - if ! podman exec -u root omnia_core test -f "$backup_path/configs/omnia_core.container"; then - echo "[ERROR] [ROLLBACK] Missing container config: $backup_path/configs/omnia_core.container" - return 1 - fi - - # Verify metadata contains version information - if ! podman exec -u root omnia_core grep -q "^omnia_version:" "$backup_path/metadata/oim_metadata.yml"; then - echo "[ERROR] [ROLLBACK] Metadata file does not contain version information" - return 1 - fi - - echo "[INFO] [ROLLBACK] Backup validation successful" - return 0 -} - -# Stop container gracefully with timeout -stop_container_gracefully() { - local container_name="$1" - local timeout="${2:-30}" - - echo "[INFO] [ROLLBACK] Stopping $container_name container gracefully..." - - # Try graceful stop first - if podman stop -t "$timeout" "$container_name" >/dev/null 2>&1; then - echo "[INFO] [ROLLBACK] Container stopped gracefully" - return 0 - fi - - # Check if container is still running - if podman ps --format '{{.Names}}' | grep -qw "$container_name"; then - echo "[WARN] [ROLLBACK] Graceful stop failed, force stopping container..." - if podman stop "$container_name" >/dev/null 2>&1; then - echo "[INFO] [ROLLBACK] Container force stopped" + if [ "$git_root" != "/" ] && [ -d "$git_root/.git" ]; then + local tag + tag=$(cd "$git_root" && git tag --points-at HEAD 2>/dev/null | head -n 1) + if [[ "$tag" =~ ^v(.+)$ ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + elif [ -n "$tag" ]; then + echo "$tag" return 0 - else - echo "[ERROR] [ROLLBACK] Failed to stop container" - return 1 fi fi - - return 0 -} - -# Restore files from backup -restore_from_backup() { - local backup_path="$1" - - echo "[INFO] [ROLLBACK] Restoring from backup: $backup_path" - - # Restore input files - if ! podman exec -u root omnia_core bash -c " - set -e - rm -rf /opt/omnia/input - cp -a '$backup_path/input' /opt/omnia/ - "; then - echo "[ERROR] [ROLLBACK] Failed to restore input files" - return 1 - fi - - # Restore metadata - if ! podman exec -u root omnia_core cp -a "$backup_path/metadata/oim_metadata.yml" /opt/omnia/.data/; then - echo "[ERROR] [ROLLBACK] Failed to restore metadata" - return 1 - fi - - # Restore container config on host - if ! podman cp "omnia_core:$backup_path/configs/omnia_core.container" /etc/containers/systemd/; then - echo "[ERROR] [ROLLBACK] Failed to restore container config" - return 1 - fi - - echo "[INFO] [ROLLBACK] Files restored successfully" - return 0 -} - -# Display cleanup instructions for failed upgrade/rollback -display_cleanup_instructions() { - echo "" - echo -e "${RED}================================================================================${NC}" - echo -e "${RED} UPGRADE/ROLLBACK FAILED${NC}" - echo -e "${RED}================================================================================${NC}" - echo "" - echo -e "${YELLOW}Operation failed. Manual cleanup is required to restore a clean state before retrying.${NC}" - echo "" - echo -e "${BLUE}Choose the appropriate cleanup scenario:${NC}" - echo "" - echo -e "${GREEN}CASE 1: If you can log into omnia_core container:${NC}" - echo -e "${YELLOW}1. Enter omnia_core container: podman exec -it omnia_core bash${NC}" - echo -e "${YELLOW}2. Run oim cleanup: ansible-playbook /omnia/oim_cleanup.yml${NC}" - echo -e "${YELLOW}3. Run uninstall inside container: ./omnia.sh --uninstall${NC}" - echo -e "${YELLOW}4. Exit container: exit${NC}" - echo -e "${YELLOW}5. Clean shared path: rm -rf ${NC}" - echo -e "${YELLOW}6. Install required version: ./omnia.sh --install${NC}" - echo "" - echo -e "${GREEN}CASE 2: If you cannot log into omnia_core container (but other containers are running):${NC}" - echo -e "${YELLOW}1. Remove all container definitions: cd /etc/containers/systemd${NC}" - echo -e "${YELLOW}2. Delete all container files: rm -rf *${NC}" - echo -e "${YELLOW}3. Reload systemd daemon: systemctl daemon-reload${NC}" - echo -e "${YELLOW}4. Stop all containers: podman stop $(podman ps -aq)${NC}" - echo -e "${YELLOW}5. Remove all containers: podman rm -f $(podman ps -aq)${NC}" - echo -e "${YELLOW}6. Clean shared path: rm -rf ${NC}" - echo -e "${YELLOW}7. Install required version: ./omnia.sh --install${NC}" - echo "" - echo -e "${BLUE}Note: Replace with your actual Omnia shared path.${NC}" echo "" + return 1 } -rollback_omnia_core() { - # FIRST THING: Check if user has root privileges - if [ "$(id -u)" -ne 0 ]; then - echo -e "${RED}ERROR: Rollback requires root or sudo privileges${NC}" - echo -e "${YELLOW}Please run this script with sudo or login as root user.${NC}" - echo -e "${YELLOW}Example: sudo $0 --rollback${NC}" - exit 1 - fi - - echo -e "${GREEN}================================================================================${NC}" - echo -e "${GREEN} OMNIA CORE ROLLBACK${NC}" - echo -e "${GREEN}================================================================================${NC}" - echo "" - - # Audit log start - local rollback_start=$(date -Iseconds) - echo "[AUDIT] Rollback operation started at: $rollback_start" - - # Check if omnia_core container is running - if ! podman ps --format '{{.Names}}' | grep -qw "omnia_core"; then - echo -e "${RED}ERROR: Omnia core container is not running.${NC}" - exit 1 - fi - - # ═══════════════════════════════════════════════════════════════════════════ - # SAFETY CHECK: Prevent core container rollback if upgrade.yml was run but - # rollback.yml has not completed successfully inside the container. - # This prevents inconsistent state where core is 2.1 but other components are 2.2. - # ═══════════════════════════════════════════════════════════════════════════ - local upgrade_manifest_path="/opt/omnia/.data/upgrade_manifest.yml" - local rollback_manifest_path="/opt/omnia/.data/rollback_manifest.yml" - - # Check if upgrade_manifest.yml exists (indicates upgrade process was started) - if podman exec -u root omnia_core test -f "$upgrade_manifest_path" 2>/dev/null; then - echo "[INFO] [ROLLBACK] Checking upgrade state before proceeding..." - - # Read component statuses from upgrade_manifest.yml - local component_statuses - component_statuses=$(podman exec -u root omnia_core grep -A20 'component_status:' "$upgrade_manifest_path" 2>/dev/null | grep -E '^\s+\w+:' | head -8) - - # Check if any component has been upgraded (status is not "pending") - local has_upgraded_components=false - if echo "$component_statuses" | grep -qvE ':\s*"?pending"?\s*$'; then - has_upgraded_components=true - fi - - if [ "$has_upgraded_components" = true ]; then - echo "[INFO] [ROLLBACK] Detected upgraded components. Checking rollback.yml completion status..." - - # Components have been upgraded - check if rollback.yml completed successfully - if podman exec -u root omnia_core test -f "$rollback_manifest_path" 2>/dev/null; then - local rollback_status - rollback_status=$(podman exec -u root omnia_core grep '^rollback_status:' "$rollback_manifest_path" 2>/dev/null | cut -d':' -f2 | tr -d ' \t\n\r"') - - if [ "$rollback_status" != "completed" ]; then - echo "" - echo -e "${RED}═══════════════════════════════════════════════════════════════════════════${NC}" - echo -e "${RED} CORE CONTAINER ROLLBACK BLOCKED${NC}" - echo -e "${RED}═══════════════════════════════════════════════════════════════════════════${NC}" - echo "" - echo -e "${RED}ERROR: Cannot rollback core container at this time.${NC}" - echo "" - echo -e "${YELLOW}Reason: upgrade.yml has upgraded components, but rollback.yml has not${NC}" - echo -e "${YELLOW} completed successfully inside the container.${NC}" - echo "" - echo -e "${YELLOW}Current rollback status: ${rollback_status:-'unknown'}${NC}" - echo "" - echo -e "${YELLOW}Rolling back the core container now would leave your cluster in an${NC}" - echo -e "${YELLOW}inconsistent state where:${NC}" - echo -e "${YELLOW} - Core container: 2.1 (rolled back)${NC}" - echo -e "${YELLOW} - Other components: 2.2 (not rolled back)${NC}" - echo "" - echo -e "${GREEN}Required Action:${NC}" - echo -e "${GREEN} 1. First run rollback.yml inside the container to rollback all components${NC}" - echo -e "${GREEN} 2. Wait for rollback.yml to complete successfully${NC}" - echo -e "${GREEN} 3. Then run 'omnia.sh --rollback' to rollback the core container${NC}" - echo "" - exit 1 - fi - echo "[INFO] [ROLLBACK] Rollback playbook completed successfully. Proceeding with core container rollback." - else - # Rollback manifest doesn't exist but components were upgraded - echo "" - echo -e "${RED}═══════════════════════════════════════════════════════════════════════════${NC}" - echo -e "${RED} CORE CONTAINER ROLLBACK BLOCKED${NC}" - echo -e "${RED}═══════════════════════════════════════════════════════════════════════════${NC}" - echo "" - echo -e "${RED}ERROR: Cannot rollback core container at this time.${NC}" - echo "" - echo -e "${YELLOW}Reason: upgrade.yml has upgraded components, but rollback.yml has not${NC}" - echo -e "${YELLOW} been executed inside the container.${NC}" - echo "" - echo -e "${YELLOW}Rolling back the core container now would leave your cluster in an${NC}" - echo -e "${YELLOW}inconsistent state where:${NC}" - echo -e "${YELLOW} - Core container: 2.1 (rolled back)${NC}" - echo -e "${YELLOW} - Other components: 2.2 (not rolled back)${NC}" - echo "" - echo -e "${GREEN}Required Action:${NC}" - echo -e "${GREEN} 1. First run rollback.yml inside the container to rollback all components${NC}" - echo -e "${GREEN} 2. Wait for rollback.yml to complete successfully${NC}" - echo -e "${GREEN} 3. Then run 'omnia.sh --rollback' to rollback the core container${NC}" - echo "" - exit 1 - fi - else - echo "[INFO] [ROLLBACK] No components upgraded yet. Core container rollback is safe to proceed." - fi - else - echo -e "${WHITE}[INFO] [ROLLBACK] No upgrade manifest found. Core container rollback is safe to proceed.${NC}" - fi - # ═══════════════════════════════════════════════════════════════════════════ - - # Create lock file to prevent concurrent rollbacks - local lock_file="/tmp/omnia_rollback.lock" - if [ -f "$lock_file" ]; then - local existing_pid - existing_pid=$(cat "$lock_file" 2>/dev/null | tr -d ' \t\n\r') - - if [ -n "$existing_pid" ] && kill -0 "$existing_pid" >/dev/null 2>&1; then - echo -e "${RED}ERROR: Another rollback process is already running (PID: $existing_pid)${NC}" - echo -e "${YELLOW}If this is incorrect, remove the lock file: rm -f $lock_file${NC}" - exit 1 - fi - - if [ -n "$existing_pid" ]; then - echo -e "${YELLOW}[WARN] Stale rollback lock file found (PID: $existing_pid); removing: $lock_file${NC}" - fi - rm -f "$lock_file" >/dev/null 2>&1 || true - fi - - echo "$$" > "$lock_file" - trap 'rm -f "$lock_file"' EXIT INT TERM - - # Get current version - if ! podman exec -u root omnia_core test -f "/opt/omnia/.data/oim_metadata.yml"; then - echo -e "${RED}ERROR: Metadata file not found: /opt/omnia/.data/oim_metadata.yml${NC}" - exit 1 - fi - - local current_version=$(podman exec -u root omnia_core grep '^omnia_version:' /opt/omnia/.data/oim_metadata.yml 2>/dev/null | cut -d':' -f2 | tr -d ' \t\n\r') - - # Use upgrade_backup_dir from metadata as the authoritative rollback target - local selected_backup - selected_backup=$(podman exec -u root omnia_core grep '^upgrade_backup_dir:' /opt/omnia/.data/oim_metadata.yml 2>/dev/null | cut -d':' -f2- | tr -d ' \t\n\r') - if [ -z "$selected_backup" ]; then - echo -e "${RED}ERROR: upgrade_backup_dir not found in metadata; cannot determine rollback target.${NC}" - exit 1 - fi - - local selected_version - selected_version=$(echo "$selected_backup" | sed -n 's/.*version_\([^/]*\).*/\1/p') - if [ -z "$selected_version" ]; then - echo -e "${RED}ERROR: Could not derive rollback version from upgrade_backup_dir: $selected_backup${NC}" - exit 1 - fi - - local selected_container_tag=$(get_container_tag_from_version "$selected_version") - local current_container_tag=$(get_container_tag_from_version "$current_version") - - # Check if target image exists locally and inform user before confirmation - local image_status="✓ Available" - if ! podman inspect "omnia_core:$selected_container_tag" >/dev/null 2>&1; then - image_status="✗ Missing (build required)" - echo -e "${RED}ERROR: Required image omnia_core:$selected_container_tag is not available locally.${NC}" - echo -e "${YELLOW}Please build or load the image before retrying rollback.${NC}" - exit 1 - fi - - echo "" - echo "Rollback target derived from metadata:" - echo " - Version: $selected_version" - echo " - Backup path: $selected_backup" - echo " - Container tag: $selected_container_tag ($image_status)" - echo -n "Proceed with rollback using this backup? [y/N]: " - read -r confirm - if [[ ! "$confirm" =~ ^[yY] ]]; then - echo "Rollback cancelled by user." - exit 0 - fi - - # Pre-validation: Check if target container image exists locally - if ! validate_container_image "$selected_version" "$selected_container_tag" "rollback"; then - exit 1 - fi +display_version() { + echo -e "${BLUE}Omnia Infrastructure Manager${NC}" + echo -e " Release: ${GREEN}$OMNIA_RELEASE${NC}" - # Validate selected backup exists - if ! podman exec -u root omnia_core test -d "$selected_backup" 2>/dev/null; then - echo -e "${RED}ERROR: Backup directory does not exist: $selected_backup${NC}" - exit 1 + local git_ver + git_ver=$(get_version_from_git_tag 2>/dev/null || true) + if [ -n "$git_ver" ]; then + echo -e " Git tag: ${GREEN}$git_ver${NC}" fi - echo "" - if [ "$current_container_tag" = "$selected_container_tag" ]; then - echo -e "${BLUE}Rollback within same container tag ($selected_container_tag)${NC}" - echo -e "${BLUE}Will restart container instead of swapping${NC}" - - # Perform same-tag rollback (restore files + container restart) - if ! rollback_same_tag "$selected_version" "$current_version" "$selected_backup"; then - echo "[ERROR] [ROLLBACK] Rollback failed in same-tag rollback" - exit 1 - fi - - echo "[INFO] [ROLLBACK] Rollback completed successfully" - echo "[INFO] [ROLLBACK] Version rolled back to: $selected_version" - - # Update metadata with git tag version from inside container - update_metadata_with_git_tag "$selected_version" - - # Clean up lock file before starting long-running ssh session - rm -f "$lock_file" >/dev/null 2>&1 || true - - # Clear upgrade guard lock if it exists - local upgrade_guard_lock_path - upgrade_guard_lock_path=$(get_upgrade_guard_lock_path) - rm -f "$upgrade_guard_lock_path" >/dev/null 2>&1 || true - echo "[INFO] [ROLLBACK] Cleared upgrade guard lock: $upgrade_guard_lock_path" - - # Remove upgrade_backup_dir and previous_omnia_version from metadata - # These are upgrade-only fields that should not persist after rollback - podman exec -u root omnia_core bash -c " - sed -i '/^upgrade_backup_dir:/d' '$CONTAINER_METADATA_FILE' 2>/dev/null || true - sed -i '/^previous_omnia_version:/d' '$CONTAINER_METADATA_FILE' 2>/dev/null || true - " 2>/dev/null || true - echo "[INFO] [ROLLBACK] Cleaned upgrade_backup_dir and previous_omnia_version from metadata" - - # Fetch config from restored metadata (populates omnia_path, domain_name, etc.) - fetch_config - - # Wait for sshd inside the restarted container before configuring keys / connecting - wait_for_ssh_ready - # Initialize SSH config and start container session - init_ssh_config - remove_container_omnia_sh - start_container_session - exit 0 + if [ -d "${OMNIA_VENV_PATH:-/opt/omnia/venv}" ]; then + echo -e " Venv: ${GREEN}${OMNIA_VENV_PATH:-/opt/omnia/venv}${NC}" else - echo -e "${BLUE}Container tag change: ${current_container_tag} -> ${selected_container_tag}${NC}" - echo "[INFO] [ROLLBACK] Starting rollback process..." - fi - - # Capture metadata version from backup for later verification - local backup_metadata_version - backup_metadata_version=$(podman exec -u root omnia_core grep '^omnia_version:' "$selected_backup/metadata/oim_metadata.yml" 2>/dev/null | cut -d':' -f2 | tr -d ' \t\n\r') - if [ -z "$backup_metadata_version" ]; then - echo -e "${RED}ERROR: Backup metadata does not contain omnia_version in $selected_backup/metadata/oim_metadata.yml${NC}" - exit 1 - fi - - echo "" - echo "[INFO] [ROLLBACK] Starting rollback process..." - - # Step 1: Stop current container gracefully - echo "" - echo "[INFO] [ROLLBACK] Step 1: Stopping Omnia core $current_container_tag container..." - if ! stop_container_gracefully "omnia_core" 30; then - echo -e "${RED}ERROR: Failed to stop container.${NC}" - display_cleanup_instructions - exit 1 - fi - - # Step 2: Update Quadlet file to use target container tag - echo "" - echo "[INFO] [ROLLBACK] Step 2: Updating Quadlet file to use container tag $selected_container_tag..." - local quadlet_file="/etc/containers/systemd/omnia_core.container" - - if ! sed -i "s/^Image=omnia_core:.*/Image=omnia_core:$selected_container_tag/" "$quadlet_file"; then - echo -e "${RED}ERROR: Failed to update Image to $selected_container_tag in quadlet file${NC}" - display_cleanup_instructions - exit 1 - fi - - echo "[INFO] [ROLLBACK] Quadlet file updated to use omnia_core:$selected_container_tag" - - # Step 3: Start target container - echo "" - echo "[INFO] [ROLLBACK] Step 3: Starting Omnia core $selected_container_tag container..." - systemctl daemon-reload - if ! systemctl start omnia_core.service; then - echo -e "${RED}ERROR: Failed to start container service.${NC}" - display_cleanup_instructions - exit 1 + echo -e " Venv: ${YELLOW}(not set up — run ./omnia.sh --setup-venv)${NC}" fi - - # Step 4: Wait for container to be healthy - echo "" - echo "[INFO] [ROLLBACK] Step 4: Waiting for container to be healthy..." - local health_timeout=60 - local health_count=0 - - while [ $health_count -lt $health_timeout ]; do - if podman ps --format '{{.Names}} {{.Status}}' | grep -E "omnia_core.*Up" | grep -q "healthy\|Up"; then - echo "[INFO] [ROLLBACK] Container is healthy" - break - fi - sleep 1 - health_count=$((health_count + 1)) - echo -n "." - done - - if [ $health_count -ge $health_timeout ]; then - echo "" - echo -e "${RED}ERROR: Container failed to become healthy within 60 seconds.${NC}" - display_cleanup_instructions - exit 1 - fi - - # Step 5: Validate backup directory structure - echo "" - echo "[INFO] [ROLLBACK] Step 5: Validating backup directory structure..." - if ! validate_backup_directory "$selected_backup"; then - echo -e "${RED}ERROR: Backup validation failed.${NC}" - display_cleanup_instructions - exit 1 - fi - - # Step 6: Restore files from backup - echo "" - echo "[INFO] [ROLLBACK] Step 6: Restoring files from backup..." - if ! restore_from_backup "$selected_backup"; then - echo -e "${RED}ERROR: Failed to restore from backup.${NC}" - display_cleanup_instructions - exit 1 - fi - - # Step 7: Verify container version - echo "" - echo "[INFO] [ROLLBACK] Step 7: Verifying container version from restored metadata..." - local verify_version=$(podman exec -u root omnia_core grep '^omnia_version:' /opt/omnia/.data/oim_metadata.yml 2>/dev/null | cut -d':' -f2 | tr -d ' \t\n\r') - - if [ "$verify_version" != "$backup_metadata_version" ]; then - echo -e "${RED}ERROR: Version verification failed. Expected: $backup_metadata_version, Found: $verify_version${NC}" - display_cleanup_instructions - exit 1 - fi - - # Audit log end - local rollback_end=$(date -Iseconds) - echo "[AUDIT] Rollback operation completed at: $rollback_end" - echo "[AUDIT] Rolled back from version $current_version to $selected_version" - - echo "" - echo -e "${GREEN}================================================================================${NC}" - echo -e "${GREEN} ROLLBACK COMPLETED SUCCESSFULLY${NC}" - echo -e "${GREEN}================================================================================${NC}" - echo "" - echo -e "${GREEN}✓ Omnia core has been rolled back to version $selected_version${NC}" - echo -e "${GREEN}✓ Container is running and healthy${NC}" - echo -e "${GREEN}✓ Configuration restored from backup${NC}" - echo "" - - # Update metadata with git tag version from inside container - update_metadata_with_git_tag "$selected_version" - - # Clean up lock file before starting long-running ssh session - rm -f "$lock_file" >/dev/null 2>&1 || true - echo "[INFO] Rollback lock file removed before starting container session" - - # Clear upgrade guard lock if it exists (shared path visible to container and host) - local upgrade_guard_lock_path - upgrade_guard_lock_path=$(get_upgrade_guard_lock_path) - - rm -f "$upgrade_guard_lock_path" >/dev/null 2>&1 || true - echo "[INFO] [ROLLBACK] Cleared upgrade guard lock: $upgrade_guard_lock_path" - - # Remove upgrade_backup_dir and previous_omnia_version from metadata - # These are upgrade-only fields that should not persist after rollback - podman exec -u root omnia_core bash -c " - sed -i '/^upgrade_backup_dir:/d' '$CONTAINER_METADATA_FILE' 2>/dev/null || true - sed -i '/^previous_omnia_version:/d' '$CONTAINER_METADATA_FILE' 2>/dev/null || true - " 2>/dev/null || true - echo "[INFO] [ROLLBACK] Cleaned upgrade_backup_dir and previous_omnia_version from metadata" - - # Fetch config from restored metadata (populates omnia_path, domain_name, etc.) - fetch_config +} - # Wait for sshd inside the restored container before configuring keys / connecting - wait_for_ssh_ready - # Initialize SSH config and start container session - init_ssh_config - remove_container_omnia_sh - start_container_session +# ───────────────────────────────────────────────────────────────────────────── +# Help +# ───────────────────────────────────────────────────────────────────────────── +show_help() { + cat < + +COMMANDS: + --setup-venv Create/update the shared Python venv (pip + Galaxy collections) + Discovers and installs requirements from all domains automatically. + --version, -v Display Omnia version information + --help, -h Show this help message + +ENVIRONMENT: + All configuration is via src/main/omnia.env: + OMNIA_ADMIN_NIC_IP Admin NIC IP (default: 172.16.107.254) + OMNIA_DATA_PATH Root data directory (default: /opt/omnia) + OMNIA_PROJECT_NAME Project name (default: project_default) + OMNIA_HOSTNAME OIM hostname (default: oim) + OMNIA_DOMAIN_NAME Domain name (default: omnia.cluster) + OMNIA_VENV_PATH Python venv path (default: /opt/omnia/venv) + +EXAMPLES: + # First-time setup: + vi src/main/omnia.env # Set OMNIA_ADMIN_NIC_IP and other vars + ./omnia.sh --setup-venv # Install Python + Ansible into venv + + # Run component playbooks (after venv setup): + source /opt/omnia/venv/bin/activate + cd src/image_build_manager + ansible-playbook playbooks/image_build_manager.yml --tags validate + + cd src/orchestrator + ansible-playbook orchestrator.yml +EOF } -# Main function to check if omnia_core container is already running. -# If yes, ask the user if they want to enter the container or reinstall. -# If no, set it up. +# ───────────────────────────────────────────────────────────────────────────── +# Main Dispatch +# ───────────────────────────────────────────────────────────────────────────── main() { - case "$1" in - --install|-i) - install_omnia_core - ;; - --uninstall|-u) - cleanup_omnia_core - ;; - --upgrade) - upgrade_omnia_core - ;; - --rollback) - rollback_omnia_core - ;; - --build|-b) - build_omnia_core_image + case "${1:-}" in + --setup-venv) + setup_venv ;; --version|-v) + load_env 2>/dev/null || true display_version ;; --help|-h|"") show_help ;; *) - echo "Unknown option: $1" + echo -e "${RED}Unknown option: $1${NC}" show_help exit 1 ;; esac } -# Call the main function -main "$1" +main "$@" From 7731c8d4ac7706fef0df8a3d2bfbe8cee5146f41 Mon Sep 17 00:00:00 2001 From: Abhishek S A Date: Wed, 29 Jul 2026 10:55:10 +0530 Subject: [PATCH 3/3] omnia script update Signed-off-by: Abhishek S A --- src/main/README.md | 144 +++++++++++++++++++++++++++++++++++ src/main/README_omnia-cli.md | 130 +++++++++++++++++++++++++++++++ src/main/README_omnia.env.md | 133 ++++++++++++++++++++++++++++++++ src/main/README_omnia.sh.md | 130 +++++++++++++++++++++++++++++++ src/main/omnia.sh | 81 ++++++-------------- 5 files changed, 560 insertions(+), 58 deletions(-) create mode 100644 src/main/README.md create mode 100644 src/main/README_omnia-cli.md create mode 100644 src/main/README_omnia.env.md create mode 100644 src/main/README_omnia.sh.md diff --git a/src/main/README.md b/src/main/README.md new file mode 100644 index 0000000000..05e96cf456 --- /dev/null +++ b/src/main/README.md @@ -0,0 +1,144 @@ +# Omnia Main Directory + +This directory contains the core entry points for the Omnia Infrastructure Manager (OIM) deployment: + +- **`omnia.sh`** — Setup and initialization script +- **`omnia-cli`** — Status and diagnostics CLI +- **`omnia.env`** — Environment configuration file + +--- + +## Quick Start + +```bash +# 1. Configure environment +vi omnia.env # Set OMNIA_ADMIN_NIC_IP and other required variables + +# 2. Set up Python virtual environment (one-time) +./omnia.sh --setup-venv # or: ./omnia.sh -s + +# 3. Check domain status +./omnia-cli status + +# 4. Run domain playbooks +source /opt/omnia/venv/bin/activate +cd ..//playbooks +ansible-playbook .yml --tags validate,prepare,build +``` + +--- + +## Documentation + +- **[README_omnia.sh.md](README_omnia.sh.md)** — Detailed omnia.sh setup script documentation +- **[README_omnia-cli.md](README_omnia-cli.md)** — Detailed omnia-cli diagnostics CLI documentation +- **[README_omnia.env.md](README_omnia.env.md)** — Detailed omnia.env environment configuration documentation + +--- + +## Directory Structure + +After setup, Omnia creates the following directory structure: + +``` +/opt/omnia/ +├── venv/ # Python virtual environment +│ ├── bin/ +│ │ ├── activate +│ │ ├── ansible +│ │ ├── ansible-playbook +│ │ └── python +│ └── lib/ +├── log/ # Shared log directory +│ ├── repo_manager/ +│ ├── image_build_manager/ +│ ├── discovery/ +│ ├── orchestrator/ +│ ├── telemetry/ +│ └── build_stream/ +├── .data/ # Internal data +├── repo_manager/ # Repo manager output +│ └── output/ +│ └── project_default/ +│ ├── repo_status.yml +│ └── functional_group_packages.yml +├── image_build_manager/ # Image build output +│ ├── output/ +│ │ └── project_default/ +│ │ └── build_status.yml +│ └── log/ +├── discovery/ # Discovery output +│ ├── output/ +│ └── log/ +├── orchestrator/ # Orchestrator output +│ ├── output/ +│ └── log/ +├── telemetry/ # Telemetry output +│ ├── output/ +│ └── log/ +└── build_stream/ # Build stream output + ├── output/ + └── log/ +``` + +--- + +## Running Domain Playbooks + +After venv setup, run domain playbooks directly: + +```bash +# Activate the venv +source /opt/omnia/venv/bin/activate + +# Navigate to a domain's playbook directory +cd ../repo_manager/playbooks + +# Run the playbook +ansible-playbook repo_manager.yml + +# Run with specific tags +ansible-playbook repo_manager.yml --tags validate,prepare +``` + +### Common Tags + +Most domain playbooks support these tags: + +- `validate` — Schema and runtime validation +- `prepare` — Deploy prerequisites (containers, services) +- `build` — Main build/execution phase +- `cleanup` — Stop services and remove artifacts +- `upgrade` - Upgrade of the domain +- `rollback` - Rollback of the domain + +--- + +## Multi-Project Deployments + +To manage multiple projects (e.g., `dev`, `staging`, `prod`): + +```bash +# Set different project names +OMNIA_PROJECT_NAME=dev ./omnia-cli status +OMNIA_PROJECT_NAME=staging ./omnia-cli status +OMNIA_PROJECT_NAME=prod ./omnia-cli status + +# Or use the --project flag +./omnia-cli status --project dev +./omnia-cli status --project staging +./omnia-cli status --project prod +``` + +Each project will have its own input/output directories under each domain. + +--- + +## Version Information + +Check your Omnia version: + +```bash +./omnia-cli version +``` + diff --git a/src/main/README_omnia-cli.md b/src/main/README_omnia-cli.md new file mode 100644 index 0000000000..d0141ab7d4 --- /dev/null +++ b/src/main/README_omnia-cli.md @@ -0,0 +1,130 @@ +# omnia-cli — Diagnostics CLI Documentation + +The `omnia-cli` script provides status checking and diagnostics for all Omnia domains. + +## Commands + +| Command | Description | +|---------|-------------| +| `status [--project ]` | Show all domain statuses for a project | +| `repo-manager [--project ]` | Detailed repo_manager diagnostics | +| `image-build [--project ]` | Detailed image_build_manager diagnostics | +| `orchestrator [--project ]` | Orchestrator status | +| `discovery [--project ]` | Discovery domain status | +| `telemetry [--project ]` | Telemetry stack status | +| `build-stream [--project ]` | Build stream (GitLab) status | +| `version` | Show Omnia version info | +| `help []` | Show help (or domain-specific help) | + +## Options + +- `--project ` or `-p ` — Project name (default: `$OMNIA_PROJECT_NAME` or `project_default`) + +## Examples + +```bash +# Check all domains for default project +./omnia-cli status + +# Check all domains for a specific project +./omnia-cli status --project my_cluster + +# Detailed repo_manager check +./omnia-cli repo-manager + +# Image build status for production +./omnia-cli image-build --project prod + +# Show version information +./omnia-cli version + +# Get help for a specific domain +./omnia-cli help repo-manager +``` + +## Status Output Format + +The CLI uses symbols to indicate status: + +- `✔` — Success/completed +- `✗` — Failure/error +- `⚠` — Warning/incomplete +- `–` — Skipped/not run + +## Example: `omnia-cli status` + +``` +Omnia Domain Status (project: project_default) +------------------------------------------------------------ + + ✔ repo manager completed + RPM repository synchronization (Pulp) + Last: 2026-07-29 10:30:15 (repo_status.yml) + + ✔ image build manager completed + OS image building (MinIO + Registry + OpenCHAMI) + Last: 2026-07-29 11:15:42 (build_status.yml) + + – orchestrator + Cluster orchestration and provisioning + No output directory + + – discovery + Hardware discovery and inventory + No output directory + +------------------------------------------------------------ + 2/6 domains completed for project project_default +``` + +## Domain-Specific Commands + +### repo-manager + +Detailed diagnostics for the repo_manager domain: + +- Checks output directory exists +- Validates `repo_status.yml` and `overall_status` +- Verifies `functional_group_packages.yml` exists +- Checks Pulp certificate files +- Counts RPM repos by architecture + +```bash +./omnia-cli repo-manager +``` + +### image-build + +Detailed diagnostics for the image_build_manager domain: + +- Checks output directory exists +- Validates `build_status.yml` and `overall_status` +- Counts built images by architecture +- Shows latest build log location +- Displays last modified timestamp + +```bash +./omnia-cli image-build +``` + +## Output Directory Resolution + +The CLI resolves output directories based on domain type: + +- **repo_manager**: `$OMNIA_DATA_PATH/repo_manager/output//` +- **Other domains**: `$OMNIA_DATA_PATH//output//` + +## Status File Detection + +Each domain has a known status file pattern: + +| Domain | Status File | +|--------|-------------| +| repo_manager | `repo_status.yml` | +| image_build_manager | `build_status.yml` | +| orchestrator | `orchestrator_status.yml` | +| discovery | `discovery_status.yml` | +| telemetry | `telemetry_status.yml` | +| build_stream | `build_stream_status.yml` | + +The CLI will also search for any `*status*.yml` or `*status*.yaml` files if the expected file is not found. diff --git a/src/main/README_omnia.env.md b/src/main/README_omnia.env.md new file mode 100644 index 0000000000..867d697516 --- /dev/null +++ b/src/main/README_omnia.env.md @@ -0,0 +1,133 @@ +# omnia.env — Environment Configuration Documentation + +The `omnia.env` file is the **single source of configuration** for Omnia deployments. All domains read their configuration from this file. + +## Required Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `OMNIA_ADMIN_NIC_IP` | Admin NIC IP address of the OIM host. Used for Pulp, S3, container registry, and provisioning. | `172.16.107.254` | + +## Optional Variables (with Defaults) + +| Variable | Description | Default | +|----------|-------------|---------| +| `OMNIA_DATA_PATH` | Root data directory for all Omnia persistent data | `/opt/omnia` | +| `OMNIA_PROJECT_NAME` | Project name — maps to input/output subdirectories | `project_default` | +| `OMNIA_HOSTNAME` | Short hostname of the OIM host (NOT FQDN) | `oim` | +| `OMNIA_DOMAIN_NAME` | Domain name of the OIM host | `omnia.cluster` | +| `OMNIA_VENV_PATH` | Path to the shared Python virtual environment | `/opt/omnia/venv` | + +## Component Override Variables + +Each component stores data under `$OMNIA_DATA_PATH//`. Override these only for non-standard layouts: + +| Variable | Default | +|----------|---------| +| `IMAGE_BUILD_MANAGER_DATA_PATH` | `${OMNIA_DATA_PATH}/image_build_manager` | +| `REPO_MANAGER_DATA_PATH` | `${OMNIA_DATA_PATH}/repo_manager` | +| `DISCOVERY_DATA_PATH` | `${OMNIA_DATA_PATH}/discovery` | +| `ORCHESTRATOR_DATA_PATH` | `${OMNIA_DATA_PATH}/orchestrator` | +| `TELEMETRY_DATA_PATH` | `${OMNIA_DATA_PATH}/telemetry` | +| `BUILD_STREAM_DATA_PATH` | `${OMNIA_DATA_PATH}/build_stream` | + +## Example omnia.env + +```bash +# ============================================================================= +# omnia.env — Omnia Environment Configuration +# ============================================================================= +# +# FILL THIS FILE BEFORE RUNNING omnia.sh OR ANY ANSIBLE PLAYBOOK. +# +# This file is the SINGLE source of configuration for the Omnia deployment. +# Uncomment and set the required variables below. Optional variables have +# sensible defaults — change them only if your setup differs. +# +# Usage: +# 1. Edit this file: vi src/main/omnia.env +# 2. Setup venv: ./omnia.sh --setup-venv +# -- OR -- +# Source manually: set -a; source src/main/omnia.env; set +a +# Run playbooks: cd src/ && ansible-playbook playbooks/.yml +# +# ============================================================================= + +# ┌───────────────────────────────────────────────────────────────────────────┐ +# │ REQUIRED — You MUST set these before running anything │ +# └───────────────────────────────────────────────────────────────────────────┘ + +# Admin NIC IP address of the OIM host. +# This is the IP used for Pulp, S3, container registry, and provisioning. +OMNIA_ADMIN_NIC_IP=172.16.107.254 + +# ┌───────────────────────────────────────────────────────────────────────────┐ +# │ OPTIONAL — Defaults work for most deployments │ +# └───────────────────────────────────────────────────────────────────────────┘ + +# Root data directory for all Omnia persistent data (NFS-mounted or local). +# All component data paths are derived from this root. +OMNIA_DATA_PATH=/opt/omnia + +# Project name — maps to input/output subdirectories. +# Override for multi-project deployments. +OMNIA_PROJECT_NAME=project_default + +# Short hostname of the OIM host (NOT FQDN). +OMNIA_HOSTNAME=oim + +# Domain name of the OIM host. +OMNIA_DOMAIN_NAME=omnia.cluster + +# Path to the shared Omnia Python virtual environment. +# Created by: ./omnia.sh --setup-venv +OMNIA_VENV_PATH=/opt/omnia/venv + +# Omnia release version. +# OMNIA_VERSION=2.2 + +# ┌───────────────────────────────────────────────────────────────────────────┐ +# │ COMPONENT OVERRIDES — Change only for non-standard layouts │ +# └───────────────────────────────────────────────────────────────────────────┘ +# Each component stores data under OMNIA_DATA_PATH//. +# Uncomment to override: + +# IMAGE_BUILD_MANAGER_DATA_PATH=${OMNIA_DATA_PATH}/image_build_manager +# REPO_MANAGER_DATA_PATH=${OMNIA_DATA_PATH}/repo_manager +# DISCOVERY_DATA_PATH=${OMNIA_DATA_PATH}/discovery +# ORCHESTRATOR_DATA_PATH=${OMNIA_DATA_PATH}/orchestrator +# TELEMETRY_DATA_PATH=${OMNIA_DATA_PATH}/telemetry +# BUILD_STREAM_DATA_PATH=${OMNIA_DATA_PATH}/build_stream +``` + +## Environment Loading + +Both `omnia.sh` and `omnia-cli` source the `omnia.env` file using: + +```bash +set -a +. "$ENV_FILE" +set +a +``` + +This ensures all variables are exported to the shell environment. + +## Validation + +The `omnia.sh --setup-venv` command validates that required variables are set before proceeding. If validation fails, the script will exit with an error message indicating which variables are missing. + +## Multi-Project Deployments + +To manage multiple projects, you can either: + +1. **Set `OMNIA_PROJECT_NAME` in omnia.env** for the default project +2. **Override it on the command line**: + ```bash + OMNIA_PROJECT_NAME=prod ./omnia-cli status + ``` +3. **Use the --project flag with omnia-cli**: + ```bash + ./omnia-cli status --project prod + ``` + +Each project will have its own input/output directories under each domain's data path. diff --git a/src/main/README_omnia.sh.md b/src/main/README_omnia.sh.md new file mode 100644 index 0000000000..04a1d99ad7 --- /dev/null +++ b/src/main/README_omnia.sh.md @@ -0,0 +1,130 @@ +# omnia.sh — Setup Script Documentation + +The `omnia.sh` script handles initial setup and environment configuration for Omnia. + +## Commands + +| Command | Description | +|---------|-------------| +| `--setup-venv, -s` | Create/update the shared Python venv with pip and Galaxy collections | +| `--help, -h` | Show help message | + +## What `--setup-venv` Does + +1. **Validates `omnia.env`** — Checks required variables are set +2. **Creates base directories** — Sets up `/opt/omnia/{log,.data}` +3. **Finds Python 3.11+** — Searches for python3.12, python3.11, or python3 +4. **Creates or updates venv** — Sets up virtual environment at `$OMNIA_VENV_PATH` +5. **Upgrades pip** — Ensures latest pip, setuptools, wheel +6. **Installs per-domain pip packages** — Discovers and installs `requirements.txt` from all domains +7. **Installs Galaxy collections** — Discovers and installs `requirements.yml` from all domains +8. **Displays summary** — Shows venv path, Python version, Ansible version, and installed collections + +## Example Output + +``` +================================================================================ + Omnia Virtual Environment Setup +================================================================================ + + Venv path: /opt/omnia/venv + Source dir: /path/to/omnia/src + +Using Python: python3.12 (3.12.1) +Creating venv at /opt/omnia/venv ... +Upgrading pip... +Installing pip packages for image_build_manager ... +Installing pip packages for telemetry ... +Installing Galaxy collections for repo_manager ... +Installing Galaxy collections for discovery ... + +================================================================================ + Omnia Venv Setup Complete +================================================================================ + + Venv: /opt/omnia/venv + Python: Python 3.12.1 + Ansible: ansible [core 2.17.0] + +Installed collections: +ansible.builtin +ansible.posix +community.general +kubernetes.core +... + +Activate in your shell: + source /opt/omnia/venv/bin/activate +``` + +## Domain Discovery + +The script automatically discovers and installs dependencies from all known domains: + +- `build_stream` +- `discovery` +- `image_build_manager` +- `orchestrator` +- `repo_manager` +- `telemetry` +- `utils` + +For each domain, it looks for: +- `requirements.txt` — Python pip packages +- `requirements.yml` — Ansible Galaxy collections + +## Troubleshooting + +### Python Version Error + +``` +ERROR: Python >= 3.11 required. Found: 3.10 +``` + +**Solution**: Install Python 3.11 or later: +```bash +dnf install -y python3.12 +``` + +### Missing omnia.env + +``` +ERROR: omnia.env not found at /path/to/omnia.env +``` + +**Solution**: Ensure you're in the `src/main` directory and `omnia.env` exists: +```bash +cd src/main +ls -la omnia.env +``` + +### Variable Not Set + +``` +ERROR: OMNIA_ADMIN_NIC_IP is not set in omnia.env +``` + +**Solution**: Edit `omnia.env` and set the required variable: +```bash +vi omnia.env # Set OMNIA_ADMIN_NIC_IP +``` + +### Ansible Not Found After Setup + +``` +ERROR: ansible not found after pip install +``` + +**Solution**: Check that the venv was created correctly and activate it: +```bash +source /opt/omnia/venv/bin/activate +ansible --version +``` + +### No requirements.txt Found + +``` +WARNING: No requirements.txt found in any domain +``` + +**Solution**: This is a warning, not an error. It means no domains have Python dependencies. If you expect dependencies, check that domain directories exist under `src/`. diff --git a/src/main/omnia.sh b/src/main/omnia.sh index ffe98a0bab..50231a0d78 100755 --- a/src/main/omnia.sh +++ b/src/main/omnia.sh @@ -44,7 +44,7 @@ readonly YELLOW='\033[0;33m' readonly NC='\033[0m' # Omnia release metadata -readonly OMNIA_RELEASE="2.2.0.0" +readonly OMNIA_RELEASE="2.3.0.0" # Known domain directories (each may contain requirements.txt / requirements.yml) readonly DOMAINS=( @@ -243,53 +243,12 @@ setup_venv() { deactivate 2>/dev/null || true } -# ───────────────────────────────────────────────────────────────────────────── -# Version -# ───────────────────────────────────────────────────────────────────────────── -get_version_from_git_tag() { - local git_root="$SCRIPT_DIR" - while [ "$git_root" != "/" ] && [ ! -d "$git_root/.git" ]; do - git_root="$(dirname "$git_root")" - done - - if [ "$git_root" != "/" ] && [ -d "$git_root/.git" ]; then - local tag - tag=$(cd "$git_root" && git tag --points-at HEAD 2>/dev/null | head -n 1) - if [[ "$tag" =~ ^v(.+)$ ]]; then - echo "${BASH_REMATCH[1]}" - return 0 - elif [ -n "$tag" ]; then - echo "$tag" - return 0 - fi - fi - echo "" - return 1 -} - -display_version() { - echo -e "${BLUE}Omnia Infrastructure Manager${NC}" - echo -e " Release: ${GREEN}$OMNIA_RELEASE${NC}" - - local git_ver - git_ver=$(get_version_from_git_tag 2>/dev/null || true) - if [ -n "$git_ver" ]; then - echo -e " Git tag: ${GREEN}$git_ver${NC}" - fi - - if [ -d "${OMNIA_VENV_PATH:-/opt/omnia/venv}" ]; then - echo -e " Venv: ${GREEN}${OMNIA_VENV_PATH:-/opt/omnia/venv}${NC}" - else - echo -e " Venv: ${YELLOW}(not set up — run ./omnia.sh --setup-venv)${NC}" - fi -} - # ───────────────────────────────────────────────────────────────────────────── # Help # ───────────────────────────────────────────────────────────────────────────── show_help() { cat < -COMMANDS: - --setup-venv Create/update the shared Python venv (pip + Galaxy collections) - Discovers and installs requirements from all domains automatically. - --version, -v Display Omnia version information - --help, -h Show this help message +SETUP COMMANDS: + --setup-venv, -s Create/update the shared Python venv (pip + Galaxy collections) + Discovers and installs requirements from all domains automatically. + --help, -h Show this help message + +DIAGNOSTICS (see omnia-cli): + ./omnia-cli status [--project ] All domain statuses + ./omnia-cli repo-manager [--project ] Repo manager details + ./omnia-cli image-build [--project ] Image build details + ./omnia-cli [--project ] Any domain status + ./omnia-cli version Version info + ./omnia-cli help [] CLI help ENVIRONMENT: All configuration is via src/main/omnia.env: @@ -317,13 +283,14 @@ EXAMPLES: vi src/main/omnia.env # Set OMNIA_ADMIN_NIC_IP and other vars ./omnia.sh --setup-venv # Install Python + Ansible into venv + # Check domain status: + ./omnia-cli status # All domains + ./omnia-cli repo-manager # Repo manager details + # Run component playbooks (after venv setup): source /opt/omnia/venv/bin/activate - cd src/image_build_manager - ansible-playbook playbooks/image_build_manager.yml --tags validate - - cd src/orchestrator - ansible-playbook orchestrator.yml + cd src/image_build_manager/playbooks + ansible-playbook image_build_manager.yml --tags validate EOF } @@ -332,18 +299,16 @@ EOF # ───────────────────────────────────────────────────────────────────────────── main() { case "${1:-}" in - --setup-venv) + --setup-venv|-s) setup_venv ;; - --version|-v) - load_env 2>/dev/null || true - display_version - ;; --help|-h|"") show_help ;; *) echo -e "${RED}Unknown option: $1${NC}" + echo -e "${YELLOW}For status & diagnostics, use: ./omnia-cli${NC}" + echo "" show_help exit 1 ;;