cisco-sccfm-devkit 0.39.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cisco_sccfm_cli/__init__.py +3 -0
- cisco_sccfm_cli/cli.py +66 -0
- cisco_sccfm_cli/commands/__init__.py +3 -0
- cisco_sccfm_cli/commands/base.py +337 -0
- cisco_sccfm_cli/commands/configure.py +123 -0
- cisco_sccfm_cli/commands/inventory/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/command.py +38 -0
- cisco_sccfm_cli/commands/inventory/devices/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/change_boot_image/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/change_boot_image/command.py +225 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/cli/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/cli/command.py +36 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/cli/execute/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/cli/execute/command.py +129 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/cli_result_renderer.py +66 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/command.py +76 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/disk/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/disk/command.py +38 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/disk/list_files/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/disk/list_files/command.py +122 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/ha_check/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/ha_check/command.py +153 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/list_boot_registry/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/list_boot_registry/command.py +211 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/list_local_users/__init__.py +11 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/list_local_users/command.py +123 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/list_not_on_version/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/list_not_on_version/command.py +194 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/onboard/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/onboard/command.py +277 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shared.py +243 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/add/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/add/command.py +254 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/clear/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/clear/command.py +97 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/command.py +46 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/remove/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/remove/command.py +114 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/show/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/shun/show/command.py +230 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/smartlicense/__init__.py +3 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/smartlicense/command.py +294 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/upgrade/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/upgrade/command.py +42 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/upgrade/compatible_versions/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/upgrade/compatible_versions/command.py +216 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/upgrade/trigger/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/upgrade/trigger/command.py +391 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/user/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/user/change_password/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/user/change_password/command.py +164 -0
- cisco_sccfm_cli/commands/inventory/devices/asa/user/command.py +38 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/cli/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/cli/command.py +38 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/cli/execute/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/cli/execute/command.py +90 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/cli_result_renderer.py +57 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/command.py +56 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/configure_manager/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/configure_manager/command.py +259 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/deploy/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/deploy/command.py +218 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/onboard/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/onboard/command.py +222 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/onboard_ztp/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/onboard_ztp/command.py +227 -0
- cisco_sccfm_cli/commands/inventory/devices/cdfmc_managed_ftd/shared.py +220 -0
- cisco_sccfm_cli/commands/inventory/devices/command.py +44 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/command.py +51 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/list_not_on_version/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/list_not_on_version/command.py +318 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/shared.py +243 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/upgrade/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/upgrade/command.py +42 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/upgrade/compatible_versions/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/upgrade/compatible_versions/command.py +228 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/upgrade/trigger/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/devices/ftd/upgrade/trigger/command.py +316 -0
- cisco_sccfm_cli/commands/inventory/devices/list/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/devices/list/command.py +46 -0
- cisco_sccfm_cli/commands/inventory/devices/rendering.py +129 -0
- cisco_sccfm_cli/commands/inventory/manager/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/manager/access_policies/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/manager/access_policies/command.py +40 -0
- cisco_sccfm_cli/commands/inventory/manager/access_policies/list/__init__.py +9 -0
- cisco_sccfm_cli/commands/inventory/manager/access_policies/list/command.py +71 -0
- cisco_sccfm_cli/commands/inventory/manager/command.py +40 -0
- cisco_sccfm_cli/commands/inventory/manager/list/__init__.py +7 -0
- cisco_sccfm_cli/commands/inventory/manager/list/command.py +82 -0
- cisco_sccfm_cli/commands/inventory/options.py +53 -0
- cisco_sccfm_cli/commands/objects/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/add_override/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/add_override/command.py +96 -0
- cisco_sccfm_cli/commands/objects/apply_override_as_default/__init__.py +9 -0
- cisco_sccfm_cli/commands/objects/apply_override_as_default/command.py +88 -0
- cisco_sccfm_cli/commands/objects/command.py +60 -0
- cisco_sccfm_cli/commands/objects/delete_override/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/delete_override/command.py +88 -0
- cisco_sccfm_cli/commands/objects/edit_override/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/edit_override/command.py +96 -0
- cisco_sccfm_cli/commands/objects/network/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/network/command.py +46 -0
- cisco_sccfm_cli/commands/objects/network/create/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/network/create/command.py +93 -0
- cisco_sccfm_cli/commands/objects/network/delete/__init__.py +9 -0
- cisco_sccfm_cli/commands/objects/network/delete/command.py +65 -0
- cisco_sccfm_cli/commands/objects/network/list/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/network/list/command.py +72 -0
- cisco_sccfm_cli/commands/objects/network/update/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/network/update/command.py +119 -0
- cisco_sccfm_cli/commands/objects/network_group/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/network_group/add_member/__init__.py +9 -0
- cisco_sccfm_cli/commands/objects/network_group/add_member/command.py +161 -0
- cisco_sccfm_cli/commands/objects/network_group/command.py +52 -0
- cisco_sccfm_cli/commands/objects/network_group/create/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/network_group/create/command.py +121 -0
- cisco_sccfm_cli/commands/objects/network_group/delete/__init__.py +11 -0
- cisco_sccfm_cli/commands/objects/network_group/delete/command.py +65 -0
- cisco_sccfm_cli/commands/objects/network_group/list/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/network_group/list/command.py +80 -0
- cisco_sccfm_cli/commands/objects/network_group/remove_member/__init__.py +9 -0
- cisco_sccfm_cli/commands/objects/network_group/remove_member/command.py +161 -0
- cisco_sccfm_cli/commands/objects/network_group/update/__init__.py +9 -0
- cisco_sccfm_cli/commands/objects/network_group/update/command.py +142 -0
- cisco_sccfm_cli/commands/objects/options.py +319 -0
- cisco_sccfm_cli/commands/objects/show/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/show/command.py +105 -0
- cisco_sccfm_cli/commands/objects/update_default/__init__.py +7 -0
- cisco_sccfm_cli/commands/objects/update_default/command.py +88 -0
- cisco_sccfm_cli/commands/objects/utils.py +237 -0
- cisco_sccfm_cli/commands/policies/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_group/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_group/command.py +42 -0
- cisco_sccfm_cli/commands/policies/access_group/get/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_group/get/command.py +71 -0
- cisco_sccfm_cli/commands/policies/access_group/list/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_group/list/command.py +86 -0
- cisco_sccfm_cli/commands/policies/access_rule/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_rule/command.py +48 -0
- cisco_sccfm_cli/commands/policies/access_rule/create/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_rule/create/command.py +258 -0
- cisco_sccfm_cli/commands/policies/access_rule/delete/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_rule/delete/command.py +50 -0
- cisco_sccfm_cli/commands/policies/access_rule/get/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_rule/get/command.py +81 -0
- cisco_sccfm_cli/commands/policies/access_rule/list/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_rule/list/command.py +96 -0
- cisco_sccfm_cli/commands/policies/access_rule/update/__init__.py +7 -0
- cisco_sccfm_cli/commands/policies/access_rule/update/command.py +178 -0
- cisco_sccfm_cli/commands/policies/command.py +40 -0
- cisco_sccfm_cli/commands/schema/__init__.py +7 -0
- cisco_sccfm_cli/commands/schema/command.py +88 -0
- cisco_sccfm_cli/commands/shared_options.py +77 -0
- cisco_sccfm_cli/commands/status.py +70 -0
- cisco_sccfm_cli/commands/transaction.py +89 -0
- cisco_sccfm_cli/interactive.py +426 -0
- cisco_sccfm_cli/interactive_commands.py +125 -0
- cisco_sccfm_cli/models/__init__.py +7 -0
- cisco_sccfm_cli/models/config.py +9 -0
- cisco_sccfm_cli/option_metadata.py +26 -0
- cisco_sccfm_cli/py.typed +0 -0
- cisco_sccfm_cli/schema.py +871 -0
- cisco_sccfm_cli/services/__init__.py +9 -0
- cisco_sccfm_cli/services/config_service.py +9 -0
- cisco_sccfm_cli/utils/__init__.py +11 -0
- cisco_sccfm_cli/utils/json_output.py +36 -0
- cisco_sccfm_cli/utils/redaction.py +50 -0
- cisco_sccfm_cli/utils/spinner.py +52 -0
- cisco_sccfm_core/__init__.py +106 -0
- cisco_sccfm_core/constants.py +76 -0
- cisco_sccfm_core/errors.py +137 -0
- cisco_sccfm_core/factories/__init__.py +7 -0
- cisco_sccfm_core/factories/api_client_factory.py +28 -0
- cisco_sccfm_core/models/asa_boot_image_change_result.py +19 -0
- cisco_sccfm_core/models/asa_boot_registry.py +19 -0
- cisco_sccfm_core/models/asa_disk_file.py +41 -0
- cisco_sccfm_core/models/asa_failover_status.py +62 -0
- cisco_sccfm_core/models/asa_local_user.py +20 -0
- cisco_sccfm_core/models/asa_password_change_result.py +16 -0
- cisco_sccfm_core/models/asa_shun_entry.py +38 -0
- cisco_sccfm_core/models/asa_upgrade_version.py +22 -0
- cisco_sccfm_core/models/cdo_transaction_status.py +20 -0
- cisco_sccfm_core/models/ftd_cli_result.py +26 -0
- cisco_sccfm_core/models/ftd_upgrade_version.py +24 -0
- cisco_sccfm_core/models/profile.py +16 -0
- cisco_sccfm_core/parsers/__init__.py +28 -0
- cisco_sccfm_core/parsers/asa_boot_registry_parser.py +75 -0
- cisco_sccfm_core/parsers/asa_cli_table_parser.py +53 -0
- cisco_sccfm_core/parsers/asa_disk_file_parser.py +45 -0
- cisco_sccfm_core/parsers/asa_failover_parser.py +216 -0
- cisco_sccfm_core/parsers/asa_local_user_parser.py +49 -0
- cisco_sccfm_core/parsers/asa_shun_parser.py +82 -0
- cisco_sccfm_core/py.typed +0 -0
- cisco_sccfm_core/services/__init__.py +70 -0
- cisco_sccfm_core/services/health_service.py +35 -0
- cisco_sccfm_core/services/inventory/__init__.py +84 -0
- cisco_sccfm_core/services/inventory/asa_boot_image_service.py +505 -0
- cisco_sccfm_core/services/inventory/asa_boot_registry_service.py +89 -0
- cisco_sccfm_core/services/inventory/asa_cli_service.py +65 -0
- cisco_sccfm_core/services/inventory/asa_disk_file_service.py +48 -0
- cisco_sccfm_core/services/inventory/asa_ha_check_service.py +393 -0
- cisco_sccfm_core/services/inventory/asa_onboard_service.py +42 -0
- cisco_sccfm_core/services/inventory/asa_shun_service.py +208 -0
- cisco_sccfm_core/services/inventory/asa_upgrade_service.py +81 -0
- cisco_sccfm_core/services/inventory/asa_upgrade_version_service.py +113 -0
- cisco_sccfm_core/services/inventory/asa_user_password_service.py +205 -0
- cisco_sccfm_core/services/inventory/cdfmc_access_policy_service.py +68 -0
- cisco_sccfm_core/services/inventory/ftd_cli_service.py +262 -0
- cisco_sccfm_core/services/inventory/ftd_configure_manager_service.py +399 -0
- cisco_sccfm_core/services/inventory/ftd_deploy_service.py +69 -0
- cisco_sccfm_core/services/inventory/ftd_onboard_service.py +37 -0
- cisco_sccfm_core/services/inventory/ftd_register_service.py +40 -0
- cisco_sccfm_core/services/inventory/ftd_upgrade_service.py +73 -0
- cisco_sccfm_core/services/inventory/ftd_upgrade_version_service.py +81 -0
- cisco_sccfm_core/services/inventory/ftd_ztp_onboard_service.py +37 -0
- cisco_sccfm_core/services/inventory/inventory_service.py +25 -0
- cisco_sccfm_core/services/object_management/__init__.py +41 -0
- cisco_sccfm_core/services/object_management/network_group_service.py +690 -0
- cisco_sccfm_core/services/object_management/network_object_service.py +316 -0
- cisco_sccfm_core/services/object_management/object_api_helper.py +52 -0
- cisco_sccfm_core/services/object_management/object_override_service.py +583 -0
- cisco_sccfm_core/services/object_management/utils.py +82 -0
- cisco_sccfm_core/services/policy/__init__.py +23 -0
- cisco_sccfm_core/services/policy/access_group_service.py +117 -0
- cisco_sccfm_core/services/policy/access_rule_service.py +340 -0
- cisco_sccfm_core/services/policy/policy_api_helper.py +54 -0
- cisco_sccfm_core/services/profile_service.py +634 -0
- cisco_sccfm_core/services/transaction_service.py +54 -0
- cisco_sccfm_core/types.py +15 -0
- cisco_sccfm_core/utils/__init__.py +7 -0
- cisco_sccfm_core/utils/validation.py +45 -0
- cisco_sccfm_devkit-0.39.3.dist-info/METADATA +257 -0
- cisco_sccfm_devkit-0.39.3.dist-info/RECORD +241 -0
- cisco_sccfm_devkit-0.39.3.dist-info/WHEEL +4 -0
- cisco_sccfm_devkit-0.39.3.dist-info/entry_points.txt +4 -0
- cisco_sccfm_devkit-0.39.3.dist-info/licenses/LICENSE +201 -0
- cisco_sccfm_devkit-0.39.3.dist-info/licenses/LICENSES/Apache-2.0.txt +201 -0
cisco_sccfm_cli/cli.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from cisco_sccfm_cli.commands.base import BaseCommand
|
|
11
|
+
from cisco_sccfm_cli.commands.configure import ConfigureCommand
|
|
12
|
+
from cisco_sccfm_cli.commands.inventory import InventoryCommand
|
|
13
|
+
from cisco_sccfm_cli.commands.objects import ObjectsCommand
|
|
14
|
+
from cisco_sccfm_cli.commands.policies import PoliciesCommand
|
|
15
|
+
from cisco_sccfm_cli.commands.schema import SchemaCommand
|
|
16
|
+
from cisco_sccfm_cli.commands.status import StatusCommand
|
|
17
|
+
from cisco_sccfm_cli.commands.transaction import TransactionCommand
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_commands(console: Console) -> list[BaseCommand]:
|
|
21
|
+
return [
|
|
22
|
+
ConfigureCommand(console=console),
|
|
23
|
+
StatusCommand(console=console),
|
|
24
|
+
TransactionCommand(console=console),
|
|
25
|
+
InventoryCommand(console=console),
|
|
26
|
+
ObjectsCommand(console=console),
|
|
27
|
+
PoliciesCommand(console=console),
|
|
28
|
+
SchemaCommand(console=console),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_cli() -> click.Group:
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
@click.group(help="SCC Firewall Manager CLI")
|
|
36
|
+
@click.option(
|
|
37
|
+
"--profile",
|
|
38
|
+
default="default",
|
|
39
|
+
show_default=True,
|
|
40
|
+
help="Configuration profile to use",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--silent",
|
|
44
|
+
is_flag=True,
|
|
45
|
+
default=False,
|
|
46
|
+
help="Suppress progress indicators (useful for piping output)",
|
|
47
|
+
)
|
|
48
|
+
@click.pass_context
|
|
49
|
+
def group(ctx: click.Context, profile: str, silent: bool) -> None:
|
|
50
|
+
ctx.ensure_object(dict)
|
|
51
|
+
ctx.obj["profile"] = profile
|
|
52
|
+
ctx.obj["silent"] = silent
|
|
53
|
+
|
|
54
|
+
for command in _build_commands(console):
|
|
55
|
+
command.register(group)
|
|
56
|
+
return group
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
cli = _build_cli()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
try:
|
|
64
|
+
cli()
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
raise SystemExit(130)
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Sequence, cast
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.live import Live
|
|
18
|
+
from rich.spinner import Spinner
|
|
19
|
+
from scc_firewall_manager_sdk import ApiException, CdoTransaction, ConnectivityState, Device
|
|
20
|
+
|
|
21
|
+
from cisco_sccfm_cli.option_metadata import is_sensitive_option
|
|
22
|
+
from cisco_sccfm_cli.services import ConfigService
|
|
23
|
+
from cisco_sccfm_cli.utils import print_json, redact_data, redact_text
|
|
24
|
+
from cisco_sccfm_core import SccApiError
|
|
25
|
+
from cisco_sccfm_core.constants import DEFAULT_POLLING_INTERVAL_SEC, DEFAULT_TRANSACTION_TIMEOUT_SEC
|
|
26
|
+
from cisco_sccfm_core.models.cdo_transaction_status import CdoTransactionStatus
|
|
27
|
+
from cisco_sccfm_core.services.transaction_service import TransactionService
|
|
28
|
+
from cisco_sccfm_core.types import ConfigLike
|
|
29
|
+
|
|
30
|
+
_WINDOWS_SHELL = sys.platform == "win32"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _join_shell_command(arguments: Sequence[str]) -> str:
|
|
34
|
+
"""Render arguments for the platform's default command shell."""
|
|
35
|
+
if _WINDOWS_SHELL:
|
|
36
|
+
return subprocess.list2cmdline(arguments)
|
|
37
|
+
return shlex.join(arguments)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BaseCommand(ABC):
|
|
41
|
+
"""Base class implementing the command pattern for CLI commands."""
|
|
42
|
+
|
|
43
|
+
_SENSITIVE_VALUES_META_KEY = "sccfm_sensitive_values"
|
|
44
|
+
|
|
45
|
+
def __init__(self, console: Console) -> None:
|
|
46
|
+
self._console = console
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def name(self) -> str:
|
|
51
|
+
"""Return the Click command name."""
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def help_text(self) -> str:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
def register(self, group: click.Group) -> None:
|
|
58
|
+
group.add_command(self.build())
|
|
59
|
+
|
|
60
|
+
def build(self) -> click.Command:
|
|
61
|
+
return click.Command(
|
|
62
|
+
name=self.name,
|
|
63
|
+
help=self.help_text,
|
|
64
|
+
callback=self._dispatch,
|
|
65
|
+
params=list(self.build_params()),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def get_profile(self, ctx: click.Context, **kwargs: Any) -> ConfigLike:
|
|
69
|
+
profile = ctx.obj["profile"]
|
|
70
|
+
config_path = cast(Path | None, kwargs.get("config_path"))
|
|
71
|
+
config_service = ConfigService(path=config_path)
|
|
72
|
+
config = config_service.load(profile)
|
|
73
|
+
if not config:
|
|
74
|
+
setup_arguments = ["sccfm-cli", "--profile", profile, "configure"]
|
|
75
|
+
if config_path is not None:
|
|
76
|
+
setup_arguments.extend(["--config-path", str(config_path)])
|
|
77
|
+
setup_command = _join_shell_command(setup_arguments)
|
|
78
|
+
raise click.ClickException(
|
|
79
|
+
f"Profile '{profile}' not found. Run this command to set it up:\n{setup_command}"
|
|
80
|
+
)
|
|
81
|
+
self._register_sensitive_value(ctx, config.api_token)
|
|
82
|
+
return cast(ConfigLike, cast(object, config))
|
|
83
|
+
|
|
84
|
+
def build_params(self) -> Sequence[click.Parameter]:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
def _dispatch(self, **kwargs: Any) -> None:
|
|
88
|
+
ctx = click.get_current_context()
|
|
89
|
+
self._register_sensitive_parameters(ctx, kwargs)
|
|
90
|
+
exit_code: int | None = None
|
|
91
|
+
click_exception: click.ClickException | None = None
|
|
92
|
+
try:
|
|
93
|
+
self.handle(ctx=ctx, **kwargs)
|
|
94
|
+
except ApiException as e:
|
|
95
|
+
sensitive_values = self._sensitive_values(ctx)
|
|
96
|
+
output_format = cast(str | None, kwargs.get("format"))
|
|
97
|
+
error = SccApiError.from_exception(e)
|
|
98
|
+
|
|
99
|
+
if output_format == "json":
|
|
100
|
+
print_json(redact_data(error.to_dict(), sensitive_values))
|
|
101
|
+
else:
|
|
102
|
+
self.console.print(
|
|
103
|
+
"[yellow]Error executing operation using the SCC Firewall Manager API. "
|
|
104
|
+
"If you think you should not be getting this error, please file a Github issue"
|
|
105
|
+
" with the details below.[/yellow]"
|
|
106
|
+
)
|
|
107
|
+
self.console.print(
|
|
108
|
+
f"[bold]Error message:[/bold] "
|
|
109
|
+
f"{redact_text(error.message, sensitive_values)}"
|
|
110
|
+
)
|
|
111
|
+
error_code = redact_text(str(error.error_code), sensitive_values)
|
|
112
|
+
self.console.print(f"[bold]Error Code:[/bold] {error_code}")
|
|
113
|
+
error_details = redact_data(error.details, sensitive_values)
|
|
114
|
+
self.console.print(
|
|
115
|
+
f"[bold]Error Details:[/bold]\n{json.dumps(error_details, indent=2)}"
|
|
116
|
+
)
|
|
117
|
+
exit_code = -1
|
|
118
|
+
except click.ClickException as exc:
|
|
119
|
+
# Preserve Click's default error handling so usage/help is shown for user errors.
|
|
120
|
+
exc.message = redact_text(str(exc.message), self._sensitive_values(ctx))
|
|
121
|
+
exc.args = (exc.message,)
|
|
122
|
+
exc.__context__ = None
|
|
123
|
+
exc.__cause__ = None
|
|
124
|
+
exc.__suppress_context__ = True
|
|
125
|
+
exc.__traceback__ = None
|
|
126
|
+
click_exception = exc
|
|
127
|
+
except (click.Abort, click.exceptions.Exit):
|
|
128
|
+
raise
|
|
129
|
+
except KeyboardInterrupt:
|
|
130
|
+
sys.exit(130)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
message = redact_text(str(e), self._sensitive_values(ctx))
|
|
133
|
+
self.console.print(f"[red]Error: {message}[/red]")
|
|
134
|
+
exit_code = -1
|
|
135
|
+
|
|
136
|
+
if click_exception is not None:
|
|
137
|
+
raise click_exception
|
|
138
|
+
if exit_code is not None:
|
|
139
|
+
sys.exit(exit_code)
|
|
140
|
+
|
|
141
|
+
def _register_sensitive_value(self, ctx: click.Context, value: str) -> None:
|
|
142
|
+
"""Register a secret for command-scoped output and exception redaction."""
|
|
143
|
+
if not value:
|
|
144
|
+
return
|
|
145
|
+
values = self._sensitive_values(ctx)
|
|
146
|
+
if value not in values:
|
|
147
|
+
ctx.meta[self._SENSITIVE_VALUES_META_KEY] = (*values, value)
|
|
148
|
+
|
|
149
|
+
def _register_sensitive_parameters(self, ctx: click.Context, kwargs: dict[str, Any]) -> None:
|
|
150
|
+
"""Register values from Click options explicitly marked as sensitive."""
|
|
151
|
+
for parameter in ctx.command.params:
|
|
152
|
+
if not isinstance(parameter, click.Option) or not is_sensitive_option(parameter):
|
|
153
|
+
continue
|
|
154
|
+
value = kwargs.get(parameter.name or "")
|
|
155
|
+
if isinstance(value, str):
|
|
156
|
+
self._register_sensitive_value(ctx, value)
|
|
157
|
+
|
|
158
|
+
def _sensitive_values(self, ctx: click.Context) -> tuple[str, ...]:
|
|
159
|
+
"""Return secrets registered for the active Click command context."""
|
|
160
|
+
raw_values = ctx.meta.get(self._SENSITIVE_VALUES_META_KEY, ())
|
|
161
|
+
if not isinstance(raw_values, tuple):
|
|
162
|
+
return ()
|
|
163
|
+
return tuple(value for value in raw_values if isinstance(value, str) and value)
|
|
164
|
+
|
|
165
|
+
def _prompt_sensitive(
|
|
166
|
+
self,
|
|
167
|
+
text: str,
|
|
168
|
+
*,
|
|
169
|
+
default: str | None = None,
|
|
170
|
+
show_default: bool = True,
|
|
171
|
+
) -> str:
|
|
172
|
+
"""Prompt without echoing and immediately register the acquired secret."""
|
|
173
|
+
value = cast(
|
|
174
|
+
str,
|
|
175
|
+
click.prompt(
|
|
176
|
+
text,
|
|
177
|
+
default=default,
|
|
178
|
+
hide_input=True,
|
|
179
|
+
show_default=show_default,
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
self._register_sensitive_value(click.get_current_context(), value)
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
def _active_sensitive_values(
|
|
186
|
+
self, sensitive_values: Sequence[str] | None = None
|
|
187
|
+
) -> Sequence[str]:
|
|
188
|
+
"""Resolve explicit secrets or inherit the active command registry."""
|
|
189
|
+
if sensitive_values is not None:
|
|
190
|
+
return sensitive_values
|
|
191
|
+
ctx = click.get_current_context(silent=True)
|
|
192
|
+
return self._sensitive_values(ctx) if ctx is not None else ()
|
|
193
|
+
|
|
194
|
+
@abstractmethod
|
|
195
|
+
def handle(self, ctx: click.Context, **kwargs: Any) -> None:
|
|
196
|
+
"""Execute the command logic."""
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def console(self) -> Console:
|
|
200
|
+
return self._console
|
|
201
|
+
|
|
202
|
+
def filter_online_devices(self, devices: list[Device]) -> list[Device]:
|
|
203
|
+
"""Return only devices with ``ONLINE`` connectivity state.
|
|
204
|
+
|
|
205
|
+
Prints a warning for each skipped device so the operator knows
|
|
206
|
+
which devices were excluded. Raises :class:`click.ClickException`
|
|
207
|
+
if *no* devices are online.
|
|
208
|
+
"""
|
|
209
|
+
online: list[Device] = []
|
|
210
|
+
offline: list[Device] = []
|
|
211
|
+
for device in devices:
|
|
212
|
+
if device.connectivity_state == ConnectivityState.ONLINE:
|
|
213
|
+
online.append(device)
|
|
214
|
+
else:
|
|
215
|
+
offline.append(device)
|
|
216
|
+
|
|
217
|
+
for device in offline:
|
|
218
|
+
self.console.print(
|
|
219
|
+
f"[yellow]Skipping '{device.name}' "
|
|
220
|
+
f"(state: {device.connectivity_state}) "
|
|
221
|
+
f"— CLI commands require ONLINE devices.[/yellow]"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if not online:
|
|
225
|
+
raise click.ClickException(
|
|
226
|
+
"No online devices found. CLI commands can only be "
|
|
227
|
+
"executed on devices with connectivity state ONLINE."
|
|
228
|
+
)
|
|
229
|
+
return online
|
|
230
|
+
|
|
231
|
+
def wait_for_transaction(
|
|
232
|
+
self,
|
|
233
|
+
*,
|
|
234
|
+
config: ConfigLike,
|
|
235
|
+
transaction: CdoTransaction,
|
|
236
|
+
polling_interval_sec: int = DEFAULT_POLLING_INTERVAL_SEC,
|
|
237
|
+
spinner_text: str | None = None,
|
|
238
|
+
**kwargs: Any,
|
|
239
|
+
) -> CdoTransaction:
|
|
240
|
+
"""Poll a transaction until it reaches a terminal state, printing status on each poll.
|
|
241
|
+
|
|
242
|
+
Reads ``wait`` (default False) and ``timeout`` (default 3600) from
|
|
243
|
+
*kwargs* so callers can forward CLI kwargs directly.
|
|
244
|
+
Returns the original transaction unchanged when ``wait`` is False.
|
|
245
|
+
|
|
246
|
+
When *spinner_text* is provided, a checkmark line is printed
|
|
247
|
+
(e.g. ``\u2713 Triggering ASA upgrade\u2026``) and a live spinner
|
|
248
|
+
shows the latest transaction status in-place. All human-facing
|
|
249
|
+
output is written to *stderr* so that stdout remains clean for
|
|
250
|
+
machine-parseable formats like JSON.
|
|
251
|
+
"""
|
|
252
|
+
wait = cast(bool, kwargs.get("wait", False))
|
|
253
|
+
if not wait:
|
|
254
|
+
return transaction
|
|
255
|
+
if transaction.transaction_uid is None:
|
|
256
|
+
raise click.ClickException("Transaction UID missing \u2014 cannot poll for status.")
|
|
257
|
+
|
|
258
|
+
ctx = click.get_current_context()
|
|
259
|
+
silent = (ctx.obj or {}).get("silent", False)
|
|
260
|
+
|
|
261
|
+
stderr_console = Console(stderr=True)
|
|
262
|
+
|
|
263
|
+
if spinner_text and not silent:
|
|
264
|
+
stderr_console.print(f"[green]\u2713[/green] {spinner_text}")
|
|
265
|
+
stderr_console.print(f" [bold]Transaction UID:[/bold] {transaction.transaction_uid}")
|
|
266
|
+
if transaction.transaction_polling_url:
|
|
267
|
+
stderr_console.print(
|
|
268
|
+
f" [bold]Polling URL:[/bold] {transaction.transaction_polling_url}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
timeout_sec = cast(int, kwargs.get("timeout", DEFAULT_TRANSACTION_TIMEOUT_SEC))
|
|
272
|
+
service = TransactionService(config=config)
|
|
273
|
+
|
|
274
|
+
if silent:
|
|
275
|
+
return service.wait_for_transaction_to_finish(
|
|
276
|
+
transaction_uid=transaction.transaction_uid,
|
|
277
|
+
timeout_sec=timeout_sec,
|
|
278
|
+
polling_interval_sec=polling_interval_sec,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
spinner = Spinner("dots", text="Polling transaction status...")
|
|
282
|
+
|
|
283
|
+
def _update_status(t: CdoTransaction) -> None:
|
|
284
|
+
spinner.text = f"Status: {t.cdo_transaction_status}"
|
|
285
|
+
|
|
286
|
+
with Live(spinner, console=stderr_console, refresh_per_second=10, transient=True):
|
|
287
|
+
return service.wait_for_transaction_to_finish(
|
|
288
|
+
transaction_uid=transaction.transaction_uid,
|
|
289
|
+
timeout_sec=timeout_sec,
|
|
290
|
+
polling_interval_sec=polling_interval_sec,
|
|
291
|
+
on_poll=_update_status,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def is_failed_transaction(transaction: CdoTransaction) -> bool:
|
|
296
|
+
"""Return True if the transaction reached a terminal failure state."""
|
|
297
|
+
return transaction.cdo_transaction_status in (
|
|
298
|
+
CdoTransactionStatus.ERROR,
|
|
299
|
+
CdoTransactionStatus.CANCELLED,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def print_failed_transaction_details(
|
|
303
|
+
self,
|
|
304
|
+
cdo_transaction: CdoTransaction,
|
|
305
|
+
format: str = "table",
|
|
306
|
+
*,
|
|
307
|
+
sensitive_values: Sequence[str] | None = None,
|
|
308
|
+
) -> None:
|
|
309
|
+
sensitive_values = self._active_sensitive_values(sensitive_values)
|
|
310
|
+
if format == "json":
|
|
311
|
+
print_json(redact_data(cdo_transaction.to_dict(), sensitive_values))
|
|
312
|
+
else:
|
|
313
|
+
transaction_uid = redact_text(str(cdo_transaction.transaction_uid), sensitive_values)
|
|
314
|
+
transaction_status = redact_text(
|
|
315
|
+
str(cdo_transaction.cdo_transaction_status), sensitive_values
|
|
316
|
+
)
|
|
317
|
+
error_message = redact_text(str(cdo_transaction.error_message), sensitive_values)
|
|
318
|
+
transaction_details = redact_data(cdo_transaction.transaction_details, sensitive_values)
|
|
319
|
+
self.console.print("[yellow]The execution failed. Transaction Details:[/yellow]")
|
|
320
|
+
self.console.print("[bold]Transaction UID: [/bold]" f"{transaction_uid}")
|
|
321
|
+
self.console.print("[bold]Transaction Status: [/bold]" f"{transaction_status}")
|
|
322
|
+
self.console.print("[bold]Transaction Error Message: [/bold]" f"{error_message}")
|
|
323
|
+
self.console.print(
|
|
324
|
+
"[bold]Transaction Details: [/bold]\n" f"{json.dumps(transaction_details)}"
|
|
325
|
+
)
|
|
326
|
+
sys.exit(-1)
|
|
327
|
+
|
|
328
|
+
def print_submitted_transaction(
|
|
329
|
+
self, cdo_transaction: CdoTransaction, format: str = "table"
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Render a submitted (not-yet-completed) transaction."""
|
|
332
|
+
if format == "json":
|
|
333
|
+
print_json(cdo_transaction.to_dict())
|
|
334
|
+
else:
|
|
335
|
+
self.console.print("[green]✓[/green] Transaction submitted.")
|
|
336
|
+
self.console.print(f"[bold]Transaction UID:[/bold] {cdo_transaction.transaction_uid}")
|
|
337
|
+
self.console.print(f"[bold]Status:[/bold] {cdo_transaction.cdo_transaction_status}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Final, Sequence, cast
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from click.core import ParameterSource
|
|
13
|
+
from click_option_group import GroupedOption, OptionGroup
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from cisco_sccfm_cli.commands.base import BaseCommand
|
|
17
|
+
from cisco_sccfm_cli.models import Config
|
|
18
|
+
from cisco_sccfm_cli.option_metadata import sensitive_option
|
|
19
|
+
from cisco_sccfm_cli.services import ConfigService
|
|
20
|
+
from cisco_sccfm_core.constants import SCCFM_REGION_CHOICES, SCCFM_REGIONS, normalize_sccfm_region
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigureCommand(BaseCommand):
|
|
24
|
+
_API_TOKEN_ENVVAR: Final[str] = "SCCFM_API_TOKEN"
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
console: Console,
|
|
29
|
+
config_service: ConfigService | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(console)
|
|
32
|
+
self._config_service = config_service
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def name(self) -> str:
|
|
36
|
+
return "configure"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def help_text(self) -> str:
|
|
40
|
+
return "Configure API connectivity."
|
|
41
|
+
|
|
42
|
+
def build_params(self) -> Sequence[click.Parameter]:
|
|
43
|
+
profile_group = OptionGroup("Profile", help="Profile storage overrides.")
|
|
44
|
+
credential_group = OptionGroup(
|
|
45
|
+
"Credentials",
|
|
46
|
+
help="Region and API token settings.",
|
|
47
|
+
)
|
|
48
|
+
return [
|
|
49
|
+
GroupedOption(
|
|
50
|
+
["--config-path"],
|
|
51
|
+
type=click.Path(path_type=Path, resolve_path=False),
|
|
52
|
+
default=None,
|
|
53
|
+
envvar="SCCFM_CONFIG",
|
|
54
|
+
show_default=False,
|
|
55
|
+
help=("Path to the configuration file " "(defaults to ~/.sccfm-cli/config.json)."),
|
|
56
|
+
group=profile_group,
|
|
57
|
+
),
|
|
58
|
+
GroupedOption(
|
|
59
|
+
["--region"],
|
|
60
|
+
type=click.Choice(SCCFM_REGION_CHOICES, case_sensitive=False),
|
|
61
|
+
help=f"SCCFM region ({', '.join(SCCFM_REGIONS)})",
|
|
62
|
+
group=credential_group,
|
|
63
|
+
required=True,
|
|
64
|
+
),
|
|
65
|
+
sensitive_option(
|
|
66
|
+
GroupedOption(
|
|
67
|
+
["--api-token"],
|
|
68
|
+
type=str,
|
|
69
|
+
default=None,
|
|
70
|
+
envvar=self._API_TOKEN_ENVVAR,
|
|
71
|
+
show_envvar=True,
|
|
72
|
+
hide_input=True,
|
|
73
|
+
help=(
|
|
74
|
+
"API token for the chosen region. Passing it directly is supported for "
|
|
75
|
+
"compatibility but may expose it in process listings and shell history; "
|
|
76
|
+
f"prefer {self._API_TOKEN_ENVVAR} or the hidden prompt."
|
|
77
|
+
),
|
|
78
|
+
group=credential_group,
|
|
79
|
+
required=False,
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
def handle(self, ctx: click.Context, **kwargs: Any) -> None:
|
|
85
|
+
profile = ctx.obj["profile"]
|
|
86
|
+
region = kwargs["region"]
|
|
87
|
+
api_token = self._resolve_api_token(ctx=ctx, **kwargs)
|
|
88
|
+
config_path = kwargs["config_path"]
|
|
89
|
+
|
|
90
|
+
config_service = ConfigService(config_path)
|
|
91
|
+
normalized_region = normalize_sccfm_region(region)
|
|
92
|
+
assert normalized_region
|
|
93
|
+
config = Config(profile=profile, region=normalized_region, api_token=api_token)
|
|
94
|
+
config_service.save(config)
|
|
95
|
+
self.console.print(f"[green]Profile '{profile}' updated[/green]")
|
|
96
|
+
|
|
97
|
+
def _resolve_api_token(self, ctx: click.Context, **kwargs: Any) -> str:
|
|
98
|
+
api_token = cast(str | None, kwargs.get("api_token"))
|
|
99
|
+
source = ctx.get_parameter_source("api_token")
|
|
100
|
+
|
|
101
|
+
if api_token is None:
|
|
102
|
+
if not self._can_prompt():
|
|
103
|
+
ctx.fail(
|
|
104
|
+
"An API token is required. Set "
|
|
105
|
+
f"{self._API_TOKEN_ENVVAR} or run interactively for a hidden prompt."
|
|
106
|
+
)
|
|
107
|
+
api_token = self._prompt_sensitive("API token")
|
|
108
|
+
elif source is ParameterSource.COMMANDLINE:
|
|
109
|
+
click.echo(
|
|
110
|
+
"Warning: passing --api-token directly may expose it in process listings and "
|
|
111
|
+
f"shell history; prefer {self._API_TOKEN_ENVVAR} or the hidden prompt.",
|
|
112
|
+
err=True,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return self._validate_api_token(ctx=ctx, api_token=api_token)
|
|
116
|
+
|
|
117
|
+
def _validate_api_token(self, ctx: click.Context, api_token: str) -> str:
|
|
118
|
+
if not api_token.strip():
|
|
119
|
+
ctx.fail("The API token cannot be empty.")
|
|
120
|
+
return api_token
|
|
121
|
+
|
|
122
|
+
def _can_prompt(self) -> bool:
|
|
123
|
+
return sys.stdin.isatty()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from cisco_sccfm_cli.commands.base import BaseCommand
|
|
13
|
+
from cisco_sccfm_cli.commands.inventory.devices import DevicesCommand
|
|
14
|
+
from cisco_sccfm_cli.commands.inventory.manager import ManagersCommand
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InventoryCommand(BaseCommand):
|
|
18
|
+
def __init__(self, console: Console) -> None:
|
|
19
|
+
super().__init__(console)
|
|
20
|
+
self._devices_command = DevicesCommand(console)
|
|
21
|
+
self._managers_command = ManagersCommand(console)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
return "inventory"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def help_text(self) -> str:
|
|
29
|
+
return "Browse SCC Firewall Management inventory."
|
|
30
|
+
|
|
31
|
+
def build(self) -> click.Command:
|
|
32
|
+
group = click.Group(name=self.name, help=self.help_text)
|
|
33
|
+
group.add_command(self._devices_command.build())
|
|
34
|
+
group.add_command(self._managers_command.build())
|
|
35
|
+
return group
|
|
36
|
+
|
|
37
|
+
def handle(self, ctx: click.Context, **kwargs: Any) -> None: # pragma: no cover
|
|
38
|
+
ctx.fail("Specify a subcommand: devices or manager")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
__all__ = ["AsaChangeBootImageCommand"]
|
|
6
|
+
|
|
7
|
+
from cisco_sccfm_cli.commands.inventory.devices.asa.change_boot_image.command import (
|
|
8
|
+
AsaChangeBootImageCommand,
|
|
9
|
+
)
|