From 503111db1dfa7efad2da45b28ebd51090e15c6eb Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Tue, 21 Oct 2025 18:02:53 -0500 Subject: [PATCH 1/8] chore: retire nova-flavors operator code and container We've moved to using flavor definitions that are defined in the deploy repo which are then applied on deployment rather than having a specific operator like this apply them. --- .github/workflows/containers.yaml | 2 - containers/nova-flavors/Dockerfile | 35 - operators/nova-flavors/.env-example | 8 - operators/nova-flavors/README.md | 0 .../nova-flavors/nova_flavors/__init__.py | 0 .../nova_flavors/flavor_synchronizer.py | 99 -- operators/nova-flavors/nova_flavors/logger.py | 19 - .../nova-flavors/nova_flavors/reconcile.py | 48 - .../nova_flavors/spec_changed_handler.py | 38 - operators/nova-flavors/poetry.lock | 996 ------------------ operators/nova-flavors/pyproject.toml | 57 - .../tests/test_flavor_synchronizer.py | 106 -- .../nova-flavors/tests/test_reconcile.py | 64 -- .../tests/test_spec_changed_handler.py | 60 -- 14 files changed, 1532 deletions(-) delete mode 100644 containers/nova-flavors/Dockerfile delete mode 100644 operators/nova-flavors/.env-example delete mode 100644 operators/nova-flavors/README.md delete mode 100644 operators/nova-flavors/nova_flavors/__init__.py delete mode 100644 operators/nova-flavors/nova_flavors/flavor_synchronizer.py delete mode 100644 operators/nova-flavors/nova_flavors/logger.py delete mode 100644 operators/nova-flavors/nova_flavors/reconcile.py delete mode 100644 operators/nova-flavors/nova_flavors/spec_changed_handler.py delete mode 100644 operators/nova-flavors/poetry.lock delete mode 100644 operators/nova-flavors/pyproject.toml delete mode 100644 operators/nova-flavors/tests/test_flavor_synchronizer.py delete mode 100644 operators/nova-flavors/tests/test_reconcile.py delete mode 100644 operators/nova-flavors/tests/test_spec_changed_handler.py diff --git a/.github/workflows/containers.yaml b/.github/workflows/containers.yaml index ce1eb3696..f977fd3a0 100644 --- a/.github/workflows/containers.yaml +++ b/.github/workflows/containers.yaml @@ -124,7 +124,6 @@ jobs: # if you add a container here, add it to the 'clean_containers' job below container: - name: ironic-nautobot-client - - name: nova-flavors - name: ansible - name: understack-tests @@ -186,7 +185,6 @@ jobs: - openstack-client - dnsmasq - ironic-nautobot-client - - nova-flavors - ansible - understack-tests diff --git a/containers/nova-flavors/Dockerfile b/containers/nova-flavors/Dockerfile deleted file mode 100644 index 437c10712..000000000 --- a/containers/nova-flavors/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM ghcr.io/rackerlabs/understack/argo-python3.12.2-alpine3.19 AS builder - -RUN --mount=type=cache,target=/var/cache/apk apk add --virtual build-deps gcc python3-dev musl-dev linux-headers -RUN --mount=type=cache,target=/root/.cache/pip pip install 'wheel==0.43.0' -RUN --mount=type=cache,target=/root/.cache/pip \ - python -m venv /opt/poetry && \ - /opt/poetry/bin/pip install 'poetry==1.7.1' && \ - /opt/poetry/bin/poetry self add 'poetry-dynamic-versioning[plugin]==1.3.0' - -# copy in the code -COPY --chown=appuser:appgroup operators/nova-flavors /app -COPY --chown=appuser:appgroup python/understack-flavor-matcher /understack-flavor-matcher -# need watchdog and psutil built AS a wheel -RUN --mount=type=cache,target=/root/.cache/pip pip wheel --wheel-dir /app/dist watchdog psutil==6.1.1 -CMD ["nova-flavors-sync"] - -WORKDIR /app -RUN cd /app && /opt/poetry/bin/poetry build -f wheel && /opt/poetry/bin/poetry export --without-hashes -f requirements.txt -o dist/requirements.txt - -######################## PROD ######################## -FROM ghcr.io/rackerlabs/understack/argo-python3.12.2-alpine3.19 AS prod - -ENV FLAVORS_DIR="/flavors" -ENV NOVA_FLAVOR_MONITOR_LOGLEVEL="info" - -LABEL org.opencontainers.image.description="Nova-Flavors synchronizer" - -RUN mkdir -p /opt/venv/wheels/ -COPY --from=builder /app/dist/*.whl /app/dist/requirements.txt /opt/venv/wheels/ -COPY --chown=appuser:appgroup python/understack-flavor-matcher /python/understack-flavor-matcher - -RUN --mount=type=cache,target=/root/.cache/pip cd /app && /opt/venv/bin/pip install --find-links /opt/venv/wheels/ --only-binary watchdog --only-binary psutil -r /opt/venv/wheels/requirements.txt nova-flavors - -USER appuser -CMD ["nova-flavors-sync"] diff --git a/operators/nova-flavors/.env-example b/operators/nova-flavors/.env-example deleted file mode 100644 index 5b1a41990..000000000 --- a/operators/nova-flavors/.env-example +++ /dev/null @@ -1,8 +0,0 @@ -OS_USERNAME=flavorsync -OS_PASSWORD=abcd1234 -OS_AUTH_URL=https://keystone.environment.undercloud.rackspace.net/v3 -OS_USER_DOMAIN_NAME=service -OS_PROJECT_NAME=admin -OS_PROJECT_DOMAIN_NAME=default -FLAVORS_DIR=/home/someuser/flavors/ -FLAVORS_ENV=nonprod diff --git a/operators/nova-flavors/README.md b/operators/nova-flavors/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/operators/nova-flavors/nova_flavors/__init__.py b/operators/nova-flavors/nova_flavors/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/operators/nova-flavors/nova_flavors/flavor_synchronizer.py b/operators/nova-flavors/nova_flavors/flavor_synchronizer.py deleted file mode 100644 index 8c144059e..000000000 --- a/operators/nova-flavors/nova_flavors/flavor_synchronizer.py +++ /dev/null @@ -1,99 +0,0 @@ -from functools import cached_property - -from flavor_matcher.flavor_spec import FlavorSpec -from novaclient import client as novaclient - -from nova_flavors.logger import setup_logger - -logger = setup_logger(__name__) - - -class FlavorSynchronizer: - def __init__( - self, - username: str | None = "", - password: str = "", - project_name: str | None = "admin", - project_domain_name: str = "default", - user_domain_name="service", - auth_url: str | None = None, - ) -> None: - self.username = username - self.password = password - self.project_name = str(project_name) - self.project_domain_name = str(project_domain_name) - self.user_domain_name = user_domain_name - self.auth_url = auth_url - - @cached_property - def _nova(self): - return novaclient.Client( - "2", - username=self.username, - password=self.password, - project_name=self.project_name, - project_domain_name=self.project_domain_name, - user_domain_name=self.user_domain_name, - auth_url=self.auth_url, - ) - - def reconcile(self, desired_flavors: list[FlavorSpec]): - if len(desired_flavors) < 1: - raise Exception(f"Empty desired_flavors list.") - - existing_flavors = self._nova.flavors.list() - for flavor in desired_flavors: - nova_flavor = next( - (flv for flv in existing_flavors if flv.name == flavor.name), - None, - ) - - update_needed = False - if nova_flavor: - logger.info(f"Flavor: {flavor.name} already exists. Syncing values") - if nova_flavor.ram != flavor.memory_mib: - logger.info( - f"{flavor.name} RAM mismatch - {nova_flavor.ram=} {flavor.memory_mib=}" - ) - update_needed = True - - if nova_flavor.disk != max(flavor.drives): - logger.info( - f"{flavor.name} Disk mismatch - {nova_flavor.disk=} {flavor.drives=}" - ) - update_needed = True - - if nova_flavor.vcpus != flavor.cpu_cores: - logger.info( - f"{flavor.name} CPU mismatch - {nova_flavor.vcpus=} {flavor.cpu_cores=}" - ) - update_needed = True - - if update_needed: - logger.debug( - f"{flavor.name} is outdated. Deleting so it can be recreated." - ) - nova_flavor.delete() - - else: - update_needed = True - - if update_needed: - logger.info(f"Creating {flavor.name}") - self._create(flavor) - - def _create(self, flavor: FlavorSpec): - nova_flavor = self._nova.flavors.create( - flavor.name, - flavor.memory_mib, - flavor.cpu_cores, - min(flavor.drives), - ) - nova_flavor.set_keys( - { - "resources:DISK_GB": 0, - "resources:MEMORY_MB": 0, - "resources:VCPU": 0, - flavor.baremetal_nova_resource_class: 1, - } - ) diff --git a/operators/nova-flavors/nova_flavors/logger.py b/operators/nova-flavors/nova_flavors/logger.py deleted file mode 100644 index c93b16910..000000000 --- a/operators/nova-flavors/nova_flavors/logger.py +++ /dev/null @@ -1,19 +0,0 @@ -import logging - - -def setup_logger(name: str | None = None, level: int = logging.DEBUG): - """Standardize our logging. - - Configures the root logger to prefix messages with a timestamp - and to output the log level we want to see by default. - - params: - name: logger hierarchy or root logger - level: default log level (DEBUG) - """ - logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S %z", - level=level, - ) - return logging.getLogger(name) diff --git a/operators/nova-flavors/nova_flavors/reconcile.py b/operators/nova-flavors/nova_flavors/reconcile.py deleted file mode 100644 index f1d097eb4..000000000 --- a/operators/nova-flavors/nova_flavors/reconcile.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging -import os -import time - -from flavor_matcher.flavor_spec import FlavorSpec -from watchdog.observers import Observer - -from nova_flavors.flavor_synchronizer import FlavorSynchronizer -from nova_flavors.logger import setup_logger -from nova_flavors.spec_changed_handler import SpecChangedHandler - -loglevel = getattr(logging, os.getenv("NOVA_FLAVOR_MONITOR_LOGLEVEL", "info").upper()) -logging.getLogger().setLevel(loglevel) -logger = setup_logger(__name__, level=loglevel) - - -def main(): - # nonprod vs prod - flavors_dir = os.getenv("FLAVORS_DIR", "") - if not os.path.isdir(flavors_dir): - raise ValueError(f"flavors_dir '{flavors_dir}' is not a directory") - synchronizer = FlavorSynchronizer( - username=os.getenv("OS_USERNAME", ""), - password=os.getenv("OS_PASSWORD", ""), - project_name=os.getenv("OS_PROJECT_NAME", "admin"), - project_domain_name=os.getenv("OS_PROJECT_DOMAIN_NAME", "default"), - user_domain_name=os.getenv("OS_USER_DOMAIN_NAME", "service"), - auth_url=os.getenv("OS_AUTH_URL"), - ) - - handler = SpecChangedHandler( - synchronizer, lambda: FlavorSpec.from_directory(flavors_dir) - ) - observer = Observer() - observer.schedule(handler, flavors_dir, recursive=True) - logger.info(f"Watching for changes in {flavors_dir}") - observer.start() - - try: - while True: - time.sleep(1) - finally: - observer.stop() - observer.join() - - -if __name__ == "__main__": - main() diff --git a/operators/nova-flavors/nova_flavors/spec_changed_handler.py b/operators/nova-flavors/nova_flavors/spec_changed_handler.py deleted file mode 100644 index 48e0e07c0..000000000 --- a/operators/nova-flavors/nova_flavors/spec_changed_handler.py +++ /dev/null @@ -1,38 +0,0 @@ -import time -from typing import Callable - -from watchdog.events import DirModifiedEvent -from watchdog.events import FileModifiedEvent -from watchdog.events import FileSystemEventHandler - -from nova_flavors.flavor_synchronizer import FlavorSynchronizer -from nova_flavors.logger import setup_logger - -logger = setup_logger(__name__) - - -class SpecChangedHandler(FileSystemEventHandler): - COOLDOWN_SECONDS = 30 - - def __init__( - self, synchronizer: FlavorSynchronizer, flavors_cback: Callable - ) -> None: - self.last_call = None - self.synchronizer = synchronizer - self.flavors_cback = flavors_cback - - def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None: - if isinstance(event, DirModifiedEvent): - self._run(event) - - def _run(self, event): - now = time.time() - if not self.last_call: - self.last_call = now - else: - if self.last_call + self.COOLDOWN_SECONDS > now: - logger.debug("Cooldown period.") - return - self.last_call = now - logger.info(f"Flavors directory {event.src_path} has changed.") - self.synchronizer.reconcile(self.flavors_cback()) diff --git a/operators/nova-flavors/poetry.lock b/operators/nova-flavors/poetry.lock deleted file mode 100644 index 7087e9e00..000000000 --- a/operators/nova-flavors/poetry.lock +++ /dev/null @@ -1,996 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. - -[[package]] -name = "certifi" -version = "2024.12.14" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["test"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.10.7" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, - {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, - {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, - {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, - {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, - {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, - {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, - {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, - {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, - {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, - {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, - {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, - {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, - {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, - {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, - {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, - {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, - {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, - {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, - {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, - {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, - {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, - {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, - {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, - {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, - {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, -] - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "debtcollector" -version = "3.0.0" -description = "A collection of Python deprecation patterns and strategies that help you collect your technical debt in a non-destructive manner." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "debtcollector-3.0.0-py3-none-any.whl", hash = "sha256:46f9dacbe8ce49c47ebf2bf2ec878d50c9443dfae97cc7b8054be684e54c3e91"}, - {file = "debtcollector-3.0.0.tar.gz", hash = "sha256:2a8917d25b0e1f1d0d365d3c1c6ecfc7a522b1e9716e8a1a4a915126f7ccea6f"}, -] - -[package.dependencies] -wrapt = ">=1.7.0" - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -groups = ["test"] -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "iso8601" -version = "2.1.0" -description = "Simple module to parse ISO 8601 dates" -optional = false -python-versions = ">=3.7,<4.0" -groups = ["main"] -files = [ - {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, - {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, -] - -[[package]] -name = "keystoneauth1" -version = "5.9.1" -description = "Authentication Library for OpenStack Identity" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "keystoneauth1-5.9.1-py3-none-any.whl", hash = "sha256:71b98835aec72a01f71c5b919c3193dac95342555e89aa35c86d3d86c4ff5f73"}, - {file = "keystoneauth1-5.9.1.tar.gz", hash = "sha256:fb0c66d842d5b964752264fff20b3b4ab73610d66d9b8d20d0dcf796ba09dc43"}, -] - -[package.dependencies] -iso8601 = ">=2.0.0" -os-service-types = ">=1.2.0" -pbr = ">=2.0.0" -requests = ">=2.14.2" -stevedore = ">=1.20.0" -typing-extensions = ">=4.12" - -[package.extras] -betamax = ["PyYAML (>=3.13)", "betamax (>=0.7.0)", "fixtures (>=3.0.0)"] -kerberos = ["requests-kerberos (>=0.8.0)"] -oauth1 = ["oauthlib (>=0.6.2)"] -saml2 = ["lxml (>=4.2.0)"] -test = ["PyYAML (>=3.12)", "bandit (>=1.7.6,<1.8.0)", "betamax (>=0.7.0)", "coverage (>=4.0)", "fixtures (>=3.0.0)", "flake8-docstrings (>=1.7.0,<1.8.0)", "flake8-import-order (>=0.18.2,<0.19.0)", "hacking (>=6.1.0,<6.2.0)", "lxml (>=4.2.0)", "oauthlib (>=0.6.2)", "oslo.config (>=5.2.0)", "oslo.utils (>=3.33.0)", "oslotest (>=3.2.0)", "requests-kerberos (>=0.8.0)", "requests-mock (>=1.2.0)", "stestr (>=1.0.0)", "testresources (>=2.0.0)", "testtools (>=2.2.0)"] - -[[package]] -name = "msgpack" -version = "1.1.0" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, - {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, - {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, - {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, - {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, - {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, - {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, - {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, - {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, - {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, - {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, - {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, - {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, - {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, -] - -[[package]] -name = "netaddr" -version = "1.3.0" -description = "A network address manipulation library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe"}, - {file = "netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a"}, -] - -[package.extras] -nicer-shell = ["ipython"] - -[[package]] -name = "os-service-types" -version = "1.7.0" -description = "Python library for consuming OpenStack sevice-types-authority data" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "os-service-types-1.7.0.tar.gz", hash = "sha256:31800299a82239363995b91f1ebf9106ac7758542a1e4ef6dc737a5932878c6c"}, - {file = "os_service_types-1.7.0-py2.py3-none-any.whl", hash = "sha256:0505c72205690910077fb72b88f2a1f07533c8d39f2fe75b29583481764965d6"}, -] - -[package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" - -[[package]] -name = "oslo-i18n" -version = "6.5.0" -description = "Oslo i18n library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "oslo.i18n-6.5.0-py3-none-any.whl", hash = "sha256:41e54addeb215bd70c5c76bcdd93f93dc563d11c51117531025f2d02a9d8f80d"}, - {file = "oslo.i18n-6.5.0.tar.gz", hash = "sha256:9393bcae92eadc5f771132d1c6ab239b19896ff6d885e3afc21a9faa4de924d3"}, -] - -[package.dependencies] -pbr = ">=2.0.0" - -[[package]] -name = "oslo-serialization" -version = "5.6.0" -description = "Oslo Serialization library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "oslo.serialization-5.6.0-py3-none-any.whl", hash = "sha256:a30c30009db5ea8da5da31a4ac6d75256dcb719f48abc3132c36196eb5bb3821"}, - {file = "oslo.serialization-5.6.0.tar.gz", hash = "sha256:4c7d4e12da853cc4f04b9123041134e886e8c9ff57ab57c1962d3ad4a87b7f7c"}, -] - -[package.dependencies] -msgpack = ">=0.5.2" -"oslo.utils" = ">=3.33.0" -pbr = ">=2.0.0" -tzdata = ">=2022.4" - -[[package]] -name = "oslo-utils" -version = "8.0.0" -description = "Oslo Utility library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "oslo.utils-8.0.0-py3-none-any.whl", hash = "sha256:a1a124aa6718e3c2d833c0b53543c90d41df72550eb5ac80f8175d78a9556cb3"}, - {file = "oslo.utils-8.0.0.tar.gz", hash = "sha256:906fcf1c86f671f224c1925b2a8d375a0539143fb6158b13e202a79dd8e6c694"}, -] - -[package.dependencies] -debtcollector = ">=1.2.0" -iso8601 = ">=0.1.11" -netaddr = ">=0.10.0" -"oslo.i18n" = ">=3.15.3" -packaging = ">=20.4" -psutil = ">=3.2.2" -pyparsing = ">=2.1.0" -PyYAML = ">=3.13" -tzdata = ">=2022.4" - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "test"] -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pbr" -version = "6.1.0" -description = "Python Build Reasonableness" -optional = false -python-versions = ">=2.6" -groups = ["main"] -files = [ - {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, - {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "prettytable" -version = "3.12.0" -description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "prettytable-3.12.0-py3-none-any.whl", hash = "sha256:77ca0ad1c435b6e363d7e8623d7cc4fcf2cf15513bf77a1c1b2e814930ac57cc"}, - {file = "prettytable-3.12.0.tar.gz", hash = "sha256:f04b3e1ba35747ac86e96ec33e3bb9748ce08e254dc2a1c6253945901beec804"}, -] - -[package.dependencies] -wcwidth = "*" - -[package.extras] -tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] - -[[package]] -name = "psutil" -version = "6.1.1" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -groups = ["main"] -files = [ - {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, - {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, - {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"}, - {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"}, - {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"}, - {file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"}, - {file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"}, - {file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"}, - {file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"}, - {file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"}, - {file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"}, - {file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"}, - {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"}, - {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] -test = ["pytest", "pytest-xdist", "setuptools"] - -[[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyparsing" -version = "3.2.1" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, - {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "8.4.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, - {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "7.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, - {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, -] - -[package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" - -[package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-github-actions-annotate-failures" -version = "0.3.0" -description = "pytest plugin to annotate failed tests with a workflow command for GitHub Actions" -optional = false -python-versions = ">=3.8" -groups = ["test"] -files = [ - {file = "pytest_github_actions_annotate_failures-0.3.0-py3-none-any.whl", hash = "sha256:41ea558ba10c332c0bfc053daeee0c85187507b2034e990f21e4f7e5fef044cf"}, - {file = "pytest_github_actions_annotate_failures-0.3.0.tar.gz", hash = "sha256:d4c3177c98046c3900a7f8ddebb22ea54b9f6822201b5d3ab8fcdea51e010db7"}, -] - -[package.dependencies] -pytest = ">=6.0.0" - -[[package]] -name = "pytest-mock" -version = "3.15.1" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, - {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, -] - -[package.dependencies] -pytest = ">=6.2.5" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "python-novaclient" -version = "18.11.0" -description = "Client library for OpenStack Compute API" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "python_novaclient-18.11.0-py3-none-any.whl", hash = "sha256:fc4ce032a6ce0fb911f4c640d260318d770ee171eedce7c12f887307fc24dc0c"}, - {file = "python_novaclient-18.11.0.tar.gz", hash = "sha256:0a31ae20779d4cd171bb29c1fe4e6b22b9c7d97c79670db5149bbdfac03f57dc"}, -] - -[package.dependencies] -iso8601 = ">=0.1.11" -keystoneauth1 = ">=3.5.0" -"oslo.i18n" = ">=3.15.3" -"oslo.serialization" = ">=2.20.0" -"oslo.utils" = ">=3.33.0" -pbr = ">=3.0.0" -PrettyTable = ">=0.7.2" -stevedore = ">=2.0.1" - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "stevedore" -version = "5.4.0" -description = "Manage dynamic plugins for Python applications" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857"}, - {file = "stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d"}, -] - -[package.dependencies] -pbr = ">=2.0.0" - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "tzdata" -version = "2024.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main"] -files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, -] - -[[package]] -name = "understack-flavor-matcher" -version = "0.0.0" -description = "Baremetal node flavor classifier" -optional = false -python-versions = "^3.10" -groups = ["main"] -files = [] -develop = false - -[package.dependencies] -pyyaml = "^6.0" - -[package.source] -type = "directory" -url = "../../python/understack-flavor-matcher" - -[[package]] -name = "urllib3" -version = "2.3.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "watchdog" -version = "6.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, - {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, - {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, - {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, - {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "wrapt" -version = "1.17.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, - {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, - {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, - {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, - {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, - {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, - {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, - {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, - {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, - {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, - {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, - {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, - {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, - {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, - {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, - {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, - {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, - {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, - {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, - {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, - {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, - {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, - {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, -] - -[metadata] -lock-version = "2.1" -python-versions = "^3.11" -content-hash = "0037818706617fa5806ad3e5dbddd4045fa4835e8a5c4a927faa08640b5447f8" diff --git a/operators/nova-flavors/pyproject.toml b/operators/nova-flavors/pyproject.toml deleted file mode 100644 index a6223c427..000000000 --- a/operators/nova-flavors/pyproject.toml +++ /dev/null @@ -1,57 +0,0 @@ -[tool.poetry] -name = "nova-flavors" -version = "0.0.1" -description = "Monitors FlavorSpec repository and reconciles it with Nova" -authors = ["Marek Skrobacki "] -license = "Apache License 2.0" -readme = "README.md" -packages = [ - { include = "nova_flavors" } -] - -[tool.poetry.dependencies] -python = "^3.11" -understack-flavor-matcher = {path = "../../python/understack-flavor-matcher"} -python-novaclient = "^18.8.0" -watchdog = "^6.0.0" - -[tool.poetry.group.dev.dependencies] - - -[tool.poetry.group.test.dependencies] -pytest = "^8.3.3" -pytest-cov = "^7.0.0" -pytest-github-actions-annotate-failures = "*" -pytest-mock = "^3.14.0" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - -[tool.pytest.ini_options] -minversion = "6.0" -addopts = "-ra --cov=nova_flavors" -testpaths = [ - "tests", -] - -[tool.ruff] -target-version = "py310" -fix = true - -[tool.ruff.lint] -select = [ - "S", # flake8-bandit -] - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # allow 'assert' for pytest - "S105", # allow hardcoded passwords for testing - "S106", # allow hardcoded passwords for testing -] -[tool.isort] -profile = "open_stack" - -[tool.poetry.scripts] -nova-flavors-sync = "nova_flavors.reconcile:main" diff --git a/operators/nova-flavors/tests/test_flavor_synchronizer.py b/operators/nova-flavors/tests/test_flavor_synchronizer.py deleted file mode 100644 index 3d322f87a..000000000 --- a/operators/nova-flavors/tests/test_flavor_synchronizer.py +++ /dev/null @@ -1,106 +0,0 @@ -from unittest.mock import MagicMock - -from flavor_matcher.flavor_spec import FlavorSpec -from novaclient import client as novaclient -import pytest -from nova_flavors.flavor_synchronizer import FlavorSynchronizer - - -@pytest.fixture -def flavor_synchronizer(): - return FlavorSynchronizer( - username="test_username", - password="test_password", - auth_url="test_auth_url", - ) - - -@pytest.fixture -def mock_nova_client(mocker): - mock_nova_client = mocker.patch.object(novaclient, "Client") - mock_nova_client.return_value = MagicMock() - return mock_nova_client - - -@pytest.fixture -def mock_flavor(mocker): - return mocker.patch.object(FlavorSpec, "__init__", return_value=None) - - -@pytest.fixture -def flavor(): - return FlavorSpec( - name="maybeprod.test_flavor", - memory_gb=1, - cpu_cores=2, - drives=[10, 10], - model="xyz", - manufacturer="EvilCorp", - cpu_model="Pentium 60", - pci=[], - ) - - -def test_flavor_synchronizer_init(flavor_synchronizer): - assert flavor_synchronizer.username == "test_username" - assert flavor_synchronizer.password == "test_password" - assert flavor_synchronizer.user_domain_name == "service" - assert flavor_synchronizer.auth_url == "test_auth_url" - - -def test_flavor_synchronizer_reconcile_new_flavor( - flavor_synchronizer, mock_nova_client, flavor -): - mock_nova_client.return_value.flavors.list.return_value = [] - flavor_synchronizer.reconcile([flavor]) - mock_nova_client.return_value.flavors.create.assert_called_once_with( - flavor.name, flavor.memory_mib, flavor.cpu_cores, min(flavor.drives) - ) - - -def test_flavor_synchronizer_reconcile_existing_flavor( - flavor_synchronizer, mock_nova_client, flavor -): - existing_flavor = MagicMock() - existing_flavor.name = flavor.name - existing_flavor.ram = flavor.memory_mib - existing_flavor.disk = max(flavor.drives) - existing_flavor.vcpus = flavor.cpu_cores - mock_nova_client.return_value.flavors.list.return_value = [existing_flavor] - flavor_synchronizer.reconcile([flavor]) - mock_nova_client.return_value.flavors.create.assert_not_called() - - -def test_flavor_synchronizer_reconcile_existing_flavor_update_needed( - flavor_synchronizer, mock_nova_client, flavor -): - existing_flavor = MagicMock() - existing_flavor.name = flavor.name - existing_flavor.ram = flavor.memory_mib + 1 - existing_flavor.disk = max(flavor.drives) - existing_flavor.vcpus = flavor.cpu_cores - existing_flavor.delete = MagicMock() - mock_nova_client.return_value.flavors.list.return_value = [existing_flavor] - flavor_synchronizer.reconcile([flavor]) - existing_flavor.delete.assert_called_once() - mock_nova_client.return_value.flavors.create.assert_called_once_with( - flavor.name, flavor.memory_mib, flavor.cpu_cores, min(flavor.drives) - ) - - -def test_flavor_synchronizer_create_flavor( - mock_nova_client, flavor_synchronizer, flavor -): - mock_create_flavor = mock_nova_client.return_value.flavors.create.return_value - flavor_synchronizer._create(flavor) - mock_nova_client.return_value.flavors.create.assert_called_once_with( - flavor.name, flavor.memory_mib, flavor.cpu_cores, min(flavor.drives) - ) - mock_create_flavor.set_keys.assert_called_once_with( - { - "resources:DISK_GB": 0, - "resources:MEMORY_MB": 0, - "resources:VCPU": 0, - flavor.baremetal_nova_resource_class: 1, - } - ) diff --git a/operators/nova-flavors/tests/test_reconcile.py b/operators/nova-flavors/tests/test_reconcile.py deleted file mode 100644 index 7734ee77a..000000000 --- a/operators/nova-flavors/tests/test_reconcile.py +++ /dev/null @@ -1,64 +0,0 @@ -import logging -import os -import pytest -from unittest.mock import patch - -from nova_flavors.reconcile import ( - FlavorSynchronizer, - SpecChangedHandler, - main, -) -from watchdog.observers import Observer - - -@pytest.fixture -def mock_logger(mocker): - return mocker.Mock(spec=logging.Logger) - - -@pytest.mark.parametrize("return_value", [None, "/non/existent/directory"]) -def test_flavors_dir_env_var_not_set(mocker, return_value): - # Set up - mocker.patch("os.getenv", return_value=return_value) - if return_value == "/non/existent/directory": - mocker.patch("os.path.isdir", return_value=False) - - # Execute and Verify - with pytest.raises(Exception): - main() - - -@patch.dict( - "os.environ", - { - "FLAVORS_ENV": "testenv", - "NOVA_FLAVOR_MONITOR_LOGLEVEL": "info", - "FLAVORS_DIR": "/", - }, -) -def test_main_exception(mocker, mock_logger): - # Set up - mocker.patch("nova_flavors.reconcile.setup_logger", return_value=mock_logger) - mock_flavor_synchronizer = mocker.Mock(spec=FlavorSynchronizer) - mocker.patch( - "nova_flavors.reconcile.FlavorSynchronizer", - return_value=mock_flavor_synchronizer, - ) - mock_spec_changed_handler = mocker.Mock(spec=SpecChangedHandler) - mocker.patch( - "nova_flavors.reconcile.SpecChangedHandler", - return_value=mock_spec_changed_handler, - ) - mock_observer = mocker.Mock(spec=Observer) - mocker.patch("nova_flavors.reconcile.Observer", return_value=mock_observer) - mocker.patch("time.sleep", side_effect=Exception("Mock exception")) - - # Execute - with pytest.raises(Exception): - main() - - # Verify - mock_observer.schedule.assert_called_once() - mock_observer.start.assert_called_once() - mock_observer.stop.assert_called_once() - mock_observer.join.assert_called_once() diff --git a/operators/nova-flavors/tests/test_spec_changed_handler.py b/operators/nova-flavors/tests/test_spec_changed_handler.py deleted file mode 100644 index 2a85dfeca..000000000 --- a/operators/nova-flavors/tests/test_spec_changed_handler.py +++ /dev/null @@ -1,60 +0,0 @@ -import pytest -import time -from unittest.mock import MagicMock -from nova_flavors.flavor_synchronizer import FlavorSynchronizer -from nova_flavors.spec_changed_handler import SpecChangedHandler -from watchdog.events import DirModifiedEvent, FileModifiedEvent - - -@pytest.fixture -def handler(): - synchronizer = MagicMock(spec=FlavorSynchronizer) - flavors_cback = MagicMock() - return SpecChangedHandler(synchronizer, flavors_cback) - - -def test_init(handler): - assert handler.last_call is None - assert handler.synchronizer is not None - assert handler.flavors_cback is not None - - -def test_on_modified_dir(handler): - event = DirModifiedEvent("/path/to/dir") - handler.on_modified(event) - handler.synchronizer.reconcile.assert_called_once_with(handler.flavors_cback()) - - -def test_on_modified_file(handler): - event = MagicMock(spec=FileModifiedEvent) - handler.on_modified(event) - handler.synchronizer.reconcile.assert_not_called() - - -def test_run_cool_down(handler): - event = DirModifiedEvent("/path/to/dir") - handler.last_call = time.time() - 29 - handler._run(event) - handler.synchronizer.reconcile.assert_not_called() - - -def test_run_no_cool_down(handler): - event = DirModifiedEvent("/path/to/dir") - handler.last_call = time.time() - 31 - handler._run(event) - handler.synchronizer.reconcile.assert_called_once_with(handler.flavors_cback()) - - -def test_run_first_call(handler): - event = DirModifiedEvent("/path/to/dir") - handler._run(event) - handler.synchronizer.reconcile.assert_called_once_with(handler.flavors_cback()) - - -@pytest.mark.parametrize("last_call", [None, time.time() - 100]) -def test_run_logging(handler, last_call, caplog): - event = DirModifiedEvent("/path/to/dir") - handler.last_call = last_call - with caplog.at_level("INFO"): - handler._run(event) - assert "Flavors directory /path/to/dir has changed." in caplog.text From 60014a39ff2a3f2a76f73c1f2c35749a2e62062f Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 22 Oct 2025 17:40:44 -0500 Subject: [PATCH 2/8] chore(understack-workflows): remove resource class matching in enroll Remove the resource class matching from the enroll code so that we can migrate to utilizing the inspection hooks directly. --- python/understack-workflows/pyproject.toml | 4 --- .../understack_workflows/flavor_detect.py | 35 ------------------- .../understack_workflows/ironic_node.py | 3 -- .../main/enroll_server.py | 5 --- python/understack-workflows/uv.lock | 20 ----------- 5 files changed, 67 deletions(-) delete mode 100644 python/understack-workflows/understack_workflows/flavor_detect.py diff --git a/python/understack-workflows/pyproject.toml b/python/understack-workflows/pyproject.toml index 07b6e6c93..38cdf3968 100644 --- a/python/understack-workflows/pyproject.toml +++ b/python/understack-workflows/pyproject.toml @@ -25,7 +25,6 @@ dependencies = [ "python-ironicclient>=5,<6", "sushy>=5.3.0,<6", "kubernetes==33.1.0", - "understack-flavor-matcher", "netapp-ontap>=9.17.1.0", ] @@ -55,9 +54,6 @@ test = [ [tool.uv] default-groups = ["test"] -[tool.uv.sources] -understack-flavor-matcher = { path = "../understack-flavor-matcher" } - [tool.hatch.build.targets.sdist] include = ["understack_workflows"] diff --git a/python/understack-workflows/understack_workflows/flavor_detect.py b/python/understack-workflows/understack_workflows/flavor_detect.py deleted file mode 100644 index b12397be8..000000000 --- a/python/understack-workflows/understack_workflows/flavor_detect.py +++ /dev/null @@ -1,35 +0,0 @@ -import os - -from flavor_matcher.machine import Machine -from flavor_matcher.matcher import FlavorSpec -from flavor_matcher.matcher import Matcher - -from understack_workflows import bmc_disk -from understack_workflows.bmc import Bmc -from understack_workflows.bmc_chassis_info import ChassisInfo -from understack_workflows.helpers import setup_logger - -logger = setup_logger(__name__) -FLAVORS_DIR = os.getenv("FLAVORS_DIR", "/etc/understack_flavors/") -FLAVORS = FlavorSpec.from_directory(FLAVORS_DIR) -logger.info("Loaded %d flavor specifications.", len(FLAVORS)) - - -def guess_machine_flavor(device_info: ChassisInfo, bmc: Bmc) -> str: - memory_mb = (device_info.memory_gib * 1024**3) // 10**6 - - machine = Machine( - memory_mb=memory_mb, - cpu=device_info.cpu, - disk_gb=bmc_disk.smallest_disk_size(bmc), - model=device_info.model_number, - ) - - flavor_name = Matcher(FLAVORS).pick_best_flavor(machine) - if not flavor_name: - raise Exception( - f"Machine: {machine} could not be classified into any flavor {FLAVORS=}" - ) - logger.info("Device has been classified as flavor: %s", flavor_name.name) - - return flavor_name.name diff --git a/python/understack-workflows/understack_workflows/ironic_node.py b/python/understack-workflows/understack_workflows/ironic_node.py index 070def6e7..718579ce2 100644 --- a/python/understack-workflows/understack_workflows/ironic_node.py +++ b/python/understack-workflows/understack_workflows/ironic_node.py @@ -17,7 +17,6 @@ class NodeMetadata: uuid: str hostname: str manufacturer: str - resource_class: str @property def driver(self): @@ -58,7 +57,6 @@ def update_ironic_node(client, node_meta, bmc): "driver_info/redfish_verify_ca=false", f"driver_info/redfish_username={bmc.username}", f"driver_info/redfish_password={bmc.password}", - f"resource_class={node_meta.resource_class}", "boot_interface=http-ipxe", ] @@ -85,7 +83,6 @@ def create_ironic_node( "redfish_username": bmc.username, "redfish_password": bmc.password, }, - "resource_class": node_meta.resource_class, "boot_interface": "http-ipxe", } ) diff --git a/python/understack-workflows/understack_workflows/main/enroll_server.py b/python/understack-workflows/understack_workflows/main/enroll_server.py index a5d1c8c30..11e7cb6d6 100644 --- a/python/understack-workflows/understack_workflows/main/enroll_server.py +++ b/python/understack-workflows/understack_workflows/main/enroll_server.py @@ -16,7 +16,6 @@ from understack_workflows.bmc_network_config import bmc_set_permanent_ip_addr from understack_workflows.bmc_settings import update_dell_drac_settings from understack_workflows.discover import discover_chassis_info -from understack_workflows.flavor_detect import guess_machine_flavor from understack_workflows.helpers import credential from understack_workflows.helpers import parser_nautobot_args from understack_workflows.helpers import setup_logger @@ -137,15 +136,11 @@ def enroll_server(bmc: Bmc, nautobot, old_password: str | None) -> NautobotDevic # any pending BIOS jobs, so do BIOS settings after the DRAC settings. update_dell_bios_settings(bmc, pxe_interface=pxe_interface) - flavor = guess_machine_flavor(device_info, bmc) - resource_class = f"baremetal.{flavor}" - _ironic_provision_state = ironic_node.create_or_update( ironic_node.NodeMetadata( uuid=nb_device.id, hostname=nb_device.name, manufacturer=device_info.manufacturer, - resource_class=resource_class, ), bmc, ) diff --git a/python/understack-workflows/uv.lock b/python/understack-workflows/uv.lock index bff373649..8b8f3b49a 100644 --- a/python/understack-workflows/uv.lock +++ b/python/understack-workflows/uv.lock @@ -990,24 +990,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586, upload-time = "2024-09-23T18:56:45.478Z" }, ] -[[package]] -name = "understack-flavor-matcher" -version = "0.0.0" -source = { directory = "../understack-flavor-matcher" } -dependencies = [ - { name = "pyyaml" }, -] - -[package.metadata] -requires-dist = [{ name = "pyyaml", specifier = "~=6.0" }] - -[package.metadata.requires-dev] -test = [ - { name = "pytest", specifier = ">=8.3.2,<9" }, - { name = "pytest-cov", specifier = ">=7,<8" }, - { name = "pytest-github-actions-annotate-failures" }, -] - [[package]] name = "understack-workflows" version = "0.0.0" @@ -1019,7 +1001,6 @@ dependencies = [ { name = "pynautobot" }, { name = "python-ironicclient" }, { name = "sushy" }, - { name = "understack-flavor-matcher" }, ] [package.dev-dependencies] @@ -1040,7 +1021,6 @@ requires-dist = [ { name = "pynautobot", specifier = ">=2.2.1,<3" }, { name = "python-ironicclient", specifier = ">=5,<6" }, { name = "sushy", specifier = ">=5.3.0,<6" }, - { name = "understack-flavor-matcher", directory = "../understack-flavor-matcher" }, ] [package.metadata.requires-dev] From 2af56cb7b1e1c4f5f2bf6c7735a9351e9c01797e Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 22 Oct 2025 11:23:45 -0500 Subject: [PATCH 3/8] chore: use correct import of dataclass This code is trying to use the standard library dataclasses, it should import that and not rely on a leaked export of the standard library data class. --- .../understack-workflows/understack_workflows/ironic_node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/understack-workflows/understack_workflows/ironic_node.py b/python/understack-workflows/understack_workflows/ironic_node.py index 718579ce2..632d2a931 100644 --- a/python/understack-workflows/understack_workflows/ironic_node.py +++ b/python/understack-workflows/understack_workflows/ironic_node.py @@ -1,5 +1,6 @@ +from dataclasses import dataclass + import ironicclient.common.apiclient.exceptions -from flavor_matcher.flavor_spec import dataclass from ironicclient.common.utils import args_array_to_patch from understack_workflows.bmc import Bmc From 736b7fb1e6eddf8d88ad7f1b29140ca55b43016b Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 22 Oct 2025 11:20:14 -0500 Subject: [PATCH 4/8] feat(hardware categorization): update code to read new schemaed data Update the code and the tests to read data that conforms to the new schema. --- .../ironic_understack/conf.py | 6 +- .../redfish_inspect_understack.py | 27 +- .../ironic_understack/resource_class.py | 30 +- .../flavor_matcher/device_type.py | 139 ++++++++ .../flavor_matcher/flavor_spec.py | 134 ------- .../flavor_matcher/machine.py | 2 + .../flavor_matcher/matcher.py | 72 ++-- .../tests/test_device_type.py | 143 ++++++++ .../tests/test_flavor_spec.py | 335 ------------------ .../tests/test_machine.py | 36 +- .../tests/test_matcher.py | 290 +++++++++++---- 11 files changed, 636 insertions(+), 578 deletions(-) create mode 100644 python/understack-flavor-matcher/flavor_matcher/device_type.py delete mode 100644 python/understack-flavor-matcher/flavor_matcher/flavor_spec.py create mode 100644 python/understack-flavor-matcher/tests/test_device_type.py delete mode 100644 python/understack-flavor-matcher/tests/test_flavor_spec.py diff --git a/python/ironic-understack/ironic_understack/conf.py b/python/ironic-understack/ironic_understack/conf.py index 6ea1bba64..d41bbb234 100644 --- a/python/ironic-understack/ironic_understack/conf.py +++ b/python/ironic-understack/ironic_understack/conf.py @@ -7,9 +7,9 @@ def setup_conf(): grp = cfg.OptGroup("ironic_understack") opts = [ cfg.StrOpt( - "flavors_dir", - help="directory storing Flavor description YAML files", - default="/var/lib/understack/flavors/undercloud-nautobot-device-types.git/flavors", + "device_types_dir", + help="directory storing Device Type description YAML files", + default="/var/lib/understack/device-types", ) ] cfg.CONF.register_group(grp) diff --git a/python/ironic-understack/ironic_understack/redfish_inspect_understack.py b/python/ironic-understack/ironic_understack/redfish_inspect_understack.py index d6483c37c..b45fbc883 100644 --- a/python/ironic-understack/ironic_understack/redfish_inspect_understack.py +++ b/python/ironic-understack/ironic_understack/redfish_inspect_understack.py @@ -12,8 +12,9 @@ """Redfish Inspect Interface modified for Understack.""" import re +from pathlib import Path -from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.device_type import DeviceType from flavor_matcher.machine import Machine from flavor_matcher.matcher import Matcher from ironic.drivers.drac import IDRACHardware @@ -27,8 +28,8 @@ from ironic_understack.conf import CONF LOG = log.getLogger(__name__) -FLAVORS = FlavorSpec.from_directory(CONF.ironic_understack.flavors_dir) -LOG.info("Loaded %d flavor specifications.", len(FLAVORS)) +DEVICE_TYPES = DeviceType.from_directory(Path(CONF.ironic_understack.device_types_dir)) +LOG.info("Loaded %d device types.", len(DEVICE_TYPES)) class FlavorInspectMixin: @@ -85,21 +86,29 @@ def inspect_hardware(self, task): else: model_name = model_name_match.group(1) + # Extract additional fields for new Machine API + cpu_cores = inventory.get("cpu", {}).get("count", 0) + manufacturer = inventory.get("system_vendor", {}).get("manufacturer", "") + machine = Machine( memory_mb=inventory["memory"]["physical_mb"], disk_gb=smallest_disk_gb, cpu=inventory["cpu"]["model_name"], + cpu_cores=cpu_cores, + manufacturer=manufacturer, model=model_name, ) - matcher = Matcher(FLAVORS) - best_flavor = matcher.pick_best_flavor(machine) - if not best_flavor: - LOG.warning("No flavor matched for %s", task.node.uuid) + matcher = Matcher(device_types=DEVICE_TYPES) + match_result = matcher.match(machine) + if not match_result: + LOG.warning("No resource class matched for %s", task.node.uuid) return upstream_state - LOG.info("Matched %s to flavor %s", task.node.uuid, best_flavor) - task.node.resource_class = f"baremetal.{best_flavor.name}" + device_type, resource_class = match_result + LOG.info("Matched %s to resource class %s", task.node.uuid, resource_class.name) + + task.node.resource_class = resource_class.name task.node.save() return upstream_state diff --git a/python/ironic-understack/ironic_understack/resource_class.py b/python/ironic-understack/ironic_understack/resource_class.py index dce2435b1..7678bcd10 100644 --- a/python/ironic-understack/ironic_understack/resource_class.py +++ b/python/ironic-understack/ironic_understack/resource_class.py @@ -1,7 +1,8 @@ # from ironic.drivers.modules.inspector.hooks import base import re +from pathlib import Path -from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.device_type import DeviceType from flavor_matcher.machine import Machine from flavor_matcher.matcher import Matcher from ironic.common import exception @@ -12,8 +13,8 @@ LOG = logging.getLogger(__name__) -FLAVORS = FlavorSpec.from_directory(CONF.ironic_understack.flavors_dir) -LOG.info("Loaded %d flavor specifications.", len(FLAVORS)) +DEVICE_TYPES = DeviceType.from_directory(Path(CONF.ironic_understack.device_types_dir)) +LOG.info("Loaded %d device types.", len(DEVICE_TYPES)) class NoMatchError(Exception): @@ -24,7 +25,7 @@ class UndercloudResourceClassHook(base.InspectionHook): """Hook to set the node's resource_class based on the inventory.""" def __call__(self, task, inventory, plugin_data): - """Update node resource_class with deducted flavor.""" + """Update node resource_class with matched resource class.""" try: memory_mb = inventory["memory"]["physical_mb"] disk_size_gb = int(int(inventory["disks"][0]["size"]) / 10**9) @@ -35,15 +36,21 @@ def __call__(self, task, inventory, plugin_data): ) if not model_name: - LOG.warning("No model_name detected. skipping flavor setting.") + LOG.warning("No model_name detected. skipping resource class setting.") raise NoMatchError("mode_name not matched") else: model_name = model_name.group(1) + # Extract additional fields for new Machine API + cpu_cores = inventory.get("cpu", {}).get("count", 0) + manufacturer = inventory.get("system_vendor", {}).get("manufacturer", "") + machine = Machine( memory_mb=memory_mb, cpu=cpu_model_name, + cpu_cores=cpu_cores, disk_gb=disk_size_gb, + manufacturer=manufacturer, model=model_name, ) @@ -65,13 +72,14 @@ def __call__(self, task, inventory, plugin_data): node=task.node.uuid, reason=msg ) from None except NoMatchError: - msg = f"No matching flavor found for {task.node.uuid}" + msg = f"No matching resource class found for {task.node.uuid}" LOG.error(msg) def classify(self, machine): - matcher = Matcher(FLAVORS) - flavor = matcher.pick_best_flavor(machine) - if not flavor: - raise NoMatchError(f"No flavor found for {machine}") + matcher = Matcher(device_types=DEVICE_TYPES) + match_result = matcher.match(machine) + if not match_result: + raise NoMatchError(f"No resource class found for {machine}") else: - return flavor + device_type, resource_class = match_result + return resource_class.name diff --git a/python/understack-flavor-matcher/flavor_matcher/device_type.py b/python/understack-flavor-matcher/flavor_matcher/device_type.py new file mode 100644 index 000000000..5c031f046 --- /dev/null +++ b/python/understack-flavor-matcher/flavor_matcher/device_type.py @@ -0,0 +1,139 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +import yaml + +logger = logging.getLogger(__name__) + + +@dataclass +class CpuSpec: + cores: int + model: str + + +@dataclass +class MemorySpec: + size: int # MB + + +@dataclass +class DriveSpec: + size: int # GB + + +@dataclass +class InterfaceSpec: + name: str + type: str + mgmt_only: bool = False + + +@dataclass +class PowerPortSpec: + name: str + type: str + maximum_draw: int | None = None + + +@dataclass +class ResourceClass: + name: str + cpu: CpuSpec + memory: MemorySpec + drives: list[DriveSpec] + nic_count: int + + +@dataclass +class DeviceType: + class_: str # Using class_ since class is a Python keyword + manufacturer: str + model: str + u_height: float + is_full_depth: bool + resource_class: list[ResourceClass] + interfaces: list[InterfaceSpec] | None = None + power_ports: list[PowerPortSpec] | None = None + + @staticmethod + def from_yaml(yaml_str: str) -> "DeviceType": + data = yaml.safe_load(yaml_str) + + # Parse resource classes + resource_classes = [] + for rc_data in data.get("resource_class", []): + cpu = CpuSpec(cores=rc_data["cpu"]["cores"], model=rc_data["cpu"]["model"]) + memory = MemorySpec(size=rc_data["memory"]["size"]) + drives = [DriveSpec(size=d["size"]) for d in rc_data["drives"]] + resource_classes.append( + ResourceClass( + name=rc_data["name"], + cpu=cpu, + memory=memory, + drives=drives, + nic_count=rc_data["nic_count"], + ) + ) + + # Parse interfaces + interfaces = None + if "interfaces" in data: + interfaces = [ + InterfaceSpec( + name=i["name"], + type=i["type"], + mgmt_only=i.get("mgmt_only", False), + ) + for i in data["interfaces"] + ] + + # Parse power ports + power_ports = None + if "power-ports" in data: + power_ports = [ + PowerPortSpec( + name=p["name"], + type=p["type"], + maximum_draw=p.get("maximum_draw"), + ) + for p in data["power-ports"] + ] + + return DeviceType( + class_=data["class"], + manufacturer=data["manufacturer"], + model=data["model"], + u_height=data["u_height"], + is_full_depth=data["is_full_depth"], + resource_class=resource_classes, + interfaces=interfaces, + power_ports=power_ports, + ) + + @staticmethod + def from_directory(data_dir: Path) -> list["DeviceType"]: + """Load all device type definitions from a directory.""" + device_types = [] + if not data_dir.exists(): + return device_types + + for pattern in ("*.yaml", "*.yml"): + for filepath in data_dir.rglob(pattern): + try: + yaml_content = filepath.read_text() + device_type = DeviceType.from_yaml(yaml_content) + device_types.append(device_type) + except yaml.YAMLError as e: + logger.error("Error parsing YAML file %s: %s", filepath.name, e) + except Exception as e: + logger.error("Error processing file %s: %s", filepath.name, e) + return device_types + + def get_resource_class(self, name: str) -> ResourceClass | None: + """Get a specific resource class by name.""" + for rc in self.resource_class: + if rc.name == name: + return rc + return None diff --git a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py deleted file mode 100644 index cf16c3452..000000000 --- a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py +++ /dev/null @@ -1,134 +0,0 @@ -import os -import re -from dataclasses import dataclass - -import yaml - -from flavor_matcher.machine import Machine - - -@dataclass -class PciSpec: - vendor_id: str - device_id: str - sub_vendor_id: str - sub_device_id: str - - -@dataclass -class FlavorSpec: - name: str - manufacturer: str - model: str - memory_gb: int - cpu_cores: int - cpu_model: str - drives: list[int] - pci: list[PciSpec] - - @staticmethod - def from_yaml(yaml_str: str) -> "FlavorSpec": - data = yaml.safe_load(yaml_str) - return FlavorSpec( - name=data["name"], - manufacturer=data["manufacturer"], - model=data["model"], - memory_gb=data["memory_gb"], - cpu_cores=data["cpu_cores"], - cpu_model=data["cpu_model"], - drives=data["drives"], - pci=data.get("pci", []), - ) - - @property - def baremetal_nova_resource_class(self): - """Returns flavor name converted to be used with Nova flavor resources. - - https://docs.openstack.org/ironic/latest/install/configure-nova-flavors.html - """ - converted_name = re.sub(r"[^\w]", "_", self.name).upper() - return f"resources:CUSTOM_BAREMETAL_{converted_name}" - - @property - def memory_mib(self): - """Returns memory size in MiB.""" - return self.memory_gb * 1024 - - @staticmethod - def from_directory(directory: str = "/etc/flavors/") -> list["FlavorSpec"]: - flavor_specs = [] - for root, _, files in os.walk(directory): - for filename in files: - if filename.endswith(".yaml") or filename.endswith(".yml"): - filepath = os.path.join(root, filename) - try: - with open(filepath) as file: - yaml_content = file.read() - flavor_spec = FlavorSpec.from_yaml(yaml_content) - flavor_specs.append(flavor_spec) - except yaml.YAMLError as e: - print(f"Error parsing YAML file {filename}: {e}") - except Exception as e: - print(f"Error processing file {filename}: {e}") - return flavor_specs - - def score_machine(self, machine: Machine): - # Scoring Rules: - # - # 1. 100% match gets highest priority, no further evaluation needed - # 2. If the machine has less memory size than specified in the flavor, - # it cannot be used - the score should be 0. - # 3. If the machine has smaller disk size than specified in the flavor, - # it cannot be used - the score should be 0. - # 4. If the machine's model does not match exactly, score should be 0 - # 5. Machine must match the flavor on one of the CPU models exactly. - # 6. If the machine has exact amount memory as specified in flavor, but - # more disk space it is less desirable than the machine that matches - # exactly on both disk and memory. - # 7. If the machine has exact amount of disk as specified in flavor, - # but more memory space it is less desirable than the machine that - # matches exactly on both disk and memory. - - # Rule 1: 100% match gets the highest priority - if ( - machine.memory_gb == self.memory_gb - and machine.disk_gb in self.drives - and machine.cpu == self.cpu_model - and machine.model == self.model - ): - return 100 - - # Rule 2: If machine has less memory than specified in the - # flavor, it cannot be used - if machine.memory_gb < self.memory_gb: - return 0 - - # Rule 3: If machine has smaller disk than specified in the - # flavor, it cannot be used - if any(machine.disk_gb < drive for drive in self.drives): - return 0 - - # Rule 4: Machine's model must match exactly - if machine.model != self.model: - return 0 - - # Rule 5: Machine must match the flavor on one of the CPU models exactly - if machine.cpu != self.cpu_model: - return 0 - - # Rule 6 and 7: Rank based on exact matches or excess capacity - score = 0 - - # Exact memory match gives preference - if machine.memory_gb == self.memory_gb: - score += 10 - elif machine.memory_gb > self.memory_gb: - score += 5 # Less desirable but still usable - - # Exact disk match gives preference - if machine.disk_gb in self.drives: - score += 10 - elif all(machine.disk_gb > drive for drive in self.drives): - score += 5 # Less desirable but still usable - - return score diff --git a/python/understack-flavor-matcher/flavor_matcher/machine.py b/python/understack-flavor-matcher/flavor_matcher/machine.py index cef753190..55597624f 100644 --- a/python/understack-flavor-matcher/flavor_matcher/machine.py +++ b/python/understack-flavor-matcher/flavor_matcher/machine.py @@ -5,7 +5,9 @@ class Machine: memory_mb: int cpu: str + cpu_cores: int disk_gb: int + manufacturer: str model: str @property diff --git a/python/understack-flavor-matcher/flavor_matcher/matcher.py b/python/understack-flavor-matcher/flavor_matcher/matcher.py index fac5be3be..d8080b093 100644 --- a/python/understack-flavor-matcher/flavor_matcher/matcher.py +++ b/python/understack-flavor-matcher/flavor_matcher/matcher.py @@ -1,28 +1,56 @@ -from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.device_type import DeviceType +from flavor_matcher.device_type import ResourceClass from flavor_matcher.machine import Machine class Matcher: - def __init__(self, flavors: list[FlavorSpec]): - self.flavors = flavors - - def match(self, machine: Machine) -> list[FlavorSpec]: - """Find list of all flavors that the machine is eligible for.""" - results = [] - for flavor in self.flavors: - score = flavor.score_machine(machine) - if score > 0: - results.append(flavor) - return results - - def pick_best_flavor(self, machine: Machine) -> FlavorSpec | None: - """Selects the best patching flavor. - - Obtains list of all flavors that particular machine can be classified as, - then tries to select "the best" one. + def __init__(self, device_types: list[DeviceType]): + self.device_types = device_types + + def match(self, machine: Machine) -> tuple[DeviceType, ResourceClass] | None: + """Find the resource class that matches the machine's hardware specs. + + Returns a tuple of (DeviceType, ResourceClass) that matches the machine, + or None if no match is found. + + Matching rules: + 1. Manufacturer and model must match exactly + 2. CPU model must match exactly + 3. CPU cores must match exactly + 4. Memory must match exactly (in MB) + 5. Must have at least as many drives as specified in resource class + 6. Each drive must be at least as large as the smallest drive in resource class """ - possible = self.match(machine) + for device_type in self.device_types: + # Check manufacturer and model + if ( + device_type.manufacturer != machine.manufacturer + or device_type.model != machine.model + ): + continue + + # Check each resource class in this device type + for resource_class in device_type.resource_class: + # Check CPU model + if resource_class.cpu.model != machine.cpu: + continue + + # Check CPU cores + if resource_class.cpu.cores != machine.cpu_cores: + continue + + # Check memory (in MB) + if resource_class.memory.size != machine.memory_mb: + continue + + # Check drives - machine must have enough storage + # For simplicity, we check if machine's total disk meets the minimum + # drive size specified in resource class + min_drive_size = min(d.size for d in resource_class.drives) + if machine.disk_gb < min_drive_size: + continue + + # Found a match + return (device_type, resource_class) - if len(possible) == 0: - return None - return max(possible, key=lambda flv: flv.memory_gb) + return None diff --git a/python/understack-flavor-matcher/tests/test_device_type.py b/python/understack-flavor-matcher/tests/test_device_type.py new file mode 100644 index 000000000..d2a2c9282 --- /dev/null +++ b/python/understack-flavor-matcher/tests/test_device_type.py @@ -0,0 +1,143 @@ +import pytest + +from flavor_matcher.device_type import DeviceType + + +@pytest.fixture +def valid_device_type_yaml(): + return """ +class: server +manufacturer: Dell +model: PowerEdge R7615 +u_height: 2 +is_full_depth: true + +interfaces: + - name: iDRAC + type: 1000base-t + mgmt_only: true + +resource_class: + - name: m1.small + cpu: + cores: 16 + model: AMD EPYC 9124 + memory: + size: 128 + drives: + - size: 480 + - size: 480 + nic_count: 2 + + - name: m1.medium + cpu: + cores: 32 + model: AMD EPYC 9334 + memory: + size: 256 + drives: + - size: 960 + - size: 960 + nic_count: 2 +""" + + +def test_device_type_from_yaml(valid_device_type_yaml): + device_type = DeviceType.from_yaml(valid_device_type_yaml) + + assert device_type.class_ == "server" + assert device_type.manufacturer == "Dell" + assert device_type.model == "PowerEdge R7615" + assert device_type.u_height == 2 + assert device_type.is_full_depth is True + + # Check interfaces + assert device_type.interfaces is not None + assert len(device_type.interfaces) == 1 + assert device_type.interfaces[0].name == "iDRAC" + assert device_type.interfaces[0].type == "1000base-t" + assert device_type.interfaces[0].mgmt_only is True + + # Check resource classes + assert len(device_type.resource_class) == 2 + + # Check first resource class + rc1 = device_type.resource_class[0] + assert rc1.name == "m1.small" + assert rc1.cpu.cores == 16 + assert rc1.cpu.model == "AMD EPYC 9124" + assert rc1.memory.size == 128 + assert len(rc1.drives) == 2 + assert rc1.drives[0].size == 480 + assert rc1.drives[1].size == 480 + assert rc1.nic_count == 2 + + # Check second resource class + rc2 = device_type.resource_class[1] + assert rc2.name == "m1.medium" + assert rc2.cpu.cores == 32 + assert rc2.cpu.model == "AMD EPYC 9334" + assert rc2.memory.size == 256 + + +def test_get_resource_class(valid_device_type_yaml): + device_type = DeviceType.from_yaml(valid_device_type_yaml) + + rc = device_type.get_resource_class("m1.small") + assert rc is not None + assert rc.name == "m1.small" + assert rc.cpu.cores == 16 + + rc = device_type.get_resource_class("m1.medium") + assert rc is not None + assert rc.name == "m1.medium" + assert rc.cpu.cores == 32 + + rc = device_type.get_resource_class("nonexistent") + assert rc is None + + +def test_device_type_from_directory(tmp_path): + # Create test files + yaml_content = """ +class: server +manufacturer: Dell +model: TestModel +u_height: 1 +is_full_depth: false +resource_class: + - name: test.small + cpu: + cores: 4 + model: Test CPU + memory: + size: 8 + drives: + - size: 100 + nic_count: 1 +""" + test_file = tmp_path / "test-device.yaml" + test_file.write_text(yaml_content) + + device_types = DeviceType.from_directory(tmp_path) + assert len(device_types) == 1 + assert device_types[0].model == "TestModel" + assert device_types[0].resource_class[0].name == "test.small" + + +def test_device_type_minimal_yaml(): + """Test with minimal required fields only.""" + minimal_yaml = """ +class: server +manufacturer: Dell +model: TestModel +u_height: 1 +is_full_depth: true +""" + device_type = DeviceType.from_yaml(minimal_yaml) + assert device_type.class_ == "server" + assert device_type.manufacturer == "Dell" + assert device_type.model == "TestModel" + assert device_type.resource_class == [] + assert device_type.interfaces is None + assert device_type.power_ports is None diff --git a/python/understack-flavor-matcher/tests/test_flavor_spec.py b/python/understack-flavor-matcher/tests/test_flavor_spec.py deleted file mode 100644 index f158f6bd8..000000000 --- a/python/understack-flavor-matcher/tests/test_flavor_spec.py +++ /dev/null @@ -1,335 +0,0 @@ -import os -from unittest.mock import mock_open -from unittest.mock import patch - -import pytest - -from flavor_matcher.flavor_spec import FlavorSpec -from flavor_matcher.machine import Machine - - -@pytest.fixture -def valid_yaml(): - return """ ---- -name: gp2.ultramedium -manufacturer: Dell -model: PowerEdge R7615 -memory_gb: 7777 -cpu_cores: 245 -cpu_model: AMD EPYC 9254 245-Core Processor -drives: - - 960 - - 960 -""" - - -@pytest.fixture -def invalid_yaml(): - return """ ---- -name: gp2.ultramedium -x: abcd -malformed_field: 123: invalid -""" - - -@pytest.fixture -def yaml_directory(tmp_path, valid_yaml, invalid_yaml): - valid_file = tmp_path / "valid.yaml" - invalid_file = tmp_path / "invalid.yaml" - - valid_file.write_text(valid_yaml) - invalid_file.write_text(invalid_yaml) - - return tmp_path - - -def test_from_yaml(valid_yaml): - spec = FlavorSpec.from_yaml(valid_yaml) - assert spec.name == "gp2.ultramedium" - assert spec.manufacturer == "Dell" - assert spec.model == "PowerEdge R7615" - assert spec.memory_gb == 7777 - assert spec.cpu_cores == 245 - assert spec.cpu_model == "AMD EPYC 9254 245-Core Processor" - assert spec.drives == [960, 960] - - -def test_from_yaml_invalid(invalid_yaml): - with pytest.raises(Exception): # noqa:B017 - FlavorSpec.from_yaml(invalid_yaml) - - -@patch("os.walk") -@patch("builtins.open", new_callable=mock_open) -@patch.dict(os.environ, {"FLAVORS_ENV": "nonprod"}) -def test_from_directory(mocked_open, mock_walk, valid_yaml, invalid_yaml): - mock_walk.return_value = [ - ("/etc/flavors", [], ["valid.yaml", "invalid.yaml"]), - ] - mock_file_handles = [ - mock_open(read_data=valid_yaml).return_value, - mock_open(read_data=invalid_yaml).return_value, - ] - mocked_open.side_effect = mock_file_handles - specs = FlavorSpec.from_directory("/etc/flavors/") - - assert len(specs) == 1 - assert specs[0].name == "gp2.ultramedium" - assert specs[0].memory_gb == 7777 - assert specs[0].cpu_cores == 245 - - -@patch.dict(os.environ, {"FLAVORS_ENV": "nonprod"}) -def test_from_directory_with_real_files(yaml_directory): - specs = FlavorSpec.from_directory(str(yaml_directory)) - - assert len(specs) == 1 - assert specs[0].name == "gp2.ultramedium" - assert specs[0].memory_gb == 7777 - assert specs[0].cpu_cores == 245 - - -def test_empty_directory(tmp_path): - specs = FlavorSpec.from_directory(str(tmp_path)) - assert len(specs) == 0 - - -@pytest.fixture -def machines(): - return [ - # 1024 GB, exact CPU, medium - Machine( - memory_mb=102400, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=1000, - model="Dell XPS1319", - ), - # 800 GB, non-matching CPU - Machine( - memory_mb=800000, - cpu="Intel Xeon E5-2676 v3", - disk_gb=500, - model="Dell XPS1319", - ), - # 200 GB, exact CPU, medium - Machine( - memory_mb=200000, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=150, - model="Dell XPS1319", - ), - # 300 GB, non-matching CPU - Machine( - memory_mb=300000, - cpu="Intel Xeon E5-2676 v3", - disk_gb=500, - model="Dell XPS1319", - ), - # 409 GB, exact CPU, large - Machine( - memory_mb=409600, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=2000, - model="Dell XPS1319", - ), - ] - - -@pytest.fixture -def flavors(): - return [ - FlavorSpec( - name="small", - manufacturer="Dell", - model="Dell XPS1319", - memory_gb=100, - cpu_cores=13, - cpu_model="AMD EPYC 9254 245-Core Processor", - drives=[500, 500], - pci=[], - ), - FlavorSpec( - name="medium", - manufacturer="Dell", - model="Fake Machine", - memory_gb=200, - cpu_cores=15, - cpu_model="AMD EPYC 9254 245-Core Processor", - drives=[1500, 1500], - pci=[], - ), - FlavorSpec( - name="large", - manufacturer="Dell", - model="Dell XPS1319", - memory_gb=400, - cpu_cores=27, - cpu_model="AMD EPYC 9254 245-Core Processor", - drives=[1800, 1800], - pci=[], - ), - ] - - -def test_exact_match(flavors): - machine = Machine( - memory_mb=102400, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=500, - model="Dell XPS1319", - ) - assert flavors[0].score_machine(machine) == 100 - assert flavors[1].score_machine(machine) == 0 - - -def test_wrong_model_non_match(flavors): - machine = Machine( - memory_mb=102400, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=500, - model="Some other model", - ) - for flavor in flavors: - assert flavor.score_machine(machine) == 0 - - -def test_memory_too_small(flavors): - machine = Machine( - memory_mb=51200, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=500, - model="Dell XPS1319", - ) - for flavor in flavors: - assert flavor.score_machine(machine) == 0 - - -def test_disk_too_small(flavors): - machine = Machine( - memory_mb=204800, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=100, - model="Dell XPS1319", - ) - assert all(flavor.score_machine(machine) == 0 for flavor in flavors) - - -def test_cpu_model_not_matching(flavors): - machine = Machine( - memory_mb=102400, - cpu="Non-Existent CPU Model", - disk_gb=500, - model="Dell XPS1319", - ) - assert all(flavor.score_machine(machine) == 0 for flavor in flavors) - - -def test_memory_match_but_more_disk(flavors): - machine = Machine( - memory_mb=102400, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=1000, - model="Dell XPS1319", - ) - assert flavors[0].score_machine(machine) > 0 - - -def test_disk_match_but_more_memory(flavors): - machine = Machine( - memory_mb=204800, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=500, - model="Dell XPS1319", - ) - - assert flavors[0].score_machine(machine) > 0 - assert flavors[1].score_machine(machine) == 0 - assert flavors[2].score_machine(machine) == 0 - - -# Edge cases -def test_memory_slightly_less(flavors): - # Machine with slightly less memory than required by the smallest flavor - machine = Machine( - memory_mb=102300, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=500, - model="Dell XPS1319", - ) - # Should not match because memory is slightly less - assert all(flavor.score_machine(machine) == 0 for flavor in flavors) - - -def test_disk_slightly_less(flavors): - # Machine with slightly less disk space than required by the smallest flavor - machine = Machine( - memory_mb=102400, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=499, - model="Dell XPS1319", - ) - # Should not match because disk space is slightly less - assert all(flavor.score_machine(machine) == 0 for flavor in flavors) - - -def test_memory_exact_disk_slightly_more(flavors): - # Machine with exact memory but slightly more disk space than required - machine = Machine( - memory_mb=102400, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=501, - model="Dell XPS1319", - ) - assert flavors[0].score_machine(machine) > 0 - assert flavors[1].score_machine(machine) == 0 - assert flavors[2].score_machine(machine) == 0 - - -def test_disk_exact_memory_slightly_more(flavors): - # Machine with exact disk space but slightly more memory than required - machine = Machine( - memory_mb=102500, - cpu="AMD EPYC 9254 245-Core Processor", - disk_gb=500, - model="Dell XPS1319", - ) - assert flavors[0].score_machine(machine) > 0 - assert flavors[1].score_machine(machine) == 0 - assert flavors[2].score_machine(machine) == 0 - - -def test_cpu_model_not_exact_but_memory_and_disk_match(flavors): - # Machine with exact memory and disk space but CPU model is close but not exact - machine = Machine( - memory_mb=102400, - cpu="AMD EPYC 9254 245-Core Processor v2", - disk_gb=500, - model="Dell XPS1319", - ) - # Should not match because CPU model is not exactly listed - assert all(flavor.score_machine(machine) == 0 for flavor in flavors) - - -def test_large_flavor_memory_slightly_less_disk_exact(flavors): - # Machine with slightly less memory than required for the medium - # flavor, exact disk space - machine = Machine( - memory_mb=204600, cpu="Intel 80386DX", disk_gb=1800, model="Dell XPS1319" - ) - # Should not match because memory is slightly less than required - assert all(flavor.score_machine(machine) == 0 for flavor in flavors) - - -def test_memory_gib(valid_yaml): - flv = FlavorSpec.from_yaml(valid_yaml) - assert flv.memory_mib == 7963648 - - -def test_baremetal_nova_resource_class(valid_yaml): - flv = FlavorSpec.from_yaml(valid_yaml) - assert ( - flv.baremetal_nova_resource_class - == "resources:CUSTOM_BAREMETAL_GP2_ULTRAMEDIUM" - ) diff --git a/python/understack-flavor-matcher/tests/test_machine.py b/python/understack-flavor-matcher/tests/test_machine.py index f9bba7af9..099a04828 100644 --- a/python/understack-flavor-matcher/tests/test_machine.py +++ b/python/understack-flavor-matcher/tests/test_machine.py @@ -3,17 +3,45 @@ def test_memory_gb_property(): # Test a machine with exactly 1 GB of memory - machine = Machine(memory_mb=1024, cpu="x86", disk_gb=50, model="SomeModel") + machine = Machine( + memory_mb=1024, + cpu="x86", + cpu_cores=4, + disk_gb=50, + manufacturer="Dell", + model="SomeModel", + ) assert machine.memory_gb == 1 # Test a machine with 2 GB of memory - machine = Machine(memory_mb=2048, cpu="x86", disk_gb=50, model="SomeModel") + machine = Machine( + memory_mb=2048, + cpu="x86", + cpu_cores=4, + disk_gb=50, + manufacturer="Dell", + model="SomeModel", + ) assert machine.memory_gb == 2 # Test a machine with non-exact GB memory (should floor the value) - machine = Machine(memory_mb=3072, cpu="x86", disk_gb=50, model="SomeModel") + machine = Machine( + memory_mb=3072, + cpu="x86", + cpu_cores=4, + disk_gb=50, + manufacturer="Dell", + model="SomeModel", + ) assert machine.memory_gb == 3 # Test a machine with less than 1 GB of memory - machine = Machine(memory_mb=512, cpu="x86", disk_gb=50, model="SomeModel") + machine = Machine( + memory_mb=512, + cpu="x86", + cpu_cores=4, + disk_gb=50, + manufacturer="Dell", + model="SomeModel", + ) assert machine.memory_gb == 0 diff --git a/python/understack-flavor-matcher/tests/test_matcher.py b/python/understack-flavor-matcher/tests/test_matcher.py index fd0189e7e..bae5c231c 100644 --- a/python/understack-flavor-matcher/tests/test_matcher.py +++ b/python/understack-flavor-matcher/tests/test_matcher.py @@ -1,80 +1,250 @@ import pytest -from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.device_type import CpuSpec +from flavor_matcher.device_type import DeviceType +from flavor_matcher.device_type import DriveSpec +from flavor_matcher.device_type import MemorySpec +from flavor_matcher.device_type import ResourceClass from flavor_matcher.machine import Machine from flavor_matcher.matcher import Matcher @pytest.fixture -def sample_flavors(): - return [ - FlavorSpec( - name="small", - manufacturer="Dell", - model="Fake Machine", - memory_gb=4, - cpu_cores=2, - cpu_model="x86", - drives=[20], - pci=[], - ), - FlavorSpec( - name="medium", - manufacturer="Dell", - model="Fake Machine", - memory_gb=8, - cpu_cores=4, - cpu_model="x86", - drives=[40], - pci=[], - ), - FlavorSpec( - name="large", - manufacturer="Dell", - model="Fake Machine", - memory_gb=16, - cpu_cores=8, - cpu_model="x86", - drives=[80], - pci=[], - ), - ] +def device_types(): + """Create sample device types for testing.""" + # Dell PowerEdge R7615 with multiple resource classes + dell_r7615 = DeviceType( + class_="server", + manufacturer="Dell", + model="PowerEdge R7615", + u_height=2, + is_full_depth=True, + resource_class=[ + ResourceClass( + name="m1.small", + cpu=CpuSpec(cores=16, model="AMD EPYC 9124"), + memory=MemorySpec(size=131072), + drives=[DriveSpec(size=480), DriveSpec(size=480)], + nic_count=2, + ), + ResourceClass( + name="m1.medium", + cpu=CpuSpec(cores=32, model="AMD EPYC 9334"), + memory=MemorySpec(size=262144), + drives=[DriveSpec(size=960), DriveSpec(size=960)], + nic_count=2, + ), + ResourceClass( + name="m1.large", + cpu=CpuSpec(cores=64, model="AMD EPYC 9554"), + memory=MemorySpec(size=524288), + drives=[DriveSpec(size=1920), DriveSpec(size=1920)], + nic_count=4, + ), + ], + ) + + return [dell_r7615] @pytest.fixture -def matcher(sample_flavors): - return Matcher(flavors=sample_flavors) +def matcher(device_types): + """Create a matcher with test data.""" + return Matcher(device_types=device_types) -@pytest.fixture -def machine(): - return Machine(memory_mb=8192, cpu="x86", disk_gb=50, model="Fake Machine") +def test_match_exact_machine(matcher): + """Test matching a machine that exactly matches m1.small resource class.""" + machine = Machine( + memory_mb=131072, # 128 GB + cpu="AMD EPYC 9124", + cpu_cores=16, + disk_gb=480, + manufacturer="Dell", + model="PowerEdge R7615", + ) + + result = matcher.match(machine) + assert result is not None + device_type, resource_class = result + assert resource_class.name == "m1.small" + assert device_type.manufacturer == "Dell" + assert device_type.model == "PowerEdge R7615" + + +def test_match_machine_small(matcher): + """Test that machine matches the m1.small resource class.""" + machine = Machine( + memory_mb=131072, # 128 GB + cpu="AMD EPYC 9124", + cpu_cores=16, + disk_gb=480, + manufacturer="Dell", + model="PowerEdge R7615", + ) + + result = matcher.match(machine) + assert result is not None + _, resource_class = result + assert resource_class.name == "m1.small" + + +def test_match_machine_verifies_specs(matcher): + """Test that machine matches based on hardware specs.""" + machine = Machine( + memory_mb=131072, # 128 GB + cpu="AMD EPYC 9124", + cpu_cores=16, + disk_gb=480, + manufacturer="Dell", + model="PowerEdge R7615", + ) + + result = matcher.match(machine) + assert result is not None + _, resource_class = result + assert resource_class.name == "m1.small" + assert resource_class.cpu.cores == 16 + assert resource_class.memory.size == 131072 + + +def test_match_medium_machine(matcher): + """Test matching a machine to m1.medium resource class.""" + machine = Machine( + memory_mb=262144, # 256 GB + cpu="AMD EPYC 9334", + cpu_cores=32, + disk_gb=960, + manufacturer="Dell", + model="PowerEdge R7615", + ) + + result = matcher.match(machine) + assert result is not None + _, resource_class = result + assert resource_class.name == "m1.medium" + + +def test_match_medium_machine_with_specs(matcher): + """Test matching a medium machine and verify its specs.""" + machine = Machine( + memory_mb=262144, # 256 GB + cpu="AMD EPYC 9334", + cpu_cores=32, + disk_gb=960, + manufacturer="Dell", + model="PowerEdge R7615", + ) + + result = matcher.match(machine) + assert result is not None + _, resource_class = result + assert resource_class.name == "m1.medium" + assert resource_class.cpu.cores == 32 + assert resource_class.memory.size == 262144 + + +def test_no_match_wrong_manufacturer(matcher): + """Test that wrong manufacturer doesn't match.""" + machine = Machine( + memory_mb=131072, # 128 GB + cpu="AMD EPYC 9124", + cpu_cores=16, + disk_gb=480, + manufacturer="HP", # Wrong manufacturer + model="PowerEdge R7615", + ) + + result = matcher.match(machine) + assert result is None + + +def test_no_match_wrong_model(matcher): + """Test that wrong model doesn't match.""" + machine = Machine( + memory_mb=131072, # 128 GB + cpu="AMD EPYC 9124", + cpu_cores=16, + disk_gb=480, + manufacturer="Dell", + model="PowerEdge R7525", # Wrong model + ) + + result = matcher.match(machine) + assert result is None + + +def test_no_match_wrong_cpu_model(matcher): + """Test that wrong CPU model doesn't match.""" + machine = Machine( + memory_mb=131072, # 128 GB + cpu="Intel Xeon", # Wrong CPU + cpu_cores=16, + disk_gb=480, + manufacturer="Dell", + model="PowerEdge R7615", + ) + + result = matcher.match(machine) + assert result is None + + +def test_no_match_wrong_cpu_cores(matcher): + """Test that wrong CPU cores doesn't match.""" + machine = Machine( + memory_mb=131072, # 128 GB + cpu="AMD EPYC 9124", + cpu_cores=8, # Wrong core count + disk_gb=480, + manufacturer="Dell", + model="PowerEdge R7615", + ) + + result = matcher.match(machine) + assert result is None + +def test_no_match_wrong_memory(matcher): + """Test that wrong memory size doesn't match.""" + machine = Machine( + memory_mb=65536, # 64 GB - wrong size + cpu="AMD EPYC 9124", + cpu_cores=16, + disk_gb=480, + manufacturer="Dell", + model="PowerEdge R7615", + ) -def test_match(matcher, machine): - # This machine should match the small and medium flavors - results = matcher.match(machine) - assert len(results) == 2 - assert results[0].name == "small" - assert results[1].name == "medium" + result = matcher.match(machine) + assert result is None -def test_match_no_flavor(matcher): - # A machine that does not meet any flavor specs - machine = Machine(memory_mb=2048, cpu="x86", disk_gb=10, model="SomeModel") - results = matcher.match(machine) - assert len(results) == 0 +def test_no_match_insufficient_disk(matcher): + """Test that insufficient disk space doesn't match.""" + machine = Machine( + memory_mb=131072, # 128 GB + cpu="AMD EPYC 9124", + cpu_cores=16, + disk_gb=200, # Too small + manufacturer="Dell", + model="PowerEdge R7615", + ) + result = matcher.match(machine) + assert result is None -def test_pick_best_flavor2(matcher, machine): - # This machine should pick the medium flavor as the best - best_flavor = matcher.pick_best_flavor(machine) - assert best_flavor is not None - assert best_flavor.name == "medium" +def test_match_empty_device_types(): + """Test matcher with no device types.""" + matcher = Matcher(device_types=[]) + machine = Machine( + memory_mb=131072, + cpu="AMD EPYC 9124", + cpu_cores=16, + disk_gb=480, + manufacturer="Dell", + model="PowerEdge R7615", + ) -def test_pick_best_flavor_no_match(matcher): - # A machine that does not meet any flavor specs - machine = Machine(memory_mb=1024, cpu="ARM", disk_gb=10, model="SomeModel") - best_flavor = matcher.pick_best_flavor(machine) - assert best_flavor is None + result = matcher.match(machine) + assert result is None From 4d87599c22f5045640c9ace9546253f9aa01d49c Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 22 Oct 2025 17:03:43 -0500 Subject: [PATCH 5/8] chore(workflows): update enroll-server Create an OpenStack automation account for enroll-server and update the secret references to this account. Drop all references to the old flavor data since this is not where it will be used. Added a call to 'inspect' during the enrollment process. --- .../automation-baremetal-manage.yaml.tpl | 47 ++++++++++++++ .../templates/secretstore-openstack.yaml.tpl | 1 + workflows/argo-events/kustomization.yaml | 1 + .../argo-events/secrets/baremetal-manage.yaml | 16 +++++ .../workflowtemplates/enroll-server.yaml | 62 ++++++++++--------- 5 files changed, 99 insertions(+), 28 deletions(-) create mode 100644 components/openstack/templates/automation-baremetal-manage.yaml.tpl create mode 100644 workflows/argo-events/secrets/baremetal-manage.yaml diff --git a/components/openstack/templates/automation-baremetal-manage.yaml.tpl b/components/openstack/templates/automation-baremetal-manage.yaml.tpl new file mode 100644 index 000000000..6558ef920 --- /dev/null +++ b/components/openstack/templates/automation-baremetal-manage.yaml.tpl @@ -0,0 +1,47 @@ +--- +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: "baremetal-manage-{{ .Values.regionName }}" +spec: + length: 32 + digits: 6 + symbols: 4 + symbolCharacters: "~!@#$%^*()_+-={}[]<>?" +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: "baremetal-manage-{{ .Values.regionName }}" +spec: + refreshInterval: 20160m + target: + name: baremetal-manage + template: + engineVersion: v2 + type: Opaque + metadata: + labels: + understack.rackspace.com/keystone-role: infra-readwrite + understack.rackspace.com/keystone-user: "baremetal-manage-{{ .Values.regionName }}" + data: + password: "{{ `{{ .password }}` }}" + clouds.yaml: | + clouds: + understack: + auth: + auth_url: "{{ .Values.keystoneUrl }}" + user_domain_name: "service" + username: "baremetal-manage-{{ .Values.regionName }}" + password: "{{ `{{ .password }}` }}" + project_domain_name: "infra" + project_name: "baremetal" + region_name: "{{ .Values.regionName }}" + interface: "public" + identity_api_version: 3 + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: "baremetal-manage-{{ .Values.regionName }}" diff --git a/components/openstack/templates/secretstore-openstack.yaml.tpl b/components/openstack/templates/secretstore-openstack.yaml.tpl index a70a6a107..d0483d1e4 100644 --- a/components/openstack/templates/secretstore-openstack.yaml.tpl +++ b/components/openstack/templates/secretstore-openstack.yaml.tpl @@ -25,6 +25,7 @@ rules: - list - watch resourceNames: + - baremetal-manage - svc-acct-argoworkflow - svc-acct-netapp - cinder-netapp-config diff --git a/workflows/argo-events/kustomization.yaml b/workflows/argo-events/kustomization.yaml index 9adb42311..91f8f204d 100644 --- a/workflows/argo-events/kustomization.yaml +++ b/workflows/argo-events/kustomization.yaml @@ -6,6 +6,7 @@ resources: - eventbus/poddisruptionbudget-eventbus-default-pdb.yaml - secrets/openstack-svc-acct.yaml - secrets/operate-workflow-sa.token.yaml + - secrets/baremetal-manage.yaml - eventsources/nautobot-webhook.yaml - serviceaccounts/serviceaccount-sensor-submit-workflow.yaml - serviceaccounts/openstack-sensor-submit-workflow.yaml diff --git a/workflows/argo-events/secrets/baremetal-manage.yaml b/workflows/argo-events/secrets/baremetal-manage.yaml new file mode 100644 index 000000000..f09adfb3b --- /dev/null +++ b/workflows/argo-events/secrets/baremetal-manage.yaml @@ -0,0 +1,16 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: baremetal-manage +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: openstack + target: + name: baremetal-manage + data: + - secretKey: clouds.yaml + remoteRef: + key: baremetal-manage + property: clouds.yaml diff --git a/workflows/argo-events/workflowtemplates/enroll-server.yaml b/workflows/argo-events/workflowtemplates/enroll-server.yaml index 912f18f03..6cc145ffb 100644 --- a/workflows/argo-events/workflowtemplates/enroll-server.yaml +++ b/workflows/argo-events/workflowtemplates/enroll-server.yaml @@ -8,6 +8,7 @@ metadata: Defined in `workflows/argo-events/workflowtemplates/enroll-server.yaml` kind: WorkflowTemplate spec: + serviceAccountName: workflow entrypoint: main arguments: parameters: @@ -58,6 +59,15 @@ spec: parameters: - name: device_id value: "{{steps.enroll-server.outputs.result}}" + - - name: inspect-server + template: openstack-wait-cmd + arguments: + parameters: + - name: operation + value: "inspect" + - name: device_id + value: "{{steps.enroll-server.outputs.result}}" + when: "{{steps.server-manage-state.outputs.result}} == manageable" - - name: avail-server template: openstack-wait-cmd arguments: @@ -77,7 +87,7 @@ spec: - "{{workflow.parameters.ip_address}}" volumeMounts: - mountPath: /etc/openstack - name: openstack-svc-acct + name: baremetal-manage readOnly: true - mountPath: /etc/nb-token/ name: nb-token @@ -85,9 +95,6 @@ spec: - mountPath: /etc/bmc_master/ name: bmc-master readOnly: true - - mountPath: /etc/understack_flavors/ - name: understack-flavors - readOnly: true env: - name: WF_NS value: "{{workflow.namespace}}" @@ -95,12 +102,6 @@ spec: value: "{{workflow.name}}" - name: WF_UID value: "{{workflow.uid}}" - - name: FLAVORS_DIR - valueFrom: - configMapKeyRef: - name: understack-flavors - key: FLAVORS_DIR - optional: true volumes: - name: bmc-master secret: @@ -108,13 +109,12 @@ spec: - name: nb-token secret: secretName: nautobot-token - - name: openstack-svc-acct + - name: baremetal-manage secret: - secretName: openstack-svc-acct - - name: understack-flavors - persistentVolumeClaim: - claimName: understack-flavors - readOnly: true + secretName: baremetal-manage + items: + - key: clouds.yaml + path: clouds.yaml - name: openstack-wait-cmd inputs: parameters: @@ -136,12 +136,14 @@ spec: value: understack volumeMounts: - mountPath: /etc/openstack - name: openstack-svc-acct - readOnly: true + name: baremetal-manage volumes: - - name: openstack-svc-acct + - name: baremetal-manage secret: - secretName: openstack-svc-acct + secretName: baremetal-manage + items: + - key: clouds.yaml + path: clouds.yaml - name: openstack-set-baremetal-node-raid-config inputs: parameters: @@ -190,12 +192,14 @@ spec: value: understack volumeMounts: - mountPath: /etc/openstack - name: openstack-svc-acct - readOnly: true + name: baremetal-manage volumes: - - name: openstack-svc-acct + - name: baremetal-manage secret: - secretName: openstack-svc-acct + secretName: baremetal-manage + items: + - key: clouds.yaml + path: clouds.yaml - name: openstack-state-cmd inputs: parameters: @@ -218,9 +222,11 @@ spec: value: understack volumeMounts: - mountPath: /etc/openstack - name: openstack-svc-acct - readOnly: true + name: baremetal-manage volumes: - - name: openstack-svc-acct + - name: baremetal-manage secret: - secretName: openstack-svc-acct + secretName: baremetal-manage + items: + - key: clouds.yaml + path: clouds.yaml From bf716f95421023986ec0a66b2a31e7ddcc50169a Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 23 Oct 2025 09:42:43 -0500 Subject: [PATCH 6/8] chore(ironic): add resource class to default inspection hooks Added the resource class to be called by default in the agent inspector hooks. Switch to using agent inspection for now by default as well. Add a mount to include the data into the conductor. --- components/ironic/values.yaml | 7 ++++++- .../ironic-understack/ironic_understack/resource_class.py | 2 +- python/ironic-understack/pyproject.toml | 2 +- .../understack_workflows/ironic_node.py | 2 ++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/components/ironic/values.yaml b/components/ironic/values.yaml index 22d38e957..3981f7708 100644 --- a/components/ironic/values.yaml +++ b/components/ironic/values.yaml @@ -88,7 +88,7 @@ conf: loader_file_paths: "snponly.efi:/usr/lib/ipxe/snponly.efi" inspector: extra_kernel_params: ipa-collect-lldp=1 - hooks: "$default_hooks,parse-lldp,local-link-connection,physical-network" + hooks: "$default_hooks,parse-lldp,local-link-connection,physical-network,resource-class" # enable sensors and metrics for redfish metrics - https://docs.openstack.org/ironic/latest/admin/drivers/redfish/metrics.html sensor_data: send_sensor_data: true @@ -217,6 +217,8 @@ pod: mountPath: /var/lib/dnsmasq/ - name: understack-data mountPath: /var/lib/understack + - name: device-types + mountPath: /var/lib/understack/device-types - name: ironic-etc-snippets mountPath: /etc/ironic/ironic.conf.d readOnly: true @@ -230,6 +232,9 @@ pod: - name: understack-data persistentVolumeClaim: claimName: understack-data + - name: device-types + configMap: + name: device-types - name: ironic-etc-snippets projected: sources: diff --git a/python/ironic-understack/ironic_understack/resource_class.py b/python/ironic-understack/ironic_understack/resource_class.py index 7678bcd10..eca3836f5 100644 --- a/python/ironic-understack/ironic_understack/resource_class.py +++ b/python/ironic-understack/ironic_understack/resource_class.py @@ -21,7 +21,7 @@ class NoMatchError(Exception): pass -class UndercloudResourceClassHook(base.InspectionHook): +class ResourceClassHook(base.InspectionHook): """Hook to set the node's resource_class based on the inventory.""" def __call__(self, task, inventory, plugin_data): diff --git a/python/ironic-understack/pyproject.toml b/python/ironic-understack/pyproject.toml index c4fab7bd2..cb3c5f52d 100644 --- a/python/ironic-understack/pyproject.toml +++ b/python/ironic-understack/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ ] [project.entry-points."ironic.inspection.hooks"] -resource-class = "ironic_understack.resource_class:UndercloudResourceClassHook" +resource-class = "ironic_understack.resource_class:ResourceClassHook" [project.entry-points."ironic.hardware.interfaces.inspect"] redfish-understack = "ironic_understack.redfish_inspect_understack:UnderstackRedfishInspect" diff --git a/python/understack-workflows/understack_workflows/ironic_node.py b/python/understack-workflows/understack_workflows/ironic_node.py index 632d2a931..577e0cf2c 100644 --- a/python/understack-workflows/understack_workflows/ironic_node.py +++ b/python/understack-workflows/understack_workflows/ironic_node.py @@ -59,6 +59,7 @@ def update_ironic_node(client, node_meta, bmc): f"driver_info/redfish_username={bmc.username}", f"driver_info/redfish_password={bmc.password}", "boot_interface=http-ipxe", + "inspect_interface=agent", ] patches = args_array_to_patch("add", updates) @@ -85,5 +86,6 @@ def create_ironic_node( "redfish_password": bmc.password, }, "boot_interface": "http-ipxe", + "inspect_interface": "agent", } ) From 614d21d94421fb5c746bd822fe29df45f345f48c Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 23 Oct 2025 16:33:51 -0500 Subject: [PATCH 7/8] feat(hardware categorization): remove CPU core count from matching rules Unfortunately the IPA image reports the number of CPU threads and not the number of CPU cores without giving us the ability to calculate the cores because there's no value to say that hyperthreading is enabled or not. The model of the CPU should be detailed enough for us to be happy that this is correct so we'll just drop CPU core count. --- .../flavor_matcher/matcher.py | 5 ----- .../tests/test_matcher.py | 15 --------------- 2 files changed, 20 deletions(-) diff --git a/python/understack-flavor-matcher/flavor_matcher/matcher.py b/python/understack-flavor-matcher/flavor_matcher/matcher.py index d8080b093..a1d78594e 100644 --- a/python/understack-flavor-matcher/flavor_matcher/matcher.py +++ b/python/understack-flavor-matcher/flavor_matcher/matcher.py @@ -16,7 +16,6 @@ def match(self, machine: Machine) -> tuple[DeviceType, ResourceClass] | None: Matching rules: 1. Manufacturer and model must match exactly 2. CPU model must match exactly - 3. CPU cores must match exactly 4. Memory must match exactly (in MB) 5. Must have at least as many drives as specified in resource class 6. Each drive must be at least as large as the smallest drive in resource class @@ -35,10 +34,6 @@ def match(self, machine: Machine) -> tuple[DeviceType, ResourceClass] | None: if resource_class.cpu.model != machine.cpu: continue - # Check CPU cores - if resource_class.cpu.cores != machine.cpu_cores: - continue - # Check memory (in MB) if resource_class.memory.size != machine.memory_mb: continue diff --git a/python/understack-flavor-matcher/tests/test_matcher.py b/python/understack-flavor-matcher/tests/test_matcher.py index bae5c231c..ae3e87428 100644 --- a/python/understack-flavor-matcher/tests/test_matcher.py +++ b/python/understack-flavor-matcher/tests/test_matcher.py @@ -189,21 +189,6 @@ def test_no_match_wrong_cpu_model(matcher): assert result is None -def test_no_match_wrong_cpu_cores(matcher): - """Test that wrong CPU cores doesn't match.""" - machine = Machine( - memory_mb=131072, # 128 GB - cpu="AMD EPYC 9124", - cpu_cores=8, # Wrong core count - disk_gb=480, - manufacturer="Dell", - model="PowerEdge R7615", - ) - - result = matcher.match(machine) - assert result is None - - def test_no_match_wrong_memory(matcher): """Test that wrong memory size doesn't match.""" machine = Machine( From 9f907832a59cd379d56cf50cd187362cc94268e3 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 23 Oct 2025 17:13:17 -0500 Subject: [PATCH 8/8] fix(hardware categorization): trim ' Inc.' from the manufacturer The manufacturer sometimes includes ' Inc.' and sometimes not, so let's just trim that to make things consistent. --- python/ironic-understack/ironic_understack/resource_class.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/ironic-understack/ironic_understack/resource_class.py b/python/ironic-understack/ironic_understack/resource_class.py index eca3836f5..001b7a748 100644 --- a/python/ironic-understack/ironic_understack/resource_class.py +++ b/python/ironic-understack/ironic_understack/resource_class.py @@ -45,6 +45,9 @@ def __call__(self, task, inventory, plugin_data): cpu_cores = inventory.get("cpu", {}).get("count", 0) manufacturer = inventory.get("system_vendor", {}).get("manufacturer", "") + # trim ' Inc.' + manufacturer = manufacturer.replace(" Inc.", "") + machine = Machine( memory_mb=memory_mb, cpu=cpu_model_name,