From d083d62ea362358edd018d341b429f861cac1237 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 19 Nov 2024 14:22:54 +0000 Subject: [PATCH 01/19] flavor_spec: add memory_mib and baremetal_nova_resource_class helpers --- .../flavor_matcher/flavor_spec.py | 16 +++++++++++++++- .../tests/test_flavor_spec.py | 19 +++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py index 409170279..790a3ce53 100644 --- a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py +++ b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py @@ -1,8 +1,8 @@ import os +import re from dataclasses import dataclass import yaml - from flavor_matcher.machine import Machine @@ -49,6 +49,20 @@ def stripped_name(self): name = name.replace(".", "") return name + @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.stripped_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 = [] diff --git a/python/understack-flavor-matcher/tests/test_flavor_spec.py b/python/understack-flavor-matcher/tests/test_flavor_spec.py index aedfa9db6..26ac5ce98 100644 --- a/python/understack-flavor-matcher/tests/test_flavor_spec.py +++ b/python/understack-flavor-matcher/tests/test_flavor_spec.py @@ -9,7 +9,7 @@ def valid_yaml(): return """ --- -name: gp2.ultramedium +name: nonprod.gp2.ultramedium manufacturer: Dell model: PowerEdge R7615 memory_gb: 7777 @@ -44,7 +44,8 @@ def yaml_directory(tmp_path, valid_yaml, invalid_yaml): def test_from_yaml(valid_yaml): spec = FlavorSpec.from_yaml(valid_yaml) - assert spec.name == "gp2.ultramedium" + assert spec.name == "nonprod.gp2.ultramedium" + assert spec.stripped_name == "gp2.ultramedium" assert spec.manufacturer == "Dell" assert spec.model == "PowerEdge R7615" assert spec.memory_gb == 7777 @@ -74,7 +75,7 @@ def test_from_directory(mocked_open, mock_walk, valid_yaml, invalid_yaml): specs = FlavorSpec.from_directory("/etc/flavors/") assert len(specs) == 1 - assert specs[0].name == "gp2.ultramedium" + assert specs[0].name == "nonprod.gp2.ultramedium" assert specs[0].memory_gb == 7777 assert specs[0].cpu_cores == 245 @@ -83,7 +84,7 @@ 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].name == "nonprod.gp2.ultramedium" assert specs[0].memory_gb == 7777 assert specs[0].cpu_cores == 245 @@ -316,3 +317,13 @@ def test_large_flavor_memory_slightly_less_disk_exact(flavors): ) # 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_GP2ULTRAMEDIUM" From bd3295630781443bbdcd1d597516cd32d268c5f7 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 19 Nov 2024 14:55:26 +0000 Subject: [PATCH 02/19] nova_flavors: initial code to reconcile with Nova This adds code that performs simple synchronization of the Flavor Specs defined in a YAML files in particular directory (presumably downloaded through git-sync) with a chosen Nova API instance. --- operators/nova-flavors/.env-example | 8 + operators/nova-flavors/README.md | 0 operators/nova-flavors/__init__.py | 0 operators/nova-flavors/flavor_synchronizer.py | 95 +++ operators/nova-flavors/logger.py | 19 + operators/nova-flavors/poetry.lock | 758 ++++++++++++++++++ operators/nova-flavors/pyproject.toml | 42 + operators/nova-flavors/reconcile.py | 23 + 8 files changed, 945 insertions(+) create mode 100644 operators/nova-flavors/.env-example create mode 100644 operators/nova-flavors/README.md create mode 100644 operators/nova-flavors/__init__.py create mode 100644 operators/nova-flavors/flavor_synchronizer.py create mode 100644 operators/nova-flavors/logger.py create mode 100644 operators/nova-flavors/poetry.lock create mode 100644 operators/nova-flavors/pyproject.toml create mode 100644 operators/nova-flavors/reconcile.py diff --git a/operators/nova-flavors/.env-example b/operators/nova-flavors/.env-example new file mode 100644 index 000000000..5b1a41990 --- /dev/null +++ b/operators/nova-flavors/.env-example @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000..e69de29bb diff --git a/operators/nova-flavors/__init__.py b/operators/nova-flavors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/operators/nova-flavors/flavor_synchronizer.py b/operators/nova-flavors/flavor_synchronizer.py new file mode 100644 index 000000000..5b36cbc68 --- /dev/null +++ b/operators/nova-flavors/flavor_synchronizer.py @@ -0,0 +1,95 @@ +from functools import cached_property + +from flavor_matcher.flavor_spec import FlavorSpec +from novaclient import client as novaclient + +from logger import setup_logger + +logger = setup_logger(__name__) + + +class FlavorSynchronizer: + def __init__( + self, + username: str | None = "", + password: str = "", + project_id: str | None = None, + user_domain_id=None, + auth_url: str | None = None, + ) -> None: + self.username = username + self.password = password + self.project_id = str(project_id) + self.user_domain_id = user_domain_id + self.auth_url = auth_url + + @cached_property + def _nova(self): + return novaclient.Client( + "2", + self.username, + self.password, + self.project_id, + self.auth_url, + user_domain_id=self.user_domain_id, + ) + + def reconcile(self, desired_flavors: list[FlavorSpec]): + existing_flavors = self._nova.flavors.list() + for flavor in desired_flavors: + nova_flavor = next( + (flv for flv in existing_flavors if flv.name == flavor.stripped_name), + None, + ) + + update_needed = False + if nova_flavor: + logger.info( + f"Flavor: {flavor.stripped_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.stripped_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/logger.py b/operators/nova-flavors/logger.py new file mode 100644 index 000000000..c93b16910 --- /dev/null +++ b/operators/nova-flavors/logger.py @@ -0,0 +1,19 @@ +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/poetry.lock b/operators/nova-flavors/poetry.lock new file mode 100644 index 000000000..b575c2242 --- /dev/null +++ b/operators/nova-flavors/poetry.lock @@ -0,0 +1,758 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[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" +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 = "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" +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" +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" +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" +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.8.0" +description = "Authentication Library for OpenStack Identity" +optional = false +python-versions = ">=3.8" +files = [ + {file = "keystoneauth1-5.8.0-py3-none-any.whl", hash = "sha256:e69dff80c509ab64d4de4494658d914e81f26af720828dc584ceee74ecd666d9"}, + {file = "keystoneauth1-5.8.0.tar.gz", hash = "sha256:3157c212e121164de64d63e5ef7e1daad2bd3649a68de1e971b76877019ef1c4"}, +] + +[package.dependencies] +iso8601 = ">=0.1.11" +os-service-types = ">=1.2.0" +pbr = ">=2.0.0" +requests = ">=2.14.2" +stevedore = ">=1.20.0" + +[package.extras] +betamax = ["betamax (>=0.7.0)", "fixtures (>=3.0.0)", "mock (>=2.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" +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" +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 = "*" +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.4.0" +description = "Oslo i18n library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "oslo.i18n-6.4.0-py3-none-any.whl", hash = "sha256:5417778ba3b1920b70b99859d730ac9bf37f18050dc28af890c66345ba855bc0"}, + {file = "oslo.i18n-6.4.0.tar.gz", hash = "sha256:66e04c041e9ff17d07e13ec7f48295fbc36169143c72ca2352a3efcc98e7b608"}, +] + +[package.dependencies] +pbr = ">=2.0.0" + +[[package]] +name = "oslo-serialization" +version = "5.5.0" +description = "Oslo Serialization library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "oslo.serialization-5.5.0-py3-none-any.whl", hash = "sha256:cd2297c2006be104298843c4d176fb659eba0c6b618a3e3760d650dc771a6df5"}, + {file = "oslo.serialization-5.5.0.tar.gz", hash = "sha256:9e752fc5d8a975956728dd96a82186783b3fefcacbb3553acd933058861e15a6"}, +] + +[package.dependencies] +msgpack = ">=0.5.2" +"oslo.utils" = ">=3.33.0" +pbr = ">=2.0.0" +tzdata = {version = ">=2022.4", markers = "python_version >= \"3.9\""} + +[[package]] +name = "oslo-utils" +version = "7.4.0" +description = "Oslo Utility library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "oslo.utils-7.4.0-py3-none-any.whl", hash = "sha256:6dd15c9fc4fb98d38e5b017f2f5ae171d35a73c5f2ae62a93d5f3bfd9384074b"}, + {file = "oslo.utils-7.4.0.tar.gz", hash = "sha256:aa5dcb5daa05ddf4b534f2cdeda56f7f21485c96f5cbaf6a8c0871d803b73ece"}, +] + +[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" +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" +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" +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" +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.0" +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" +files = [ + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, +] + +[package.extras] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "pyparsing" +version = "3.2.0" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "python-novaclient" +version = "18.7.0" +description = "Client library for OpenStack Compute API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-novaclient-18.7.0.tar.gz", hash = "sha256:94cad0f0f4c161ced52a5ecd85d134fd673b997da7194a031c0ca69344340b0d"}, + {file = "python_novaclient-18.7.0-py3-none-any.whl", hash = "sha256:4e9f1ff681a635168c7635b1facce369d9117b60400ea03a5b58487880670553"}, +] + +[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" +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" +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.3.0" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, + {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, +] + +[package.dependencies] +pbr = ">=2.0.0" + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +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" +files = [] +develop = false + +[package.dependencies] +pyyaml = "^6.0" + +[package.source] +type = "directory" +url = "../../python/understack-flavor-matcher" + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +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.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "b2ac0001ccd3fff36667db0605235eb33196a9a8d577e512bd00b7d066271784" diff --git a/operators/nova-flavors/pyproject.toml b/operators/nova-flavors/pyproject.toml new file mode 100644 index 000000000..edec0efba --- /dev/null +++ b/operators/nova-flavors/pyproject.toml @@ -0,0 +1,42 @@ +[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" + +[tool.poetry.dependencies] +python = "^3.11" +understack-flavor-matcher = {path = "../../python/understack-flavor-matcher"} +python-novaclient = "^18.7.0" + +[tool.poetry.group.dev.dependencies] + + +[tool.poetry.group.test.dependencies] +pytest-mock = "^3.14.0" +pytest = "^8.3.3" + +[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.isort] +profile = "open_stack" diff --git a/operators/nova-flavors/reconcile.py b/operators/nova-flavors/reconcile.py new file mode 100644 index 000000000..5412b58ef --- /dev/null +++ b/operators/nova-flavors/reconcile.py @@ -0,0 +1,23 @@ +from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.flavor_spec import os +from flavor_synchronizer import FlavorSynchronizer +from logger import setup_logger + +logger = setup_logger(__name__) + +# nonprod vs prod +flv_env = os.getenv("FLAVORS_ENV", "undefined") +all_flavors = FlavorSpec.from_directory(os.getenv("FLAVORS_DIR", "")) +defined_flavors = [flv for flv in all_flavors if flv.name.startswith(flv_env)] + + + +synchronizer = FlavorSynchronizer( + username=os.environ.get("OS_USERNAME", ""), + password=os.environ.get("OS_PASSWORD", ""), + project_id=os.environ.get("OS_PROJECT_ID"), + user_domain_id=os.environ.get("OS_USER_DOMAIN_ID", ""), + auth_url=os.environ.get("OS_AUTH_URL"), +) +synchronizer.reconcile(defined_flavors) + From 7aa2ad2326a72fa58aceb01b6768f05ba3286d52 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 19 Nov 2024 16:22:46 +0000 Subject: [PATCH 03/19] nova_flavors: watch filesystem for modifications --- operators/nova-flavors/poetry.lock | 44 +++++++++++++++- operators/nova-flavors/pyproject.toml | 1 + operators/nova-flavors/reconcile.py | 52 ++++++++++++++----- .../nova-flavors/spec_changed_handler.py | 37 +++++++++++++ 4 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 operators/nova-flavors/spec_changed_handler.py diff --git a/operators/nova-flavors/poetry.lock b/operators/nova-flavors/poetry.lock index b575c2242..71ba33c0b 100644 --- a/operators/nova-flavors/poetry.lock +++ b/operators/nova-flavors/poetry.lock @@ -662,6 +662,48 @@ 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" +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" @@ -755,4 +797,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b2ac0001ccd3fff36667db0605235eb33196a9a8d577e512bd00b7d066271784" +content-hash = "6f43c04c92509848d5f8834f7016fdd309e95b96b2e20c61e971c350ee1f7933" diff --git a/operators/nova-flavors/pyproject.toml b/operators/nova-flavors/pyproject.toml index edec0efba..1ab844eaf 100644 --- a/operators/nova-flavors/pyproject.toml +++ b/operators/nova-flavors/pyproject.toml @@ -10,6 +10,7 @@ readme = "README.md" python = "^3.11" understack-flavor-matcher = {path = "../../python/understack-flavor-matcher"} python-novaclient = "^18.7.0" +watchdog = "^6.0.0" [tool.poetry.group.dev.dependencies] diff --git a/operators/nova-flavors/reconcile.py b/operators/nova-flavors/reconcile.py index 5412b58ef..cdb0496da 100644 --- a/operators/nova-flavors/reconcile.py +++ b/operators/nova-flavors/reconcile.py @@ -1,23 +1,51 @@ +import logging +import os +import time + from flavor_matcher.flavor_spec import FlavorSpec -from flavor_matcher.flavor_spec import os +from watchdog.observers import Observer + from flavor_synchronizer import FlavorSynchronizer from logger import setup_logger +from spec_changed_handler import SpecChangedHandler -logger = setup_logger(__name__) +loglevel = getattr(logging, os.getenv("NOVA_FLAVOR_MONITOR_LOGLEVEL", "info").upper()) +logging.getLogger().setLevel(loglevel) +logger = setup_logger(__name__, level=loglevel) # nonprod vs prod flv_env = os.getenv("FLAVORS_ENV", "undefined") -all_flavors = FlavorSpec.from_directory(os.getenv("FLAVORS_DIR", "")) -defined_flavors = [flv for flv in all_flavors if flv.name.startswith(flv_env)] +FLAVORS_DIR = os.getenv("FLAVORS_DIR", "") +if not os.path.isdir(FLAVORS_DIR): + raise ValueError(f"FLAVORS_DIR '{FLAVORS_DIR}' is not a directory") + + +def defined_flavors(): + all_flavors = FlavorSpec.from_directory(FLAVORS_DIR) + return [flv for flv in all_flavors if flv.name.startswith(flv_env)] + +def main(): + synchronizer = FlavorSynchronizer( + username=os.environ.get("OS_USERNAME", ""), + password=os.environ.get("OS_PASSWORD", ""), + project_id=os.environ.get("OS_PROJECT_ID"), + user_domain_id=os.environ.get("OS_USER_DOMAIN_ID", ""), + auth_url=os.environ.get("OS_AUTH_URL"), + ) + handler = SpecChangedHandler(synchronizer, defined_flavors) + observer = Observer() + observer.schedule(handler, FLAVORS_DIR, recursive=True) + logger.info(f"Watching for changes in {FLAVORS_DIR}") + observer.start() -synchronizer = FlavorSynchronizer( - username=os.environ.get("OS_USERNAME", ""), - password=os.environ.get("OS_PASSWORD", ""), - project_id=os.environ.get("OS_PROJECT_ID"), - user_domain_id=os.environ.get("OS_USER_DOMAIN_ID", ""), - auth_url=os.environ.get("OS_AUTH_URL"), -) -synchronizer.reconcile(defined_flavors) + try: + while True: + time.sleep(1) + finally: + observer.stop() + observer.join() +if __name__ == "__main__": + main() diff --git a/operators/nova-flavors/spec_changed_handler.py b/operators/nova-flavors/spec_changed_handler.py new file mode 100644 index 000000000..c97e573eb --- /dev/null +++ b/operators/nova-flavors/spec_changed_handler.py @@ -0,0 +1,37 @@ + +from watchdog.events import DirModifiedEvent +from typing import Callable +from watchdog.events import FileModifiedEvent +from watchdog.events import FileSystemEventHandler +from flavor_synchronizer import FlavorSynchronizer +import time + +from 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()) From 495382084b173cb11d2576feb215a9b74e594b2d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 19 Nov 2024 17:33:01 +0000 Subject: [PATCH 04/19] FlavorSpec: pre-filter specs on loading Prior to this change it was down to the client of FlavorSpec to filter out the irrelevant flavors, but now for consistency this filtering happens immediately when the flavors are loaded. --- operators/nova-flavors/reconcile.py | 8 +++----- .../flavor_matcher/flavor_spec.py | 10 ++++++++++ .../tests/test_flavor_spec.py | 13 ++++++++++--- .../understack_workflows/flavor_detect.py | 4 ---- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/operators/nova-flavors/reconcile.py b/operators/nova-flavors/reconcile.py index cdb0496da..d56ac1e14 100644 --- a/operators/nova-flavors/reconcile.py +++ b/operators/nova-flavors/reconcile.py @@ -14,15 +14,13 @@ logger = setup_logger(__name__, level=loglevel) # nonprod vs prod -flv_env = os.getenv("FLAVORS_ENV", "undefined") FLAVORS_DIR = os.getenv("FLAVORS_DIR", "") if not os.path.isdir(FLAVORS_DIR): raise ValueError(f"FLAVORS_DIR '{FLAVORS_DIR}' is not a directory") -def defined_flavors(): - all_flavors = FlavorSpec.from_directory(FLAVORS_DIR) - return [flv for flv in all_flavors if flv.name.startswith(flv_env)] +def read_flavors(): + return FlavorSpec.from_directory(FLAVORS_DIR) def main(): @@ -34,7 +32,7 @@ def main(): auth_url=os.environ.get("OS_AUTH_URL"), ) - handler = SpecChangedHandler(synchronizer, defined_flavors) + handler = SpecChangedHandler(synchronizer, read_flavors) observer = Observer() observer.schedule(handler, FLAVORS_DIR, recursive=True) logger.info(f"Watching for changes in {FLAVORS_DIR}") diff --git a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py index 790a3ce53..a7b5ea265 100644 --- a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py +++ b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py @@ -39,6 +39,10 @@ def from_yaml(yaml_str: str) -> "FlavorSpec": pci=data.get("pci", []), ) + @staticmethod + def configured_envtype(): + return os.getenv("FLAVORS_ENV", "unconfigured") + @property def stripped_name(self): """Returns actual flavor name with the prod/nonprod prefix removed.""" @@ -58,6 +62,10 @@ def baremetal_nova_resource_class(self): converted_name = re.sub(r"[^\w]", "", self.stripped_name).upper() return f"RESOURCES:CUSTOM_BAREMETAL_{converted_name}" + @property + def env_type(self): + return self.name.split(".")[0] + @property def memory_mib(self): """Returns memory size in MiB""" @@ -74,6 +82,8 @@ def from_directory(directory: str = "/etc/flavors/") -> list["FlavorSpec"]: with open(filepath, "r") as file: yaml_content = file.read() flavor_spec = FlavorSpec.from_yaml(yaml_content) + if flavor_spec.env_type != FlavorSpec.configured_envtype(): + continue flavor_specs.append(flavor_spec) except yaml.YAMLError as e: print(f"Error parsing YAML file {filename}: {e}") diff --git a/python/understack-flavor-matcher/tests/test_flavor_spec.py b/python/understack-flavor-matcher/tests/test_flavor_spec.py index 26ac5ce98..a88931070 100644 --- a/python/understack-flavor-matcher/tests/test_flavor_spec.py +++ b/python/understack-flavor-matcher/tests/test_flavor_spec.py @@ -61,6 +61,7 @@ def test_from_yaml_invalid(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"]), @@ -69,9 +70,7 @@ def test_from_directory(mocked_open, mock_walk, valid_yaml, invalid_yaml): 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 @@ -80,6 +79,7 @@ def test_from_directory(mocked_open, mock_walk, valid_yaml, invalid_yaml): 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)) @@ -326,4 +326,11 @@ def test_memory_gib(valid_yaml): def test_baremetal_nova_resource_class(valid_yaml): flv = FlavorSpec.from_yaml(valid_yaml) - assert flv.baremetal_nova_resource_class == "RESOURCES:CUSTOM_BAREMETAL_GP2ULTRAMEDIUM" + assert ( + flv.baremetal_nova_resource_class == "RESOURCES:CUSTOM_BAREMETAL_GP2ULTRAMEDIUM" + ) + + +def test_envtype(valid_yaml): + flv = FlavorSpec.from_yaml(valid_yaml) + assert flv.env_type == "nonprod" diff --git a/python/understack-workflows/understack_workflows/flavor_detect.py b/python/understack-workflows/understack_workflows/flavor_detect.py index 27865cf19..4a7215848 100644 --- a/python/understack-workflows/understack_workflows/flavor_detect.py +++ b/python/understack-workflows/understack_workflows/flavor_detect.py @@ -1,16 +1,12 @@ -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__) -ENV_TYPE = os.getenv("FLAVOR_TYPES", "nonprod") FLAVORS = FlavorSpec.from_directory("/etc/understack_flavors/") logger.info(f"Loaded {len(FLAVORS)} flavor specifications.") From eeab9eeafab1b6d862c982447626c171071a469d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 19 Nov 2024 17:43:04 +0000 Subject: [PATCH 05/19] keystone: setup serviceaccount and role for flavorsync --- components/keystone/aio-values.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/keystone/aio-values.yaml b/components/keystone/aio-values.yaml index ced666ed4..c9a2392cb 100644 --- a/components/keystone/aio-values.yaml +++ b/components/keystone/aio-values.yaml @@ -19,6 +19,11 @@ bootstrap: # give 'argoworkflow' 'admin' over the 'baremetal' project openstack role add --user-domain infra --project-domain infra --user argoworkflow --project baremetal admin + # create 'flavorsync' user to allow synchronization of the flavors to nova + openstack user create --or-show --domain service --password abcd1234 flavorsync + openstack role create --or-show --domain service flavorsync + openstack role add --user-domain service --user flavorsync flavorsync + # create 'monitoring' user for monitoring usage openstack user create --or-show --domain infra --password monitoring_demo monitoring # give 'monitoring' the 'admin' over the 'baremetal' project From c3a378b25544c41985ee0ef337dc6e4290f64da9 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 19 Nov 2024 17:56:33 +0000 Subject: [PATCH 06/19] nova_flavors: reorganise into a package --- .../{ => nova_flavors}/__init__.py | 0 .../{ => nova_flavors}/flavor_synchronizer.py | 2 +- .../nova-flavors/{ => nova_flavors}/logger.py | 0 .../{ => nova_flavors}/reconcile.py | 23 +++++++++++-------- .../spec_changed_handler.py | 9 ++++---- operators/nova-flavors/pyproject.toml | 6 +++++ 6 files changed, 25 insertions(+), 15 deletions(-) rename operators/nova-flavors/{ => nova_flavors}/__init__.py (100%) rename operators/nova-flavors/{ => nova_flavors}/flavor_synchronizer.py (98%) rename operators/nova-flavors/{ => nova_flavors}/logger.py (100%) rename operators/nova-flavors/{ => nova_flavors}/reconcile.py (62%) rename operators/nova-flavors/{ => nova_flavors}/spec_changed_handler.py (90%) diff --git a/operators/nova-flavors/__init__.py b/operators/nova-flavors/nova_flavors/__init__.py similarity index 100% rename from operators/nova-flavors/__init__.py rename to operators/nova-flavors/nova_flavors/__init__.py diff --git a/operators/nova-flavors/flavor_synchronizer.py b/operators/nova-flavors/nova_flavors/flavor_synchronizer.py similarity index 98% rename from operators/nova-flavors/flavor_synchronizer.py rename to operators/nova-flavors/nova_flavors/flavor_synchronizer.py index 5b36cbc68..db8aac13c 100644 --- a/operators/nova-flavors/flavor_synchronizer.py +++ b/operators/nova-flavors/nova_flavors/flavor_synchronizer.py @@ -3,7 +3,7 @@ from flavor_matcher.flavor_spec import FlavorSpec from novaclient import client as novaclient -from logger import setup_logger +from nova_flavors.logger import setup_logger logger = setup_logger(__name__) diff --git a/operators/nova-flavors/logger.py b/operators/nova-flavors/nova_flavors/logger.py similarity index 100% rename from operators/nova-flavors/logger.py rename to operators/nova-flavors/nova_flavors/logger.py diff --git a/operators/nova-flavors/reconcile.py b/operators/nova-flavors/nova_flavors/reconcile.py similarity index 62% rename from operators/nova-flavors/reconcile.py rename to operators/nova-flavors/nova_flavors/reconcile.py index d56ac1e14..e1953ed70 100644 --- a/operators/nova-flavors/reconcile.py +++ b/operators/nova-flavors/nova_flavors/reconcile.py @@ -5,25 +5,27 @@ from flavor_matcher.flavor_spec import FlavorSpec from watchdog.observers import Observer -from flavor_synchronizer import FlavorSynchronizer -from logger import setup_logger -from spec_changed_handler import SpecChangedHandler +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) -# 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") + +flavors_dir = "" def read_flavors(): - return FlavorSpec.from_directory(FLAVORS_DIR) + return FlavorSpec.from_directory(flavors_dir) 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.environ.get("OS_USERNAME", ""), password=os.environ.get("OS_PASSWORD", ""), @@ -34,8 +36,8 @@ def main(): handler = SpecChangedHandler(synchronizer, read_flavors) observer = Observer() - observer.schedule(handler, FLAVORS_DIR, recursive=True) - logger.info(f"Watching for changes in {FLAVORS_DIR}") + observer.schedule(handler, flavors_dir, recursive=True) + logger.info(f"Watching for changes in {flavors_dir}") observer.start() try: @@ -45,5 +47,6 @@ def main(): observer.stop() observer.join() + if __name__ == "__main__": main() diff --git a/operators/nova-flavors/spec_changed_handler.py b/operators/nova-flavors/nova_flavors/spec_changed_handler.py similarity index 90% rename from operators/nova-flavors/spec_changed_handler.py rename to operators/nova-flavors/nova_flavors/spec_changed_handler.py index c97e573eb..48e0e07c0 100644 --- a/operators/nova-flavors/spec_changed_handler.py +++ b/operators/nova-flavors/nova_flavors/spec_changed_handler.py @@ -1,15 +1,16 @@ +import time +from typing import Callable from watchdog.events import DirModifiedEvent -from typing import Callable from watchdog.events import FileModifiedEvent from watchdog.events import FileSystemEventHandler -from flavor_synchronizer import FlavorSynchronizer -import time -from logger import setup_logger +from nova_flavors.flavor_synchronizer import FlavorSynchronizer +from nova_flavors.logger import setup_logger logger = setup_logger(__name__) + class SpecChangedHandler(FileSystemEventHandler): COOLDOWN_SECONDS = 30 diff --git a/operators/nova-flavors/pyproject.toml b/operators/nova-flavors/pyproject.toml index 1ab844eaf..4443a250b 100644 --- a/operators/nova-flavors/pyproject.toml +++ b/operators/nova-flavors/pyproject.toml @@ -5,6 +5,9 @@ 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" @@ -41,3 +44,6 @@ select = [ [tool.isort] profile = "open_stack" + +[tool.poetry.scripts] +nova-flavors-sync = "nova_flavors.reconcile:main" From 4822ef45227285c1bb89daaca43fb6bca2822b97 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 19 Nov 2024 18:13:48 +0000 Subject: [PATCH 07/19] nova_flavors: add Docker image --- .../nova-flavors/Dockerfile.nova-flavors | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 containers/nova-flavors/Dockerfile.nova-flavors diff --git a/containers/nova-flavors/Dockerfile.nova-flavors b/containers/nova-flavors/Dockerfile.nova-flavors new file mode 100644 index 000000000..2730a5ff3 --- /dev/null +++ b/containers/nova-flavors/Dockerfile.nova-flavors @@ -0,0 +1,35 @@ +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 +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 psutil -r /opt/venv/wheels/requirements.txt nova-flavors + +USER appuser +CMD ["nova-flavors-sync"] From 496730005b51722e95bb2b1a0647ad50747f3a4e Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 19 Nov 2024 18:17:02 +0000 Subject: [PATCH 08/19] nova_flavors: build container image in GH --- .github/workflows/containers.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/containers.yaml b/.github/workflows/containers.yaml index 1255db26d..1e1f66e50 100644 --- a/.github/workflows/containers.yaml +++ b/.github/workflows/containers.yaml @@ -116,6 +116,7 @@ jobs: matrix: container: - name: ironic-nautobot-client + - name: nova-flavors steps: - name: setup docker buildx From c367109646c54fc7b4e4ed3fcc800817802c5e23 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 Nov 2024 11:13:14 +0000 Subject: [PATCH 09/19] nova_flavors: add tests for flavor_synchronizer --- operators/nova-flavors/poetry.lock | 134 ++++++++++++++++-- operators/nova-flavors/pyproject.toml | 10 +- .../tests/test_flavor_synchronizer.py | 109 ++++++++++++++ 3 files changed, 238 insertions(+), 15 deletions(-) create mode 100644 operators/nova-flavors/tests/test_flavor_synchronizer.py diff --git a/operators/nova-flavors/poetry.lock b/operators/nova-flavors/poetry.lock index 71ba33c0b..f04a0c2cc 100644 --- a/operators/nova-flavors/poetry.lock +++ b/operators/nova-flavors/poetry.lock @@ -136,6 +136,80 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.6.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e"}, + {file = "coverage-7.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c"}, + {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777"}, + {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314"}, + {file = "coverage-7.6.7-cp310-cp310-win32.whl", hash = "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a"}, + {file = "coverage-7.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163"}, + {file = "coverage-7.6.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469"}, + {file = "coverage-7.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b"}, + {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d"}, + {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4"}, + {file = "coverage-7.6.7-cp311-cp311-win32.whl", hash = "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2"}, + {file = "coverage-7.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f"}, + {file = "coverage-7.6.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9"}, + {file = "coverage-7.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1"}, + {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f"}, + {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb"}, + {file = "coverage-7.6.7-cp312-cp312-win32.whl", hash = "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76"}, + {file = "coverage-7.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c"}, + {file = "coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3"}, + {file = "coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc"}, + {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55"}, + {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384"}, + {file = "coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30"}, + {file = "coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42"}, + {file = "coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413"}, + {file = "coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b"}, + {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b"}, + {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3"}, + {file = "coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8"}, + {file = "coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56"}, + {file = "coverage-7.6.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874"}, + {file = "coverage-7.6.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7"}, + {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c"}, + {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289"}, + {file = "coverage-7.6.7-cp39-cp39-win32.whl", hash = "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c"}, + {file = "coverage-7.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13"}, + {file = "coverage-7.6.7-pp39.pp310-none-any.whl", hash = "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671"}, + {file = "coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24"}, +] + +[package.extras] +toml = ["tomli"] + [[package]] name = "debtcollector" version = "3.0.0" @@ -314,13 +388,13 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] name = "oslo-i18n" -version = "6.4.0" +version = "6.5.0" description = "Oslo i18n library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "oslo.i18n-6.4.0-py3-none-any.whl", hash = "sha256:5417778ba3b1920b70b99859d730ac9bf37f18050dc28af890c66345ba855bc0"}, - {file = "oslo.i18n-6.4.0.tar.gz", hash = "sha256:66e04c041e9ff17d07e13ec7f48295fbc36169143c72ca2352a3efcc98e7b608"}, + {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] @@ -328,20 +402,20 @@ pbr = ">=2.0.0" [[package]] name = "oslo-serialization" -version = "5.5.0" +version = "5.6.0" description = "Oslo Serialization library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "oslo.serialization-5.5.0-py3-none-any.whl", hash = "sha256:cd2297c2006be104298843c4d176fb659eba0c6b618a3e3760d650dc771a6df5"}, - {file = "oslo.serialization-5.5.0.tar.gz", hash = "sha256:9e752fc5d8a975956728dd96a82186783b3fefcacbb3553acd933058861e15a6"}, + {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 = {version = ">=2022.4", markers = "python_version >= \"3.9\""} +tzdata = ">=2022.4" [[package]] name = "oslo-utils" @@ -483,6 +557,38 @@ pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-github-actions-annotate-failures" +version = "0.2.0" +description = "pytest plugin to annotate failed tests with a workflow command for GitHub Actions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-github-actions-annotate-failures-0.2.0.tar.gz", hash = "sha256:844ab626d389496e44f960b42f0a72cce29ae06d363426d17ea9ae1b4bef2288"}, + {file = "pytest_github_actions_annotate_failures-0.2.0-py3-none-any.whl", hash = "sha256:8bcef65fed503faaa0524b59cfeccc8995130972dd7b008d64193cc41b9cde85"}, +] + +[package.dependencies] +pytest = ">=4.0.0" + [[package]] name = "pytest-mock" version = "3.14.0" @@ -606,13 +712,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "stevedore" -version = "5.3.0" +version = "5.4.0" description = "Manage dynamic plugins for Python applications" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, - {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, + {file = "stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857"}, + {file = "stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d"}, ] [package.dependencies] @@ -797,4 +903,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "6f43c04c92509848d5f8834f7016fdd309e95b96b2e20c61e971c350ee1f7933" +content-hash = "1fea5ba83e0877586cc9dca4b70ad32ae138021f3e14342d775b4dc92af0bd94" diff --git a/operators/nova-flavors/pyproject.toml b/operators/nova-flavors/pyproject.toml index 4443a250b..75043cbde 100644 --- a/operators/nova-flavors/pyproject.toml +++ b/operators/nova-flavors/pyproject.toml @@ -19,8 +19,10 @@ watchdog = "^6.0.0" [tool.poetry.group.test.dependencies] -pytest-mock = "^3.14.0" pytest = "^8.3.3" +pytest-cov = "^5.0.0" +pytest-github-actions-annotate-failures = "*" +pytest-mock = "^3.14.0" [build-system] requires = ["poetry-core"] @@ -42,6 +44,12 @@ 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" diff --git a/operators/nova-flavors/tests/test_flavor_synchronizer.py b/operators/nova-flavors/tests/test_flavor_synchronizer.py new file mode 100644 index 000000000..34135c82b --- /dev/null +++ b/operators/nova-flavors/tests/test_flavor_synchronizer.py @@ -0,0 +1,109 @@ +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", + project_id="test_project_id", + user_domain_id="test_user_domain_id", + 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.project_id == "test_project_id" + assert flavor_synchronizer.user_domain_id == "test_user_domain_id" + 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.stripped_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.stripped_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.stripped_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.stripped_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.stripped_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, + } + ) From 6b009ae8f390e0920178f9a8fed5d8cc4c0bde88 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 Nov 2024 11:39:49 +0000 Subject: [PATCH 10/19] nova_flavors: add tests for handler --- .../tests/test_spec_changed_handler.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 operators/nova-flavors/tests/test_spec_changed_handler.py diff --git a/operators/nova-flavors/tests/test_spec_changed_handler.py b/operators/nova-flavors/tests/test_spec_changed_handler.py new file mode 100644 index 000000000..2a85dfeca --- /dev/null +++ b/operators/nova-flavors/tests/test_spec_changed_handler.py @@ -0,0 +1,60 @@ +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 a90d0108651b058fcd47d30f47ed448dfc2b2d21 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 Nov 2024 12:27:23 +0000 Subject: [PATCH 11/19] nova_flavors: add tests for reconcile.py --- .../nova-flavors/tests/test_reconcile.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 operators/nova-flavors/tests/test_reconcile.py diff --git a/operators/nova-flavors/tests/test_reconcile.py b/operators/nova-flavors/tests/test_reconcile.py new file mode 100644 index 000000000..7e21a192e --- /dev/null +++ b/operators/nova-flavors/tests/test_reconcile.py @@ -0,0 +1,89 @@ +import logging +import os +import pytest +from unittest.mock import patch + +from nova_flavors.reconcile import ( + FlavorSpec, + FlavorSynchronizer, + SpecChangedHandler, + main, + read_flavors, +) +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_read_flavors(mocker): + # Set up + mock_flavor_spec = mocker.Mock(spec=FlavorSpec) + mocker.patch( + "nova_flavors.reconcile.FlavorSpec.from_directory", + return_value=mock_flavor_spec, + ) + + # Execute + result = read_flavors() + + # Verify + assert result == mock_flavor_spec + + +@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() From 044bb3ee9c95d02b8c570af50cde46aac2dd462f Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 Nov 2024 12:44:41 +0000 Subject: [PATCH 12/19] flavor_matcher: add missing imports --- python/understack-flavor-matcher/tests/test_flavor_spec.py | 4 +++- .../understack_workflows/flavor_detect.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/python/understack-flavor-matcher/tests/test_flavor_spec.py b/python/understack-flavor-matcher/tests/test_flavor_spec.py index a88931070..4c353c112 100644 --- a/python/understack-flavor-matcher/tests/test_flavor_spec.py +++ b/python/understack-flavor-matcher/tests/test_flavor_spec.py @@ -1,4 +1,6 @@ -from unittest.mock import mock_open, patch +import os +from unittest.mock import mock_open +from unittest.mock import patch import pytest from flavor_matcher.flavor_spec import FlavorSpec diff --git a/python/understack-workflows/understack_workflows/flavor_detect.py b/python/understack-workflows/understack_workflows/flavor_detect.py index 4a7215848..f779c9f40 100644 --- a/python/understack-workflows/understack_workflows/flavor_detect.py +++ b/python/understack-workflows/understack_workflows/flavor_detect.py @@ -2,6 +2,7 @@ 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 From e058394651b4af889e204453af4483cdf9b0d146 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 Nov 2024 13:58:32 +0000 Subject: [PATCH 13/19] nova_flavors: stop using hardcoded IDs for auth --- .../nova_flavors/flavor_synchronizer.py | 21 +++++++++++-------- .../nova-flavors/nova_flavors/reconcile.py | 11 +++++----- .../tests/test_flavor_synchronizer.py | 5 +---- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/operators/nova-flavors/nova_flavors/flavor_synchronizer.py b/operators/nova-flavors/nova_flavors/flavor_synchronizer.py index db8aac13c..288e1d714 100644 --- a/operators/nova-flavors/nova_flavors/flavor_synchronizer.py +++ b/operators/nova-flavors/nova_flavors/flavor_synchronizer.py @@ -13,25 +13,28 @@ def __init__( self, username: str | None = "", password: str = "", - project_id: str | None = None, - user_domain_id=None, + 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_id = str(project_id) - self.user_domain_id = user_domain_id + 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", - self.username, - self.password, - self.project_id, - self.auth_url, - user_domain_id=self.user_domain_id, + 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]): diff --git a/operators/nova-flavors/nova_flavors/reconcile.py b/operators/nova-flavors/nova_flavors/reconcile.py index e1953ed70..e87beb790 100644 --- a/operators/nova-flavors/nova_flavors/reconcile.py +++ b/operators/nova-flavors/nova_flavors/reconcile.py @@ -27,11 +27,12 @@ def main(): if not os.path.isdir(flavors_dir): raise ValueError(f"flavors_dir '{flavors_dir}' is not a directory") synchronizer = FlavorSynchronizer( - username=os.environ.get("OS_USERNAME", ""), - password=os.environ.get("OS_PASSWORD", ""), - project_id=os.environ.get("OS_PROJECT_ID"), - user_domain_id=os.environ.get("OS_USER_DOMAIN_ID", ""), - auth_url=os.environ.get("OS_AUTH_URL"), + 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, read_flavors) diff --git a/operators/nova-flavors/tests/test_flavor_synchronizer.py b/operators/nova-flavors/tests/test_flavor_synchronizer.py index 34135c82b..f7732a1b9 100644 --- a/operators/nova-flavors/tests/test_flavor_synchronizer.py +++ b/operators/nova-flavors/tests/test_flavor_synchronizer.py @@ -11,8 +11,6 @@ def flavor_synchronizer(): return FlavorSynchronizer( username="test_username", password="test_password", - project_id="test_project_id", - user_domain_id="test_user_domain_id", auth_url="test_auth_url", ) @@ -46,8 +44,7 @@ def flavor(): def test_flavor_synchronizer_init(flavor_synchronizer): assert flavor_synchronizer.username == "test_username" assert flavor_synchronizer.password == "test_password" - assert flavor_synchronizer.project_id == "test_project_id" - assert flavor_synchronizer.user_domain_id == "test_user_domain_id" + assert flavor_synchronizer.user_domain_name == "service" assert flavor_synchronizer.auth_url == "test_auth_url" From a3d99a8a085be9fe325a1540583478c3a4d73da6 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 Nov 2024 14:39:59 +0000 Subject: [PATCH 14/19] nova_flavors: remove awkward global --- .../nova_flavors/flavor_synchronizer.py | 3 +++ .../nova-flavors/nova_flavors/reconcile.py | 11 +++----- .../nova-flavors/tests/test_reconcile.py | 25 ------------------- 3 files changed, 6 insertions(+), 33 deletions(-) diff --git a/operators/nova-flavors/nova_flavors/flavor_synchronizer.py b/operators/nova-flavors/nova_flavors/flavor_synchronizer.py index 288e1d714..a84a6e894 100644 --- a/operators/nova-flavors/nova_flavors/flavor_synchronizer.py +++ b/operators/nova-flavors/nova_flavors/flavor_synchronizer.py @@ -38,6 +38,9 @@ def _nova(self): ) 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( diff --git a/operators/nova-flavors/nova_flavors/reconcile.py b/operators/nova-flavors/nova_flavors/reconcile.py index e87beb790..f1d097eb4 100644 --- a/operators/nova-flavors/nova_flavors/reconcile.py +++ b/operators/nova-flavors/nova_flavors/reconcile.py @@ -14,13 +14,6 @@ logger = setup_logger(__name__, level=loglevel) -flavors_dir = "" - - -def read_flavors(): - return FlavorSpec.from_directory(flavors_dir) - - def main(): # nonprod vs prod flavors_dir = os.getenv("FLAVORS_DIR", "") @@ -35,7 +28,9 @@ def main(): auth_url=os.getenv("OS_AUTH_URL"), ) - handler = SpecChangedHandler(synchronizer, read_flavors) + 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}") diff --git a/operators/nova-flavors/tests/test_reconcile.py b/operators/nova-flavors/tests/test_reconcile.py index 7e21a192e..7734ee77a 100644 --- a/operators/nova-flavors/tests/test_reconcile.py +++ b/operators/nova-flavors/tests/test_reconcile.py @@ -4,11 +4,9 @@ from unittest.mock import patch from nova_flavors.reconcile import ( - FlavorSpec, FlavorSynchronizer, SpecChangedHandler, main, - read_flavors, ) from watchdog.observers import Observer @@ -30,29 +28,6 @@ def test_flavors_dir_env_var_not_set(mocker, return_value): main() -@patch.dict( - "os.environ", - { - "FLAVORS_ENV": "testenv", - "NOVA_FLAVOR_MONITOR_LOGLEVEL": "info", - "FLAVORS_DIR": "/", - }, -) -def test_read_flavors(mocker): - # Set up - mock_flavor_spec = mocker.Mock(spec=FlavorSpec) - mocker.patch( - "nova_flavors.reconcile.FlavorSpec.from_directory", - return_value=mock_flavor_spec, - ) - - # Execute - result = read_flavors() - - # Verify - assert result == mock_flavor_spec - - @patch.dict( "os.environ", { From 74f7316fa9bd8283cd4e90268f78e68a8be8187f Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 21 Nov 2024 09:47:26 +0000 Subject: [PATCH 15/19] nova_flavors: fix the casing on the extra_specs --- python/understack-flavor-matcher/flavor_matcher/flavor_spec.py | 2 +- python/understack-flavor-matcher/tests/test_flavor_spec.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py index a7b5ea265..4de28ec3d 100644 --- a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py +++ b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py @@ -60,7 +60,7 @@ def baremetal_nova_resource_class(self): https://docs.openstack.org/ironic/latest/install/configure-nova-flavors.html """ converted_name = re.sub(r"[^\w]", "", self.stripped_name).upper() - return f"RESOURCES:CUSTOM_BAREMETAL_{converted_name}" + return f"resources:CUSTOM_BAREMETAL_{converted_name}" @property def env_type(self): diff --git a/python/understack-flavor-matcher/tests/test_flavor_spec.py b/python/understack-flavor-matcher/tests/test_flavor_spec.py index 4c353c112..400ac60e7 100644 --- a/python/understack-flavor-matcher/tests/test_flavor_spec.py +++ b/python/understack-flavor-matcher/tests/test_flavor_spec.py @@ -329,7 +329,7 @@ def test_memory_gib(valid_yaml): def test_baremetal_nova_resource_class(valid_yaml): flv = FlavorSpec.from_yaml(valid_yaml) assert ( - flv.baremetal_nova_resource_class == "RESOURCES:CUSTOM_BAREMETAL_GP2ULTRAMEDIUM" + flv.baremetal_nova_resource_class == "resources:CUSTOM_BAREMETAL_GP2ULTRAMEDIUM" ) From 047b5ab44fae9dca9c0c34a28d3c681d01bdd706 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 21 Nov 2024 19:15:43 +0000 Subject: [PATCH 16/19] nova_flavors: fix the role assignments --- components/keystone/aio-values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/keystone/aio-values.yaml b/components/keystone/aio-values.yaml index c9a2392cb..9304e1efb 100644 --- a/components/keystone/aio-values.yaml +++ b/components/keystone/aio-values.yaml @@ -21,8 +21,8 @@ bootstrap: # create 'flavorsync' user to allow synchronization of the flavors to nova openstack user create --or-show --domain service --password abcd1234 flavorsync - openstack role create --or-show --domain service flavorsync - openstack role add --user-domain service --user flavorsync flavorsync + openstack role create --or-show flavorsync + openstack role add --user flavorsync --user-domain service --domain default --inherited flavorsync # create 'monitoring' user for monitoring usage openstack user create --or-show --domain infra --password monitoring_demo monitoring From e839d3c02c9f9d8b39958fd068c4c054983fa7a7 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 25 Nov 2024 11:00:56 +0000 Subject: [PATCH 17/19] nova_flavors: fix punctuation handling in nova resource class --- python/understack-flavor-matcher/flavor_matcher/flavor_spec.py | 2 +- python/understack-flavor-matcher/tests/test_flavor_spec.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py index 4de28ec3d..5ae585a40 100644 --- a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py +++ b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py @@ -59,7 +59,7 @@ def baremetal_nova_resource_class(self): https://docs.openstack.org/ironic/latest/install/configure-nova-flavors.html """ - converted_name = re.sub(r"[^\w]", "", self.stripped_name).upper() + converted_name = re.sub(r"[^\w]", "_", self.stripped_name).upper() return f"resources:CUSTOM_BAREMETAL_{converted_name}" @property diff --git a/python/understack-flavor-matcher/tests/test_flavor_spec.py b/python/understack-flavor-matcher/tests/test_flavor_spec.py index 400ac60e7..5dc092a52 100644 --- a/python/understack-flavor-matcher/tests/test_flavor_spec.py +++ b/python/understack-flavor-matcher/tests/test_flavor_spec.py @@ -329,7 +329,8 @@ def test_memory_gib(valid_yaml): def test_baremetal_nova_resource_class(valid_yaml): flv = FlavorSpec.from_yaml(valid_yaml) assert ( - flv.baremetal_nova_resource_class == "resources:CUSTOM_BAREMETAL_GP2ULTRAMEDIUM" + flv.baremetal_nova_resource_class + == "resources:CUSTOM_BAREMETAL_GP2_ULTRAMEDIUM" ) From 6cb4613c3e6c721212686a855e16454810ed1535 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 25 Nov 2024 11:03:59 +0000 Subject: [PATCH 18/19] Revert "fix: fix flavor to ironic resource class conversion" This reverts commit 5ac057b6ee2623e6542f5df3cae25be172bac718 which was added in order to resolve the mismatch between the nova and ironic flavors that had a punctuation in the name. The reverted commit incorrectly changed the resource class name on the Ironic side while it was the Nova side that was missing the underscores. This was resolved in the commit before this one. --- python/understack-flavor-matcher/flavor_matcher/flavor_spec.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py index 5ae585a40..04c5e79d4 100644 --- a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py +++ b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py @@ -49,8 +49,6 @@ def stripped_name(self): _, name = self.name.split(".", 1) if not name: raise Exception(f"Unable to strip envtype from flavor: {self.name}") - # need to strip '.' from the ironic flavor name - name = name.replace(".", "") return name @property From 7101e1738e4f715c826d078ec151a9ce3d18da4d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 27 Nov 2024 12:03:11 +0000 Subject: [PATCH 19/19] enroll: make flavor spec directory configurable --- .../understack_workflows/flavor_detect.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/understack-workflows/understack_workflows/flavor_detect.py b/python/understack-workflows/understack_workflows/flavor_detect.py index f779c9f40..1fcca2e1c 100644 --- a/python/understack-workflows/understack_workflows/flavor_detect.py +++ b/python/understack-workflows/understack_workflows/flavor_detect.py @@ -1,3 +1,5 @@ +import os + from flavor_matcher.machine import Machine from flavor_matcher.matcher import FlavorSpec from flavor_matcher.matcher import Matcher @@ -8,7 +10,8 @@ from understack_workflows.helpers import setup_logger logger = setup_logger(__name__) -FLAVORS = FlavorSpec.from_directory("/etc/understack_flavors/") +FLAVORS_DIR = os.getenv("FLAVORS_DIR", "/etc/understack_flavors/") +FLAVORS = FlavorSpec.from_directory(FLAVORS_DIR) logger.info(f"Loaded {len(FLAVORS)} flavor specifications.")