From e9249bceff7f96144780e5645c61e5588d86a852 Mon Sep 17 00:00:00 2001 From: Andrei Huides Date: Fri, 18 Sep 2026 10:57:00 +0300 Subject: [PATCH 1/4] chore(sdk): upgrade firewall manager sdk Upgrade scc-firewall-manager-sdk to 1.22.1573 so inventory responses with UNKNOWN compliance and license statuses deserialize correctly. Refresh the lockfile and add regression coverage for the new contract. --- .../tests/test_packaging_metadata.py | 2 +- .../tests/test_sdk_compatibility.py | 20 +++++++++++++++++++ poetry.lock | 6 +++--- pyproject.toml | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 cisco_sccfm_core/tests/test_sdk_compatibility.py diff --git a/cisco_sccfm_core/tests/test_packaging_metadata.py b/cisco_sccfm_core/tests/test_packaging_metadata.py index aaf5ad85..c126abd3 100644 --- a/cisco_sccfm_core/tests/test_packaging_metadata.py +++ b/cisco_sccfm_core/tests/test_packaging_metadata.py @@ -59,7 +59,7 @@ 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"] + assert "scc-firewall-manager-sdk==1.22.1573" in _project_config()["dependencies"] 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..e902e8cd --- /dev/null +++ b/cisco_sccfm_core/tests/test_sdk_compatibility.py @@ -0,0 +1,20 @@ +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +"""Regression tests for compatibility with the generated SDK contract.""" + +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", From e1c836808cb05373f32adec608c8d700a428d8a6 Mon Sep 17 00:00:00 2001 From: Andrei Huides Date: Fri, 18 Sep 2026 11:56:45 +0300 Subject: [PATCH 2/4] fix(sdk): adapt FTD deployments to SDK API split Use DeviceDeploymentsApi for multi-device FTD deployments introduced by SDK 1.22.1573 while retaining InventoryApi for single-device operations. Update regression tests and derive the SDK pin from pyproject so future upgrades do not duplicate the version string. --- .../services/inventory/ftd_deploy_service.py | 7 ++-- .../tests/test_ftd_deploy_service.py | 36 ++++++++++++++----- .../tests/test_packaging_metadata.py | 13 +++++-- .../tests/test_sdk_compatibility.py | 2 ++ 4 files changed, 46 insertions(+), 12 deletions(-) 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 c126abd3..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.22.1573" 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 index e902e8cd..a7722388 100644 --- a/cisco_sccfm_core/tests/test_sdk_compatibility.py +++ b/cisco_sccfm_core/tests/test_sdk_compatibility.py @@ -4,6 +4,8 @@ """Regression tests for compatibility with the generated SDK contract.""" +from __future__ import annotations + from scc_firewall_manager_sdk.models.device import Device From 0b426b6e9fc6fa3aa5cb7009d84c39dd22b5a8f6 Mon Sep 17 00:00:00 2001 From: Andrei Huides Date: Fri, 18 Sep 2026 13:02:33 +0300 Subject: [PATCH 3/4] docs(release): prepare ansible changelog for 0.42.1 Add the 0.42.1 Ansible release entry required by release preparation so the next patch release preserves the existing changelog history. --- sccfm-ansible/CHANGELOG.rst | 8 ++++++++ sccfm-ansible/changelogs/changelog.yaml | 7 +++++++ 2 files changed, 15 insertions(+) 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: From 54fe47bf4a9a735f5a0129cdde3f6827b70982c1 Mon Sep 17 00:00:00 2001 From: Andrei Huides Date: Fri, 18 Sep 2026 13:27:37 +0300 Subject: [PATCH 4/4] ci(release): derive SDK pin in artifact validation Read the SDK requirement from the built package metadata instead of retaining the old version literal. Require one exact pin and verify the installed SDK matches it so future deliberate SDK bumps are validated automatically. --- .github/workflows/ci.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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",