diff --git a/.gitignore b/.gitignore index 97719ef4..0885f60a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ __pycache__/ *.py[cod] *$py.class +# HTTP artifacts fetched by build.sh from artifacts.txt +**/*.tar.gz diff --git a/README.md b/README.md index 12daf1fd..9c5e838d 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ The repository currently builds: - `openstack-tempest`; - `openstack-unbound`; - `openstack-watcher-base`; +- `openstack-tobiko`; ## Quick start diff --git a/build.sh b/build.sh index 2c7a73f3..74649352 100755 --- a/build.sh +++ b/build.sh @@ -85,6 +85,12 @@ # build all packages from source instead of using wheels. # REGISTRY_AUTH_FILE Authentication file passed explicitly to buildah push. # REGISTRY_CERT_DIR TLS certificate directory passed explicitly to buildah push. +# +# Extra HTTP artifacts: +# If containers///artifacts.txt exists, build.sh downloads +# each pinned file into that directory before buildah bud. Format: +# +# Files are checksum-verified and gitignored; rebuilds reuse a matching copy. set -euo pipefail @@ -231,6 +237,46 @@ ensure_project_constraints() { return 1 } +# Fetch extra HTTP artifacts listed in artifacts.txt into the image directory. +# Format: +ensure_artifacts() { + local dir_name="$1" + local artifacts_file="${CONTAINERS_DIR}/${dir_name}/artifacts.txt" + [[ -f "${artifacts_file}" ]] || return 0 + + if ! command -v curl >/dev/null; then + echo "ERROR: curl is required to fetch artifacts from ${artifacts_file}" >&2 + return 1 + fi + + local dest_dir + dest_dir="$(dirname "${artifacts_file}")" + while IFS=' ' read -r filename sha256 url; do + [[ -z "${filename}" || "${filename}" == \#* ]] && continue + local dest="${dest_dir}/${filename}" + local actual="" + if [[ -f "${dest}" ]]; then + actual="$(sha256sum "${dest}" | awk '{print $1}')" + if [[ "${actual}" == "${sha256}" ]]; then + echo "--- Artifact ${filename} already present ---" + continue + fi + echo "--- Artifact ${filename} checksum mismatch, re-fetching ---" + rm -f "${dest}" + fi + echo "--- Fetching ${filename} ---" + curl -fsSL -o "${dest}" "${url}" + actual="$(sha256sum "${dest}" | awk '{print $1}')" + if [[ "${actual}" != "${sha256}" ]]; then + echo "ERROR: checksum mismatch for ${filename}" >&2 + echo " expected ${sha256}" >&2 + echo " got ${actual}" >&2 + rm -f "${dest}" + return 1 + fi + done < "${artifacts_file}" +} + # Clone a repo at a specific commit hash if not already present # Args: clone_at_hash() { @@ -322,6 +368,8 @@ build_image() { echo "=== Building ${full_tag} ===" + ensure_artifacts "${dir_name}" + # openstack-base image: no service source, build context is its own directory if [[ -z "${project}" ]]; then local base_constraints="${CONSTRAINTS_FILE}.${STREAM}" @@ -1590,7 +1638,7 @@ case "${ACTION}" in fi ;; install-deps) - SYSTEM_DEPS=(git buildah podman) + SYSTEM_DEPS=(git buildah podman curl) echo "=== Installing system dependencies ===" echo "Packages: ${SYSTEM_DEPS[*]}" if command -v dnf &>/dev/null; then diff --git a/containers/tobiko/OWNERS b/containers/tobiko/OWNERS new file mode 100644 index 00000000..05844c2f --- /dev/null +++ b/containers/tobiko/OWNERS @@ -0,0 +1,12 @@ +# See the OWNERS docs at https://www.kubernetes.dev/docs/guide/owners/ +approvers: + - kstrenkova + - fyanac + - eduolivares + - slawqo + +reviewers: + - kstrenkova + - fyanac + - eduolivares + - slawqo diff --git a/containers/tobiko/buildrequirements.lock b/containers/tobiko/buildrequirements.lock new file mode 120000 index 00000000..823367e2 --- /dev/null +++ b/containers/tobiko/buildrequirements.lock @@ -0,0 +1 @@ +buildrequirements.lock.master \ No newline at end of file diff --git a/containers/tobiko/buildrequirements.lock.master b/containers/tobiko/buildrequirements.lock.master new file mode 100644 index 00000000..39bb511e --- /dev/null +++ b/containers/tobiko/buildrequirements.lock.master @@ -0,0 +1,36 @@ +build==1.5.0 +calver==2025.10.20 +cffi==2.0.0 +coherent-licensed==0.5.2 +cython==3.3.0 +expandvars==1.1.2 +flit-core==3.12.0 +flit-core==4.0.2 +hatch-fancy-pypi-readme==25.1.0 +hatch-vcs==0.5.0 +hatchling==1.32.0 +importlib-metadata==9.0.1 +maturin==1.15.0 +packaging==26.2 +pathspec==1.1.1 +pbr==7.0.3 +pkgconfig==1.6.0 +pluggy==1.6.0 +poetry-core==2.4.1 +pycparser==3.0 +pyproject-hooks==1.2.0 +semantic-version==2.10.0 +setuptools-rust==1.13.0 +setuptools-scm==10.2.1 +setuptools-scm==7.1.0 +tomlkit==0.15.1 +trove-classifiers==2026.6.1.19 +typing-extensions==4.15.0 +vcs-versioning==2.3.1 +wheel==0.46.2 +wheel==0.48.0 +zipp==4.1.0 + +# The following packages are considered to be unsafe in a requirements file: +setuptools==82.0.1 +setuptools==84.0.0 diff --git a/containers/tobiko/requirements.lock b/containers/tobiko/requirements.lock new file mode 120000 index 00000000..c466dc02 --- /dev/null +++ b/containers/tobiko/requirements.lock @@ -0,0 +1 @@ +requirements.lock.master \ No newline at end of file diff --git a/containers/tobiko/requirements.lock.master b/containers/tobiko/requirements.lock.master new file mode 100644 index 00000000..79a3c5ec --- /dev/null +++ b/containers/tobiko/requirements.lock.master @@ -0,0 +1,156 @@ +aiohappyeyeballs==2.6.2 +aiohttp==3.14.1 +aiosignal==1.4.0 +alembic==1.18.5 +amqp==5.3.1 +attrs==26.1.0 +autopage==0.6.0 +bcrypt==5.0.0 +build==1.5.0 +cachetools==7.1.4 +certifi==2026.7.22 +cffi==2.0.0 +chardet==6.0.0.post1 +charset-normalizer==3.4.7 +cliff==4.15.0 +cmd2==4.0.0 +colorama==0.4.6 +coverage==7.14.3 +debtcollector==3.1.0 +decorator==5.3.1 +distlib==0.4.3 +dnspython==2.8.0 +dogpile-cache==1.5.0 +dpkt==1.9.8 +eventlet==0.41.0 +execnet==2.1.2 +fasteners==0.20 +filelock==3.29.4 +fixtures==4.3.2 +frozenlist==1.8.0 +futurist==3.4.0 +greenlet==3.5.3 +idna==3.18 +iniconfig==2.3.0 +invoke==3.0.3 +iso8601==2.1.0 +jinja2==3.1.6 +jmespath==1.1.0 +jsonpatch==1.33 +jsonpointer==3.1.1 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +keystoneauth1==5.15.0 +kombu==5.6.2 +lxml==6.1.1 +mako==1.3.12 +markdown-it-py==4.2.0 +markupsafe==3.0.3 +mdurl==0.1.2 +metalsmith==2.5.0 +msgpack==1.2.1 +multidict==6.7.1 +ncclient==0.7.1 +netaddr==1.3.0 +neutron-lib==4.2.0 +openshift-client==2.0.5 +openstacksdk==4.18.0 +os-ken==4.2.1 +os-service-types==1.9.0 +os-traits==3.8.0 +osc-lib==4.7.0 +oslo-concurrency==7.6.1 +oslo-config==10.7.0 +oslo-context==6.5.0 +oslo-db==18.1.0 +oslo-i18n==6.9.0 +oslo-log==8.3.0 +oslo-messaging==18.2.0 +oslo-metrics==0.16.0 +oslo-middleware==8.2.0 +oslo-policy==6.0.0 +oslo-serialization==5.11.0 +oslo-service==4.8.0 +oslo-utils==10.1.1 +oslo-versionedobjects==3.11.0 +osprofiler==4.4.0 +ovs==3.7.1 +packaging==26.2 +paramiko==4.0.0 +paste==3.10.1 +pastedeploy==3.1.0 +pbr==7.0.3 +pecan==1.8.0 +platformdirs==4.10.0 +pluggy==1.6.0 +podman==4.7.0 +prettytable==3.18.0 +prometheus-client==0.25.0 +prompt-toolkit==3.0.52 +propcache==0.5.2 +psutil==7.2.2 +pycparser==3.0 +pygments==2.20.0 +pynacl==1.6.2 +pyopenssl==24.2.1 +pyparsing==3.3.2 +pyperclip==1.11.0 +pyproject-api==1.11.0 +pyproject-hooks==1.2.0 +pytest==9.0.3 +pytest-cov==7.1.0 +pytest-html==4.2.0 +pytest-metadata==3.1.1 +pytest-reportportal==5.6.10 +pytest-rerunfailures==16.5 +pytest-subtests==0.15.0 +pytest-timeout==2.4.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 +python-designateclient==7.0.0 +python-discovery==1.4.2 +python-glanceclient==4.12.0 +python-heatclient==5.2.0 +python-ironicclient==6.2.0 +python-keystoneclient==5.8.0 +python-manilaclient==6.2.0 +python-neutronclient==13.0.0 +python-novaclient==18.13.0 +python-octaviaclient==3.14.0 +python-openstackclient==10.2.1 +python-swiftclient==4.10.0 +pyxdg==0.28 +pyyaml==6.0.3 +referencing==0.37.0 +reportportal-client==5.7.9 +repoze-lru==0.8 +requests==2.34.2 +rfc3986==2.0.0 +rich==15.0.0 +rich-argparse==1.8.0 +routes==2.5.1 +rpds-py==2026.5.1 +setproctitle==1.3.7 +six==1.17.0 +sortedcontainers==2.4.0 +sqlalchemy==2.0.51 +sshtunnel==0.4.0 +statsd==4.0.1 +stevedore==5.9.0 +testresources==2.1.2 +testscenarios==0.6.2 +testtools==2.9.1 +tox==4.13.0 +typing-extensions==4.15.0 +tzdata==2026.2 +urllib3==2.7.0 +vine==5.1.0 +virtualenv==21.5.1 +warlock==2.1.0 +wcwidth==0.8.1 +webob==1.8.10 +wrapt==2.2.2 +yappi==1.7.6 +yarl==1.24.2 + +# The following packages are considered to be unsafe in a requirements file: diff --git a/containers/tobiko/rpms.in.yaml b/containers/tobiko/rpms.in.yaml new file mode 100644 index 00000000..53a09c73 --- /dev/null +++ b/containers/tobiko/rpms.in.yaml @@ -0,0 +1,38 @@ +contentOrigin: + repofiles: + - ./rpms.repo +context: + bare: true + +arches: + - x86_64 + - aarch64 + +packages: + - cargo + - findutils + - gcc + - gcc-c++ + - git-core + - iperf3 + - iproute + - iputils + - libffi-devel + - libxml2-devel + - libxslt-devel + - net-tools + - nmap-ncat + - openssh-clients + - openssl-devel + - podman + - procps-ng + - python3 + - python3-cryptography-43.0.0-4.el10 + - python3-devel + - python3-pip + - python3-setuptools + - python3-wheel + - rust + - tar + - tcpdump + - which diff --git a/containers/tobiko/sources.txt b/containers/tobiko/sources.txt new file mode 100644 index 00000000..de0d4815 --- /dev/null +++ b/containers/tobiko/sources.txt @@ -0,0 +1,2 @@ +master upper-constraints https://opendev.org/openstack/requirements.git master b79ae30778228441fe49b61fc51b0ff2f440546c +master tobiko https://opendev.org/x/tobiko.git master 681c83a674eae124bf0c44a9e315288b309707eb diff --git a/containers/tobiko/src/.gitkeep b/containers/tobiko/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/containers/tobiko/tobiko/Containerfile b/containers/tobiko/tobiko/Containerfile new file mode 100644 index 00000000..3a335c4d --- /dev/null +++ b/containers/tobiko/tobiko/Containerfile @@ -0,0 +1,147 @@ +ARG BASE_IMAGE=localhost/openstack/openstack-base:latest + +# --- Build stage: compile wheels from source --- +FROM ${BASE_IMAGE} AS build + +ARG CONSTRAINTS_FILE=requirements.lock +COPY ${CONSTRAINTS_FILE} /deps-upper-constraints.txt +COPY src/ /src/ +COPY tobiko/src/ /src/ + +# We can force build from source of all packages optionally +ARG PIP_NO_BINARY="" +ENV PIP_NO_BINARY=${PIP_NO_BINARY} +# required to build rpds-py from source +ENV MATURIN_NO_INSTALL_RUST=true + +COPY tobiko/builddeps.txt /tmp/builddeps.txt +RUN pkgs=$(cat /tmp/builddeps.txt | grep -v '^#' | grep -v '^$' | tr '\n' ' ') && \ + if [ -n "${pkgs}" ]; then microdnf -y install ${pkgs} && microdnf clean all; fi + +COPY tobiko/pythonbuilddeps.txt /tmp/pythonbuilddeps.txt +RUN pkgs=$(cat /tmp/pythonbuilddeps.txt | grep -v '^#' | grep -v '^$' | tr '\n' ' ') && \ + if [ -n "${pkgs}" ]; then pip3 install --no-cache-dir -c /deps-upper-constraints.txt ${pkgs}; fi && \ + rm /tmp/pythonbuilddeps.txt + +RUN cp /deps-upper-constraints.txt /tmp/build-constraints.txt && \ + for src_dir in /src/*/ /src/overrides/*/; do \ + if [ -d "${src_dir}" ] && [ -f "${src_dir}/setup.cfg" ]; then \ + # Normalize underscores to hyphens: setup.cfg may use either form + # but the lockfile always uses hyphens. + pkg_name=$(grep -m1 '^name' "${src_dir}/setup.cfg" | sed 's/.*=\s*//' | tr '_' '-'); \ + sed -i "/^${pkg_name}[=!<>]/Id" /tmp/build-constraints.txt; \ + fi; \ + done + +# First build the wheels for the packages themselves +RUN for pkg in /src/*/ /src/overrides/*/; do \ + if [ -d "${pkg}" ] && [ -f "${pkg}/setup.cfg" -o -f "${pkg}/setup.py" -o -f "${pkg}/pyproject.toml" ]; then \ + pip3 wheel --no-cache-dir --no-deps \ + --wheel-dir=/wheels/pkgs "${pkg}"; \ + fi; \ + done + +# Second, build the wheels for the dependencies and place them in a different directory +RUN pip3 wheel --no-cache-dir --no-deps \ + --find-links=/wheels/pkgs \ + --wheel-dir=/wheels/deps -r /deps-upper-constraints.txt + +RUN for src_dir in /src/*/ /src/overrides/*/; do \ + if [ -d "${src_dir}" ] && [ -f "${src_dir}/setup.cfg" ]; then \ + pkg_name=$(grep -m1 '^name' "${src_dir}/setup.cfg" | sed 's/.*=\s*//'); \ + commit=$(git -C "${src_dir}" rev-parse HEAD 2>/dev/null || echo "unknown"); \ + version=$(ls /wheels/pkgs/${pkg_name//-/_}-*.whl 2>/dev/null | head -1 | sed 's/.*-\([0-9][^-]*\)-.*/\1/' || echo "unknown"); \ + echo "${pkg_name},${commit},${version}"; \ + fi; \ + done > /source-built-packages.txt + +# --- Runtime stage --- +FROM ${BASE_IMAGE} + +LABEL summary="OpenStack Tobiko" \ + io.k8s.description="Tobiko testing framework image built from source with the Kolla interface" + +RUN uid_gid_manage tobiko + +COPY tobiko/bindeps.txt /tmp/bindeps.txt +RUN pkgs=$(cat /tmp/bindeps.txt | grep -v '^#' | grep -v '^$' | tr '\n' ' ') && \ + if [ -n "${pkgs}" ]; then microdnf -y install ${pkgs} && microdnf clean all; fi && \ + rm /tmp/bindeps.txt + +COPY --from=build /wheels /wheels +COPY --from=build /tmp/build-constraints.txt /deps-upper-constraints.txt +COPY --from=build /source-built-packages.txt /source-built-packages.txt +COPY tobiko/pythondeps.txt /tmp/pythondeps.txt +RUN extrapkgs=$(cat /tmp/pythondeps.txt | grep -v '^#' | grep -v '^$' | tr '\n' ' ') && \ + pip3 install --no-cache-dir --prefix=/usr \ + -c /deps-upper-constraints.txt \ + --find-links=/wheels/deps \ + --find-links=/wheels/pkgs \ + /wheels/pkgs/*.whl ${extrapkgs} && \ + rm -rf /wheels /tmp/pythondeps.txt + +# Lockfile is newer than some tobiko APIs: urllib3 2.x dropped +# poolmanager._key_fields; testtools 2.8+ dropped TestCase.skip. +COPY tobiko/scripts/tobiko_urllib3_compat.py \ + tobiko/scripts/tobiko_testtools_compat.py \ + tobiko/scripts/tobiko_skip_git_metadata.py \ + tobiko/scripts/tobiko_urllib3_compat.pth /tmp/ +RUN python3 -c "\ +import importlib.util, os, shutil; \ +spec = importlib.util.find_spec('tobiko'); \ +assert spec and spec.origin, 'tobiko is not installed'; \ +sp = os.path.dirname(os.path.dirname(os.path.abspath(spec.origin))); \ +shutil.copy('/tmp/tobiko_urllib3_compat.py', sp); \ +shutil.copy('/tmp/tobiko_testtools_compat.py', sp); \ +shutil.copy('/tmp/tobiko_skip_git_metadata.py', sp); \ +shutil.copy('/tmp/tobiko_urllib3_compat.pth', sp)" && \ + rm -f /tmp/tobiko_urllib3_compat.py /tmp/tobiko_testtools_compat.py \ + /tmp/tobiko_skip_git_metadata.py /tmp/tobiko_urllib3_compat.pth + +# Heat templates / Ansible playbooks are not always packed into the wheel. +COPY --from=build /src/tobiko/tobiko /tmp/tobiko-src +COPY tobiko/scripts/install_package_data.py /tmp/install_package_data.py +RUN python3 /tmp/install_package_data.py /tmp/tobiko-src && \ + rm -rf /tmp/tobiko-src /tmp/install_package_data.py + +# oc/kubectl for tobiko.podified. Prefetched by build.sh from artifacts.txt +ARG TARGETARCH=amd64 +COPY tobiko/openshift-client-linux-${TARGETARCH}.tar.gz /tmp/openshift-client-linux.tar.gz +RUN tar -zx -C /usr/local/bin -f /tmp/openshift-client-linux.tar.gz oc kubectl && \ + rm -f /tmp/openshift-client-linux.tar.gz + +# tox.ini commands = tools/run_tests.py (not pytest). That wrapper writes +# HTML/XML/log then calls pytest; it imports tools.common. Neither file is +# in the wheel (setup.cfg packages only tobiko). +COPY --from=build /src/tobiko/tools/run_tests.py \ + /src/tobiko/tools/common.py /usr/share/tobiko/tools/ +COPY --from=build /src/tobiko/tox.ini /usr/share/tobiko/tox.ini +RUN printf '%s\n' '#!/bin/sh' 'exec python3 /usr/share/tobiko/tools/run_tests.py "$@"' \ + > /usr/bin/run_tests.py && chmod +x /usr/bin/run_tests.py && \ + chown -R tobiko:tobiko /usr/share/tobiko + +# Image layout only. Testenv setenv (OS_TEST_PATH, PYTEST_TIMEOUT, …) comes +# from tox.ini at runtime. +ENV TOBIKO_DIR=/var/lib/tobiko \ + HOME=/var/lib/tobiko \ + TMPDIR=/tmp \ + USE_EXTERNAL_FILES=true + +RUN mkdir -p /etc/tobiko /var/log/tobiko \ + ${TOBIKO_DIR}/external_files \ + /var/lib/kolla/config_files && \ + touch /var/lib/kolla/config_files/config.json && \ + chown -R tobiko:tobiko /etc/tobiko /var/log/tobiko ${TOBIKO_DIR} + +COPY tobiko/config/etc/sudoers.d/tobiko_sudoers /etc/sudoers.d/tobiko_sudoers +COPY tobiko/scripts/run_tobiko.sh \ + tobiko/scripts/load_tox_setenv.py /usr/local/bin/ + +RUN chmod +x /usr/local/bin/run_tobiko.sh /usr/local/bin/load_tox_setenv.py && \ + chmod 440 /etc/sudoers.d/tobiko_sudoers + +WORKDIR ${TOBIKO_DIR} + +USER tobiko + +ENTRYPOINT ["/usr/local/bin/run_tobiko.sh"] diff --git a/containers/tobiko/tobiko/artifacts.txt b/containers/tobiko/tobiko/artifacts.txt new file mode 100644 index 00000000..bcd74691 --- /dev/null +++ b/containers/tobiko/tobiko/artifacts.txt @@ -0,0 +1,7 @@ +# Extra HTTP artifacts fetched by build.sh into this directory before buildah. +# Format: +# +# OpenShift client (same source as TCIB). Pin a versioned path, not "stable". +# Containerfile copies openshift-client-linux-${TARGETARCH}.tar.gz. +openshift-client-linux-amd64.tar.gz 76e22160684a4313daec380437abf54a029bddc3249a824928a84379069840d1 https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/4.22.9/openshift-client-linux.tar.gz +openshift-client-linux-arm64.tar.gz 3bb533a5ca471790adbf1d1edfb103c1db6c6b3651183c30a0a8419b7d448f17 https://mirror.openshift.com/pub/openshift-v4/aarch64/clients/ocp/4.22.9/openshift-client-linux.tar.gz diff --git a/containers/tobiko/tobiko/bindeps.txt b/containers/tobiko/tobiko/bindeps.txt new file mode 100644 index 00000000..3083a165 --- /dev/null +++ b/containers/tobiko/tobiko/bindeps.txt @@ -0,0 +1,21 @@ +# Runtime Python stack +python3 +python3-pip +# Pinned to 43.x to match upper-constraints.txt (cryptography==43.0.3). +# The default el10 RPM ships 49.x which conflicts with pyOpenSSL 24.2.1 +# (requires cryptography<44), causing an ImportError at runtime. Bump this +# pin once pyOpenSSL >= 25.0 lands in upper-constraints (drops the <44 cap). +python3-cryptography-43.0.0-4.el10 +tar +# Network / diagnostics tools used by Tobiko tests +findutils +iperf3 +iproute +iputils +net-tools +nmap-ncat +openssh-clients +podman +procps-ng +tcpdump +which diff --git a/containers/tobiko/tobiko/builddeps.txt b/containers/tobiko/tobiko/builddeps.txt new file mode 100644 index 00000000..f77da457 --- /dev/null +++ b/containers/tobiko/tobiko/builddeps.txt @@ -0,0 +1,21 @@ +# Compiler toolchain and headers for building Python C extensions +git-core +gcc +gcc-c++ +python3 +python3-pip +python3-devel +python3-setuptools +python3-wheel +libffi-devel +openssl-devel +# Pinned to 43.x to match upper-constraints.txt (cryptography==43.0.3). +# See bindeps.txt for the pyOpenSSL compatibility note. +python3-cryptography-43.0.0-4.el10 +# Required to build lxml +libxml2-devel +libxslt-devel +# To build bcrypt from source +rust +# To build rpds-py from source +cargo diff --git a/containers/tobiko/tobiko/config/etc/sudoers.d/tobiko_sudoers b/containers/tobiko/tobiko/config/etc/sudoers.d/tobiko_sudoers new file mode 100644 index 00000000..b7976aff --- /dev/null +++ b/containers/tobiko/tobiko/config/etc/sudoers.d/tobiko_sudoers @@ -0,0 +1,3 @@ +# Sourced from openstack-k8s-operators/tcib +# container-images/tcib/base/tobiko/tobiko_sudoers +tobiko ALL=(ALL) NOPASSWD: ALL diff --git a/containers/tobiko/tobiko/pythonbuilddeps.txt b/containers/tobiko/tobiko/pythonbuilddeps.txt new file mode 100644 index 00000000..f003e984 --- /dev/null +++ b/containers/tobiko/tobiko/pythonbuilddeps.txt @@ -0,0 +1,2 @@ +pbr +wheel diff --git a/containers/tobiko/tobiko/pythondeps.txt b/containers/tobiko/tobiko/pythondeps.txt new file mode 100644 index 00000000..91fe2b09 --- /dev/null +++ b/containers/tobiko/tobiko/pythondeps.txt @@ -0,0 +1,18 @@ +# Test runner (tobiko test-requirements.txt). +# Do not use extras syntax here (pytest-xdist[psutil]): Containerfile expands +# pythondeps unquoted and bash would glob the brackets. +pytest +pytest-html +pytest-metadata +pytest-xdist +psutil +# Extra deps not always covered by upper-constraints +# (tobiko extra-requirements.txt) +dpkt +openshift-client +podman==4.7.0 +pytest-cov +pytest-reportportal +pytest-rerunfailures +pytest-subtests +pytest-timeout diff --git a/containers/tobiko/tobiko/scripts/install_package_data.py b/containers/tobiko/tobiko/scripts/install_package_data.py new file mode 100755 index 00000000..e4828a6f --- /dev/null +++ b/containers/tobiko/tobiko/scripts/install_package_data.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Copy non-Python package data (Heat templates, playbooks) onto the install. + +Do not import tobiko: that pulls urllib3 internals and fails with urllib3 2.x +(poolmanager._key_fields was removed). +""" +import importlib.util +import os +import shutil +import sys + + +def installed_tobiko_dir() -> str: + spec = importlib.util.find_spec("tobiko") + if spec is None or not spec.origin: + raise SystemExit("tobiko is not installed") + return os.path.dirname(os.path.abspath(spec.origin)) + + +def main(src: str) -> None: + dst = installed_tobiko_dir() + for root, _dirs, files in os.walk(src): + rel = os.path.relpath(root, src) + for name in files: + if name.endswith(".py"): + continue + dest_dir = dst if rel == os.curdir else os.path.join(dst, rel) + os.makedirs(dest_dir, exist_ok=True) + shutil.copy2(os.path.join(root, name), os.path.join(dest_dir, name)) + + +if __name__ == "__main__": + main(sys.argv[1]) diff --git a/containers/tobiko/tobiko/scripts/load_tox_setenv.py b/containers/tobiko/tobiko/scripts/load_tox_setenv.py new file mode 100755 index 00000000..47b736fb --- /dev/null +++ b/containers/tobiko/tobiko/scripts/load_tox_setenv.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Print shell exports for a tox env's setenv. Reads tox.ini; does not use tox.""" +from __future__ import annotations + +import configparser +import os +import re +import shlex +import sys + +# Install-time / venv keys. Reports use TOBIKO_REPORT_DIR, not tox's envlogdir. +SKIP = { + "TOX_CONSTRAINTS", + "TOX_EXTRA_REQUIREMENTS", + "VIRTUAL_ENV", + "TOX_REPORT_DIR", + "TOX_COVER_DIR", +} + +INCLUDE_SETENV = re.compile(r"^\{\[([^\]]+)\]setenv\}$") +ENV_FACTOR = re.compile(r"\{env:([^:}]+)(?::([^}]*))?\}") + + +def read_ini(path: str) -> configparser.ConfigParser: + parser = configparser.ConfigParser(interpolation=None) + parser.optionxform = str + with open(path, encoding="utf-8") as handle: + parser.read_file(handle) + return parser + + +def setenv_lines( + parser: configparser.ConfigParser, + section: str, + seen: set[str] | None = None, +) -> list[str]: + if seen is None: + seen = set() + if section in seen or not parser.has_section(section): + return [] + seen.add(section) + raw = parser.get(section, "setenv", fallback="") + lines: list[str] = [] + for line in raw.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + match = INCLUDE_SETENV.match(line) + if match: + lines.extend(setenv_lines(parser, match.group(1), seen)) + continue + lines.append(line) + return lines + + +def substitute(value: str, envname: str, toxinidir: str, environ: dict[str, str]) -> str: + value = ( + value.replace("{toxinidir}", toxinidir) + .replace("{envname}", envname) + .replace("{envpython}", sys.executable) + .replace("{envdir}", "") + .replace("{toxworkdir}", "") + .replace("{envlogdir}", toxinidir) + ) + + def env_repl(match: re.Match[str]) -> str: + name, default = match.group(1), match.group(2) + if default is None: + default = "" + return environ.get(name, default) + + return ENV_FACTOR.sub(env_repl, value) + + +def resolve_env(parser: configparser.ConfigParser, envname: str, toxinidir: str) -> dict[str, str]: + section = f"testenv:{envname}" + if not parser.has_section(section): + raise SystemExit(f"Unknown tox env {envname!r}: no [{section}] in tox.ini") + if parser.has_option(section, "setenv"): + lines = setenv_lines(parser, section) + else: + lines = setenv_lines(parser, "testenv") + + environ = dict(os.environ) + resolved: dict[str, str] = {} + for line in lines: + if "=" not in line: + continue + key, val = line.split("=", 1) + key, val = key.strip(), val.strip() + if key in SKIP: + continue + value = substitute(val, envname, toxinidir, environ) + resolved[key] = value + environ[key] = value + if "OS_TEST_PATH" in resolved: + resolved["TOBIKO_TEST_PATH"] = resolved["OS_TEST_PATH"] + environ["TOBIKO_TEST_PATH"] = resolved["OS_TEST_PATH"] + return resolved + + +def split_pytest_addopts(addopts: str, site_packages: str) -> tuple[list[str], list[str]]: + tokens = shlex.split(addopts or "") + flags: list[str] = [] + paths: list[str] = [] + i = 0 + while i < len(tokens): + token = tokens[i] + if token.startswith("-"): + flags.append(token) + if "=" not in token and i + 1 < len(tokens) and not tokens[i + 1].startswith("-"): + i += 1 + flags.append(tokens[i]) + else: + filepart, sep, node = token.partition("::") + if site_packages and not os.path.isabs(filepart): + candidate = os.path.join(site_packages, filepart) + if os.path.exists(candidate): + filepart = candidate + paths.append(filepart + (sep + node if sep else "")) + i += 1 + return flags, paths + + +def main() -> None: + if len(sys.argv) >= 2 and sys.argv[1] == "--split-addopts": + site = sys.argv[2] if len(sys.argv) > 2 else "" + flags, paths = split_pytest_addopts(os.environ.get("PYTEST_ADDOPTS", ""), site) + print("ADDOPTS_FLAGS_ARR=(" + " ".join(shlex.quote(x) for x in flags) + ")") + print("ADDOPTS_PATHS_ARR=(" + " ".join(shlex.quote(x) for x in paths) + ")") + return + if len(sys.argv) < 3: + raise SystemExit( + f"usage: {sys.argv[0]} ENVNAME TOXINIDIR [TOX_INI]\n" + f" {sys.argv[0]} --split-addopts SITE_PACKAGES" + ) + envname = sys.argv[1] + toxinidir = sys.argv[2] + tox_ini = sys.argv[3] if len(sys.argv) > 3 else "/usr/share/tobiko/tox.ini" + resolved = resolve_env(read_ini(tox_ini), envname, toxinidir) + extra_arr = "()" + for key, value in resolved.items(): + print(f"export {key}={shlex.quote(value)}") + if key == "RUN_TESTS_EXTRA_ARGS": + extra_arr = "(" + " ".join(shlex.quote(p) for p in shlex.split(value)) + ")" + print(f"RUN_TESTS_EXTRA_ARGS_ARR={extra_arr}") + + +if __name__ == "__main__": + main() diff --git a/containers/tobiko/tobiko/scripts/run_tobiko.sh b/containers/tobiko/tobiko/scripts/run_tobiko.sh new file mode 100755 index 00000000..cd4795c3 --- /dev/null +++ b/containers/tobiko/tobiko/scripts/run_tobiko.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Entrypoint for test-operator. Loads tox.ini setenv (stdlib, no tox package) +# then calls tools/run_tests.py. Reports go to TOBIKO_REPORT_DIR on the logs PVC. + +set -x + +if [[ "${TOBIKO_DEBUG_MODE}" == true ]]; then + trap 'echo "run_tobiko.sh error"; sleep infinity' ERR +fi + +if [[ -n "${TOBIKO_VERSION}" ]]; then + echo "WARNING: TOBIKO_VERSION is ignored; rebuild the image to change tobiko." >&2 +fi +if [[ -n "${TOBIKO_PATCH_REFSPEC}" ]]; then + echo "WARNING: TOBIKO_PATCH_REFSPEC is ignored; rebuild the image." >&2 +fi + +[[ -z "${TOBIKO_TESTENV}" ]] && echo "TOBIKO_TESTENV not set" && exit 1 + +TOBIKO_DIR="${TOBIKO_DIR:-/var/lib/tobiko}" +TOBIKO_PRIVATE_KEY_FILE="${TOBIKO_PRIVATE_KEY_FILE:-id_ecdsa}" +TOBIKO_KEYS_FOLDER="${TOBIKO_KEYS_FOLDER:-/etc/test_operator}" +TOBIKO_LOGS_DIR_NAME="${TOBIKO_LOGS_DIR_NAME:-tobiko}" + +export HOME="${TOBIKO_DIR}" +export OS_CLOUD="${TOBIKO_OS_CLOUD:-${OS_CLOUD:-default}}" +[[ -n "${TOBIKO_PYTEST_ADDOPTS}" ]] && export PYTEST_ADDOPTS="${TOBIKO_PYTEST_ADDOPTS}" +[[ -n "${TOBIKO_NUM_PROCESSES}" ]] && export TOX_NUM_PROCESSES="${TOBIKO_NUM_PROCESSES}" +[[ -n "${TOBIKO_PREVENT_CREATE}" ]] && export TOBIKO_PREVENT_CREATE="${TOBIKO_PREVENT_CREATE}" +[[ -n "${TOBIKO_RUN_TESTS_TIMEOUT}" ]] && export TOX_RUN_TESTS_TIMEOUT="${TOBIKO_RUN_TESTS_TIMEOUT}" + +export TOBIKO_REPORT_DIR="${TOBIKO_DIR}/external_files/${TOBIKO_LOGS_DIR_NAME}" +export TOX_COVER_DIR="${TOBIKO_REPORT_DIR}/cover" +mkdir -p "${TOBIKO_REPORT_DIR}" "${TOBIKO_DIR}/tobiko" "${TMPDIR:-/tmp}" + +# test-operator mounts keys at TOBIKO_KEYS_FOLDER; ssh reads ~/.ssh. +if [[ -f "${TOBIKO_KEYS_FOLDER}/${TOBIKO_PRIVATE_KEY_FILE}" ]]; then + mkdir -p "${HOME}/.ssh" + chmod 700 "${HOME}/.ssh" + cp "${TOBIKO_KEYS_FOLDER}/${TOBIKO_PRIVATE_KEY_FILE}"* "${HOME}/.ssh/" + chmod 600 "${HOME}/.ssh/${TOBIKO_PRIVATE_KEY_FILE}" 2>/dev/null || true +fi + +# Parent of the installed tobiko package (site-packages). Passed to +# load_tox_setenv.py as {toxinidir} so tox.ini paths become absolute +# (.../site-packages/tobiko/tests/...) before pytest runs. +# cd: pytest resolves relative pytestAddopts/posargs (tobiko/tests/...) +# from cwd; WORKDIR /var/lib/tobiko does not have that tree. +SITE_PACKAGES="$(python3 -c "import importlib.util, os; s = importlib.util.find_spec('tobiko'); print(os.path.dirname(os.path.dirname(os.path.abspath(s.origin))))")" +cd "${SITE_PACKAGES}" + +EXTRA_POSARGS=() +TESTENV="${TOBIKO_TESTENV}" +if [[ "${TOBIKO_TESTENV}" == *" -- "* ]]; then + # shellcheck disable=SC2206 + EXTRA_POSARGS=(${TOBIKO_TESTENV#* -- }) + TESTENV="${TOBIKO_TESTENV%% -- *}" +fi +TESTENV="${TESTENV#"${TESTENV%%[![:space:]]*}"}" +TESTENV="${TESTENV%"${TESTENV##*[![:space:]]}"}" + +setenv_exports="$(python3 /usr/local/bin/load_tox_setenv.py "${TESTENV}" "${SITE_PACKAGES}")" || exit 1 +eval "${setenv_exports}" + +# Flags in PYTEST_ADDOPTS (--skipregex, -k) stay in the env; pytest +# applies them. If pytestAddopts is a test file, pass that file to +# pytest instead of the whole tox.ini directory. +ADDOPTS_FLAGS_ARR=() +ADDOPTS_PATHS_ARR=() +if [[ -n "${PYTEST_ADDOPTS:-}" ]]; then + eval "$(python3 /usr/local/bin/load_tox_setenv.py --split-addopts "${SITE_PACKAGES}")" +fi + +# tox.ini: run_tests.py {env:RUN_TESTS_EXTRA_ARGS} {posargs:{env:TOBIKO_TEST_PATH}} +RUN_ARGS=("${RUN_TESTS_EXTRA_ARGS_ARR[@]}") +if [[ ${#EXTRA_POSARGS[@]} -gt 0 ]]; then + RUN_ARGS+=("${EXTRA_POSARGS[@]}") +elif [[ ${#ADDOPTS_PATHS_ARR[@]} -gt 0 ]]; then + RUN_ARGS+=("${ADDOPTS_PATHS_ARR[@]}") +else + RUN_ARGS+=("${TOBIKO_TEST_PATH:-${OS_TEST_PATH}}") +fi + +/usr/bin/run_tests.py "${RUN_ARGS[@]}" +RETURN_VALUE=$? + +if [[ -n "${USE_EXTERNAL_FILES}" ]]; then + if [[ -f /etc/tobiko/tobiko.conf ]]; then + cp /etc/tobiko/tobiko.conf "${TOBIKO_REPORT_DIR}/" + fi + if [[ -f "${TOBIKO_DIR}/tobiko/tobiko.log" ]]; then + cp "${TOBIKO_DIR}/tobiko/tobiko.log" "${TOBIKO_REPORT_DIR}/" + fi +fi + +if [[ "${TOBIKO_DEBUG_MODE}" == true ]]; then + sleep infinity +fi + +exit "${RETURN_VALUE}" diff --git a/containers/tobiko/tobiko/scripts/tobiko_skip_git_metadata.py b/containers/tobiko/tobiko/scripts/tobiko_skip_git_metadata.py new file mode 100644 index 00000000..f7f992cb --- /dev/null +++ b/containers/tobiko/tobiko/scripts/tobiko_skip_git_metadata.py @@ -0,0 +1,24 @@ +"""Skip tobiko/tests/conftest.py git metadata. This image has no checkout. + +pytest_configure calls subprocess.check_output(['git', 'log', ...]) and +['git', 'describe', '--tags'] with no try/except. Return empty instead of +requiring a dummy git repo. +""" + +import subprocess + +_real_check_output = subprocess.check_output + +_SKIP = { + ("git", "log", "-n", "1"), + ("git", "describe", "--tags"), +} + + +def _check_output(args, *pargs, **kwargs): + if isinstance(args, (list, tuple)) and tuple(args) in _SKIP: + return "" + return _real_check_output(args, *pargs, **kwargs) + + +subprocess.check_output = _check_output diff --git a/containers/tobiko/tobiko/scripts/tobiko_testtools_compat.py b/containers/tobiko/tobiko/scripts/tobiko_testtools_compat.py new file mode 100644 index 00000000..f9c11dca --- /dev/null +++ b/containers/tobiko/tobiko/scripts/tobiko_testtools_compat.py @@ -0,0 +1,6 @@ +"""testtools 2.8+ removed TestCase.skip; tobiko still calls self.skip().""" + +import unittest + +if not hasattr(unittest.TestCase, "skip"): + unittest.TestCase.skip = unittest.TestCase.skipTest diff --git a/containers/tobiko/tobiko/scripts/tobiko_urllib3_compat.pth b/containers/tobiko/tobiko/scripts/tobiko_urllib3_compat.pth new file mode 100644 index 00000000..59349b0b --- /dev/null +++ b/containers/tobiko/tobiko/scripts/tobiko_urllib3_compat.pth @@ -0,0 +1,3 @@ +import tobiko_urllib3_compat +import tobiko_testtools_compat +import tobiko_skip_git_metadata diff --git a/containers/tobiko/tobiko/scripts/tobiko_urllib3_compat.py b/containers/tobiko/tobiko/scripts/tobiko_urllib3_compat.py new file mode 100644 index 00000000..5bfb9444 --- /dev/null +++ b/containers/tobiko/tobiko/scripts/tobiko_urllib3_compat.py @@ -0,0 +1,9 @@ +"""urllib3 2.x removed poolmanager._key_fields. Tobiko still reads it at import +(tobiko.http._session). Restore the name from PoolKey._fields so the pinned +tobiko wheel can load against the lockfile (urllib3 2.7). +""" + +from urllib3 import poolmanager + +if not hasattr(poolmanager, "_key_fields") and hasattr(poolmanager, "PoolKey"): + poolmanager._key_fields = poolmanager.PoolKey._fields diff --git a/containers/tobiko/tobiko/src/.gitkeep b/containers/tobiko/tobiko/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/containers/tobiko/upper-constraints.txt b/containers/tobiko/upper-constraints.txt new file mode 120000 index 00000000..b2ebb3b7 --- /dev/null +++ b/containers/tobiko/upper-constraints.txt @@ -0,0 +1 @@ +upper-constraints.txt.master \ No newline at end of file diff --git a/containers/tobiko/upper-constraints.txt.master b/containers/tobiko/upper-constraints.txt.master new file mode 100644 index 00000000..2e4a3b1d --- /dev/null +++ b/containers/tobiko/upper-constraints.txt.master @@ -0,0 +1,583 @@ +# WARNING: OpenStack makes no security guarantees about third-party +# dependencies listed here, and does not keep track of any +# vulnerabilities they contain. Versions of these dependencies are +# frozen at each coordinated release in order to stabilize upstream +# testing, and can contain known vulnerabilities. Consumers are +# *STRONGLY* encouraged to rely on curated distributions of OpenStack +# or manage security patching of dependencies themselves. +voluptuous===0.16.0 +chardet===6.0.0.post1 +enum-compat===0.0.3 +netmiko===4.7.0 +sshtunnel===0.4.0 +PasteDeploy===3.1.0 +Routes===2.5.1 +rtslib-fb===2.2.4 +oslo.limit===2.12.0 +restructuredtext_lint===2.0.2 +yarl===1.24.2 +tzdata===2026.2 +smmap===5.0.3 +confget===5.1.2 +XStatic-Angular-Bootstrap===2.5.0.1 +WebOb===1.8.10 +sphinxcontrib-actdiag===3.0.0 +pecan===1.8.0 +os-api-ref===3.2.0 +python-ldap===3.4.7 +oslo.concurrency===7.6.1 +websocket-client===1.9.0 +osprofiler===4.4.0 +os-resource-classes===1.1.0 +mypy_extensions===1.1.0 +tabulate===0.10.0 +python-ironic-inspector-client===5.4.0 +lxml===6.1.1 +vintage===0.4.1 +rst2txt===1.1.0 +setproctitle===1.3.7 +pytest===9.0.3 +python-slugify===8.0.4 +cursive===0.3.0 +oslo.service===4.8.0 +django-appconf===1.2.0 +ntc_templates===9.1.0 +sphinxcontrib-nwdiag===2.0.0 +rbd-iscsi-client===0.1.8 +alabaster===1.0.0 +multidict===6.7.1 +pbr===7.0.3 +munch===4.0.0 +waiting===1.5.0 +attrs===26.1.0 +jwcrypto===1.5.8 +Pint===0.25.3 +oslo.i18n===6.9.0 +jsonpath-rw-ext===1.2.2 +python-mistralclient===6.2.0 +oslo.context===6.5.0 +rcssmin===1.2.2 +pycadf===4.1.0 +grpcio===1.81.1 +sniffio===1.3.1 +fixtures===4.3.2 +neutron-lib===4.2.0 +XStatic-FileSaver===1.3.2.1 +jaraco.functools===4.5.0 +oslo.metrics===0.16.0 +storage-interfaces===1.0.5 +pydantic===2.13.4 +pystache===0.6.8 +XStatic-Font-Awesome===6.2.1.2 +aiohttp===3.14.1 +waitress===3.0.2 +os-refresh-config===14.0.1 +pysnmp===7.1.27 +Mako===1.3.12 +sphinxcontrib-htmlhelp===2.1.0 +XStatic-jQuery===3.7.1.1 +sphinx-copybutton===0.5.2 +beartype===0.22.9 +ddt===1.7.2 +pyserial===3.5 +moto===5.2.2 +infi.dtypes.wwn===0.1.1 +awscrt===0.35.0 +pcre2===0.7.0 +python-freezerclient===6.4.0 +python-vitrageclient===5.4.0 +py-pure-client===1.89.0 +krest===1.3.8 +psycopg2===2.9.12 +networkx===3.6.1 +cheroot===11.1.2 +XStatic-Angular===1.8.2.3 +zuul-sphinx===0.8.1 +ply===3.11 +google-api-core===2.31.0 +requests-toolbelt===1.0.0 +simplejson===4.1.1 +python-swiftclient===4.10.0 +pyOpenSSL===24.2.1 +typing-inspection===0.4.2 +monasca-common===3.8.0 +hyperframe===6.1.0 +zeroconf===0.150.0 +scipy===1.17.1 +opentelemetry-exporter-otlp===1.43.0 +rsd-lib===1.2.0 +XStatic-Jasmine===2.4.1.3 +googleapis-common-protos===1.75.0 +python-glanceclient===4.12.0 +prometheus_client===0.25.0 +jaraco.classes===3.4.0 +debtcollector===3.1.0 +responses===0.26.1 +prompt_toolkit===3.0.52 +croniter===6.2.2 +horizon===26.0.0 +octavia-lib===3.11.0 +python-watcherclient===4.10.0 +MarkupSafe===3.0.3 +doc8===2.0.0 +pymongo===4.17.0 +python-cloudkittyclient===6.1.0 +soupsieve===2.8.4 +sqlparse===0.5.5 +oslotest===6.1.1 +jsonpointer===3.1.1 +defusedxml===0.7.1 +opentelemetry-sdk===1.43.0 +netaddr===1.3.0 +pyghmi===1.6.18 +sphinxcontrib-blockdiag===3.0.0 +aiosqlite===0.22.1 +priority===2.0.0 +gnocchiclient===7.2.0 +wcwidth===0.8.1 +sphinxcontrib.datatemplates===0.11.0 +jsonpath-rw===1.4.0 +prettytable===3.18.0 +vine===5.1.0 +pathspec===1.1.1 +taskflow===6.3.0 +arrow===1.4.0 +semantic-version===2.10.0 +ConfigArgParse===1.7.5 +async-timeout===5.0.1 +virtualbmc===3.3.0 +SQLAlchemy===2.0.51 +pyroute2===0.8.1 +google-auth===2.55.1 +kazoo===2.11.0 +pyspnego===0.12.1 +trio-websocket===0.12.2 +XStatic-roboto-fontface===0.8.0.1 +pyudev===0.24.4 +eventlet===0.41.0 +openstack-doc-tools===4.0.3 +oslo.messaging===18.2.0 +jira===3.10.5 +PyJWT===2.13.0 +typing_extensions===4.15.0 +zVMCloudConnector===1.6.3 +paramiko===4.0.0 +ifaddr===0.2.0 +reno===4.1.0 +ncclient===0.7.1 +imagesize===2.0.0 +pydot===4.0.1 +urllib3===2.7.0 +graphviz===0.21 +PyKMIP===0.10.0 +python-observabilityclient===1.3.0 +whereto===0.6.0 +networking-generic-switch===10.0.0 +pywbem===1.9.0 +python-subunit===1.4.6 +pycparser===3.0 +mock===5.2.0 +PyYAML===6.0.3 +beautifulsoup4===4.15.0 +ovs===3.7.1 +cryptography===43.0.3 +httpcore===1.0.9 +URLObject===3.0.0 +psycopg2-binary===2.9.12 +glance_store===5.6.0 +openstack-release-test===8.5.0 +requests-mock===1.12.1 +os-apply-config===14.0.1 +gunicorn===26.0.0 +storpool===7.3.0 +textfsm===2.1.0 +python-3parclient===4.4 +libvirt-python===12.4.0 +python-zunclient===5.4.0 +tzlocal===5.4.3 +sysv_ipc===1.2.0 +sphinxcontrib-jsmath===1.0.1 +django_compressor===4.6.0 +awscurl===0.44 +trio===0.33.0 +python-novaclient===18.13.0 +pact===1.12.0 +bcrypt===5.0.0 +os-client-config===2.3.0 +XStatic-Angular-Gettext===2.4.1.1 +Deprecated===1.3.1 +h11===0.16.0 +Pygments===2.20.0 +XStatic-Hogan===2.0.0.5 +api_object_schema===2.0.0 +XStatic-objectpath===1.2.1.1 +python-manilaclient===6.2.0 +sphinxcontrib-serializinghtml===2.0.0 +requests===2.34.2 +snowballstemmer===3.1.1 +Jinja2===3.1.6 +XStatic-Bootstrap-SCSS===3.4.1.1 +pyzabbix===1.3.1 +ptyprocess===0.7.0 +amqp===5.3.1 +ruamel.yaml===0.19.1 +websockify===0.13.0 +gssapi===1.11.1 +XStatic-JQuery.quicksearch===2.0.3.3 +pyasn1_modules===0.4.2 +mpmath===1.3.0 +python-binary-memcached===0.32.0 +jaraco.context===6.1.2 +django-debreach===2.1.0 +sphinx-feature-classification===2.1.0 +XStatic-JQuery-Migrate===3.3.2.2 +pytest-html===4.2.0 +appdirs===1.4.4 +google-auth-httplib2===0.4.0 +daiquiri===3.4.0 +influxdb===5.3.2 +funcparserlib===2.0.0a0 +passlib===1.7.4 +cliff===4.15.0 +os-brick===7.1.0 +valkey===6.1.1 +scp===0.15.0 +lark===1.3.1 +python-zaqarclient===4.6.0 +ldappool===3.0.0 +hpack===4.2.0 +joblib===1.5.3 +roman-numerals===4.1.0 +google-api-python-client===2.198.0 +castellan===5.8.0 +oslo.versionedobjects===3.11.0 +enmerkar===0.7.1 +webcolors===25.10.0 +aodhclient===3.11.0 +autobahn===26.6.2 +SQLAlchemy-Utils===0.42.1 +retryz===0.1.9 +pluggy===1.6.0 +coverage===7.14.3 +pyee===13.0.1 +freezegun===1.5.5 +mdurl===0.1.2 +toml===0.10.2 +pycdlib===1.16.0 +pyperclip===1.11.0 +cassandra-driver===3.30.0 +XStatic-Angular-Schema-Form===0.8.13.1 +opentelemetry-exporter-otlp-proto-http===1.43.0 +gabbi===4.2.0 +nwdiag===3.0.0 +XStatic-bootswatch===3.3.7.1 +annotated-types===0.7.0 +pytest-xdist===3.8.0 +XStatic-JS-Yaml===3.13.1.2 +XStatic-term.js===0.0.7.1 +oslo.log===8.3.0 +nodeenv===1.10.0 +gossip===2.5.0 +suds-community===1.2.0 +os_vif===5.1.0 +qrcode===8.2 +oslo.middleware===8.2.0 +XStatic-mdi===1.6.50.3 +pydantic_core===2.46.4 +uritemplate===4.2.0 +docutils===0.21.2 +threadpoolctl===3.6.0 +os-ken===4.2.1 +ujson===5.13.0 +selenium===4.45.0 +pytest-subtests===0.15.0 +mistral-lib===3.5.1 +dogtag-pki===11.2.1 +XStatic-Angular-UUID===0.0.4.1 +dfs_sdk===1.2.27 +sphinxcontrib-seqdiag===3.0.0 +os-win===5.9.0 +capacity===1.3.14 +playwright===1.60.0 +markdown-it-py===4.2.0 +retrying===1.4.2 +python-discovery===1.4.2 +platformdirs===4.10.0 +pydotplus===2.0.2 +boto3===1.35.99 +jeepney===0.9.0 +stestr===4.2.1 +pillow===12.2.0 +infoblox-client===0.6.2 +oslo.serialization===5.11.0 +warlock===2.1.0 +exabgp===5.0.9 +aiomysql===0.3.2 +sphinxcontrib-httpdomain===2.0.0 +metalsmith===2.5.0 +s3transfer===0.10.4 +text-unidecode===1.3 +sphinxcontrib-svg2pdfconverter===2.1.0 +oslo.vmware===4.10.0 +autopage===0.6.0 +gitdb===4.0.12 +python-monascaclient===2.8.0 +opentelemetry-api===1.43.0 +frozenlist===1.8.0 +automaton===3.4.0 +os-service-types===1.9.0 +keyring===25.7.0 +elementpath===4.8.0 +wsgi_intercept===1.13.1 +jsonschema-specifications===2025.9.1 +testscenarios===0.6.2 +sphinxcontrib-pecanwsme===0.11.0 +sadisplay===0.4.9 +enum34===1.1.10 +infinisdk===289.1.3 +rich-argparse===1.8.0 +packaging===26.2 +opentelemetry-exporter-otlp-proto-grpc===1.43.0 +psutil===7.2.2 +txaio===26.6.1 +elasticsearch===9.4.1 +asgiref===3.11.1 +XStatic-JQuery.TableSorter===2.14.5.3 +pifpaf===3.4.0 +blockdiag===3.0.0 +testtools===2.9.1 +infi.dtypes.iqn===0.4.0 +jsonpath-ng===1.8.0 +XStatic-tv4===1.2.7.1 +XStatic-JSEncrypt===2.3.1.2 +python-cinderclient===9.9.0 +keystonemiddleware===13.0.0 +django-formtools===2.6.1 +XStatic-Spin===1.2.5.3 +rich===15.0.0 +os-traits===3.8.0 +typepy===1.3.5 +SecretStorage===3.5.0 +XStatic-Rickshaw===1.5.1.3 +iso8601===2.1.0 +tooz===9.0.1 +idna===3.18 +Hypercorn===0.18.0 +yamlloader===1.6.0 +protobuf===7.35.1 +sushy===5.12.0 +python-neutronclient===13.0.0 +pika===1.4.1 +oslo.cache===4.3.0 +WebTest===3.0.7 +os-collect-config===14.0.1 +edgegrid-python===2.0.7 +python-octaviaclient===3.14.0 +pysaml2===7.5.4 +requests-oauthlib===2.0.0 +oslo.reports===3.9.0 +bitmath===2.1.1 +ceilometermiddleware===3.12.0 +testrepository===0.0.22 +sympy===1.14.0 +Logbook===1.9.2 +PyNaCl===1.6.2 +osc-lib===4.7.0 +py-consul===1.7.1 +python-consul===1.1.0 +more-itertools===11.1.0 +seqdiag===3.0.0 +numpy===2.4.6 +msgpack===1.2.1 +Sphinx===9.0.4 +oslo.config===10.7.0 +openstackdocstheme===3.6.0 +osc-placement===4.9.0 +rpds-py===2026.5.1 +zake===0.2.2 +flux===1.4.0 +flexparser===0.4 +krb5===0.9.0 +PyMySQL===1.2.0 +uhashring===2.4 +kubernetes===36.0.2 +httplib2===0.32.0 +betamax===0.9.0 +construct===2.10.70 +pytest-metadata===3.1.1 +pyparsing===3.3.2 +geomet===1.1.0 +opentelemetry-exporter-otlp-proto-common===1.43.0 +distlib===0.4.3 +ast_serialize===0.5.0 +dogpile.cache===1.5.0 +python-barbicanclient===7.5.0 +salt===3008.0 +opentelemetry-semantic-conventions===0.64b0 +blinker===1.9.0 +WSME===0.12.1 +oslo.upgradecheck===2.8.0 +sherlock===0.4.1 +stevedore===5.9.0 +botocore===1.35.99 +xmltodict===1.0.4 +pyasn1===0.6.3 +oslo.rootwrap===7.10.0 +Django===5.2.15 +pexpect===4.9.0 +elastic-transport===9.4.2 +cmd2===4.0.0 +python-json-logger===4.1.0 +redis===8.0.1 +jmespath===1.1.0 +cbor2===6.1.2 +click===8.4.2 +XStatic-smart-table===1.4.13.3 +kuryr-lib===3.4.1 +jsonpatch===1.33 +libsass===0.23.0 +os-testr===3.0.0 +cotyledon===2.2.0 +xattr===1.3.0 +systemd-python===235 +python-memcached===1.62 +openstacksdk===4.18.0 +infi.dtypes.nqn===0.1.0 +six===1.17.0 +h2===4.3.0 +dulwich===1.2.6 +sentinels===1.1.1 +kombu===5.6.2 +distro===1.9.0 +zstd===1.5.7.3 +yaql===3.2.0 +durationpy===0.10 +requestsexceptions===1.4.0 +testresources===2.1.2 +falcon===4.3.1 +tomlkit===0.15.0 +etcd3gw===2.7.0 +Flask-RESTful===0.3.10 +GitPython===3.1.50 +python-ironicclient===6.2.0 +babel===2.18.0 +XStatic===1.0.3 +XStatic-Angular-FileUpload===12.2.13.2 +python-openstackclient===10.2.1 +pyzmq===27.1.0 +oslo.db===18.1.0 +simplegeneric===0.8.1 +yappi===1.7.6 +mbstrdecoder===1.1.5 +wsproto===1.3.2 +pymemcache===4.0.0 +wrapt===2.2.2 +PySocks===1.7.1 +oslo.privsep===3.12.0 +sphinxcontrib-apidoc===0.6.0 +oslo.policy===6.0.0 +hvac===2.4.0 +pyeclib===1.8.0 +rfc3986===2.0.0 +tenacity===9.1.4 +invoke===3.0.3 +python-designateclient===7.0.0 +pytest-cov===7.1.0 +reactivex===4.1.0 +Paste===3.10.1 +pytest-django===4.12.0 +XStatic-Json2yaml===0.1.1.1 +boto===2.49.0 +hyperlink===21.0.0 +mitba===1.1.1 +python-masakariclient===8.8.0 +Werkzeug===3.1.8 +outcome===1.3.0.post0 +APScheduler===3.11.2 +xmlschema===2.5.1 +python-troveclient===8.10.0 +cachez===0.1.2 +XStatic-Bootstrap-Datepicker===1.4.0.1 +netifaces===0.11.0 +propcache===0.5.2 +cachetools===7.1.4 +flexcache===0.3 +sphinxcontrib-qthelp===2.0.0 +keystoneauth1===5.15.0 +statsd===4.0.1 +proto-plus===1.28.0 +python-keystoneclient===5.8.0 +diskimage-builder===3.42.0 +heat-translator===3.5.0 +python-magnumclient===4.11.0 +docker===7.1.0 +repoze-lru===0.8 +storops===1.2.12 +anyio===4.14.1 +aiosignal===1.4.0 +XStatic-Angular-lrdragndrop===1.0.2.7 +ovsdbapp===2.18.0 +aniso8601===10.0.1 +rjsmin===1.2.5 +icalendar===7.2.0 +configparser===7.2.0 +decorator===5.3.1 +DateTimeRange===2.3.2 +cffi===2.0.0 +python-cyborgclient===2.8.0 +futurist===3.4.0 +jsonschema===4.26.0 +sphinxcontrib-devhelp===2.0.0 +python-blazarclient===4.5.0 +alembic===1.18.5 +execnet===2.1.2 +sphinxcontrib-programoutput===0.20 +storpool.spopenstack===3.2.0 +dnspython===2.8.0 +oauthlib===3.3.1 +zipp===4.1.0 +greenlet===3.5.3 +XStatic-Angular-Vis===4.16.0.1 +iniconfig===2.3.0 +referencing===0.37.0 +confluent-kafka===2.14.2 +backports.tarfile===1.2.0 +narwhals===2.22.1 +xvfbwrapper===0.2.23 +influxdb-client===1.50.0 +tosca-parser===2.14.0 +python-consul2===0.1.5 +charset-normalizer===3.4.7 +Flask===3.1.3 +httpx===0.28.1 +sqlalchemy-filters===0.13.0 +sphinxcontrib-runcmd===0.2.0 +confspirator===0.3.0 +fasteners===0.20 +importlib_metadata===9.0.0 +sortedcontainers===2.4.0 +microversion_parse===2.1.0 +python-linstor===1.28.2 +filelock===3.29.4 +python-tackerclient===2.5.0 +python-heatclient===5.2.0 +oslo.utils===10.1.1 +requests-kerberos===0.15.0 +itsdangerous===2.2.0 +XStatic-jquery-ui===1.13.0.2 +monasca-statsd===2.7.0 +python-dateutil===2.9.0.post0 +virtualenv===21.5.1 +colorama===0.4.6 +confetti===2.5.3 +ironic-lib===7.0.0 +aiohappyeyeballs===2.6.2 +pytz===2026.2 +opentelemetry-proto===1.43.0 +XStatic-D3===3.5.17.1 +actdiag===3.0.0 +sphinxcontrib-applehelp===2.0.0 +scikit-learn===1.9.0 +hpe-storage-flowkit-py===0.9.5 +a2wsgi===1.10.10 +respx===0.23.1