From 18807c9ba0d468af895a356b13e9294f631d93e0 Mon Sep 17 00:00:00 2001 From: Fiorella Yanac Date: Tue, 18 Aug 2026 11:24:13 +0100 Subject: [PATCH] Add openstack-tobiko container built from source Build Tobiko from the sources.txt pin and pip-install that wheel into the image, then run tests from the install instead of invoking tox or cloning the repository at runtime. The entrypoint reads tox.ini setenv without installing tox and calls tools/run_tests.py so TOBIKO_TESTENV still drives the same paths, timeouts, and HTML/XML reports. Assisted-By: Cursor-Grok 4.6 --- .gitignore | 2 + README.md | 1 + build.sh | 50 +- containers/tobiko/OWNERS | 12 + containers/tobiko/buildrequirements.lock | 1 + .../tobiko/buildrequirements.lock.master | 36 ++ containers/tobiko/requirements.lock | 1 + containers/tobiko/requirements.lock.master | 156 +++++ containers/tobiko/rpms.in.yaml | 38 ++ containers/tobiko/sources.txt | 2 + containers/tobiko/src/.gitkeep | 0 containers/tobiko/tobiko/Containerfile | 147 +++++ containers/tobiko/tobiko/artifacts.txt | 7 + containers/tobiko/tobiko/bindeps.txt | 21 + containers/tobiko/tobiko/builddeps.txt | 21 + .../config/etc/sudoers.d/tobiko_sudoers | 3 + containers/tobiko/tobiko/pythonbuilddeps.txt | 2 + containers/tobiko/tobiko/pythondeps.txt | 18 + .../tobiko/scripts/install_package_data.py | 33 + .../tobiko/tobiko/scripts/load_tox_setenv.py | 150 +++++ .../tobiko/tobiko/scripts/run_tobiko.sh | 100 +++ .../scripts/tobiko_skip_git_metadata.py | 24 + .../tobiko/scripts/tobiko_testtools_compat.py | 6 + .../tobiko/scripts/tobiko_urllib3_compat.pth | 3 + .../tobiko/scripts/tobiko_urllib3_compat.py | 9 + containers/tobiko/tobiko/src/.gitkeep | 0 containers/tobiko/upper-constraints.txt | 1 + .../tobiko/upper-constraints.txt.master | 583 ++++++++++++++++++ 28 files changed, 1426 insertions(+), 1 deletion(-) create mode 100644 containers/tobiko/OWNERS create mode 120000 containers/tobiko/buildrequirements.lock create mode 100644 containers/tobiko/buildrequirements.lock.master create mode 120000 containers/tobiko/requirements.lock create mode 100644 containers/tobiko/requirements.lock.master create mode 100644 containers/tobiko/rpms.in.yaml create mode 100644 containers/tobiko/sources.txt create mode 100644 containers/tobiko/src/.gitkeep create mode 100644 containers/tobiko/tobiko/Containerfile create mode 100644 containers/tobiko/tobiko/artifacts.txt create mode 100644 containers/tobiko/tobiko/bindeps.txt create mode 100644 containers/tobiko/tobiko/builddeps.txt create mode 100644 containers/tobiko/tobiko/config/etc/sudoers.d/tobiko_sudoers create mode 100644 containers/tobiko/tobiko/pythonbuilddeps.txt create mode 100644 containers/tobiko/tobiko/pythondeps.txt create mode 100755 containers/tobiko/tobiko/scripts/install_package_data.py create mode 100755 containers/tobiko/tobiko/scripts/load_tox_setenv.py create mode 100755 containers/tobiko/tobiko/scripts/run_tobiko.sh create mode 100644 containers/tobiko/tobiko/scripts/tobiko_skip_git_metadata.py create mode 100644 containers/tobiko/tobiko/scripts/tobiko_testtools_compat.py create mode 100644 containers/tobiko/tobiko/scripts/tobiko_urllib3_compat.pth create mode 100644 containers/tobiko/tobiko/scripts/tobiko_urllib3_compat.py create mode 100644 containers/tobiko/tobiko/src/.gitkeep create mode 120000 containers/tobiko/upper-constraints.txt create mode 100644 containers/tobiko/upper-constraints.txt.master 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