diff --git a/.github/workflows/code-test.yaml b/.github/workflows/code-test.yaml index ccc9feab..2d530fa8 100644 --- a/.github/workflows/code-test.yaml +++ b/.github/workflows/code-test.yaml @@ -27,7 +27,6 @@ jobs: matrix: project: - understack-workflows - - ironic-understack - neutron-understack defaults: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 866259c1..58a0ccea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,10 +72,14 @@ repos: files: '^python/understack-workflows/' args: ["--threads"] additional_dependencies: + # python-pyright stupidly does not allow local paths + # https://github.com/pre-commit/pre-commit/issues/1752#issuecomment-754252663 + - "git+https://github.com/rackerlabs/understack.git@197e953#subdirectory=python/understack-flavor-matcher" - "kubernetes" - "pydantic" - "pynautobot" - "pytest" + - "pytest-mock" - "pytest_lazy_fixtures" - "python-ironicclient" - "requests" diff --git a/containers/ironic-nautobot-client/Dockerfile.ironic-nautobot-client b/containers/ironic-nautobot-client/Dockerfile.ironic-nautobot-client index 40899b9a..78e1499d 100644 --- a/containers/ironic-nautobot-client/Dockerfile.ironic-nautobot-client +++ b/containers/ironic-nautobot-client/Dockerfile.ironic-nautobot-client @@ -16,6 +16,7 @@ RUN --mount=type=cache,target=/root/.cache/.pip \ # copy in the code COPY --chown=${APP_USER}:${APP_GROUP} python/understack-workflows /app +COPY --chown=${APP_USER}:${APP_GROUP} python/understack-flavor-matcher /understack-flavor-matcher # need netifaces built as a wheel RUN --mount=type=cache,target=/root/.cache/.pip pip wheel --wheel-dir /app/dist netifaces # build wheels and requirements.txt, skip hashes due to building of netifaces above which won't match @@ -35,6 +36,7 @@ WORKDIR /app RUN mkdir -p /opt/venv/wheels/ COPY --from=builder /app/dist/*.whl /app/dist/requirements.txt /opt/venv/wheels/ +COPY --chown=${APP_USER}:${APP_GROUP} python/understack-flavor-matcher /understack-flavor-matcher RUN --mount=type=cache,target=/root/.cache/.pip /opt/venv/bin/pip install --find-links /opt/venv/wheels/ --only-binary netifaces -r /opt/venv/wheels/requirements.txt understack-workflows diff --git a/containers/ironic/Dockerfile.ironic b/containers/ironic/Dockerfile.ironic index c95119ad..891fb547 100644 --- a/containers/ironic/Dockerfile.ironic +++ b/containers/ironic/Dockerfile.ironic @@ -10,4 +10,5 @@ RUN apt-get update && \ && apt-get clean && rm -rf /var/lib/apt/lists/* COPY python/ironic-understack /tmp/ironic-understack +COPY python/understack-flavor-matcher /tmp/ironic-understack RUN /var/lib/openstack/bin/python -m pip install --no-cache --no-cache-dir /tmp/ironic-understack sushy-oem-idrac==6.0.0 diff --git a/python/ironic-understack/ironic_understack/redfish_inspect_understack.py b/python/ironic-understack/ironic_understack/redfish_inspect_understack.py index 3df194c1..dd575e50 100644 --- a/python/ironic-understack/ironic_understack/redfish_inspect_understack.py +++ b/python/ironic-understack/ironic_understack/redfish_inspect_understack.py @@ -13,14 +13,16 @@ Redfish Inspect Interface modified for Understack """ +import re + +from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.machine import Machine +from flavor_matcher.matcher import Matcher from ironic.drivers.drac import IDRACHardware from ironic.drivers.modules.drac.inspect import DracRedfishInspect from ironic.drivers.modules.inspect_utils import get_inspection_data from ironic.drivers.modules.redfish.inspect import RedfishInspect from ironic.drivers.redfish import RedfishHardware -from ironic_understack.flavor_spec import FlavorSpec -from ironic_understack.machine import Machine -from ironic_understack.matcher import Matcher from ironic_understack.conf import CONF from oslo_log import log from oslo_utils import units @@ -68,10 +70,27 @@ def inspect_hardware(self, task): return upstream_state smallest_disk_gb = min([disk["size"] / units.Gi for disk in inventory["disks"]]) + model_name_match = None + try: + model_name_match = re.search( + r"ModelName=(.*)\)", + inventory.get("system_vendor", {}).get("product_name", ""), + ) + except TypeError as e: + LOG.warn("Error searching for model name: %s", e) + return upstream_state + + if not model_name_match: + LOG.warn("No model_name detected. skipping flavor setting.") + return upstream_state + else: + model_name = model_name_match.group(1) + machine = Machine( memory_mb=inventory["memory"]["physical_mb"], disk_gb=smallest_disk_gb, cpu=inventory["cpu"]["model_name"], + model=model_name, ) matcher = Matcher(FLAVORS) diff --git a/python/ironic-understack/ironic_understack/resource_class.py b/python/ironic-understack/ironic_understack/resource_class.py index 633ab0e2..3f5955c9 100644 --- a/python/ironic-understack/ironic_understack/resource_class.py +++ b/python/ironic-understack/ironic_understack/resource_class.py @@ -2,10 +2,11 @@ from ironic.common import exception from ironic.drivers.modules.inspector.hooks import base from ironic_understack.conf import CONF -from ironic_understack.flavor_spec import FlavorSpec -from ironic_understack.machine import Machine -from ironic_understack.matcher import Matcher +from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.machine import Machine +from flavor_matcher.matcher import Matcher from oslo_log import log as logging +import re LOG = logging.getLogger(__name__) @@ -28,8 +29,21 @@ def __call__(self, task, inventory, plugin_data): disk_size_gb = int(int(inventory["disks"][0]["size"]) / 10**9) cpu_model_name = inventory["cpu"]["model_name"] + model_name = re.search( + r"ModelName=(.*)\)", inventory["system_vendor"]["product_name"] + ) + + if not model_name: + LOG.warn("No model_name detected. skipping flavor setting.") + raise NoMatchError("mode_name not matched") + else: + model_name = model_name.group(1) + machine = Machine( - memory_mb=memory_mb, cpu=cpu_model_name, disk_gb=disk_size_gb + memory_mb=memory_mb, + cpu=cpu_model_name, + disk_gb=disk_size_gb, + model=model_name, ) resource_class_name = self.classify(machine) diff --git a/python/ironic-understack/poetry.lock b/python/ironic-understack/poetry.lock index a37b2ad5..c14af76b 100644 --- a/python/ironic-understack/poetry.lock +++ b/python/ironic-understack/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alembic" @@ -2602,6 +2602,22 @@ files = [ {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 = "../understack-flavor-matcher" + [[package]] name = "urllib3" version = "2.2.3" @@ -2894,4 +2910,4 @@ ifaddr = ">=0.1.7" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "0109d68807924825b96390375aaa6d3422c5ef3f50f008a1c408387d35f92f9e" +content-hash = "6298d0d5e17a23e75361557e83e53ff89f41e1bd64d10542f7f4374301025d62" diff --git a/python/ironic-understack/pyproject.toml b/python/ironic-understack/pyproject.toml index 2503a126..3b097baf 100644 --- a/python/ironic-understack/pyproject.toml +++ b/python/ironic-understack/pyproject.toml @@ -13,6 +13,7 @@ packages = [ ironic = ">=24.1" python = "^3.10" pyyaml = "^6.0" +understack-flavor-matcher = {path = "../understack-flavor-matcher"} [tool.poetry.group.test.dependencies] pytest = "^8.3.2" diff --git a/python/understack-flavor-matcher/README.md b/python/understack-flavor-matcher/README.md new file mode 100644 index 00000000..ba61ac8c --- /dev/null +++ b/python/understack-flavor-matcher/README.md @@ -0,0 +1 @@ +Mini library to identify a hardware flavor of a machine. diff --git a/python/ironic-understack/ironic_understack/tests/__init__.py b/python/understack-flavor-matcher/flavor_matcher/__init__.py similarity index 100% rename from python/ironic-understack/ironic_understack/tests/__init__.py rename to python/understack-flavor-matcher/flavor_matcher/__init__.py diff --git a/python/ironic-understack/ironic_understack/flavor_spec.py b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py similarity index 80% rename from python/ironic-understack/ironic_understack/flavor_spec.py rename to python/understack-flavor-matcher/flavor_matcher/flavor_spec.py index 359a16d4..d39fd343 100644 --- a/python/ironic-understack/ironic_understack/flavor_spec.py +++ b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py @@ -3,7 +3,7 @@ import yaml -from ironic_understack.machine import Machine +from flavor_matcher.machine import Machine @dataclass @@ -39,6 +39,14 @@ def from_yaml(yaml_str: str) -> "FlavorSpec": pci=data.get("pci", []), ) + @property + def stripped_name(self): + """Returns actual flavor name with the prod/nonprod prefix removed.""" + _, name = self.name.split(".", 1) + if not name: + raise Exception(f"Unable to strip envtype from flavor: {self.name}") + return name + @staticmethod def from_directory(directory: str = "/etc/flavors/") -> list["FlavorSpec"]: flavor_specs = [] @@ -65,11 +73,12 @@ def score_machine(self, machine: Machine): # it cannot be used - the score should be 0. # 3. If the machine has smaller disk size than specified in the flavor, # it cannot be used - the score should be 0. - # 4. Machine must match the flavor on one of the CPU models exactly. - # 5. If the machine has exact amount memory as specified in flavor, but + # 4. If the machine's model does not match exactly, score should be 0 + # 5. Machine must match the flavor on one of the CPU models exactly. + # 6. If the machine has exact amount memory as specified in flavor, but # more disk space it is less desirable than the machine that matches # exactly on both disk and memory. - # 6. If the machine has exact amount of disk as specified in flavor, + # 7. If the machine has exact amount of disk as specified in flavor, # but more memory space it is less desirable than the machine that # matches exactly on both disk and memory. @@ -78,6 +87,7 @@ def score_machine(self, machine: Machine): machine.memory_gb == self.memory_gb and machine.disk_gb in self.drives and machine.cpu == self.cpu_model + and machine.model == self.model ): return 100 @@ -89,11 +99,15 @@ def score_machine(self, machine: Machine): if any(machine.disk_gb < drive for drive in self.drives): return 0 - # Rule 4: Machine must match the flavor on one of the CPU models exactly + # Rule 4: Machine's model must match exactly + if machine.model != self.model: + return 0 + + # Rule 5: Machine must match the flavor on one of the CPU models exactly if machine.cpu != self.cpu_model: return 0 - # Rule 5 and 6: Rank based on exact matches or excess capacity + # Rule 6 and 7: Rank based on exact matches or excess capacity score = 0 # Exact memory match gives preference diff --git a/python/ironic-understack/ironic_understack/machine.py b/python/understack-flavor-matcher/flavor_matcher/machine.py similarity index 92% rename from python/ironic-understack/ironic_understack/machine.py rename to python/understack-flavor-matcher/flavor_matcher/machine.py index d1fd5c1c..cef75319 100644 --- a/python/ironic-understack/ironic_understack/machine.py +++ b/python/understack-flavor-matcher/flavor_matcher/machine.py @@ -6,6 +6,7 @@ class Machine: memory_mb: int cpu: str disk_gb: int + model: str @property def memory_gb(self) -> int: diff --git a/python/ironic-understack/ironic_understack/matcher.py b/python/understack-flavor-matcher/flavor_matcher/matcher.py similarity index 89% rename from python/ironic-understack/ironic_understack/matcher.py rename to python/understack-flavor-matcher/flavor_matcher/matcher.py index 8d9f59cf..5e009f31 100644 --- a/python/ironic-understack/ironic_understack/matcher.py +++ b/python/understack-flavor-matcher/flavor_matcher/matcher.py @@ -1,5 +1,5 @@ -from ironic_understack.machine import Machine -from ironic_understack.flavor_spec import FlavorSpec +from flavor_matcher.machine import Machine +from flavor_matcher.flavor_spec import FlavorSpec class Matcher: diff --git a/python/understack-flavor-matcher/poetry.lock b/python/understack-flavor-matcher/poetry.lock new file mode 100644 index 00000000..c83dc94a --- /dev/null +++ b/python/understack-flavor-matcher/poetry.lock @@ -0,0 +1,272 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[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 = "coverage" +version = "7.6.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, + {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, + {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[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 = "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 = "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 = "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\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[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 = "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 = "tomli" +version = "2.1.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "201b69c47c41a807d40fd9a40ffb8e628915106651d79b6ca741218d3b95c5c1" diff --git a/python/understack-flavor-matcher/pyproject.toml b/python/understack-flavor-matcher/pyproject.toml new file mode 100644 index 00000000..09a9a91e --- /dev/null +++ b/python/understack-flavor-matcher/pyproject.toml @@ -0,0 +1,30 @@ +[tool.poetry] +name = "understack-flavor-matcher" +version = "0.0.0" +description = "Baremetal node flavor classifier" +authors = ["Marek Skrobacki "] +license = "MIT" +readme = "README.md" +packages = [ + { include = "flavor_matcher" } +] + +[tool.poetry.dependencies] +python = "^3.10" +pyyaml = "^6.0" + +[tool.poetry.group.test.dependencies] +pytest = "^8.3.2" +pytest-github-actions-annotate-failures = "*" +pytest-cov = "^5.0.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra --cov=flavor_matcher" +testpaths = [ + "tests", +] diff --git a/python/understack-flavor-matcher/tests/__init__.py b/python/understack-flavor-matcher/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/ironic-understack/ironic_understack/tests/test_flavor_spec.py b/python/understack-flavor-matcher/tests/test_flavor_spec.py similarity index 62% rename from python/ironic-understack/ironic_understack/tests/test_flavor_spec.py rename to python/understack-flavor-matcher/tests/test_flavor_spec.py index 8e06a3bc..aedfa9db 100644 --- a/python/ironic-understack/ironic_understack/tests/test_flavor_spec.py +++ b/python/understack-flavor-matcher/tests/test_flavor_spec.py @@ -1,8 +1,8 @@ from unittest.mock import mock_open, patch import pytest -from ironic_understack.flavor_spec import FlavorSpec -from ironic_understack.machine import Machine +from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.machine import Machine @pytest.fixture @@ -97,15 +97,40 @@ def test_empty_directory(tmp_path): def machines(): return [ # 1024 GB, exact CPU, medium - Machine(memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=1000), + Machine( + memory_mb=102400, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=1000, + model="Dell XPS1319", + ), # 800 GB, non-matching CPU - Machine(memory_mb=800000, cpu="Intel Xeon E5-2676 v3", disk_gb=500), + Machine( + memory_mb=800000, + cpu="Intel Xeon E5-2676 v3", + disk_gb=500, + model="Dell XPS1319", + ), # 200 GB, exact CPU, medium - Machine(memory_mb=200000, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=1500), + Machine( + memory_mb=200000, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=150, + model="Dell XPS1319", + ), # 300 GB, non-matching CPU - Machine(memory_mb=300000, cpu="Intel Xeon E5-2676 v3", disk_gb=500), + Machine( + memory_mb=300000, + cpu="Intel Xeon E5-2676 v3", + disk_gb=500, + model="Dell XPS1319", + ), # 409 GB, exact CPU, large - Machine(memory_mb=409600, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=2000), + Machine( + memory_mb=409600, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=2000, + model="Dell XPS1319", + ), ] @@ -115,7 +140,7 @@ def flavors(): FlavorSpec( name="small", manufacturer="Dell", - model="Fake Machine", + model="Dell XPS1319", memory_gb=100, cpu_cores=13, cpu_model="AMD EPYC 9254 245-Core Processor", @@ -135,7 +160,7 @@ def flavors(): FlavorSpec( name="large", manufacturer="Dell", - model="Fake Machine", + model="Dell XPS1319", memory_gb=400, cpu_cores=27, cpu_model="AMD EPYC 9254 245-Core Processor", @@ -147,41 +172,73 @@ def flavors(): def test_exact_match(flavors): machine = Machine( - memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500 + memory_mb=102400, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=500, + model="Dell XPS1319", ) assert flavors[0].score_machine(machine) == 100 assert flavors[1].score_machine(machine) == 0 +def test_wrong_model_non_match(flavors): + machine = Machine( + memory_mb=102400, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=500, + model="Some other model", + ) + for flavor in flavors: + assert flavor.score_machine(machine) == 0 + + def test_memory_too_small(flavors): machine = Machine( - memory_mb=51200, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500 + memory_mb=51200, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=500, + model="Dell XPS1319", ) - assert all(flavor.score_machine(machine) for flavor in flavors) == 0 + for flavor in flavors: + assert flavor.score_machine(machine) == 0 def test_disk_too_small(flavors): machine = Machine( - memory_mb=204800, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=100 + memory_mb=204800, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=100, + model="Dell XPS1319", ) - assert all(flavor.score_machine(machine) for flavor in flavors) == 0 + assert all(flavor.score_machine(machine) == 0 for flavor in flavors) def test_cpu_model_not_matching(flavors): - machine = Machine(memory_mb=102400, cpu="Non-Existent CPU Model", disk_gb=500) - assert all(flavor.score_machine(machine) for flavor in flavors) == 0 + machine = Machine( + memory_mb=102400, + cpu="Non-Existent CPU Model", + disk_gb=500, + model="Dell XPS1319", + ) + assert all(flavor.score_machine(machine) == 0 for flavor in flavors) def test_memory_match_but_more_disk(flavors): machine = Machine( - memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=1000 + memory_mb=102400, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=1000, + model="Dell XPS1319", ) assert flavors[0].score_machine(machine) > 0 def test_disk_match_but_more_memory(flavors): machine = Machine( - memory_mb=204800, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500 + memory_mb=204800, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=500, + model="Dell XPS1319", ) assert flavors[0].score_machine(machine) > 0 @@ -193,25 +250,34 @@ def test_disk_match_but_more_memory(flavors): def test_memory_slightly_less(flavors): # Machine with slightly less memory than required by the smallest flavor machine = Machine( - memory_mb=102300, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500 + memory_mb=102300, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=500, + model="Dell XPS1319", ) # Should not match because memory is slightly less - assert all(flavor.score_machine(machine) for flavor in flavors) == 0 + assert all(flavor.score_machine(machine) == 0 for flavor in flavors) def test_disk_slightly_less(flavors): # Machine with slightly less disk space than required by the smallest flavor machine = Machine( - memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=499 + memory_mb=102400, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=499, + model="Dell XPS1319", ) # Should not match because disk space is slightly less - assert all(flavor.score_machine(machine) for flavor in flavors) == 0 + assert all(flavor.score_machine(machine) == 0 for flavor in flavors) def test_memory_exact_disk_slightly_more(flavors): # Machine with exact memory but slightly more disk space than required machine = Machine( - memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=501 + memory_mb=102400, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=501, + model="Dell XPS1319", ) assert flavors[0].score_machine(machine) > 0 assert flavors[1].score_machine(machine) == 0 @@ -221,7 +287,10 @@ def test_memory_exact_disk_slightly_more(flavors): def test_disk_exact_memory_slightly_more(flavors): # Machine with exact disk space but slightly more memory than required machine = Machine( - memory_mb=102500, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500 + memory_mb=102500, + cpu="AMD EPYC 9254 245-Core Processor", + disk_gb=500, + model="Dell XPS1319", ) assert flavors[0].score_machine(machine) > 0 assert flavors[1].score_machine(machine) == 0 @@ -231,14 +300,19 @@ def test_disk_exact_memory_slightly_more(flavors): def test_cpu_model_not_exact_but_memory_and_disk_match(flavors): # Machine with exact memory and disk space but CPU model is close but not exact machine = Machine( - memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor v2", disk_gb=500 + memory_mb=102400, + cpu="AMD EPYC 9254 245-Core Processor v2", + disk_gb=500, + model="Dell XPS1319", ) # Should not match because CPU model is not exactly listed - assert all(flavor.score_machine(machine) for flavor in flavors) == 0 + assert all(flavor.score_machine(machine) == 0 for flavor in flavors) def test_large_flavor_memory_slightly_less_disk_exact(flavors): # Machine with slightly less memory than required for the medium flavor, exact disk space - machine = Machine(memory_mb=204600, cpu="Intel 80386DX", disk_gb=1800) + machine = Machine( + memory_mb=204600, cpu="Intel 80386DX", disk_gb=1800, model="Dell XPS1319" + ) # Should not match because memory is slightly less than required - assert all(flavor.score_machine(machine) for flavor in flavors) == 0 + assert all(flavor.score_machine(machine) == 0 for flavor in flavors) diff --git a/python/ironic-understack/ironic_understack/tests/test_machine.py b/python/understack-flavor-matcher/tests/test_machine.py similarity index 51% rename from python/ironic-understack/ironic_understack/tests/test_machine.py rename to python/understack-flavor-matcher/tests/test_machine.py index 86d7937e..f9bba7af 100644 --- a/python/ironic-understack/ironic_understack/tests/test_machine.py +++ b/python/understack-flavor-matcher/tests/test_machine.py @@ -1,19 +1,19 @@ -from ironic_understack.machine import Machine +from flavor_matcher.machine import Machine def test_memory_gb_property(): # Test a machine with exactly 1 GB of memory - machine = Machine(memory_mb=1024, cpu="x86", disk_gb=50) + machine = Machine(memory_mb=1024, cpu="x86", disk_gb=50, model="SomeModel") assert machine.memory_gb == 1 # Test a machine with 2 GB of memory - machine = Machine(memory_mb=2048, cpu="x86", disk_gb=50) + machine = Machine(memory_mb=2048, cpu="x86", disk_gb=50, model="SomeModel") assert machine.memory_gb == 2 # Test a machine with non-exact GB memory (should floor the value) - machine = Machine(memory_mb=3072, cpu="x86", disk_gb=50) + machine = Machine(memory_mb=3072, cpu="x86", disk_gb=50, model="SomeModel") assert machine.memory_gb == 3 # Test a machine with less than 1 GB of memory - machine = Machine(memory_mb=512, cpu="x86", disk_gb=50) + machine = Machine(memory_mb=512, cpu="x86", disk_gb=50, model="SomeModel") assert machine.memory_gb == 0 diff --git a/python/ironic-understack/ironic_understack/tests/test_matcher.py b/python/understack-flavor-matcher/tests/test_matcher.py similarity index 82% rename from python/ironic-understack/ironic_understack/tests/test_matcher.py rename to python/understack-flavor-matcher/tests/test_matcher.py index 2e4455fc..45200884 100644 --- a/python/ironic-understack/ironic_understack/tests/test_matcher.py +++ b/python/understack-flavor-matcher/tests/test_matcher.py @@ -1,7 +1,7 @@ import pytest -from ironic_understack.flavor_spec import FlavorSpec -from ironic_understack.machine import Machine -from ironic_understack.matcher import Matcher +from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.machine import Machine +from flavor_matcher.matcher import Matcher @pytest.fixture @@ -47,7 +47,7 @@ def matcher(sample_flavors): @pytest.fixture def machine(): - return Machine(memory_mb=8192, cpu="x86", disk_gb=50) + return Machine(memory_mb=8192, cpu="x86", disk_gb=50, model="Fake Machine") def test_match(matcher, machine): @@ -60,7 +60,7 @@ def test_match(matcher, machine): def test_match_no_flavor(matcher): # A machine that does not meet any flavor specs - machine = Machine(memory_mb=2048, cpu="x86", disk_gb=10) + machine = Machine(memory_mb=2048, cpu="x86", disk_gb=10, model="SomeModel") results = matcher.match(machine) assert len(results) == 0 @@ -74,6 +74,6 @@ def test_pick_best_flavor2(matcher, machine): def test_pick_best_flavor_no_match(matcher): # A machine that does not meet any flavor specs - machine = Machine(memory_mb=1024, cpu="ARM", disk_gb=10) + machine = Machine(memory_mb=1024, cpu="ARM", disk_gb=10, model="SomeModel") best_flavor = matcher.pick_best_flavor(machine) assert best_flavor is None diff --git a/python/understack-workflows/poetry.lock b/python/understack-workflows/poetry.lock index de0a22b0..4e084ee5 100644 --- a/python/understack-workflows/poetry.lock +++ b/python/understack-workflows/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -1533,6 +1533,22 @@ files = [ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] +[[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 = "../understack-flavor-matcher" + [[package]] name = "urllib3" version = "2.2.3" @@ -1659,4 +1675,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "~3.11.0" -content-hash = "1bd67ca37a025d77b90e48da9c3690fb84b830fe3a68973aa7bd6360c61ade2c" +content-hash = "332e385ac1186fe6eaea24014f23a02464f311295d587eeb2ed56426b8a0d111" diff --git a/python/understack-workflows/pyproject.toml b/python/understack-workflows/pyproject.toml index e77837d7..6483d008 100644 --- a/python/understack-workflows/pyproject.toml +++ b/python/understack-workflows/pyproject.toml @@ -29,6 +29,7 @@ pynautobot = "^2.2.1" python-ironicclient = "^5" sushy = "^5.3.0" kubernetes = "29.0.0" +understack-flavor-matcher = {path = "../understack-flavor-matcher"} [tool.peotry.group.test] optional = true diff --git a/python/understack-workflows/tests/json_samples/bmc_chassis_info/bmc_disk.json b/python/understack-workflows/tests/json_samples/bmc_chassis_info/bmc_disk.json new file mode 100644 index 00000000..797c757d --- /dev/null +++ b/python/understack-workflows/tests/json_samples/bmc_chassis_info/bmc_disk.json @@ -0,0 +1,148 @@ +{ + "@odata.context": "/redfish/v1/$metadata#Drive.Drive", + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.SL.1-1/Drives/Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.1-1", + "@odata.type": "#Drive.v1_17_0.Drive", + "Actions": { + "#Drive.SecureErase": { + "@Redfish.OperationApplyTimeSupport": { + "@odata.type": "#Settings.v1_3_5.OperationApplyTimeSupport", + "SupportedValues": [ + "Immediate", + "OnReset" + ] + }, + "target": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.SL.1-1/Drives/Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.1-1/Actions/Drive.SecureErase" + } + }, + "Assembly": { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Assembly" + }, + "BlockSizeBytes": 512, + "CapableSpeedGbs": 6, + "CapacityBytes": 479559942144, + "Description": "Disk 1 in Backplane 1 of RAID Controller in SL 1", + "EncryptionAbility": "None", + "EncryptionStatus": "Unencrypted", + "FailurePredicted": false, + "HotspareType": "None", + "Id": "Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.1-1", + "Identifiers": [ + { + "DurableName": "500056b3de2d55c1", + "DurableNameFormat": "NAA" + } + ], + "Identifiers@odata.count": 1, + "Links": { + "Chassis": { + "@odata.id": "/redfish/v1/Chassis/Enclosure.Internal.0-1:RAID.SL.1-1" + }, + "Oem": { + "Dell": { + "@odata.type": "#DellOem.v1_3_0.DellOemLinks", + "CPUAffinity": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors/CPU.Socket.1" + } + ], + "CPUAffinity@odata.count": 1 + } + }, + "PCIeFunctions": [], + "PCIeFunctions@odata.count": 0, + "Storage": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.SL.1-1" + }, + "Volumes": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.SL.1-1/Volumes/Disk.Virtual.238:RAID.SL.1-1" + } + ], + "Volumes@odata.count": 1 + }, + "Location": [], + "LocationIndicatorActive": null, + "Manufacturer": "MICRON", + "MediaType": "SSD", + "Model": "MTFDDAK480TDS", + "Name": "Solid State Disk 0:1:1", + "NegotiatedSpeedGbs": 6, + "Oem": { + "Dell": { + "@odata.type": "#DellDrive.v1_1_0.DellDrive", + "DellDriveSMARTAttributes": { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.SL.1-1/Drives/Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.1-1/Oem/Dell/DellDriveSMARTAttributes" + }, + "DellPhysicalDisk": { + "@odata.context": "/redfish/v1/$metadata#DellPhysicalDisk.DellPhysicalDisk", + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.SL.1-1/Drives/Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.1-1/Oem/Dell/DellDrives/Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.1-1", + "@odata.type": "#DellPhysicalDisk.v1_7_0.DellPhysicalDisk", + "AvailableSparePercent": null, + "Certified": "Yes", + "Connector": 0, + "CryptographicEraseCapable": "Capable", + "Description": "An instance of DellPhysicalDisk will have Physical Disk specific data.", + "DeviceProtocol": null, + "DeviceSidebandProtocol": null, + "DriveFormFactor": "2.5Inch", + "EncryptionProtocol": "None", + "ErrorDescription": null, + "ErrorRecoverable": "NotApplicable", + "ForeignKeyIdentifier": null, + "FreeSizeInBytes": 50063212544, + "Id": "Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.1-1", + "LastSystemInventoryTime": "2024-11-11T15:30:56+00:00", + "LastUpdateTime": "2024-11-11T15:30:57+00:00", + "ManufacturingDay": 0, + "ManufacturingWeek": 0, + "ManufacturingYear": 0, + "Name": "DellPhysicalDisk", + "NonRAIDDiskCachePolicy": "Unknown", + "OperationName": "None", + "OperationPercentCompletePercent": 0, + "PCIeCapableLinkWidth": "None", + "PCIeNegotiatedLinkWidth": "None", + "PPID": "MY-0KCT7J-MCP00-26D-0A4U-A00", + "PowerStatus": "On", + "PredictiveFailureState": "SmartAlertAbsent", + "ProductID": "MTFDDAK480TDS", + "RAIDType": "Unknown", + "RaidStatus": "Online", + "SASAddress": "500056B3DE2D55C1", + "Slot": 1, + "SystemEraseCapability": "CryptographicErasePD", + "T10PICapability": "NotSupported", + "UsedSizeInBytes": 429496729600, + "WWN": "500056B3DE2D55C1" + }, + "FIPS140ComplianceDescriptor": { + "ComplianceDescriptorModuleName": null, + "ComplianceDescriptorType": null, + "ComplianceDescriptorVersion": null, + "ComplianceHardwareVersion": null, + "ComplianceOverallSecurityLevel": null, + "ComplianceRelatedStandard": null + } + } + }, + "Operations": [], + "Operations@odata.count": 0, + "PartNumber": "MY-0KCT7J-MCP00-26D-0A4U-A00", + "PhysicalLocation": { + "PartLocation": { + "LocationOrdinalValue": 1, + "LocationType": "Slot" + } + }, + "PredictedMediaLifeLeftPercent": 100, + "Protocol": "SATA", + "Revision": "D3DJ004", + "RotationSpeedRPM": null, + "SerialNumber": "222338A2192A", + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "WriteCacheEnabled": false +} diff --git a/python/understack-workflows/tests/test_bmc_chassis_info.py b/python/understack-workflows/tests/test_bmc_chassis_info.py index 04269934..c198ba9f 100644 --- a/python/understack-workflows/tests/test_bmc_chassis_info.py +++ b/python/understack-workflows/tests/test_bmc_chassis_info.py @@ -131,4 +131,6 @@ def test_chassis_info_R7615(): remote_switch_port_name="Ethernet1/7", ), ], + memory_gib=96, + cpu="AMD EPYC 9124 16-Core Processor", ) diff --git a/python/understack-workflows/tests/test_bmc_disk.py b/python/understack-workflows/tests/test_bmc_disk.py new file mode 100644 index 00000000..d66ec1bf --- /dev/null +++ b/python/understack-workflows/tests/test_bmc_disk.py @@ -0,0 +1,51 @@ +import json + +import pytest +from pytest_mock import MockerFixture + +from understack_workflows.bmc_disk import Disk + +TESTED_DISK_PATH = "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.SL.1-1/Drives/Disk.Bay.1:Enclosure.Internal.0-1:RAID.SL.1-1" # noqa: E501 + + +@pytest.fixture +def mock_disk_data(): + with open("tests/json_samples/bmc_chassis_info/bmc_disk.json") as f: + return json.load(f) + + +@pytest.fixture +def mock_bmc(mock_disk_data, mocker: MockerFixture): + mock_bmc = mocker.Mock() + mock_bmc.redfish_request.return_value = mock_disk_data + return mock_bmc + + +def test_disk_from_path(mock_bmc): + disk = Disk.from_path(mock_bmc, TESTED_DISK_PATH) + assert isinstance(disk, Disk) + + +def test_disk_repr(mock_bmc): + disk = Disk.from_path(mock_bmc, TESTED_DISK_PATH) + assert repr(disk) == disk.name + + +def test_disk_attributes(mock_bmc): + disk = Disk.from_path(mock_bmc, TESTED_DISK_PATH) + assert disk.media_type == "SSD" + assert disk.model == "MTFDDAK480TDS" + assert disk.name == "Solid State Disk 0:1:1" + assert disk.health == "OK" + assert disk.capacity_bytes == 479559942144 + + +def test_disk_gb_conversion(): + disk = Disk( + media_type="SSD", + model="Irrelevant", + name="TestDisk", + health="OK", + capacity_bytes=479559942144, + ) + assert disk.capacity_gb == 480 diff --git a/python/understack-workflows/tests/test_nautobot_device.py b/python/understack-workflows/tests/test_nautobot_device.py index ab12ac2c..5c4d39aa 100644 --- a/python/understack-workflows/tests/test_nautobot_device.py +++ b/python/understack-workflows/tests/test_nautobot_device.py @@ -119,6 +119,8 @@ def test_find_or_create(dell_nautobot_device): bios_version="1.6.10", bmc_ip_address="1.2.3.4", power_on=True, + memory_gib=96, + cpu="AMD EPYC 666 444-Core Processor", interfaces=[ InterfaceInfo( name="iDRAC", diff --git a/python/understack-workflows/understack_workflows/bmc_chassis_info.py b/python/understack-workflows/understack_workflows/bmc_chassis_info.py index c34e6102..ce425abd 100644 --- a/python/understack-workflows/understack_workflows/bmc_chassis_info.py +++ b/python/understack-workflows/understack_workflows/bmc_chassis_info.py @@ -32,6 +32,8 @@ class ChassisInfo: bios_version: str power_on: bool interfaces: list[InterfaceInfo] + memory_gib: int + cpu: str @property def bmc_interface(self) -> InterfaceInfo: @@ -79,7 +81,9 @@ def chassis_info(bmc: Bmc) -> ChassisInfo: bios_version=chassis_data["BiosVersion"], power_on=(chassis_data["PowerState"] == "On"), bmc_ip_address=bmc.ip_address, + memory_gib=chassis_data.get("MemorySummary", {}).get("TotalSystemMemoryGiB", 0), interfaces=interfaces, + cpu=chassis_data.get("ProcessorSummary", {}).get("Model", ""), ) diff --git a/python/understack-workflows/understack_workflows/bmc_disk.py b/python/understack-workflows/understack_workflows/bmc_disk.py new file mode 100644 index 00000000..f65ad488 --- /dev/null +++ b/python/understack-workflows/understack_workflows/bmc_disk.py @@ -0,0 +1,57 @@ +import math +from dataclasses import dataclass + +from understack_workflows.bmc import Bmc +from understack_workflows.bmc import RedfishError +from understack_workflows.helpers import setup_logger + +logger = setup_logger(__name__) + + +REDFISH_DISKS_PATH = "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.SL.1-1" + + +@dataclass(frozen=True) +class Disk: + media_type: str + model: str + name: str + health: str + capacity_bytes: int + + def __repr__(self) -> str: + """Returns disk name.""" + return self.name + + @property + def capacity_gb(self) -> int: + return math.ceil(self.capacity_bytes / 10**9) + + @staticmethod + def from_path(bmc: Bmc, path: str): + disk_data = bmc.redfish_request(path) + + return Disk( + media_type=disk_data["MediaType"], + model=disk_data["Model"], + name=disk_data["Name"], + health=disk_data.get("Status", {}).get("Health", "Unknown"), + capacity_bytes=disk_data["CapacityBytes"], + ) + + +def physical_disks(bmc: Bmc) -> list[Disk]: + """Retrieve list of physical physical_disks.""" + try: + disks = bmc.redfish_request(REDFISH_DISKS_PATH)["Drives"] + disk_list = [Disk.from_path(bmc, path=disk["@odata.id"]) for disk in disks] + logger.debug("Retrieved %d disks.", len(disk_list)) + return disk_list + except RedfishError as err: + logger.error("Failed retrieving disk info: %s", err) + raise (err) from err + + +def smallest_disk_size(bmc: Bmc) -> int: + """Returns size of a smallest disk in a given machine (in Gigabytes).""" + return min(physical_disks(bmc), key=lambda x: x.capacity_gb).capacity_gb diff --git a/python/understack-workflows/understack_workflows/flavor_detect.py b/python/understack-workflows/understack_workflows/flavor_detect.py new file mode 100644 index 00000000..27865cf1 --- /dev/null +++ b/python/understack-workflows/understack_workflows/flavor_detect.py @@ -0,0 +1,35 @@ +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.") + + +def guess_machine_flavor(device_info: ChassisInfo, bmc: Bmc) -> str: + memory_mb = (device_info.memory_gib * 1024**3) // 10**6 + + machine = Machine( + memory_mb=memory_mb, + cpu=device_info.cpu, + disk_gb=bmc_disk.smallest_disk_size(bmc), + model=device_info.model_number, + ) + + flavor_name = Matcher(FLAVORS).pick_best_flavor(machine) + if not flavor_name: + raise Exception( + f"Machine: {machine} could not be classified into any flavor {FLAVORS=}" + ) + logger.info(f"Device has been classified as flavor: {flavor_name.stripped_name}") + + return flavor_name.stripped_name diff --git a/python/understack-workflows/understack_workflows/ironic_node.py b/python/understack-workflows/understack_workflows/ironic_node.py index 47e6c852..c110ec1d 100644 --- a/python/understack-workflows/understack_workflows/ironic_node.py +++ b/python/understack-workflows/understack_workflows/ironic_node.py @@ -1,4 +1,5 @@ import ironicclient.common.apiclient.exceptions +from flavor_matcher.flavor_spec import dataclass from ironicclient.common.utils import args_array_to_patch from understack_workflows.bmc import Bmc @@ -11,31 +12,37 @@ logger = setup_logger(__name__) -def create_or_update( - node_uuid: str, device_hostname: str, device_manufacturer: str, bmc: Bmc, logger -): +@dataclass(frozen=True) +class NodeMetadata: + uuid: str + hostname: str + manufacturer: str + resource_class: str + + @property + def driver(self): + if self.manufacturer.startswith("Dell"): + return "idrac" + else: + return "redfish" + + +def create_or_update(node_meta: NodeMetadata, bmc: Bmc): """Note interfaces/ports are not synced here, that happens elsewhere.""" client = IronicClient() - if device_manufacturer.startswith("Dell"): - driver = "idrac" - else: - driver = "redfish" - - logger.debug(f"Ensuring node with UUID {node_uuid} exists in Ironic") + logger.debug(f"Ensuring node with UUID {node_meta.uuid} exists in Ironic") try: - ironic_node = client.get_node(node_uuid) + ironic_node = client.get_node(node_meta.uuid) except ironicclient.common.apiclient.exceptions.NotFound: - logger.debug(f"Node: {node_uuid} not found in Ironic, creating.") - ironic_node = create_ironic_node( - client, node_uuid, device_hostname, driver, bmc - ) + logger.debug(f"Node: {node_meta.uuid} not found in Ironic, creating.") + ironic_node = create_ironic_node(client, node_meta, bmc) return ironic_node.provision_state # type: ignore if ironic_node.provision_state in STATES_ALLOWING_UPDATES: - update_ironic_node(client, node_uuid, device_hostname, driver, bmc) + update_ironic_node(client, node_meta, bmc) else: logger.info( - f"Device {node_uuid} in Ironic is in a " + f"Device {node_meta.uuid} in Ironic is in a " f"{ironic_node.provision_state} provision_state, " f"so no updates are allowed." ) @@ -43,40 +50,40 @@ def create_or_update( return ironic_node.provision_state -def update_ironic_node(client, node_uuid, device_hostname, driver, bmc): +def update_ironic_node(client, node_meta, bmc): updates = [ - f"name={device_hostname}", - f"driver={driver}", + f"name={node_meta.hostname}", + f"driver={node_meta.driver}", f"driver_info/redfish_address={bmc.url()}", "driver_info/redfish_verify_ca=false", f"driver_info/redfish_username={bmc.username}", f"driver_info/redfish_password={bmc.password}", + f"resource_class={node_meta.resource_class}", ] patches = args_array_to_patch("add", updates) - logger.info(f"Updating Ironic node {node_uuid} {patches=}") + logger.info(f"Updating Ironic node {node_meta.uuid} {patches=}") - response = client.update_node(node_uuid, patches) - logger.info(f"Ironic node {node_uuid} Updated: {response=}") + response = client.update_node(node_meta.uuid, patches) + logger.info(f"Ironic node {node_meta.uuid} Updated: {response=}") def create_ironic_node( client: IronicClient, - node_uuid: str, - device_hostname: str, - driver: str, + node_meta: NodeMetadata, bmc: Bmc, ) -> IronicNodeConfiguration: return client.create_node( { - "uuid": node_uuid, - "name": device_hostname, - "driver": driver, + "uuid": node_meta.uuid, + "name": node_meta.hostname, + "driver": node_meta.driver, "driver_info": { "redfish_address": bmc.url(), "redfish_verify_ca": False, "redfish_username": bmc.username, "redfish_password": bmc.password, }, + "resource_class": node_meta.resource_class, } ) diff --git a/python/understack-workflows/understack_workflows/main/enroll_server.py b/python/understack-workflows/understack_workflows/main/enroll_server.py index c9220384..4450cd09 100644 --- a/python/understack-workflows/understack_workflows/main/enroll_server.py +++ b/python/understack-workflows/understack_workflows/main/enroll_server.py @@ -16,6 +16,7 @@ from understack_workflows.bmc_network_config import bmc_set_permanent_ip_addr from understack_workflows.bmc_settings import update_dell_drac_settings from understack_workflows.discover import discover_chassis_info +from understack_workflows.flavor_detect import guess_machine_flavor from understack_workflows.helpers import credential from understack_workflows.helpers import parser_nautobot_args from understack_workflows.helpers import setup_logger @@ -64,6 +65,7 @@ def main(): - from BMC, discover basic hardware info: - manufacturer, model number, serial number + - CPU model(s), RAM size and local storage - list ethernet interfaces with: - name like BMC or SLOT.NIC.1-1 - MAC address @@ -85,10 +87,11 @@ def main(): - create BMC IP address assignment for BMC interface - convert our type "dhcp" IP Address to type "host" and associate it with the interface - - Find or create this baremetal node in Ironic + - Determine flavor of the server based on the information collected from BMC + - Find or create this baremetal node in Ironic - create ports with MACs (omit BMC port) and set one to PXE - TODO advance to available state - - TODO set flavor? what else? + - set flavor """ args = argument_parser().parse_args() @@ -134,8 +137,17 @@ def enroll_server(bmc: Bmc, nautobot, old_password: str | None) -> NautobotDevic # any pending BIOS jobs, so do BIOS settings after the DRAC settings. update_dell_bios_settings(bmc, pxe_interface=pxe_interface) + flavor = guess_machine_flavor(device_info, bmc) + resource_class = f"baremetal.{flavor}" + _ironic_provision_state = ironic_node.create_or_update( - nb_device.id, nb_device.name, device_info.manufacturer, bmc, logger + ironic_node.NodeMetadata( + uuid=nb_device.id, + hostname=nb_device.name, + manufacturer=device_info.manufacturer, + resource_class=resource_class, + ), + bmc, ) logger.info(f"{nb_device.id} {_ironic_provision_state=}") diff --git a/workflows/argo-events/workflowtemplates/enroll-server.yaml b/workflows/argo-events/workflowtemplates/enroll-server.yaml index 4bd8d5f6..73ee9f16 100644 --- a/workflows/argo-events/workflowtemplates/enroll-server.yaml +++ b/workflows/argo-events/workflowtemplates/enroll-server.yaml @@ -71,6 +71,10 @@ spec: - mountPath: /etc/bmc_master/ name: bmc-master readOnly: true + - mountPath: /etc/understack_flavors/ + name: understack-flavors + subPath: current/flavors/ + readOnly: true env: - name: WF_NS value: "{{workflow.namespace}}" @@ -88,6 +92,10 @@ spec: - name: openstack-svc-acct secret: secretName: openstack-svc-acct + - name: understack-flavors + persistentVolumeClaim: + claimName: understack-flavors + readOnly: true - name: openstack-wait-cmd inputs: parameters: