diff --git a/requirements.txt b/requirements.txt index 6a149980..ade9a108 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ cosl +distro ops >= 2.2.0 jinja2 redfish diff --git a/src/checksum.py b/src/checksum.py new file mode 100644 index 00000000..6459d349 --- /dev/null +++ b/src/checksum.py @@ -0,0 +1,235 @@ +# Copyright 2023 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# For further info, check https://github.com/canonical/charmcraft +"""Checksum definition, check functions and related utils.""" +import hashlib +import logging +import typing as t +from dataclasses import dataclass, field +from pathlib import Path + +from os_platform import Architecture, UbuntuSeries, get_os_platform + +logger = logging.getLogger(__name__) + + +class ResourceChecksumError(Exception): + """Raise if checksum does not match.""" + + +@dataclass +class ToolVersionInfo: + """Tool version information for checksum comparison.""" + + version: str + supported_architectures: t.List[Architecture] + sha256_checksum: str + link: t.Optional[str] = None + desc: str = "" + support_all_series: bool = False + supported_series: t.List[UbuntuSeries] = field(default_factory=lambda: []) + + +STORCLI_VERSION_INFOS: t.List[ToolVersionInfo] = [ + ToolVersionInfo( + version="007.2705.0000.0000", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/1232743397", + desc="MR 7.27", + sha256_checksum="45ff0d3c7fc8b77f64de7de7b3698307971546a6be00982934a19ee44f5d91bb", + ), + ToolVersionInfo( + version="007.2612.0000.0000", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/1232743291", + desc="MR 7.26", + sha256_checksum="5ab2c1b608934626817828ced85e4aeaee7dc97fbd6e3f4fed00b13a95a06e14", + ), + ToolVersionInfo( + version="007.2508.0000.0000", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/1232743203", + desc="MR 7.25", + sha256_checksum="17c3f5292de6491f1388c9305ba65836730614b6defe17039b427fced2f75e0b", + ), + ToolVersionInfo( + version="007.2408.0000.0000", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/1232743081", + desc="MR 7.24", + sha256_checksum="8ecf2d46e253e243c5d169adcd84f2701e52e3815913694f074e80af5a98cbab", + ), + ToolVersionInfo( + version="007.2310.0000.0000", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/Unified_storcli_all_os_7.2309.0000.0000.zip", + desc="MR 7.23", + sha256_checksum="94cbef2ec2ca58700a49e646a7bded3a49ddab4646a9d5d178bc4ccb2996cb73", + ), +] + +PERCCLI_VERSION_INFOS: t.List[ToolVersionInfo] = [ + ToolVersionInfo( + version="007.2313.0000.0000", + supported_series=[UbuntuSeries.JAMMY, UbuntuSeries.FOCAL], + supported_architectures=[Architecture.X86_64], + link="https://www.dell.com/support/home/zh-tw/drivers/driversdetails?driverid=tdghn", + desc="A14", + sha256_checksum="043f7d6235cf125072e95d748cb98f5db42965f218de30f6f72f5503a626e4e3", + ), + ToolVersionInfo( + version="007.1623.0000.0000", + supported_series=[UbuntuSeries.FOCAL, UbuntuSeries.BIONIC, UbuntuSeries.XENIAL], + supported_architectures=[Architecture.X86_64], + link="https://www.dell.com/support/home/zh-tw/drivers/driversdetails?driverid=j91yg", + desc="A11", + sha256_checksum="e46d955241c932023caf63862cd9dacb2b723b7f944340efb0e5afb6a2681e9d", + ), + ToolVersionInfo( + version="007.1420.0000.0000", + supported_series=[ + UbuntuSeries.FOCAL, + UbuntuSeries.BIONIC, + UbuntuSeries.XENIAL, + ], + supported_architectures=[Architecture.X86_64], + link="https://www.dell.com/support/home/zh-tw/drivers/driversdetails?driverid=n65f1", + desc="A10", + sha256_checksum="8a405000ea592e1d2999313ade07609a7abcfa24d1b9b35bb242bb6aff75a6be", + ), + ToolVersionInfo( + version="007.1327.0000.0000", + supported_series=[UbuntuSeries.FOCAL, UbuntuSeries.BIONIC, UbuntuSeries.XENIAL], + supported_architectures=[Architecture.X86_64], + link="https://www.dell.com/support/home/zh-tw/drivers/driversdetails?driverid=d6ywp", + desc="A09", + sha256_checksum="53c8ee43808779f8263c25b3cb975d816d207659684f3c7de1df4bbd2447ead4", + ), +] + +SAS2IRCU_VERSION_INFOS: t.List[ToolVersionInfo] = [ + ToolVersionInfo( + version="20.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/12351735", + desc="P20, linux_x86", + sha256_checksum="37467826d0b22aad47287efe70bb34e47f475d70e9b1b64cbd63f57607701e73", + ), + ToolVersionInfo( + version="19.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/12351734", + desc="P19, linux_x86", + sha256_checksum="4baaec21865973c0a6da617e37850cc27512715e6ab22df18b1f67d068e5095c", + ), + ToolVersionInfo( + version="18.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/12351733", + desc="P18, linux_x86", + sha256_checksum="b6ed72275066e80ebe9813cd15f1d019eba9daddbd9dfd8ad426da78801f15d8", + ), + ToolVersionInfo( + version="17.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/12351732", + desc="P17, linux_x86", + sha256_checksum="07e9236b99bbe4a3ae6148f8668e1ce0331d83c2fcb0c4841d000454c6200c1f", + ), + ToolVersionInfo( + version="16.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/12351731", + desc="P16, linux_x86", + sha256_checksum="a8653117067847042bb83e7b51f02d8f2db94e91ce95842efea0dffcb655c966", + ), +] + +SAS3IRCU_VERSION_INFOS: t.List[ToolVersionInfo] = [ + ToolVersionInfo( + version="16.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/SAS3IRCU_P15.zip", + desc="P15, linux_x86", + sha256_checksum="f150eb37bb332668949a3eccf9636e0e03f874aecd17a39d586082c6be1386bd", + ), + ToolVersionInfo( + version="15.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/SAS3IRCU_P14.zip", + desc="P14, linux_x86", + sha256_checksum="5825b90964d1950551e5ed5100ddf1141360b0acf8dd3c6ddb4fe5874d6bbabb", + ), + ToolVersionInfo( + version="14.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/SAS3IRCU_P13.zip", + desc="P13, linux_x86", + sha256_checksum="1ce45e57efa0a2d8c5c3d61a0950ab7e950a317aff3155fc1831099e19274c32", + ), + ToolVersionInfo( + version="13.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/SAS3IRCU_P12.zip", + desc="P12, linux_x86", + sha256_checksum="cb4010a3d2bc4f9b75859a0c599d889f9fb847e4dfc88abf74082a13b9490a59", + ), + ToolVersionInfo( + version="12.00.00.00", + support_all_series=True, + supported_architectures=[Architecture.X86_64], + link="https://docs.broadcom.com/docs/SAS3IRCU_P11.zip", + desc="P11, linux_x86", + sha256_checksum="458d51b030468901fc8a207088070e6ce82db34b181d9190c8f849605f1b9b6d", + ), +] + + +def validate_checksum(support_version_infos: t.List[ToolVersionInfo], path: Path) -> bool: + """Validate checksum of resource file by checking with supported versions. + + Returns True if resource is supported by the charm, architecture, and + checksum validation is successful. + """ + os_platform = get_os_platform() + + supported_checksums = [] + for info in support_version_infos: + if os_platform.machine in info.supported_architectures and ( + info.support_all_series or os_platform.series in info.supported_series + ): + supported_checksums.append(info.sha256_checksum) + + with open(path, "rb") as f: + sha256_hash = hashlib.sha256(f.read()).hexdigest() + + if sha256_hash in supported_checksums: + return True + logger.warning("Checksum validation fail, path: %s hash: %s", path, sha256_hash) + return False diff --git a/src/config.py b/src/config.py index b6a4feab..7403b59e 100644 --- a/src/config.py +++ b/src/config.py @@ -57,5 +57,5 @@ class HWTool(str, Enum): TOOLS_DIR = Path("/usr/sbin") -# SNAP envionment +# SNAP environment SNAP_COMMON = Path(f"/var/snap/{EXPORTER_NAME}/common") diff --git a/src/hw_tools.py b/src/hw_tools.py index f54618f1..845e0801 100644 --- a/src/hw_tools.py +++ b/src/hw_tools.py @@ -16,6 +16,14 @@ from redfish import redfish_client from redfish.rest.v1 import InvalidCredentialsError, RetriesExhaustedError, SessionCreationError +from checksum import ( + PERCCLI_VERSION_INFOS, + SAS2IRCU_VERSION_INFOS, + SAS3IRCU_VERSION_INFOS, + STORCLI_VERSION_INFOS, + ResourceChecksumError, + validate_checksum, +) from config import SNAP_COMMON, TOOLS_DIR, TPR_RESOURCES, HWTool, StorageVendor, SystemVendor from hardware import SUPPORTED_STORAGES, get_bmc_address, lshw from keys import HP_KEYS @@ -144,6 +152,8 @@ def install(self, path: Path) -> None: """Install storcli.""" if not check_file_size(path): raise ResourceFileSizeZeroError(tool=self._name, path=path) + if not validate_checksum(STORCLI_VERSION_INFOS, path): + raise ResourceChecksumError install_deb(self.name, path) symlink(src=self.origin_path, dst=self.symlink_bin) @@ -165,6 +175,8 @@ def install(self, path: Path) -> None: """Install perccli.""" if not check_file_size(path): raise ResourceFileSizeZeroError(tool=self._name, path=path) + if not validate_checksum(PERCCLI_VERSION_INFOS, path): + raise ResourceChecksumError install_deb(self.name, path) symlink(src=self.origin_path, dst=self.symlink_bin) @@ -185,6 +197,8 @@ def install(self, path: Path) -> None: """Install sas2ircu.""" if not check_file_size(path): raise ResourceFileSizeZeroError(tool=self._name, path=path) + if not validate_checksum(SAS2IRCU_VERSION_INFOS, path): + raise ResourceChecksumError make_executable(path) symlink(src=path, dst=self.symlink_bin) @@ -200,6 +214,15 @@ class SAS3IRCUStrategy(SAS2IRCUStrategy): _name = HWTool.SAS3IRCU symlink_bin = TOOLS_DIR / HWTool.SAS3IRCU.value + def install(self, path: Path) -> None: + """Install sas3ircu.""" + if not check_file_size(path): + raise ResourceFileSizeZeroError(tool=self._name, path=path) + if not validate_checksum(SAS3IRCU_VERSION_INFOS, path): + raise ResourceChecksumError + make_executable(path) + symlink(src=path, dst=self.symlink_bin) + class SSACLIStrategy(APTStrategyABC): """Strategy for install ssacli.""" @@ -444,6 +467,7 @@ def install(self, resources: Resources) -> t.Tuple[bool, str]: ResourceFileSizeZeroError, OSError, apt.PackageError, + ResourceChecksumError, ) as e: logger.warning("Strategy %s install fail: %s", strategy, e) fail_strategies.append(strategy.name) diff --git a/src/os_platform.py b/src/os_platform.py new file mode 100644 index 00000000..d0063d8c --- /dev/null +++ b/src/os_platform.py @@ -0,0 +1,63 @@ +# Copyright 2023 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# For further info, check https://github.com/canonical/charmcraft +"""Platform-related Charmcraft utilities.""" +import dataclasses +import platform +import typing as t +from enum import Enum + +import distro + + +class UbuntuSeries(str, Enum): + """Ubuntu Series.""" + + NOBLE = "24.04" + JAMMY = "22.04" + FOCAL = "20.04" + BIONIC = "18.04" + XENIAL = "16.04" + + +class Architecture(str, Enum): + """Host architecture.""" + + X86_64 = "x86_64" + + +@dataclasses.dataclass +class OSPlatform: + """Description of an operating system platform.""" + + release: str + machine: str + + @property + def series(self) -> t.Optional[UbuntuSeries]: + """Return series base on system and release.""" + for series in UbuntuSeries: + if series == self.release: + return series + return None + + +def get_os_platform() -> OSPlatform: + """Determine a system/release combo for an OS using /etc/os-release if available.""" + machine = platform.machine() + info = distro.info() + release = info.get("version", "") + + return OSPlatform(release=release, machine=machine) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index e1c87478..d4d86b56 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -128,7 +128,7 @@ def test_10_config_changed(self, mock_exporter): @mock.patch("charm.Exporter", return_value=mock.MagicMock()) def test_11_config_changed_before_install_complete(self, mock_exporter): - """Test: config change event is defered if charm not installed.""" + """Test: config change event is deferred if charm not installed.""" self.harness.begin() self.harness.charm._stored.installed = False diff --git a/tests/unit/test_checksum.py b/tests/unit/test_checksum.py new file mode 100644 index 00000000..bd5ae7d6 --- /dev/null +++ b/tests/unit/test_checksum.py @@ -0,0 +1,80 @@ +from unittest import mock + +from checksum import PERCCLI_VERSION_INFOS, STORCLI_VERSION_INFOS, validate_checksum +from os_platform import OSPlatform + + +class TestCheckFileSum: + @mock.patch( + "checksum.get_os_platform", + return_value=OSPlatform( + release="20.04", + machine="x86_64", + ), + ) + @mock.patch("checksum.hashlib.sha256") + def test_validate_checksum(self, mock_sha256, mock_get_os_platform, tmp_path): + mock_sha256.return_value = mock.Mock() + mock_sha256.return_value.hexdigest.return_value = ( + "53c8ee43808779f8263c25b3cb975d816d207659684f3c7de1df4bbd2447ead4" + ) + + target = tmp_path / "perccli" + target.write_text("fake file") + + ok = validate_checksum(PERCCLI_VERSION_INFOS, target) + assert ok + + @mock.patch( + "checksum.get_os_platform", + return_value=OSPlatform( + release="14.04", # old version which should be cover by field support_all_series + machine="x86_64", + ), + ) + @mock.patch("checksum.hashlib.sha256") + def test_validate_checksum_support_all_series( + self, + mock_sha256, + mock_get_os_platform, + tmp_path, + ): + mock_sha256.return_value = mock.Mock() + mock_sha256.return_value.hexdigest.return_value = ( + "45ff0d3c7fc8b77f64de7de7b3698307971546a6be00982934a19ee44f5d91bb" + ) + + target = tmp_path / "storcli" + target.write_text("fake file") + + ok = validate_checksum(STORCLI_VERSION_INFOS, target) + assert ok + + def test_validate_checksum_fail(self, tmp_path): + target = tmp_path / "perccli" + target.write_text("fake file") + + ok = validate_checksum(PERCCLI_VERSION_INFOS, target) + assert not ok + + @mock.patch( + "checksum.get_os_platform", + return_value=OSPlatform( + release="20.04", + machine="fake machine architecture", + ), + ) + @mock.patch("checksum.hashlib.sha256") + def test_validate_checksum_wrong_architecture( + self, mock_sha256, mock_get_os_platform, tmp_path + ): + mock_sha256.return_value = mock.Mock() + mock_sha256.return_value.hexdigest.return_value = ( + "53c8ee43808779f8263c25b3cb975d816d207659684f3c7de1df4bbd2447ead4" + ) + + target = tmp_path / "perccli" + target.write_text("fake file") + + ok = validate_checksum(PERCCLI_VERSION_INFOS, target) + assert not ok diff --git a/tests/unit/test_hw_tools.py b/tests/unit/test_hw_tools.py index 3705f9e7..f33a9c9e 100644 --- a/tests/unit/test_hw_tools.py +++ b/tests/unit/test_hw_tools.py @@ -11,6 +11,12 @@ from ops.model import ModelError from charm import HardwareObserverCharm +from checksum import ( + PERCCLI_VERSION_INFOS, + SAS2IRCU_VERSION_INFOS, + SAS3IRCU_VERSION_INFOS, + STORCLI_VERSION_INFOS, +) from config import SNAP_COMMON, TOOLS_DIR, TPR_RESOURCES, HWTool, StorageVendor, SystemVendor from hw_tools import ( APTStrategyABC, @@ -18,6 +24,7 @@ InvalidCredentialsError, IPMIStrategy, PercCLIStrategy, + ResourceChecksumError, ResourceFileSizeZeroError, RetriesExhaustedError, SAS2IRCUStrategy, @@ -298,7 +305,7 @@ def test_10_install_strategy_errors(self, mock_strategies, mock_hw_white_list): ) mock_strategies.return_value[1].install.side_effect = OSError("Fake os error") mock_strategies.return_value[2].install.side_effect = apt.PackageError( - "Fake apt packge error" + "Fake apt package error" ) ok, msg = self.hw_tool_helper.install(mock_resources) @@ -318,10 +325,13 @@ def test_11_check_missing_resources_zero_size_resources(self, check_file_size): class TestStorCLIStrategy(unittest.TestCase): + @mock.patch("hw_tools.validate_checksum", return_value=True) @mock.patch("hw_tools.check_file_size", return_value=True) @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.install_deb") - def test_install(self, mock_install_deb, mock_symlink, _): + def test_install( + self, mock_install_deb, mock_symlink, mock_check_file_size, mock_validate_checksum + ): strategy = StorCLIStrategy() strategy.install(path="path-a") mock_install_deb.assert_called_with("storcli", "path-a") @@ -330,13 +340,30 @@ def test_install(self, mock_install_deb, mock_symlink, _): dst=TOOLS_DIR / "storcli", ) + @mock.patch("hw_tools.validate_checksum", return_value=True) @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.install_deb") - def test_install_empty_resource(self, mock_install_deb, mock_symlink): + def test_install_empty_resource(self, mock_install_deb, mock_symlink, mock_validate_checksum): strategy = StorCLIStrategy() with pytest.raises(ResourceFileSizeZeroError): strategy.install(get_mock_path(0)) + mock_validate_checksum.assert_not_called() + mock_install_deb.assert_not_called() + mock_symlink.assert_not_called() + + @mock.patch("hw_tools.validate_checksum", return_value=False) + @mock.patch("hw_tools.check_file_size", return_value=True) + @mock.patch("hw_tools.symlink") + @mock.patch("hw_tools.install_deb") + def test_install_checksum_fail( + self, mock_install_deb, mock_symlink, mock_check_file_size, mock_validate_checksum + ): + mock_path = mock.Mock() + strategy = StorCLIStrategy() + with pytest.raises(ResourceChecksumError): + strategy.install(mock_path) + mock_validate_checksum.assert_called_with(STORCLI_VERSION_INFOS, mock_path) mock_install_deb.assert_not_called() mock_symlink.assert_not_called() @@ -400,25 +427,47 @@ def test_check_file_size_zero(self): class TestSAS2IRCUStrategy(unittest.TestCase): + @mock.patch("hw_tools.validate_checksum", return_value=True) @mock.patch("hw_tools.check_file_size", return_value=True) @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.make_executable") - def test_install(self, mock_make_executable, mock_symlink, _): + def test_install( + self, mock_make_executable, mock_symlink, mock_check_file_size, mock_validate_checksum + ): strategy = SAS2IRCUStrategy() strategy.install(path="path-a") mock_make_executable.assert_called_with("path-a") mock_symlink.assert_called_with(src="path-a", dst=TOOLS_DIR / "sas2ircu") + @mock.patch("hw_tools.validate_checksum", return_value=True) @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.make_executable") - def test_install_empty_resource(self, mock_make_executable, mock_symlink): + def test_install_empty_resource( + self, mock_make_executable, mock_symlink, mock_validate_checksum + ): strategy = SAS2IRCUStrategy() with pytest.raises(ResourceFileSizeZeroError): strategy.install(get_mock_path(0)) + mock_validate_checksum.assert_not_called() mock_make_executable.assert_not_called() mock_symlink.assert_not_called() + @mock.patch("hw_tools.validate_checksum", return_value=False) + @mock.patch("hw_tools.check_file_size", return_value=True) + @mock.patch("hw_tools.symlink") + @mock.patch("hw_tools.install_deb") + def test_install_checksum_fail( + self, mock_install_deb, mock_symlink, mock_check_file_size, mock_validate_checksum + ): + mock_path = mock.Mock() + strategy = SAS2IRCUStrategy() + with pytest.raises(ResourceChecksumError): + strategy.install(mock_path) + mock_validate_checksum.assert_called_with(SAS2IRCU_VERSION_INFOS, mock_path) + mock_install_deb.assert_not_called() + mock_symlink.assert_not_called() + def test_remove(self): strategy = SAS2IRCUStrategy() with mock.patch.object(strategy, "symlink_bin") as mock_symlink_bin: @@ -427,25 +476,47 @@ def test_remove(self): class TestSAS3IRCUStrategy(unittest.TestCase): + @mock.patch("hw_tools.validate_checksum", return_value=True) @mock.patch("hw_tools.check_file_size", return_value=True) @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.make_executable") - def test_install(self, mock_make_executable, mock_symlink, _): + def test_install( + self, mock_make_executable, mock_symlink, mock_check_file_size, mock_validate_checksum + ): strategy = SAS3IRCUStrategy() strategy.install(path="path-a") mock_make_executable.assert_called_with("path-a") mock_symlink.assert_called_with(src="path-a", dst=TOOLS_DIR / "sas3ircu") + @mock.patch("hw_tools.validate_checksum", return_value=True) @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.make_executable") - def test_install_empty_resource(self, mock_make_executable, mock_symlink): + def test_install_empty_resource( + self, mock_make_executable, mock_symlink, mock_validate_checksum + ): strategy = SAS3IRCUStrategy() with pytest.raises(ResourceFileSizeZeroError): strategy.install(get_mock_path(0)) + mock_validate_checksum.assert_not_called() mock_make_executable.assert_not_called() mock_symlink.assert_not_called() + @mock.patch("hw_tools.validate_checksum", return_value=False) + @mock.patch("hw_tools.check_file_size", return_value=True) + @mock.patch("hw_tools.symlink") + @mock.patch("hw_tools.install_deb") + def test_install_checksum_fail( + self, mock_install_deb, mock_symlink, mock_check_file_size, mock_validate_checksum + ): + mock_path = mock.Mock() + strategy = SAS3IRCUStrategy() + with pytest.raises(ResourceChecksumError): + strategy.install(mock_path) + mock_validate_checksum.assert_called_with(SAS3IRCU_VERSION_INFOS, mock_path) + mock_install_deb.assert_not_called() + mock_symlink.assert_not_called() + def test_remove(self): strategy = SAS3IRCUStrategy() with mock.patch.object(strategy, "symlink_bin") as mock_symlink_bin: @@ -454,10 +525,13 @@ def test_remove(self): class TestPercCLIStrategy(unittest.TestCase): + @mock.patch("hw_tools.validate_checksum", return_value=True) @mock.patch("hw_tools.check_file_size", return_value=True) @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.install_deb") - def test_install(self, mock_install_deb, mock_symlink, _): + def test_install( + self, mock_install_deb, mock_symlink, mock_check_file_size, mock_validate_checksum + ): strategy = PercCLIStrategy() strategy.install(path="path-a") mock_install_deb.assert_called_with("perccli", "path-a") @@ -466,9 +540,10 @@ def test_install(self, mock_install_deb, mock_symlink, _): dst=TOOLS_DIR / "perccli", ) + @mock.patch("hw_tools.validate_checksum") @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.install_deb") - def test_install_empty_resource(self, mock_install_deb, mock_symlink): + def test_install_empty_resource(self, mock_install_deb, mock_symlink, mock_check_sum): mock_path = mock.Mock() mock_path_stat = mock.Mock() mock_path.stat.return_value = mock_path_stat @@ -480,6 +555,22 @@ def test_install_empty_resource(self, mock_install_deb, mock_symlink): mock_install_deb.assert_not_called() mock_symlink.assert_not_called() + mock_check_sum.assert_not_called() + + @mock.patch("hw_tools.validate_checksum", return_value=False) + @mock.patch("hw_tools.check_file_size", return_value=True) + @mock.patch("hw_tools.symlink") + @mock.patch("hw_tools.install_deb") + def test_install_checksum_fail( + self, mock_install_deb, mock_symlink, mock_check_file_size, mock_validate_checksum + ): + mock_path = mock.Mock() + strategy = PercCLIStrategy() + with pytest.raises(ResourceChecksumError): + strategy.install(mock_path) + mock_validate_checksum.assert_called_with(PERCCLI_VERSION_INFOS, mock_path) + mock_install_deb.assert_not_called() + mock_symlink.assert_not_called() @mock.patch("hw_tools.symlink") @mock.patch("hw_tools.remove_deb") diff --git a/tests/unit/test_os_platform.py b/tests/unit/test_os_platform.py new file mode 100644 index 00000000..061fa7ef --- /dev/null +++ b/tests/unit/test_os_platform.py @@ -0,0 +1,19 @@ +from unittest.mock import patch + +import pytest + +from os_platform import OSPlatform, UbuntuSeries, get_os_platform + + +@pytest.mark.parametrize( + "release,series", + [("22.04", UbuntuSeries.JAMMY), ("20.04", UbuntuSeries.FOCAL), ("NR", None)], +) +@pytest.mark.parametrize("machine", ["AMD64", "x86_86", "arm64", "riscv64"]) +def test_os_platform_series(release, series, machine): + """Get platform from a patched machine.""" + with patch("distro.info", return_value={"version": release}): + with patch("platform.machine", return_value=machine): + result = get_os_platform() + assert result == OSPlatform(release=release, machine=machine) + assert result.series == series