diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7de9dfc3..2cbd2ec5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -452,13 +452,29 @@ jobs: --requirement "${requirements}" "${smoke_python}" -I - "${artifact_kind}" <<'PY' import importlib + import re import sys from importlib.metadata import distribution, version from importlib.util import find_spec artifact_kind = sys.argv[1] - if version("scc-firewall-manager-sdk") != "1.17.27": - raise SystemExit("the installed SDK version is not the supported release pin") + sdk_requirements = [ + requirement.split(";", 1)[0].strip() + for requirement in distribution("cisco-sccfm-devkit").requires or [] + if requirement.split(";", 1)[0].strip().startswith("scc-firewall-manager-sdk") + ] + if len(sdk_requirements) != 1: + raise SystemExit("the distribution must declare one SDK dependency") + sdk_requirement = sdk_requirements[0] + sdk_match = re.fullmatch( + r"scc-firewall-manager-sdk\s*(?:\(\s*)?==\s*(?P[^)\s]+)\s*\)?", + sdk_requirement, + ) + if sdk_match is None: + raise SystemExit("the distribution must declare an exact SDK release pin") + expected_sdk_version = sdk_match.group("version") + if version("scc-firewall-manager-sdk") != expected_sdk_version: + raise SystemExit("the installed SDK version does not match the release pin") for package in ( "scc_firewall_manager_sdk", "cisco_sccfm_cli", diff --git a/cisco_sccfm_core/services/inventory/ftd_deploy_service.py b/cisco_sccfm_core/services/inventory/ftd_deploy_service.py index 78463cad..7941ca0e 100644 --- a/cisco_sccfm_core/services/inventory/ftd_deploy_service.py +++ b/cisco_sccfm_core/services/inventory/ftd_deploy_service.py @@ -6,6 +6,7 @@ from scc_firewall_manager_sdk import ( CdoTransaction, + DeviceDeploymentsApi, FtdDeploymentInput, FtdMultiDeviceDeploymentInput, InventoryApi, @@ -20,7 +21,9 @@ class FtdDeployService: """Deploys configuration changes to cdFMC-managed FTD devices.""" def __init__(self, config: ConfigLike) -> None: - self._inventory_api = InventoryApi(ApiClientFactory().build(config=config)) + api_client = ApiClientFactory().build(config=config) + self._inventory_api = InventoryApi(api_client) + self._device_deployments_api = DeviceDeploymentsApi(api_client) def deploy_single( self, @@ -64,6 +67,6 @@ def deploy_multiple( description=description, ignoreWarnings=ignore_warnings, ) - return self._inventory_api.deploy_changes_to_multiple_ftd_devices( + return self._device_deployments_api.deploy_changes_to_multiple_ftd_devices( ftd_multi_device_deployment_input=deployment_input, ) diff --git a/cisco_sccfm_core/tests/test_ftd_deploy_service.py b/cisco_sccfm_core/tests/test_ftd_deploy_service.py index a1206260..6fd2ddd5 100644 --- a/cisco_sccfm_core/tests/test_ftd_deploy_service.py +++ b/cisco_sccfm_core/tests/test_ftd_deploy_service.py @@ -29,7 +29,19 @@ def mock_inventory_api(monkeypatch: pytest.MonkeyPatch) -> MagicMock: @pytest.fixture -def service(mock_inventory_api: MagicMock) -> FtdDeployService: +def mock_device_deployments_api(monkeypatch: pytest.MonkeyPatch) -> MagicMock: + mock_api = MagicMock() + monkeypatch.setattr( + "cisco_sccfm_core.services.inventory.ftd_deploy_service.DeviceDeploymentsApi", + lambda client: mock_api, + ) + return mock_api + + +@pytest.fixture +def service( + mock_inventory_api: MagicMock, mock_device_deployments_api: MagicMock +) -> FtdDeployService: config = MagicMock() config.region = "us" config.api_token = "test-token" @@ -85,15 +97,19 @@ def test_should_validate_uid(self, service: FtdDeployService) -> None: class TestDeployMultiple: def test_should_call_deploy_multiple_ftd_devices( - self, service: FtdDeployService, mock_inventory_api: MagicMock + self, service: FtdDeployService, mock_device_deployments_api: MagicMock ) -> None: - mock_inventory_api.deploy_changes_to_multiple_ftd_devices.return_value = SAMPLE_TRANSACTION + mock_device_deployments_api.deploy_changes_to_multiple_ftd_devices.return_value = ( + SAMPLE_TRANSACTION + ) result = service.deploy_multiple(device_uids=[UID_1, UID_2]) assert result == SAMPLE_TRANSACTION - mock_inventory_api.deploy_changes_to_multiple_ftd_devices.assert_called_once() - call_kwargs = mock_inventory_api.deploy_changes_to_multiple_ftd_devices.call_args[1] + mock_device_deployments_api.deploy_changes_to_multiple_ftd_devices.assert_called_once() + call_kwargs = mock_device_deployments_api.deploy_changes_to_multiple_ftd_devices.call_args[ + 1 + ] deployment_input = call_kwargs["ftd_multi_device_deployment_input"] assert deployment_input.device_uids == [UID_1, UID_2] assert deployment_input.deployment_notes is None @@ -101,9 +117,11 @@ def test_should_call_deploy_multiple_ftd_devices( assert deployment_input.ignore_warnings is False def test_should_pass_optional_params( - self, service: FtdDeployService, mock_inventory_api: MagicMock + self, service: FtdDeployService, mock_device_deployments_api: MagicMock ) -> None: - mock_inventory_api.deploy_changes_to_multiple_ftd_devices.return_value = SAMPLE_TRANSACTION + mock_device_deployments_api.deploy_changes_to_multiple_ftd_devices.return_value = ( + SAMPLE_TRANSACTION + ) result = service.deploy_multiple( device_uids=[UID_1, UID_2], @@ -113,7 +131,9 @@ def test_should_pass_optional_params( ) assert result == SAMPLE_TRANSACTION - call_kwargs = mock_inventory_api.deploy_changes_to_multiple_ftd_devices.call_args[1] + call_kwargs = mock_device_deployments_api.deploy_changes_to_multiple_ftd_devices.call_args[ + 1 + ] deployment_input = call_kwargs["ftd_multi_device_deployment_input"] assert deployment_input.deployment_notes == "Bulk deploy" assert deployment_input.description == "Weekend maintenance" diff --git a/cisco_sccfm_core/tests/test_packaging_metadata.py b/cisco_sccfm_core/tests/test_packaging_metadata.py index aaf5ad85..eed64880 100644 --- a/cisco_sccfm_core/tests/test_packaging_metadata.py +++ b/cisco_sccfm_core/tests/test_packaging_metadata.py @@ -7,6 +7,7 @@ from __future__ import annotations import tomllib +from importlib.metadata import version as distribution_version from pathlib import Path from typing import Any @@ -58,8 +59,16 @@ def test_published_packages_exclude_repository_only_code() -> None: } -def test_generated_sdk_is_pinned_to_the_verified_compatible_version() -> None: - assert "scc-firewall-manager-sdk==1.17.27" in _project_config()["dependencies"] +def test_generated_sdk_pin_matches_installed_version() -> None: + sdk_dependencies = [ + dependency + for dependency in _project_config()["dependencies"] + if dependency.startswith("scc-firewall-manager-sdk==") + ] + + assert len(sdk_dependencies) == 1 + pinned_version = sdk_dependencies[0].split("==", maxsplit=1)[1] + assert distribution_version("scc-firewall-manager-sdk") == pinned_version def test_interactive_entrypoint_is_published_from_the_cli_package() -> None: diff --git a/cisco_sccfm_core/tests/test_sdk_compatibility.py b/cisco_sccfm_core/tests/test_sdk_compatibility.py new file mode 100644 index 00000000..a7722388 --- /dev/null +++ b/cisco_sccfm_core/tests/test_sdk_compatibility.py @@ -0,0 +1,22 @@ +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +"""Regression tests for compatibility with the generated SDK contract.""" + +from __future__ import annotations + +from scc_firewall_manager_sdk.models.device import Device + + +def test_inventory_device_accepts_unknown_licensing_statuses() -> None: + """The API uses UNKNOWN when ASA licensing cannot be determined.""" + device = Device( + name="asa-1", + deviceType="ASA", + complianceStatus="UNKNOWN", + licenseStatus="UNKNOWN", + ) + + assert device.compliance_status == "UNKNOWN" + assert device.license_status == "UNKNOWN" diff --git a/poetry.lock b/poetry.lock index 84bcd0b0..7e5453f7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1771,13 +1771,13 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "scc-firewall-manager-sdk" -version = "1.17.27" +version = "1.22.1573" description = "Cisco Security Cloud Control Firewall Manager API" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "scc_firewall_manager_sdk-1.17.27-py3-none-any.whl", hash = "sha256:3bb04af874f7723aba60f1b1f97f6c6ab509e5971eb7cd1254e0f27180b8ac78"}, + {file = "scc_firewall_manager_sdk-1.22.1573-py3-none-any.whl", hash = "sha256:dd5e0efcd7aeadd14f5a4c05ca56d042ad7b98308ac8cb8991e6a23fdc286001"}, ] [package.dependencies] @@ -1906,4 +1906,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.12,<4.0" -content-hash = "654c3c4d58e46300af3859b2f4534286df028d0406abb676013970ea5da3a08f" +content-hash = "d60f29c240ee76e8084130daea3adcf9b39e6f5badd13eb630f73b7681f82d1a" diff --git a/pyproject.toml b/pyproject.toml index 18ba54dd..cbf23970 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "rich>=14.2.0,<15", "click-option-group>=0.5.9,<0.6", "questionary>=2.1.1,<3", - "scc-firewall-manager-sdk==1.17.27", + "scc-firewall-manager-sdk==1.22.1573", "paramiko>=5.0.0,<6", "cryptography>=50.0.0,<51", "pygments>=2.20.0,<3", diff --git a/sccfm-ansible/CHANGELOG.rst b/sccfm-ansible/CHANGELOG.rst index bfbaad97..cf507bfa 100644 --- a/sccfm-ansible/CHANGELOG.rst +++ b/sccfm-ansible/CHANGELOG.rst @@ -4,6 +4,14 @@ Cisco SCCFM Collection Release Notes .. contents:: Topics +v0.42.1 +======== + +Bugfixes +-------- + +- Bumped the SCCFM SDK to 1.22.1573 to support unknown licensing statuses and aligned multi-device FTD deployments with the current SDK API. + v0.42.0 ======== diff --git a/sccfm-ansible/changelogs/changelog.yaml b/sccfm-ansible/changelogs/changelog.yaml index a0ab5d37..14fb88f2 100644 --- a/sccfm-ansible/changelogs/changelog.yaml +++ b/sccfm-ansible/changelogs/changelog.yaml @@ -2,6 +2,13 @@ ancestor: null # sccfm-release-retarget-seed: 0.39.0 releases: + 0.42.1: + changes: + bugfixes: + - Bumped the SCCFM SDK to 1.22.1573 to support unknown licensing + statuses and aligned multi-device FTD deployments with the current SDK API. + fragments: [] + release_date: '2026-09-18' 0.42.0: changes: minor_changes: