diff --git a/.env.example b/.env.example deleted file mode 100644 index 74017272..00000000 --- a/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -# Copy this file to .env and fill in your values -# The .env file is gitignored and loaded automatically by direnv - -# SCCFM region: int (staging), us, eu, apj, au, uae, in, ci -export SCCFM_REGION=int - -# Your SCCFM API token (get from SCCFM UI > Settings > API Tokens) -export SCCFM_API_TOKEN="your-api-token-here" diff --git a/.envrc b/.envrc index 91ad98f0..86241311 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1 @@ source .venv/bin/activate -dotenv_if_exists .env diff --git a/.gitignore b/.gitignore index f140bbb6..06d85f85 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ results/ /.vscode/ /.env /.env.* -!/.env.example /.tox/ /.eggs/ .poetry_cache/ diff --git a/AGENTS.md b/AGENTS.md index 94fe6086..0899507d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ This repository ships skill files that document how to interact with the CLI and source cisco_sccfm_scripts/activate.sh # Configure credentials once -sccfm-cli configure --region us --api-token +sccfm-cli configure --region us # securely prompts for the token # Check connectivity sccfm-cli status @@ -58,19 +58,18 @@ sccfm-cli status sccfm-cli inventory devices list --format table # Interactive developer menu (test, lint, format, build collection, etc.) -devkit +sccfm-cli-interactive ``` -## Required environment variables +## Credential configuration -Copy `.env.example` to `.env` and fill in your values (loaded automatically by direnv): +The only SCCFM token configuration source is the named profile store: ```bash -export SCCFM_REGION=us # int | us | eu | apj | au | uae | in | ci -export SCCFM_API_TOKEN="..." # from SCCFM UI > Settings > API Tokens +sccfm-cli --profile default configure --region us ``` -Credentials are also stored under `~/.sccfm-cli/` after running `sccfm-cli configure`. Override the path with `--config-path` or `SCCFM_CONFIG`. +Profiles are stored in `~/.sccfm-cli/config.json` with owner-only permissions. Override the path with `--config-path` or `SCCFM_CONFIG`. Do not configure SCCFM tokens through `.env`, inline Ansible parameters, or Ansible Vault. Vault remains appropriate for Ansible-specific device secrets. ## Testing instructions @@ -101,8 +100,8 @@ No MCP servers are currently configured for this project. Skill files under `ski # Build and install locally build-ansible-collection -# Set up tokens and vault -devkit # select "change-tokens" +# Configure or select profiles interactively +sccfm-cli-interactive # Verify inventory plugin ansible-inventory -i sccfm-ansible/examples/inventory.sccfm.yml --graph @@ -122,7 +121,7 @@ Add `sccfm-ansible` to `ANSIBLE_COLLECTIONS_PATH` so IDE/mypy resolves `ansible_ git cz # or: ./cisco_sccfm_scripts/cz.sh commit ``` CI will fail on non-compliant commit messages. -- **Security**: Never commit real credentials, tokens, or secrets. Use placeholders and document required env vars. See [SECURITY.md](SECURITY.md) for vulnerability reporting. +- **Security**: Never commit real credentials, tokens, or secrets. Use placeholders and document the canonical profile flow. See [SECURITY.md](SECURITY.md) for vulnerability reporting. - New commands go in `cisco_sccfm_cli/commands/` as a `BaseCommand` subclass, registered in `cisco_sccfm_cli/cli.py`. - New SDK integrations go in `cisco_sccfm_core/services/`. - Every behavior change must be accompanied by tests. @@ -138,5 +137,5 @@ Add `sccfm-ansible` to `ANSIBLE_COLLECTIONS_PATH` so IDE/mypy resolves `ansible_ # # SPDX-License-Identifier: Apache-2.0 ``` -- **Secrets**: never read or commit `.env`, `.env.*`, `.vault_pass`, or real `vault.yml` files — use the `*.example` templates. Keep tracked `.envrc` files secret-free. `gitleaks` and `detect-private-key` block secrets in pre-commit. +- **Secrets**: never read or commit `.vault_pass`, real `vault.yml` files, or SCCFM profile files. Use the `*.example` vault templates for Ansible-specific secrets. Keep tracked `.envrc` files secret-free. `gitleaks` and `detect-private-key` block secrets in pre-commit. - See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contribution guide. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42afc396..de935827 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,13 +50,12 @@ reserve breaking changes until the next major version release. direnv allow ``` -3. Set up your SCCFM credentials: +3. Set up your SCCFM profile: ```bash - cp .env.example .env - # Edit .env with your API token + sccfm-cli configure --region us # securely prompts for the token ``` -Now whenever you `cd` into the project, the virtualenv activates and env vars load automatically. +Now whenever you `cd` into the project, the virtualenv activates automatically. ## Committing Changes diff --git a/INSTALL.md b/INSTALL.md index 432d2f90..1e06e970 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -133,18 +133,14 @@ After sourcing, tab completion will work for all `sccfm` commands and options. The same PyPI package exposes the typed `cisco_sccfm_core` library for Python automation: ```python -from dataclasses import dataclass - from cisco_sccfm_core import InventoryService +from cisco_sccfm_core.services import ProfileService +profile = ProfileService().load("default") +if profile is None: + raise RuntimeError("Configure the default profile with sccfm-cli configure") -@dataclass(frozen=True) -class Config: - region: str - api_token: str - - -inventory = InventoryService(Config(region="us", api_token="...")) +inventory = InventoryService(profile) devices = inventory.get_devices(limit=10, offset=0, query=None) ``` @@ -174,19 +170,19 @@ ansible-galaxy collection list | grep cisco.sccfm ### Try out examples -The fastest way to get going is to use the interactive devkit menu: +The fastest way to get going is to use the interactive CLI menu: ```bash -devkit -# select "change-tokens" from the menu +sccfm-cli-interactive +# select "configure-profile" from the menu ``` -Or run the token setup directly: +Or configure the canonical profile directly: ```bash -change-tokens +sccfm-cli configure --region us # securely prompts for the token ``` -This will prompt for your region, API token, and vault password, then create all the required files (.env, vars.yml, vault.yml). +The profile is shared by `sccfm-cli`, `sccfm-cli-interactive`, and the `cisco.sccfm` Ansible collection. Ansible Vault remains available separately for managed-device passwords and other playbook-specific secrets. See the [Trying out examples](sccfm-ansible/README.md#trying-out-examples) section in the Ansible collection README for the full walkthrough including how to run playbooks. diff --git a/README.md b/README.md index 858e8fcb..e7309f7c 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,14 @@ Python scripts, and collection can reuse the same SDK integrations. cisco_sccfm_scripts/setup_environment.sh # installs pyenv, Python 3.12.4, Poetry deps source cisco_sccfm_scripts/activate.sh # activates the project virtualenv sccfm-cli --help # the main SCCFM CLI -devkit # interactive developer toolkit menu +sccfm-cli-interactive # interactive CLI and developer workflow menu ``` `setup_environment.sh` keeps everything local to the repository: pyenv provides Python 3.12.4, `.venv/` hosts the runtime, and Poetry installs the project plus dev dependencies. ## Commands -- `sccfm-cli configure [--region REGION] [--api-token TOKEN] [--config-path PATH]`: Captures the SCCFM region (`int`, `us`, `eu`, `apj`, `au`, `uae`, `in`, or `ci`) plus an API token (see the [auth guide](https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/authentication/)) and stores it under `~/.sccfm-cli/` (override with `--config-path` or `SCCFM_CONFIG`). +- `sccfm-cli configure [--region REGION] [--api-token TOKEN] [--config-path PATH]`: Captures the SCCFM region (`int`, `us`, `eu`, `apj`, `au`, `uae`, `in`, or `ci`) plus an API token (see the [auth guide](https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/authentication/)) in the canonical profile store at `~/.sccfm-cli/config.json`. The directory is restricted to the current user (`0700`) and the file to owner read/write (`0600`). Override the path with `--config-path` or `SCCFM_CONFIG`. - `sccfm-cli status [--config-path PATH]`: Shows the current profile plus SCCFM connectivity health using Rich tables. - `sccfm-cli inventory devices list [--limit N] [--offset N] [--query TEXT] [--format table|json]`: Lists device inventory with pagination and optional name filtering. - `sccfm-cli inventory manager list [--limit N] [--offset N] [--query TEXT] [--format table|json]`: Lists manager inventory with the same filters. @@ -69,18 +69,14 @@ Installing the `cisco-sccfm-devkit` package also exposes `cisco_sccfm_core`, a t Python automation library built on top of the generated `scc-firewall-manager-sdk`. ```python -from dataclasses import dataclass - from cisco_sccfm_core import InventoryService +from cisco_sccfm_core.services import ProfileService +profile = ProfileService().load("default") +if profile is None: + raise RuntimeError("Configure the default profile with sccfm-cli configure") -@dataclass(frozen=True) -class Config: - region: str - api_token: str - - -inventory = InventoryService(Config(region="us", api_token="...")) +inventory = InventoryService(profile) devices = inventory.get_devices(limit=10, offset=0, query=None) ``` @@ -91,27 +87,32 @@ The package root exports the supported public service classes and response model - macOS: `brew install ansible` (this includes `ansible-galaxy`; verify with `ansible-galaxy --version`). - Build and install the collection locally: `build-ansible-collection`. -- Set up tokens interactively: `devkit` and select **change-tokens** (saves your API token, creates `.env`, `.vault_pass`, encrypts `group_vars/all/vault.yml`, and sets the region). +- Configure profiles interactively: run `sccfm-cli-interactive` and select **configure-profile**. - For IDEs/mypy, add `sccfm-ansible` to `ANSIBLE_COLLECTIONS_PATH` (or mark it as a source root) so imports under `ansible_collections.cisco.sccfm` resolve without installing. -- Configure SCCFM region (`int`, `us`, `eu`, `apj`, `au`, `uae`, `in`, or `ci`) plus `SCCFM_API_TOKEN`; you can set them via env vars or inline (i.e., write the values directly in the inventory file—useful for local dev, but prefer env vars or Ansible Vault for anything shared). +- Ansible modules and inventory select the same named SCCFM profile; they do not duplicate its region or API token in environment variables, playbooks, or Ansible Vault. +- Keep Ansible Vault for playbook-specific secrets such as managed-device passwords. - Point Ansible at an inventory file that uses the plugin, e.g. `ansible-inventory -i sccfm-ansible/examples/inventory.sccfm.yml --graph`. - A starter playbook is in `sccfm-ansible/examples/show_devices.yml`; it runs against the SCCFM devices discovered by the inventory plugin. - Generated Ansible reference docs can be previewed locally with `generate-ansible-docs`; see [docs/README.md](docs/README.md) for details. ## Development -All common development tasks are available through the interactive `devkit` menu: +All common development tasks are available through the interactive CLI menu: ```bash source cisco_sccfm_scripts/activate.sh -devkit +sccfm-cli-interactive ``` This presents an interactive selector with the following tasks: | Task | Description | |------|-------------| -| **change-tokens** | Set up SCCFM API tokens, .env, and Ansible Vault | +| **configure-profile** | Create or replace a canonical SCCFM profile | +| **manage-profiles** | Update or remove SCCFM profiles | +| **import-legacy-vault** | Copy profiles from the former vault token store without modifying it | +| **run-cli** | Discover and run an `sccfm-cli` command interactively | +| **run-ansible** | Select and run an example playbook | | **build-collection** | Build the cisco.sccfm Ansible collection tarball | | **generate-ansible-docs** | Generate Ansible reference docs from ansible-doc output | | **generate-cli-docs** | Generate CLI reference docs from Click help output | diff --git a/cisco_sccfm_cli/commands/configure.py b/cisco_sccfm_cli/commands/configure.py index 95e3ff4a..40580a4b 100644 --- a/cisco_sccfm_cli/commands/configure.py +++ b/cisco_sccfm_cli/commands/configure.py @@ -62,6 +62,8 @@ def build_params(self) -> Sequence[click.Parameter]: help="API token for the chosen region", group=credential_group, required=True, + prompt="API token", + hide_input=True, ), ] diff --git a/cisco_sccfm_cli/commands/tests/test_configure.py b/cisco_sccfm_cli/commands/tests/test_configure.py index 9d7bdd6f..98f57a64 100644 --- a/cisco_sccfm_cli/commands/tests/test_configure.py +++ b/cisco_sccfm_cli/commands/tests/test_configure.py @@ -114,3 +114,25 @@ def test_should_normalize_legacy_region_aliases(cli_runner: CliRunner, config_pa stored = ConfigService(path=config_path).load("lab") assert stored is not None assert stored.region == "au" + + +def test_should_prompt_for_token_without_echoing_it( + cli_runner: CliRunner, config_path: Path +) -> None: + result = cli_runner.invoke( + cli, + [ + "configure", + "--region", + "us", + "--config-path", + str(config_path), + ], + input="prompted-secret\n", + ) + + assert result.exit_code == 0 + assert "prompted-secret" not in result.output + stored = ConfigService(path=config_path).load("default") + assert stored is not None + assert stored.api_token == "prompted-secret" diff --git a/cisco_sccfm_cli/commands/tests/test_sccfm_cli_skill.py b/cisco_sccfm_cli/commands/tests/test_sccfm_cli_skill.py index 73071638..42442302 100644 --- a/cisco_sccfm_cli/commands/tests/test_sccfm_cli_skill.py +++ b/cisco_sccfm_cli/commands/tests/test_sccfm_cli_skill.py @@ -79,6 +79,10 @@ def test_cisco_sccfm_cli_skill_should_cover_schema_driven_operation() -> None: for fragment in expected_fragments: assert fragment in body + assert "sccfm-cli-interactive" in body + assert "SCCFM_API_TOKEN" not in body + assert "SCCFM_REGION" not in body + def test_cisco_sccfm_cli_skill_should_reference_fields_emitted_by_schema( cli_runner: CliRunner, diff --git a/cisco_sccfm_cli/e2e/README.md b/cisco_sccfm_cli/e2e/README.md index 4dd80434..8c495b0a 100644 --- a/cisco_sccfm_cli/e2e/README.md +++ b/cisco_sccfm_cli/e2e/README.md @@ -1,3 +1,16 @@ + + +## Table of Contents + +- [sccfm-cli E2E Integration Tests](#sccfm-cli-e2e-integration-tests) + - [Structure](#structure) + - [Why This Shape](#why-this-shape) + - [Prerequisites](#prerequisites) + - [Running](#running) + - [Opt-in upgrade phases](#opt-in-upgrade-phases) + + + # sccfm-cli E2E Integration Tests Tenant-backed integration tests for the `sccfm-cli` binary. The suite mirrors `sccfm-ansible/e2e/` 1:1 so the same scenarios are exercised through both surfaces. @@ -22,17 +35,17 @@ Tenant-backed integration tests for the `sccfm-cli` binary. The suite mirrors ` - Jenkins gets one test case per lifecycle phase instead of one large pass/fail result. - Phases shell out to the installed `sccfm-cli` entrypoint, so the suite exercises argv parsing, exit codes, and stdout/stderr the way real users see them — exactly the contract that unit tests with `CliRunner` skip. - Test data per suite lives in one file (`phases/test_data.py`), reducing drift between create / verify / update / delete phases. -- Credentials reuse the Ansible suite's vault (`cisco_sccfm_scripts/setup_tokens.py`). One CI bootstrap, two test surfaces. +- Credentials use the same canonical named profile as the CLI and Ansible collection. ## Prerequisites 1. Run the credential bootstrap once: ``` - poetry run change-tokens + sccfm-cli --profile default configure --region ci ``` - This creates `sccfm-ansible/examples/.vault_pass` and an encrypted `vault.yml`. + Create `sccfm-ansible/examples/.vault_pass` and encrypted `vault.yml` separately only when the test workflow needs Ansible-specific device secrets. 2. Install dev dependencies so `ansible-vault` is available for the runner to decode the vault: diff --git a/cisco_sccfm_cli/e2e/_profile.py b/cisco_sccfm_cli/e2e/_profile.py index 3004b1af..09c9d82c 100644 --- a/cisco_sccfm_cli/e2e/_profile.py +++ b/cisco_sccfm_cli/e2e/_profile.py @@ -2,33 +2,16 @@ # # SPDX-License-Identifier: Apache-2.0 -"""Bootstrap a temporary sccfm-cli profile from the Ansible vault. - -The Ansible e2e suite already manages tenant credentials via -``examples/group_vars/all/vault.yml`` (encrypted with ``.vault_pass``). -This module reuses that single source of truth: it shells out to -``ansible-vault view`` to decrypt the vault, reads the region from the -plain ``vars.yml``, and writes a temp ``sccfm-cli`` profile via the -canonical :class:`ConfigService.save` writer. Tests then point the CLI -at the temp config via ``SCCFM_CONFIG``. -""" +"""Resolve the canonical SCCFM profile used by the CLI e2e suite.""" from __future__ import annotations import os -import subprocess -import sys from dataclasses import dataclass from pathlib import Path -from typing import Any - -import yaml from cisco_sccfm_cli.e2e._state import PhaseStateStore -from cisco_sccfm_cli.models import Config -from cisco_sccfm_cli.services import ConfigService - -E2E_PROFILE_NAME = "e2e" +from cisco_sccfm_core.services.profile_service import ProfileService @dataclass(frozen=True) @@ -39,94 +22,21 @@ class ProfileContext: state: PhaseStateStore -def _repo_root() -> Path: - return Path(__file__).resolve().parents[2] - - -def _default_examples_dir() -> Path: - return _repo_root() / "sccfm-ansible" / "examples" - - -def _resolve_path(env_var: str, fallback: Path) -> Path: - override = os.environ.get(env_var) - if override: - return Path(override) - return fallback - - -def _decode_vault(vault_file: Path, vault_pass: Path) -> dict[str, Any]: - cmd = [ - sys.executable, - "-m", - "ansible.cli.vault", - "view", - str(vault_file), - "--vault-password-file", - str(vault_pass), - ] - completed = subprocess.run(cmd, capture_output=True, text=True, check=False) - if completed.returncode != 0: +def resolve_profile() -> ProfileContext: + """Load the configured e2e profile without copying its token.""" + profile_name = os.environ.get("SCCFM_E2E_PROFILE", "default") + config_path = Path( + os.environ.get("SCCFM_CONFIG", str(Path.home() / ".sccfm-cli" / "config.json")) + ).expanduser() + profile = ProfileService(config_path).load(profile_name) + if profile is None: raise RuntimeError( - f"ansible-vault view failed (rc={completed.returncode}):\n" - f"--- stdout ---\n{completed.stdout}\n" - f"--- stderr ---\n{completed.stderr}" + f"E2E profile '{profile_name}' not found in {config_path}. " + f"Run 'sccfm-cli --profile {profile_name} configure' first." ) - parsed = yaml.safe_load(completed.stdout) or {} - if not isinstance(parsed, dict): - raise RuntimeError(f"Unexpected vault payload type: {type(parsed).__name__}") - return parsed - - -def _load_plain_vars(vars_file: Path) -> dict[str, Any]: - with vars_file.open("r", encoding="utf-8") as handle: - parsed = yaml.safe_load(handle) or {} - if not isinstance(parsed, dict): - raise RuntimeError(f"Unexpected vars.yml payload type: {type(parsed).__name__}") - return parsed - - -def bootstrap_profile(config_dir: Path) -> ProfileContext: - """Decode the vault and write a temp sccfm-cli profile. - - Raises a clear error pointing at ``cisco_sccfm_scripts/setup_tokens.py`` when the - vault inputs are missing, mirroring the Ansible runner's preflight. - """ - examples_dir = _default_examples_dir() - vault_file = _resolve_path("SCCFM_E2E_VAULT_FILE", examples_dir / "group_vars/all/vault.yml") - vault_pass = _resolve_path("SCCFM_E2E_VAULT_PASS", examples_dir / ".vault_pass") - vars_file = _resolve_path("SCCFM_E2E_VARS_FILE", examples_dir / "group_vars/all/vars.yml") - - for label, path in ( - ("vault file", vault_file), - ("vault password file", vault_pass), - ("vars file", vars_file), - ): - if not path.exists(): - raise RuntimeError( - f"E2E credential bootstrap: {label} not found at {path}. " - "Run cisco_sccfm_scripts/setup_tokens.py first." - ) - - plain_vars = _load_plain_vars(vars_file) - vault_vars = _decode_vault(vault_file, vault_pass) - - region = plain_vars.get("sccfm_region") - api_token = vault_vars.get("sccfm_api_token") - if not region: - raise RuntimeError(f"sccfm_region missing from {vars_file}") - if not api_token: - raise RuntimeError(f"sccfm_api_token missing from {vault_file}") - - config_dir.mkdir(parents=True, exist_ok=True) - config_path = config_dir / "config.json" - ConfigService(path=config_path).save( - Config(profile=E2E_PROFILE_NAME, region=region, api_token=api_token) - ) - - state = PhaseStateStore() return ProfileContext( - profile=E2E_PROFILE_NAME, + profile=profile.profile, config_path=config_path, - region=region, - state=state, + region=profile.region, + state=PhaseStateStore(), ) diff --git a/cisco_sccfm_cli/e2e/conftest.py b/cisco_sccfm_cli/e2e/conftest.py index 49dbf2cc..346c3e13 100644 --- a/cisco_sccfm_cli/e2e/conftest.py +++ b/cisco_sccfm_cli/e2e/conftest.py @@ -13,13 +13,11 @@ from __future__ import annotations -import shutil -from pathlib import Path from typing import Final, Generator import pytest -from cisco_sccfm_cli.e2e._profile import ProfileContext, bootstrap_profile +from cisco_sccfm_cli.e2e._profile import ProfileContext, resolve_profile _SUITE_ORDER: Final[tuple[str, ...]] = ( "objects", @@ -45,20 +43,10 @@ def _suite_key(index_item: tuple[int, pytest.Item]) -> tuple[int, int]: @pytest.fixture(scope="session") -def e2e_profile(tmp_path_factory: pytest.TempPathFactory) -> Generator[ProfileContext, None, None]: - """Decode the Ansible vault and write a fresh ``e2e`` profile. - - Session-scoped so all four suites share the same temp profile and - state store. The config holds the decrypted tenant API token, so the - temp directory is removed eagerly on teardown rather than relying on - ``tmp_path_factory`` retention (pytest keeps the last few runs by - default, which would leave a live token on disk). Tenant-side cleanup - is each suite's ``cleanup`` phase. - """ - config_dir: Path = tmp_path_factory.mktemp("sccfm-cli-e2e") - ctx = bootstrap_profile(config_dir) +def e2e_profile() -> Generator[ProfileContext, None, None]: + """Use the canonical configured profile for all e2e suites.""" + ctx = resolve_profile() try: yield ctx finally: ctx.state.clear() - shutil.rmtree(config_dir, ignore_errors=True) diff --git a/cisco_sccfm_cli/e2e/playbooks/onboard_vasa.yml b/cisco_sccfm_cli/e2e/playbooks/onboard_vasa.yml index 5ec864c9..f362afa3 100644 --- a/cisco_sccfm_cli/e2e/playbooks/onboard_vasa.yml +++ b/cisco_sccfm_cli/e2e/playbooks/onboard_vasa.yml @@ -14,16 +14,12 @@ hosts: localhost gather_facts: false - # sccfm_region / sccfm_api_token are supplied by run_e2e.sh via - # -e "@${VARS_FILE}" -e "@${VAULT_FILE}". Do NOT add vars_files here: - # those are loaded/decrypted at parse time before the -e overrides - # apply, which would force the default example files to exist and share - # the same vault password even when the runner points elsewhere. - module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default + + vars: + profile_region: "{{ lookup('cisco.sccfm.profile', 'default', field='region') }}" tasks: # Onboarding here must agree with removal in remove_vasa.yml, whose @@ -33,10 +29,10 @@ - name: Assert this is the CI region ansible.builtin.assert: that: - - (sccfm_region | default('')) == 'ci' + - profile_region == 'ci' fail_msg: >- vASA onboarding only supports the 'ci' region (removal is hard-coded - to the CI host); got sccfm_region='{{ sccfm_region | default('(unset)') }}'. + to the CI host); got profile region '{{ profile_region }}'. - name: Validate required environment variables ansible.builtin.assert: diff --git a/cisco_sccfm_cli/e2e/playbooks/remove_vasa.yml b/cisco_sccfm_cli/e2e/playbooks/remove_vasa.yml index 9c661cec..bfd99e1d 100644 --- a/cisco_sccfm_cli/e2e/playbooks/remove_vasa.yml +++ b/cisco_sccfm_cli/e2e/playbooks/remove_vasa.yml @@ -11,14 +11,10 @@ hosts: localhost gather_facts: false - # sccfm_region / sccfm_api_token are supplied by run_e2e.sh via - # -e "@${VARS_FILE}" -e "@${VAULT_FILE}". Do NOT add vars_files here: - # those are loaded/decrypted at parse time before the -e overrides - # apply, which would force the default example files to exist and share - # the same vault password even when the runner points elsewhere. - vars: sccfm_api_base: "https://ci.manage.security.cisco.com/api/rest" + profile_region: "{{ lookup('cisco.sccfm.profile', 'default', field='region') }}" + profile_token: "{{ lookup('cisco.sccfm.profile', 'default', field='api_token') }}" asa_test_query_all: "name:ci-e2e-cli-asa-*" tasks: @@ -30,30 +26,32 @@ - name: Assert this is the CI region ansible.builtin.assert: that: - - (sccfm_region | default('')) == 'ci' + - profile_region == 'ci' fail_msg: >- remove_vasa.yml only supports the 'ci' region (sccfm_api_base is - hard-coded to the CI host); got sccfm_region='{{ sccfm_region | default('(unset)') }}'. + hard-coded to the CI host); got profile region '{{ profile_region }}'. - name: Find CI CLI vASA devices ansible.builtin.uri: url: "{{ sccfm_api_base }}/v1/inventory/devices?limit=50&offset=0&q={{ asa_test_query_all | urlencode }}" method: GET headers: - Authorization: "Bearer {{ sccfm_api_token }}" + Authorization: "Bearer {{ profile_token }}" Content-Type: "application/json" status_code: [200] register: device_list + no_log: true - name: Delete each CI CLI vASA device ansible.builtin.uri: url: "{{ sccfm_api_base }}/v1/inventory/devices/{{ item.uid }}" method: DELETE headers: - Authorization: "Bearer {{ sccfm_api_token }}" + Authorization: "Bearer {{ profile_token }}" Content-Type: "application/json" status_code: [200, 202, 204, 404] loop: "{{ device_list.json['items'] | default([]) }}" loop_control: label: "{{ item.name }} ({{ item.uid }})" when: device_list.json['items'] | default([]) | length > 0 + no_log: true diff --git a/cisco_sccfm_cli/e2e/run_e2e.sh b/cisco_sccfm_cli/e2e/run_e2e.sh index e6e249a2..e16feaea 100755 --- a/cisco_sccfm_cli/e2e/run_e2e.sh +++ b/cisco_sccfm_cli/e2e/run_e2e.sh @@ -13,7 +13,7 @@ # and block our ASA CLI script pushes. # # Prerequisites: -# - Run cisco_sccfm_scripts/setup_tokens.py first (creates vault.yml and .vault_pass) +# - Configure the selected profile with sccfm-cli configure # - Virtualenv active (or poetry will manage one) # - For onboarding: ASA_HOST + VASA_PASSWORD env vars # @@ -27,8 +27,6 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" COLLECTION_DIR="${REPO_ROOT}/sccfm-ansible" EXAMPLES_DIR="${COLLECTION_DIR}/examples" PLAYBOOKS_DIR="${SCRIPT_DIR}/playbooks" -VAULT_PASS="${VAULT_PASS:-${EXAMPLES_DIR}/.vault_pass}" -VAULT_FILE="${VAULT_FILE:-${EXAMPLES_DIR}/group_vars/all/vault.yml}" VARS_FILE="${VARS_FILE:-${EXAMPLES_DIR}/group_vars/all/vars.yml}" RESULTS_DIR="${RESULTS_DIR:-${REPO_ROOT}/results}" @@ -38,16 +36,6 @@ if ! command -v poetry >/dev/null 2>&1; then fi # ── Preflight checks ────────────────────────────────────────────── -if [[ ! -f "${VAULT_PASS}" ]]; then - echo "ERROR: ${VAULT_PASS} not found. Run cisco_sccfm_scripts/setup_tokens.py first." >&2 - exit 1 -fi - -if [[ ! -f "${VAULT_FILE}" ]]; then - echo "ERROR: ${VAULT_FILE} not found. Run cisco_sccfm_scripts/setup_tokens.py first." >&2 - exit 1 -fi - if [[ ! -f "${VARS_FILE}" ]]; then echo "ERROR: ${VARS_FILE} not found." >&2 exit 1 @@ -65,9 +53,7 @@ remove_cli_vasa() { echo "Removing CLI-dedicated vASA..." poetry run ansible-playbook \ "${PLAYBOOKS_DIR}/remove_vasa.yml" \ - --vault-password-file "${VAULT_PASS}" \ -e "@${VARS_FILE}" \ - -e "@${VAULT_FILE}" \ || echo "WARNING: vASA removal failed; continuing." >&2 fi } @@ -84,9 +70,7 @@ if [[ -n "${ASA_HOST:-}" && -n "${VASA_PASSWORD:-}" ]]; then echo "Onboarding CLI-dedicated vASA (ci-e2e-cli-asa-${ASA_HOST//[^a-zA-Z0-9]/-})..." poetry run ansible-playbook \ "${PLAYBOOKS_DIR}/onboard_vasa.yml" \ - --vault-password-file "${VAULT_PASS}" \ - -e "@${VARS_FILE}" \ - -e "@${VAULT_FILE}" + -e "@${VARS_FILE}" else echo "ASA_HOST/VASA_PASSWORD not set; skipping vASA onboarding." \ "Tests will use whatever devices already match ci-e2e-cli-asa-*." @@ -96,10 +80,6 @@ fi echo "Running sccfm-cli e2e tests..." mkdir -p "${RESULTS_DIR}" -export SCCFM_E2E_VAULT_FILE="${VAULT_FILE}" -export SCCFM_E2E_VAULT_PASS="${VAULT_PASS}" -export SCCFM_E2E_VARS_FILE="${VARS_FILE}" - poetry run python -m pytest "${SCRIPT_DIR}" \ -v \ --tb=short \ diff --git a/cisco_sccfm_cli/models/config.py b/cisco_sccfm_cli/models/config.py index 9339f69f..46b65e99 100644 --- a/cisco_sccfm_cli/models/config.py +++ b/cisco_sccfm_cli/models/config.py @@ -2,13 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations +from cisco_sccfm_core.models.profile import Profile as Config -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Config: - profile: str - region: str - api_token: str +__all__ = ["Config"] diff --git a/cisco_sccfm_cli/services/config_service.py b/cisco_sccfm_cli/services/config_service.py index faea8673..c6c5532b 100644 --- a/cisco_sccfm_cli/services/config_service.py +++ b/cisco_sccfm_cli/services/config_service.py @@ -2,56 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations +from cisco_sccfm_core.services.profile_service import ProfileService as ConfigService -import json -from pathlib import Path -from typing import Any, Dict, Mapping - -from cisco_sccfm_cli.models import Config - -_CONFIG_DIR = Path.home() / ".sccfm-cli" -_CONFIG_FILE = _CONFIG_DIR / "config.json" - - -class ConfigService: - def __init__(self, path: Path | None = None) -> None: - self._path = path or _CONFIG_FILE - - def load(self, profile: str) -> Config | None: - profiles = self._load_profiles() - profile_data = profiles.get(profile) - if not profile_data: - return None - return Config( - profile=profile, - region=profile_data["region"], - api_token=profile_data["api_token"], - ) - - def save(self, config: Config) -> None: - profiles = self._load_profiles() - profiles[config.profile] = { - "region": config.region, - "api_token": config.api_token, - } - self._persist({"profiles": profiles}) - - def list_profiles(self) -> list[Config]: - profiles = self._load_profiles() - return [ - Config(profile=name, region=data["region"], api_token=data["api_token"]) - for name, data in sorted(profiles.items()) - ] - - def _load_profiles(self) -> Dict[str, Dict[str, Any]]: - if not self._path.exists(): - return {} - with self._path.open("r", encoding="utf-8") as handle: - data = json.load(handle) - return dict(data.get("profiles", {})) - - def _persist(self, payload: Mapping[str, Any]) -> None: - self._path.parent.mkdir(parents=True, exist_ok=True) - with self._path.open("w", encoding="utf-8") as handle: - json.dump(payload, handle, indent=2) +__all__ = ["ConfigService"] diff --git a/cisco_sccfm_cli/services/tests/test_config_service.py b/cisco_sccfm_cli/services/tests/test_config_service.py index 7a13330e..591cb69e 100644 --- a/cisco_sccfm_cli/services/tests/test_config_service.py +++ b/cisco_sccfm_cli/services/tests/test_config_service.py @@ -4,8 +4,11 @@ from __future__ import annotations +import json from pathlib import Path +from _pytest.monkeypatch import MonkeyPatch + from cisco_sccfm_cli.models import Config from cisco_sccfm_cli.services import ConfigService @@ -32,3 +35,62 @@ def test_should_list_all_profiles(tmp_path: Path) -> None: profiles = service.list_profiles() assert profiles == [expected] + + +def test_should_harden_config_file_permissions(tmp_path: Path) -> None: + config_path = tmp_path / "profiles" / "config.json" + service = ConfigService(path=config_path) + + service.save(Config(profile="default", region="us", api_token="secret-token")) + + assert config_path.stat().st_mode & 0o777 == 0o600 + assert config_path.parent.stat().st_mode & 0o777 == 0o700 + + +def test_should_harden_existing_config_file_on_open(tmp_path: Path) -> None: + config_path = tmp_path / "config.json" + config_path.write_text('{"profiles": {}}\n') + config_path.chmod(0o644) + + ConfigService(path=config_path) + + assert config_path.stat().st_mode & 0o777 == 0o600 + + +def test_should_replace_config_atomically_without_leaving_temporary_files( + tmp_path: Path, +) -> None: + config_path = tmp_path / "config.json" + service = ConfigService(path=config_path) + service.save(Config(profile="default", region="us", api_token="first-token")) + + service.save(Config(profile="default", region="eu", api_token="second-token")) + + assert json.loads(config_path.read_text())["profiles"]["default"] == { + "region": "eu", + "api_token": "second-token", + } + assert list(tmp_path.glob(".config.json.*.tmp")) == [] + + +def test_should_remove_profile(tmp_path: Path) -> None: + service = ConfigService(path=tmp_path / "config.json") + service.save(Config(profile="default", region="us", api_token="default-token")) + service.save(Config(profile="lab", region="eu", api_token="lab-token")) + + assert service.remove("lab") is True + assert service.load("lab") is None + assert [profile.profile for profile in service.list_profiles()] == ["default"] + assert service.remove("missing") is False + + +def test_should_honor_canonical_config_path_override( + tmp_path: Path, monkeypatch: MonkeyPatch +) -> None: + config_path = tmp_path / "custom.json" + monkeypatch.setenv("SCCFM_CONFIG", str(config_path)) + + ConfigService().save(Config(profile="lab", region="eu", api_token="token")) + + assert ConfigService().load("lab") == Config(profile="lab", region="eu", api_token="token") + assert config_path.is_file() diff --git a/cisco_sccfm_core/models/profile.py b/cisco_sccfm_core/models/profile.py new file mode 100644 index 00000000..97bb6e01 --- /dev/null +++ b/cisco_sccfm_core/models/profile.py @@ -0,0 +1,16 @@ +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Profile: + """A named SCCFM API credential profile.""" + + profile: str + region: str + api_token: str diff --git a/cisco_sccfm_core/services/__init__.py b/cisco_sccfm_core/services/__init__.py index f802493e..8bfd4253 100644 --- a/cisco_sccfm_core/services/__init__.py +++ b/cisco_sccfm_core/services/__init__.py @@ -34,6 +34,7 @@ AccessRuleResponse, AccessRuleService, ) +from cisco_sccfm_core.services.profile_service import ProfileService __all__ = [ "AsaBootImageService", @@ -62,5 +63,6 @@ "NetworkObjectListResponse", "NetworkObjectResponse", "NetworkObjectService", + "ProfileService", "ShunEntrySpec", ] diff --git a/cisco_sccfm_core/services/profile_service.py b/cisco_sccfm_core/services/profile_service.py new file mode 100644 index 00000000..2e24c7f4 --- /dev/null +++ b/cisco_sccfm_core/services/profile_service.py @@ -0,0 +1,104 @@ +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +import os +import tempfile +from pathlib import Path +from typing import Any, Dict, Mapping + +from cisco_sccfm_core.models.profile import Profile + +_CONFIG_DIR = Path.home() / ".sccfm-cli" +_CONFIG_FILE = _CONFIG_DIR / "config.json" +_CONFIG_DIR_MODE = 0o700 +_CONFIG_FILE_MODE = 0o600 + + +class ProfileService: + """Read and write SCCFM profiles from the canonical local config file.""" + + def __init__(self, path: Path | None = None) -> None: + configured_path = os.environ.get("SCCFM_CONFIG") + self._path = path or ( + Path(configured_path).expanduser() if configured_path else _CONFIG_FILE + ) + self._harden_existing_path() + + def load(self, profile: str) -> Profile | None: + profiles = self._load_profiles() + profile_data = profiles.get(profile) + if not profile_data: + return None + return Profile( + profile=profile, + region=profile_data["region"], + api_token=profile_data["api_token"], + ) + + def save(self, config: Profile) -> None: + profiles = self._load_profiles() + profiles[config.profile] = { + "region": config.region, + "api_token": config.api_token, + } + self._persist({"profiles": profiles}) + + def list_profiles(self) -> list[Profile]: + profiles = self._load_profiles() + return [ + Profile(profile=name, region=data["region"], api_token=data["api_token"]) + for name, data in sorted(profiles.items()) + ] + + def remove(self, profile: str) -> bool: + """Remove *profile*, returning whether it existed.""" + profiles = self._load_profiles() + if profile not in profiles: + return False + del profiles[profile] + self._persist({"profiles": profiles}) + return True + + def _load_profiles(self) -> Dict[str, Dict[str, Any]]: + if not self._path.exists(): + return {} + with self._path.open("r", encoding="utf-8") as handle: + data = json.load(handle) + return dict(data.get("profiles", {})) + + def _persist(self, payload: Mapping[str, Any]) -> None: + self._ensure_config_directory() + file_descriptor, temporary_name = tempfile.mkstemp( + dir=self._path.parent, + prefix=f".{self._path.name}.", + suffix=".tmp", + ) + temporary_path = Path(temporary_name) + try: + os.fchmod(file_descriptor, _CONFIG_FILE_MODE) + with os.fdopen(file_descriptor, "w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2) + handle.write("\n") + handle.flush() + os.fsync(handle.fileno()) + temporary_path.replace(self._path) + self._path.chmod(_CONFIG_FILE_MODE) + except Exception: + temporary_path.unlink(missing_ok=True) + raise + + def _ensure_config_directory(self) -> None: + created = not self._path.parent.exists() + self._path.parent.mkdir(parents=True, mode=_CONFIG_DIR_MODE, exist_ok=True) + if created or self._path.parent == _CONFIG_DIR: + self._path.parent.chmod(_CONFIG_DIR_MODE) + + def _harden_existing_path(self) -> None: + if self._path.is_file(): + self._path.chmod(_CONFIG_FILE_MODE) + if self._path.parent == _CONFIG_DIR and self._path.parent.is_dir(): + self._path.parent.chmod(_CONFIG_DIR_MODE) diff --git a/cisco_sccfm_core/tests/test_doc_generators.py b/cisco_sccfm_core/tests/test_doc_generators.py index f2fbe6ec..6a5803fc 100644 --- a/cisco_sccfm_core/tests/test_doc_generators.py +++ b/cisco_sccfm_core/tests/test_doc_generators.py @@ -152,7 +152,7 @@ def test_ansible_docs_refuse_to_overwrite_non_empty_custom_directory(tmp_path: P def test_ansible_docs_wrap_output_in_liquid_raw_tags() -> None: - output = "api_token: \"{{ lookup('env', 'SCCFM_API_TOKEN') }}\"" + output = 'profile: "{{ selected_profile }}"' page = generate_ansible_docs._render_page( "cisco.sccfm.sccfm", @@ -165,6 +165,17 @@ def test_ansible_docs_wrap_output_in_liquid_raw_tags() -> None: assert output in page +def test_ansible_index_includes_lookup_plugins() -> None: + index = generate_ansible_docs._render_index( + (), + (), + ("cisco.sccfm.profile",), + ) + + assert "## Lookup Plugins" in index + assert "[cisco.sccfm.profile](lookup/profile.html)" in index + + def test_generated_docs_include_jekyll_front_matter() -> None: cli_page = generate_cli_docs._render_page((), "Usage: sccfm-cli [OPTIONS]") ansible_page = generate_ansible_docs._render_page( diff --git a/cisco_sccfm_core/tests/test_packaging_metadata.py b/cisco_sccfm_core/tests/test_packaging_metadata.py index 352bc356..4fe89f22 100644 --- a/cisco_sccfm_core/tests/test_packaging_metadata.py +++ b/cisco_sccfm_core/tests/test_packaging_metadata.py @@ -35,6 +35,33 @@ def test_published_packages_use_cisco_prefix() -> None: assert all(target.startswith("cisco_sccfm_") for target in script_targets) +def test_interactive_entrypoint_is_completely_renamed() -> None: + scripts = _poetry_config()["scripts"] + + assert scripts["sccfm-cli-interactive"] == "cisco_sccfm_scripts.interactive_cli:main" + assert "devkit" not in scripts + assert "change-tokens" not in scripts + + +def test_user_guidance_only_references_canonical_profile_configuration() -> None: + guidance_paths = [ + PROJECT_ROOT / "README.md", + PROJECT_ROOT / "INSTALL.md", + PROJECT_ROOT / "CONTRIBUTING.md", + PROJECT_ROOT / "sccfm-ansible" / "README.md", + PROJECT_ROOT / "skills" / "sccfm-cli" / "SKILL.md", + PROJECT_ROOT / "skills" / "sccfm-ansible" / "SKILL.md", + ] + + for path in guidance_paths: + guidance = path.read_text(encoding="utf-8") + assert "change-tokens" not in guidance, path + assert "`devkit`" not in guidance, path + assert "SCCFM_API_TOKEN" not in guidance, path + assert "SCCFM_REGION" not in guidance, path + assert ".env.example" not in guidance, path + + def test_pyinstaller_spec_uses_repository_relative_entrypoint() -> None: spec = (PROJECT_ROOT / "sccfm-cli.spec").read_text(encoding="utf-8") diff --git a/cisco_sccfm_core/tests/test_sccfm_ansible_skill.py b/cisco_sccfm_core/tests/test_sccfm_ansible_skill.py index 8828e02d..d42b20d9 100644 --- a/cisco_sccfm_core/tests/test_sccfm_ansible_skill.py +++ b/cisco_sccfm_core/tests/test_sccfm_ansible_skill.py @@ -73,6 +73,17 @@ def test_sccfm_ansible_skill_documents_safety_and_secret_rules() -> None: assert "EXECUTE cisco.sccfm " in skill +def test_sccfm_ansible_skill_only_documents_canonical_profile_auth() -> None: + skill = _skill_text() + + assert "sccfm-cli --profile configure" in skill + assert "profile: production" in skill + assert "SCCFM_API_TOKEN" not in skill + assert "SCCFM_REGION" not in skill + assert "change-tokens" not in skill + assert "Bash(devkit *)" not in skill + + def test_sccfm_ansible_skill_blocks_made_up_query_semantics() -> None: skill = _skill_text() diff --git a/cisco_sccfm_scripts/_test_setup_tokens.py b/cisco_sccfm_scripts/_test_setup_tokens.py deleted file mode 100644 index 3d9d071d..00000000 --- a/cisco_sccfm_scripts/_test_setup_tokens.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2026 Cisco Systems, Inc. and its affiliates -# -# SPDX-License-Identifier: Apache-2.0 - -"""Integration tests for setup_tokens and vault-based token_store. - -Backs up touched files before running, and restores them afterward -so the test is fully repeatable. -""" -import shutil -import subprocess -import tempfile -from collections.abc import Iterator -from pathlib import Path - -import pytest - -# ── Paths ──────────────────────────────────────────────────────── - -ROOT = Path(__file__).resolve().parent.parent -EXAMPLES = ROOT / "sccfm-ansible" / "examples" -VAULT_PATH = EXAMPLES / "group_vars" / "all" / "vault.yml" -VAULT_PASS_PATH = EXAMPLES / ".vault_pass" -VARS_PATH = EXAMPLES / "group_vars" / "all" / "vars.yml" -ENV_PATH = ROOT / ".env" - -_BACKUP_SUFFIX = ".test_bak" - -_MANAGED_FILES = [VAULT_PATH, VAULT_PASS_PATH, VARS_PATH, ENV_PATH] - - -# ── Helpers ────────────────────────────────────────────────────── - - -def _backup(path: Path) -> None: - if path.exists(): - dst = path.with_suffix(path.suffix + _BACKUP_SUFFIX) - shutil.copy2(path, dst) - print(f" backed up {path.name}") - - -def _restore(path: Path) -> None: - bak = path.with_suffix(path.suffix + _BACKUP_SUFFIX) - if bak.exists(): - shutil.copy2(bak, path) - bak.unlink() - print(f" restored {path.name}") - elif path.exists(): - path.unlink() - print(f" removed {path.name} (no backup existed)") - - -def backup_all() -> None: - print("\n--- backup originals ---") - for p in _MANAGED_FILES: - _backup(p) - - -def restore_all() -> None: - print("\n--- restore originals ---") - for p in _MANAGED_FILES: - _restore(p) - - -@pytest.fixture(scope="session", autouse=True) -def preserve_managed_files() -> Iterator[None]: - """Restore ignored credential files when tests are invoked through pytest.""" - backup_all() - try: - yield - finally: - restore_all() - - -# ── Tests ──────────────────────────────────────────────────────── - - -def test_ansible_vault_available() -> None: - from cisco_sccfm_scripts.setup_tokens import _verify_ansible_vault - - _verify_ansible_vault() - print("PASS: ansible-vault available") - - -def test_vault_pass_detected() -> None: - from cisco_sccfm_scripts.setup_tokens import _ensure_vault_pass - - vp = _ensure_vault_pass(EXAMPLES) - assert vp == VAULT_PASS_PATH, f"Expected {VAULT_PASS_PATH}, got {vp}" - print("PASS: vault password file detected") - - -def test_vault_token_store_round_trip() -> None: - """Save tokens to vault, read them back, verify contents.""" - from cisco_sccfm_scripts.token_store import SavedToken, VaultTokenStore - - store = VaultTokenStore(EXAMPLES) - - # Remove vault so we start clean - if VAULT_PATH.exists(): - VAULT_PATH.unlink() - - assert store.list_tokens() == [], "Store should start empty" - - tok1 = SavedToken(name="alpha", region="us", token="tok-aaa111") - tok2 = SavedToken(name="beta", region="eu", token="tok-bbb222") - - # Save both tokens with tok1 as active - store.save_active_and_tokens(tok1, [tok1, tok2]) - - # Vault file should be encrypted - first_line = VAULT_PATH.read_text().split("\n")[0] - assert "$ANSIBLE_VAULT" in first_line, f"Not encrypted: {first_line}" - - # Read back saved tokens - tokens = store.list_tokens() - assert len(tokens) == 2, f"Expected 2 tokens, got {len(tokens)}" - assert tokens[0].name == "alpha" - assert tokens[1].name == "beta" - assert tokens[0].token == "tok-aaa111" - assert tokens[1].region == "eu" - - # Verify active token via ansible-vault view - result = subprocess.run( - ["ansible-vault", "view", str(VAULT_PATH), "--vault-password-file", str(VAULT_PASS_PATH)], - capture_output=True, - text=True, - ) - assert result.returncode == 0, f"Decrypt failed: {result.stderr}" - assert "tok-aaa111" in result.stdout, "Active token not in vault" - - # Now switch active to tok2 and add a third - tok3 = SavedToken(name="gamma", region="int", token="tok-ccc333") - store.save_active_and_tokens(tok2, [tok1, tok2, tok3]) - - tokens = store.list_tokens() - assert len(tokens) == 3, f"Expected 3 tokens, got {len(tokens)}" - - result = subprocess.run( - ["ansible-vault", "view", str(VAULT_PATH), "--vault-password-file", str(VAULT_PASS_PATH)], - capture_output=True, - text=True, - ) - assert "tok-bbb222" in result.stdout, "Active token should be tok2 now" - - print("PASS: vault token store round-trip") - print(f"Decrypted content:\n{result.stdout}") - - -def test_vars_region_updated() -> None: - from cisco_sccfm_scripts.setup_tokens import _update_vars_region - - _update_vars_region(EXAMPLES, "eu") - content = VARS_PATH.read_text() - assert "sccfm_region: eu" in content, "Region not updated in vars.yml" - print("PASS: vars.yml region updated") - - -def test_env_file_updates_in_place() -> None: - """Verify that existing .env is updated in-place, preserving comments.""" - from cisco_sccfm_scripts.setup_tokens import _write_env_file - - seed = ( - "# Copy this file to .env and fill in your values\n" - "export SCCFM_REGION=int\n" - 'export SCCFM_API_TOKEN="old-token"\n' - ) - ENV_PATH.write_text(seed) - - _write_env_file(ROOT, "eu", "new-tok-456") - content = ENV_PATH.read_text() - - assert "export SCCFM_REGION=eu" in content, "Region not updated" - assert 'export SCCFM_API_TOKEN="new-tok-456"' in content, "Token not updated" - assert "Copy this file" in content, "Original comment was lost" - print("PASS: .env updated in-place (comments preserved)") - - -def test_env_file_created_from_example() -> None: - """When .env doesn't exist, it should be seeded from .env.example.""" - from cisco_sccfm_scripts.setup_tokens import _write_env_file - - if ENV_PATH.exists(): - ENV_PATH.unlink() - - _write_env_file(ROOT, "apj", "fresh-tok-789") - content = ENV_PATH.read_text() - - assert "export SCCFM_REGION=apj" in content, "Region not set" - assert 'export SCCFM_API_TOKEN="fresh-tok-789"' in content, "Token not set" - assert "direnv" in content or "SCCFM" in content, "Missing template content" - print("PASS: .env created from .env.example template") - - -# ── Headless-mode tests ───────────────────────────────────────── - - -def test_upsert_env_var_replaces_existing() -> None: - """_upsert_env_var should replace an existing variable in-place.""" - from cisco_sccfm_scripts.setup_tokens import _upsert_env_var - - content = "# comment\nexport FOO=old\nexport BAR=keep\n" - result = _upsert_env_var(content, "FOO", "new") - assert "export FOO=new" in result, "Variable not replaced" - assert "export BAR=keep" in result, "Other variable was lost" - assert "# comment" in result, "Comment was lost" - print("PASS: _upsert_env_var replaces existing variable") - - -def test_upsert_env_var_appends_missing() -> None: - """_upsert_env_var should append when the variable doesn't exist.""" - from cisco_sccfm_scripts.setup_tokens import _upsert_env_var - - content = "# comment\nexport OTHER=value\n" - result = _upsert_env_var(content, "NEW_VAR", "hello") - assert "export NEW_VAR=hello" in result, "Variable not appended" - assert "export OTHER=value" in result, "Existing variable was lost" - print("PASS: _upsert_env_var appends missing variable") - - -def test_merge_token_adds_new() -> None: - """_merge_token should add a new token when name doesn't exist.""" - from unittest.mock import MagicMock - - from cisco_sccfm_scripts.setup_tokens import _merge_token - from cisco_sccfm_scripts.token_store import SavedToken - - store = MagicMock() - store.list_tokens.return_value = [ - SavedToken(name="alpha", region="us", token="tok-aaa"), - ] - - new_tok = SavedToken(name="beta", region="eu", token="tok-bbb") - result = _merge_token(store, new_tok) - - assert len(result) == 2, f"Expected 2 tokens, got {len(result)}" - names = [t.name for t in result] - assert "alpha" in names and "beta" in names - print("PASS: _merge_token adds new token") - - -def test_merge_token_replaces_existing() -> None: - """_merge_token should replace a token with the same name.""" - from unittest.mock import MagicMock - - from cisco_sccfm_scripts.setup_tokens import _merge_token - from cisco_sccfm_scripts.token_store import SavedToken - - store = MagicMock() - store.list_tokens.return_value = [ - SavedToken(name="alpha", region="us", token="tok-old"), - SavedToken(name="beta", region="eu", token="tok-bbb"), - ] - - updated = SavedToken(name="alpha", region="apj", token="tok-new") - result = _merge_token(store, updated) - - assert len(result) == 2, f"Expected 2 tokens, got {len(result)}" - alpha = next(t for t in result if t.name == "alpha") - assert alpha.token == "tok-new", "Token not replaced" - assert alpha.region == "apj", "Region not updated" - print("PASS: _merge_token replaces existing token") - - -def test_merge_token_empty_store() -> None: - """_merge_token should work on an empty store.""" - from unittest.mock import MagicMock - - from cisco_sccfm_scripts.setup_tokens import _merge_token - from cisco_sccfm_scripts.token_store import SavedToken - - store = MagicMock() - store.list_tokens.return_value = [] - - tok = SavedToken(name="first", region="us", token="tok-111") - result = _merge_token(store, tok) - - assert len(result) == 1 - assert result[0].name == "first" - print("PASS: _merge_token works on empty store") - - -def test_ensure_vault_pass_headless_uses_existing(tmp_path: Path) -> None: - """When .vault_pass already exists, return it without writing.""" - from cisco_sccfm_scripts.setup_tokens import _ensure_vault_pass_headless - - vault_pass = tmp_path / ".vault_pass" - vault_pass.write_text("existing-password\n") - - result = _ensure_vault_pass_headless(tmp_path, vault_password="ignored") - assert result == vault_pass - assert vault_pass.read_text() == "existing-password\n", "File should not be overwritten" - print("PASS: _ensure_vault_pass_headless uses existing file") - - -def test_ensure_vault_pass_headless_creates_new(tmp_path: Path) -> None: - """When .vault_pass is missing, create it from --vault-password.""" - from cisco_sccfm_scripts.setup_tokens import _ensure_vault_pass_headless - - result = _ensure_vault_pass_headless(tmp_path, vault_password="my-secret") - assert result == tmp_path / ".vault_pass" - assert result.read_text() == "my-secret\n" - assert oct(result.stat().st_mode & 0o777) == "0o600", "File should be chmod 600" - print("PASS: _ensure_vault_pass_headless creates new file") - - -def test_ensure_vault_pass_headless_raises_without_password(tmp_path: Path) -> None: - """When .vault_pass is missing and no password supplied, raise.""" - import click - - from cisco_sccfm_scripts.setup_tokens import _ensure_vault_pass_headless - - try: - _ensure_vault_pass_headless(tmp_path, vault_password=None) - raise AssertionError("Should have raised ClickException") - except click.ClickException as exc: - assert "--vault-password" in str(exc), f"Unexpected message: {exc}" - print("PASS: _ensure_vault_pass_headless raises without password") - - -def test_headless_mode_detection() -> None: - """Verify that main() routes to headless when --region and --api-token are set.""" - from unittest.mock import patch - - from click.testing import CliRunner - - from cisco_sccfm_scripts.setup_tokens import main - - runner = CliRunner() - - # Missing --api-token → error - result = runner.invoke(main, ["--region", "us"]) - assert result.exit_code != 0, "Should fail with only --region" - assert "both --region and --api-token" in result.output - - # Missing --region → error - result = runner.invoke(main, ["--api-token", "tok-123"]) - assert result.exit_code != 0, "Should fail with only --api-token" - assert "both --region and --api-token" in result.output - - # Both supplied → should call _run_headless (mock it out) - with patch("cisco_sccfm_scripts.setup_tokens._run_headless") as mock_headless: - result = runner.invoke( - main, - ["--region", "us", "--api-token", "tok-123", "--name", "myenv"], - ) - assert result.exit_code == 0, f"Unexpected error: {result.output}" - mock_headless.assert_called_once() - call_kwargs = mock_headless.call_args - assert call_kwargs.kwargs["region"] == "us" - assert call_kwargs.kwargs["api_token"] == "tok-123" - assert call_kwargs.kwargs["name"] == "myenv" - - print("PASS: headless mode detection works correctly") - - -def run_tests() -> None: - test_ansible_vault_available() - test_vault_pass_detected() - test_vault_token_store_round_trip() - test_vars_region_updated() - test_env_file_updates_in_place() - test_env_file_created_from_example() - - # Headless / pure-logic tests (use a temp dir) - test_upsert_env_var_replaces_existing() - test_upsert_env_var_appends_missing() - test_merge_token_adds_new() - test_merge_token_replaces_existing() - test_merge_token_empty_store() - - with tempfile.TemporaryDirectory() as td: - test_ensure_vault_pass_headless_uses_existing(Path(td)) - with tempfile.TemporaryDirectory() as td: - test_ensure_vault_pass_headless_creates_new(Path(td)) - with tempfile.TemporaryDirectory() as td: - test_ensure_vault_pass_headless_raises_without_password(Path(td)) - - test_headless_mode_detection() - - print("\n=== ALL TESTS PASSED ===") - - -# ── Main ───────────────────────────────────────────────────────── - - -def main() -> None: - backup_all() - try: - run_tests() - finally: - restore_all() - - -if __name__ == "__main__": - main() diff --git a/cisco_sccfm_scripts/cli_commands.py b/cisco_sccfm_scripts/cli_commands.py index 718f680d..1ebe2634 100644 --- a/cisco_sccfm_scripts/cli_commands.py +++ b/cisco_sccfm_scripts/cli_commands.py @@ -4,7 +4,7 @@ """Dynamic sccfm-cli command tree built by introspecting the Click group. -Any command added to sccfm-cli is automatically available in the devkit +Any command added to sccfm-cli is automatically available in the interactive CLI interactive runner — no changes to this file are required. Infrastructure options that are not useful in an interactive session diff --git a/cisco_sccfm_scripts/generate_ansible_docs.py b/cisco_sccfm_scripts/generate_ansible_docs.py index 57ef17e0..f8f687cf 100644 --- a/cisco_sccfm_scripts/generate_ansible_docs.py +++ b/cisco_sccfm_scripts/generate_ansible_docs.py @@ -135,7 +135,11 @@ def _link(plugin_type: str, fqcn: str) -> str: return f"- [{fqcn}]({plugin_type}/{name}.html)" -def _render_index(modules: Sequence[str], inventory_plugins: Sequence[str]) -> str: +def _render_index( + modules: Sequence[str], + inventory_plugins: Sequence[str], + lookup_plugins: Sequence[str] = (), +) -> str: lines = [ GENERATED_HEADER, "", @@ -147,6 +151,8 @@ def _render_index(modules: Sequence[str], inventory_plugins: Sequence[str]) -> s "", ] lines.extend(_link("inventory", name) for name in inventory_plugins) + lines.extend(["", "## Lookup Plugins", ""]) + lines.extend(_link("lookup", name) for name in lookup_plugins) lines.extend(["", "## Modules", ""]) lines.extend(_link("modules", name) for name in modules) lines.append("") @@ -157,9 +163,10 @@ def _generate_files(project_root: Path, docs_root: Path) -> Mapping[Path, str]: with _source_collection_path(project_root) as collection_path: modules = _list_plugins(project_root, collection_path, "module") inventory_plugins = _list_plugins(project_root, collection_path, "inventory") + lookup_plugins = _list_plugins(project_root, collection_path, "lookup") files: dict[Path, str] = { - docs_root / "index.md": _render_index(modules, inventory_plugins), + docs_root / "index.md": _render_index(modules, inventory_plugins, lookup_plugins), } for fqcn in modules: output = _run_ansible_doc(project_root, collection_path, ["-t", "module", fqcn]) @@ -177,6 +184,14 @@ def _generate_files(project_root: Path, docs_root: Path) -> Mapping[Path, str]: f"ansible-doc -t inventory {fqcn}", output, ) + for fqcn in lookup_plugins: + output = _run_ansible_doc(project_root, collection_path, ["-t", "lookup", fqcn]) + name = fqcn.removeprefix(f"{COLLECTION_FQCN}.") + files[docs_root / "lookup" / f"{name}.md"] = _render_page( + fqcn, + f"ansible-doc -t lookup {fqcn}", + output, + ) return files diff --git a/cisco_sccfm_scripts/import_legacy_vault.py b/cisco_sccfm_scripts/import_legacy_vault.py new file mode 100644 index 00000000..5b54b395 --- /dev/null +++ b/cisco_sccfm_scripts/import_legacy_vault.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +"""Import SCCFM API profiles from the legacy Ansible Vault token store.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path +from typing import Any, cast + +import click +import yaml +from rich.console import Console + +from cisco_sccfm_core.models.profile import Profile +from cisco_sccfm_core.services.profile_service import ProfileService + +console = Console() + + +def read_legacy_profiles( + vault_path: Path, + vault_password_path: Path, + vars_path: Path | None, +) -> list[Profile]: + """Decrypt a legacy vault and return validated profiles without modifying it.""" + _require_file(vault_path, "Legacy vault") + _require_file(vault_password_path, "Vault password file") + completed = subprocess.run( + [ + "ansible-vault", + "view", + str(vault_path), + "--vault-password-file", + str(vault_password_path), + ], + capture_output=True, + text=True, + check=False, + ) + if completed.returncode != 0: + raise click.ClickException(f"Could not decrypt legacy vault: {completed.stderr.strip()}") + + payload = _load_mapping(completed.stdout, vault_path) + saved = cast(list[dict[str, Any]], payload.get("sccfm_saved_tokens", [])) + if saved: + return [_profile_from_saved_token(item) for item in saved] + + token = payload.get("sccfm_api_token") + if not isinstance(token, str) or not token.strip(): + return [] + if vars_path is None: + raise click.ClickException( + "Legacy vault contains an active token but no saved profiles; --vars-file is required." + ) + _require_file(vars_path, "Legacy vars file") + variables = _load_mapping(vars_path.read_text(encoding="utf-8"), vars_path) + region = variables.get("sccfm_region") + if not isinstance(region, str) or not region.strip(): + raise click.ClickException(f"sccfm_region is missing from {vars_path}") + return [Profile(profile="default", region=region.strip(), api_token=token.strip())] + + +def import_profiles( + profiles: list[Profile], + service: ProfileService, + overwrite: bool, +) -> tuple[list[str], list[str]]: + """Import profiles, returning imported and skipped profile names.""" + imported: list[str] = [] + skipped: list[str] = [] + for profile in profiles: + if service.load(profile.profile) is not None and not overwrite: + skipped.append(profile.profile) + continue + service.save(profile) + imported.append(profile.profile) + return imported, skipped + + +def _profile_from_saved_token(item: dict[str, Any]) -> Profile: + required = ("name", "region", "token") + missing = [key for key in required if not isinstance(item.get(key), str) or not item[key]] + if missing: + raise click.ClickException( + f"Legacy saved token is missing valid fields: {', '.join(missing)}" + ) + return Profile( + profile=cast(str, item["name"]).strip(), + region=cast(str, item["region"]).strip(), + api_token=cast(str, item["token"]).strip(), + ) + + +def _load_mapping(content: str, source: Path) -> dict[str, Any]: + parsed = yaml.safe_load(content) or {} + if not isinstance(parsed, dict): + raise click.ClickException(f"Expected a YAML mapping in {source}") + return cast(dict[str, Any], parsed) + + +def _require_file(path: Path, label: str) -> None: + if not path.is_file(): + raise click.ClickException(f"{label} not found: {path}") + + +@click.command(help="Import SCCFM profiles from the legacy Ansible Vault token store.") +@click.option( + "--vault-file", + type=click.Path(path_type=Path, exists=True, dir_okay=False, resolve_path=True), + default=Path("sccfm-ansible/examples/group_vars/all/vault.yml"), + show_default=True, +) +@click.option( + "--vault-password-file", + type=click.Path(path_type=Path, exists=True, dir_okay=False, resolve_path=True), + default=Path("sccfm-ansible/examples/.vault_pass"), + show_default=True, +) +@click.option( + "--vars-file", + type=click.Path(path_type=Path, exists=True, dir_okay=False, resolve_path=True), + default=Path("sccfm-ansible/examples/group_vars/all/vars.yml"), + show_default=True, +) +@click.option( + "--config-path", + type=click.Path(path_type=Path, dir_okay=False, resolve_path=True), + default=None, + help="Canonical SCCFM config path (defaults to ~/.sccfm-cli/config.json).", +) +@click.option("--overwrite", is_flag=True, help="Replace profiles with matching names.") +def main( + vault_file: Path, + vault_password_file: Path, + vars_file: Path, + config_path: Path | None, + overwrite: bool, +) -> None: + """Import legacy profiles without changing the source vault.""" + profiles = read_legacy_profiles(vault_file, vault_password_file, vars_file) + imported, skipped = import_profiles(profiles, ProfileService(config_path), overwrite) + if not profiles: + console.print("[yellow]No SCCFM API tokens found in the legacy vault.[/yellow]") + return + if imported: + console.print(f"[green]Imported profiles:[/green] {', '.join(sorted(imported))}") + if skipped: + console.print( + f"[yellow]Skipped existing profiles:[/yellow] {', '.join(sorted(skipped))} " + "(use --overwrite to replace them)" + ) + console.print("[dim]The legacy vault and password file were not modified.[/dim]") + + +if __name__ == "__main__": + main() diff --git a/cisco_sccfm_scripts/devkit_cli.py b/cisco_sccfm_scripts/interactive_cli.py similarity index 75% rename from cisco_sccfm_scripts/devkit_cli.py rename to cisco_sccfm_scripts/interactive_cli.py index 45bbf128..feec657e 100644 --- a/cisco_sccfm_scripts/devkit_cli.py +++ b/cisco_sccfm_scripts/interactive_cli.py @@ -4,10 +4,10 @@ # # SPDX-License-Identifier: Apache-2.0 -"""Unified devkit CLI — interactive entry-point for all helper scripts. +"""Interactive entry point for SCCFM CLI and development workflows. Usage: - devkit # interactive menu + sccfm-cli-interactive """ from __future__ import annotations @@ -18,6 +18,7 @@ from pathlib import Path from typing import Callable +import click import questionary from rich.console import Console from rich.panel import Panel @@ -50,11 +51,43 @@ def _ask( # ── Task implementations ───────────────────────────────────────── -def _run_change_tokens() -> None: - """Set up SCCFM API tokens, .env, and Ansible Vault (interactive).""" - from cisco_sccfm_scripts.setup_tokens import main as _setup_tokens +def _configure_profile() -> None: + """Create or replace a profile in the canonical SCCFM config store.""" + from cisco_sccfm_cli.models import Config + from cisco_sccfm_cli.services import ConfigService + from cisco_sccfm_core.constants import SCCFM_REGIONS - _setup_tokens(standalone_mode=False) + profile_answer = questionary.text("Profile name:", default="default").unsafe_ask() + profile = (profile_answer or "").strip() + if not profile: + console.print("[red]Profile name cannot be empty.[/red]") + return + + region_answer = questionary.select( + "SCCFM region:", + choices=list(SCCFM_REGIONS), + default="us", + ).unsafe_ask() + region = region_answer if isinstance(region_answer, str) else "" + if not region: + console.print("[dim]Cancelled.[/dim]") + return + + token_answer = questionary.password("SCCFM API token:").unsafe_ask() + api_token = (token_answer or "").strip() + if not api_token: + console.print("[red]API token cannot be empty.[/red]") + return + + ConfigService().save(Config(profile=profile, region=region, api_token=api_token)) + console.print(f"[green]Profile '{profile}' configured for region '{region}'.[/green]") + + +def _import_legacy_vault() -> None: + """Import SCCFM profiles from the former Ansible Vault token store.""" + from cisco_sccfm_scripts.import_legacy_vault import main as _import + + _import(standalone_mode=False) def _run_build_collection() -> None: @@ -291,137 +324,97 @@ def _run_ansible_examples() -> None: # ── Manage tokens ───────────────────────────────────────────────── -def _update_token() -> None: - """Prompt for a new API token for an existing named token.""" - from cisco_sccfm_scripts.setup_tokens import _resolve_examples_path - from cisco_sccfm_scripts.token_store import SavedToken, VaultTokenStore +def _select_profile(message: str) -> object | None: + """Select a configured SCCFM profile, returning its config.""" + from cisco_sccfm_cli.services import ConfigService - try: - examples_path = _resolve_examples_path(None) - except Exception as exc: - console.print(f"[red]{exc}[/red]") - return - - store = VaultTokenStore(examples_path) - tokens = store.list_tokens() + profiles = ConfigService().list_profiles() + if not profiles: + console.print("[yellow]No SCCFM profiles configured.[/yellow]") + return None - if not tokens: - console.print("[yellow]No saved tokens found in vault.[/yellow]") - return - - token_choices: list[questionary.Choice | str] = [ - questionary.Choice( - title=f"{t.name} ({t.region}) …{t.token[-6:]}", - value=t.name, - ) - for t in tokens + choices: list[questionary.Choice | str] = [ + questionary.Choice(title=f"{item.profile} ({item.region})", value=item.profile) + for item in profiles ] - token_choices.append("back") - - answer = _ask(token_choices, "Select a token to update:") + choices.append("back") + answer = _ask(choices, message) if answer is None or answer == "back": - return + return None + return next((item for item in profiles if item.profile == answer), None) - token_to_update = next((t for t in tokens if t.name == answer), None) - if token_to_update is None: - console.print("[red]Token not found.[/red]") - return - new_token_value = questionary.text( - f"Paste new API token for '{token_to_update.name}':", - ).unsafe_ask() - new_token_value = new_token_value.strip() - if not new_token_value: - console.print("[red]Token cannot be empty.[/red]") - return - - updated = SavedToken( - name=token_to_update.name, - region=token_to_update.region, - token=new_token_value, - ) - all_tokens = [updated if t.name == updated.name else t for t in tokens] - vault_path = store.save_active_and_tokens(updated, all_tokens) - console.print(f"[green]Updated token '{updated.name}'.[/green]") - console.print(f"[dim]Vault updated: {vault_path}[/dim]") - - -def _remove_token() -> None: - """Remove a saved token from the Ansible vault store.""" - from cisco_sccfm_scripts.setup_tokens import _resolve_examples_path - from cisco_sccfm_scripts.token_store import VaultTokenStore +def _update_profile() -> None: + """Update the region and API token for an existing profile.""" + from cisco_sccfm_cli.models import Config + from cisco_sccfm_cli.services import ConfigService + from cisco_sccfm_core.constants import SCCFM_REGIONS - try: - examples_path = _resolve_examples_path(None) - except Exception as exc: - console.print(f"[red]{exc}[/red]") + selected = _select_profile("Select a profile to update:") + if not isinstance(selected, Config): return - store = VaultTokenStore(examples_path) - tokens = store.list_tokens() - - if not tokens: - console.print("[yellow]No saved tokens found in vault.[/yellow]") + region_answer = questionary.select( + "SCCFM region:", + choices=list(SCCFM_REGIONS), + default=selected.region, + ).unsafe_ask() + region = region_answer if isinstance(region_answer, str) else "" + if not region: + console.print("[dim]Cancelled.[/dim]") return - if len(tokens) == 1: - console.print("[yellow]Only one token saved — cannot remove the last token.[/yellow]") - return + token_answer = questionary.password( + "New SCCFM API token (leave blank to keep the current token):" + ).unsafe_ask() + api_token = (token_answer or "").strip() or selected.api_token + ConfigService().save(Config(profile=selected.profile, region=region, api_token=api_token)) + console.print(f"[green]Profile '{selected.profile}' updated.[/green]") - # Use Choice so the display shows region/token context but the value is just the name. - token_choices: list[questionary.Choice | str] = [ - questionary.Choice( - title=f"{t.name} ({t.region}) …{t.token[-6:]}", - value=t.name, - ) - for t in tokens - ] - token_choices.append("back") - answer = _ask(token_choices, "Select a token to remove:") - if answer is None or answer == "back": - return +def _remove_profile() -> None: + """Remove an SCCFM profile from the canonical config store.""" + from cisco_sccfm_cli.models import Config + from cisco_sccfm_cli.services import ConfigService - token_to_remove = next((t for t in tokens if t.name == answer), None) - if token_to_remove is None: - console.print("[red]Token not found.[/red]") + selected = _select_profile("Select a profile to remove:") + if not isinstance(selected, Config): return confirmed = questionary.confirm( - f"Remove token '{token_to_remove.name}' (region={token_to_remove.region})?", - default=True, + f"Remove profile '{selected.profile}' (region={selected.region})?", + default=False, ).unsafe_ask() if not confirmed: console.print("[dim]Cancelled.[/dim]") return - remaining = [t for t in tokens if t.name != token_to_remove.name] - new_active = remaining[0] - vault_path = store.save_active_and_tokens(new_active, remaining) - console.print(f"[green]Removed token '{token_to_remove.name}'.[/green]") - console.print( - f"[green]Active token is now '{new_active.name}' (region={new_active.region}).[/green]" - ) - console.print(f"[dim]Vault updated: {vault_path}[/dim]") + ConfigService().remove(selected.profile) + console.print(f"[green]Profile '{selected.profile}' removed.[/green]") -def _manage_tokens() -> None: - """Token management sub-menu (update or remove).""" - answer = _ask(["update", "remove", "back"], "Manage tokens:") +def _manage_profiles() -> None: + """Profile management sub-menu.""" + answer = _ask(["update", "remove", "back"], "Manage profiles:") if answer is None or answer == "back": return if answer == "update": - _update_token() + _update_profile() elif answer == "remove": - _remove_token() + _remove_profile() # ── Menu definition ─────────────────────────────────────────────── _TASKS: list[tuple[str, str, Callable[[], None]]] = [ - ("change-tokens", "Set up SCCFM API tokens, .env, and Ansible Vault", _run_change_tokens), - ("manage-tokens", "Manage saved tokens (update / remove)", _manage_tokens), + ("configure-profile", "Create or replace an SCCFM profile", _configure_profile), + ("manage-profiles", "Update or remove SCCFM profiles", _manage_profiles), + ( + "import-legacy-vault", + "Import profiles from the former Ansible Vault token store", + _import_legacy_vault, + ), ("run-cli", "Run an sccfm-cli command interactively", _run_cli_commands), ("run-ansible", "Run an Ansible example playbook", _run_ansible_examples), ("build-collection", "Build the cisco.sccfm Ansible collection tarball", _run_build_collection), @@ -471,7 +464,7 @@ def _interactive_menu() -> None: """Show an interactive menu and run the selected task.""" console.print( Panel( - "[bold]SCCFM Developer Toolkit[/bold]\n" "Select a task to run.", + "[bold]SCCFM CLI Interactive[/bold]\n" "Select a task to run.", border_style="cyan", ) ) @@ -508,6 +501,7 @@ def _interactive_menu() -> None: # ── Entry-point ─────────────────────────────────────────────────── +@click.command(help="Open the interactive SCCFM CLI and development workflow menu.") def main() -> None: try: _interactive_menu() diff --git a/cisco_sccfm_scripts/setup_tokens.py b/cisco_sccfm_scripts/setup_tokens.py deleted file mode 100644 index 5d544168..00000000 --- a/cisco_sccfm_scripts/setup_tokens.py +++ /dev/null @@ -1,550 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2026 Cisco Systems, Inc. and its affiliates -# -# SPDX-License-Identifier: Apache-2.0 - -"""Setup for SCCFM API tokens, .env, and Ansible Vault. - -Runs **interactively** by default (prompts for region, token, etc.). -Supply ``--region`` and ``--api-token`` to run **headless** — suitable -for CI pipelines and scripted workflows. - -Manages a local token store so tokens can be reused across setups. -Creates / updates: - - .env (SCCFM_REGION, SCCFM_API_TOKEN) - - .vault_pass (vault password file) - - group_vars/all/vars.yml (sccfm_region) - - group_vars/all/vault.yml (encrypted sccfm_api_token) - - ~/.sccfm-cli/config.json (CLI profile) - -Examples:: - - # Interactive (default) - python cisco_sccfm_scripts/setup_tokens.py - - # Headless — minimal - python cisco_sccfm_scripts/setup_tokens.py --region us --api-token eyJ… - - # Headless — all options - python cisco_sccfm_scripts/setup_tokens.py \\ - --region int --api-token eyJ… \\ - --name staging --profile staging \\ - --vault-password s3cret -""" - -from __future__ import annotations - -import re -import stat -import subprocess -from pathlib import Path - -import click -import questionary -from rich.console import Console -from rich.panel import Panel -from rich.table import Table - -from cisco_sccfm_core.constants import SCCFM_REGIONS -from cisco_sccfm_scripts.token_store import SavedToken, VaultTokenStore - -_REGION_DESCRIPTIONS: dict[str, str] = { - "int": "Internal (Staging)", - "us": "United States", - "eu": "Europe", - "apj": "Asia Pacific & Japan", - "au": "Australia", - "uae": "UAE", - "in": "India", - "ci": "CI", -} -_REGIONS: dict[str, str] = {region: _REGION_DESCRIPTIONS[region] for region in SCCFM_REGIONS} - -_DEFAULT_EXAMPLES_PATH = "sccfm-ansible/examples" -_ENV_EXAMPLE = ".env.example" - -console = Console() - - -def _project_root() -> Path: - """Return the repository root (parent of cisco_sccfm_scripts/).""" - return Path(__file__).resolve().parent.parent - - -# ── Path resolution ────────────────────────────────────────────── - - -def _resolve_examples_path(path: str | None) -> Path: - """Return the absolute examples directory, raising if not found.""" - if path: - resolved = Path(path).resolve() - if not resolved.is_dir(): - raise click.ClickException(f"Directory not found: {resolved}") - return resolved - - default = Path.cwd() / _DEFAULT_EXAMPLES_PATH - if default.is_dir(): - return default.resolve() - - # Maybe the user already cd'd into the examples dir - cwd = Path.cwd() - if (cwd / "group_vars").is_dir() and (cwd / ".vault_pass.example").exists(): - return cwd.resolve() - - raise click.ClickException( - "Could not locate the ansible examples directory.\n" - "Run from the project root or pass --path explicitly." - ) - - -# ── Ansible-vault availability ─────────────────────────────────── - - -def _verify_ansible_vault() -> None: - try: - subprocess.run( - ["ansible-vault", "--version"], - capture_output=True, - check=True, - ) - except FileNotFoundError: - raise click.ClickException( - "ansible-vault not found. Install ansible-core:\n" " poetry install --with dev" - ) - - -# ── Token selection / creation ─────────────────────────────────── - - -def _select_or_create_token(store: VaultTokenStore) -> tuple[SavedToken, list[SavedToken]]: - """Let the user pick a saved token or create a new one. - - Returns the selected token and the full list of all tokens (for re-saving). - """ - saved = store.list_tokens() - if saved: - return _choose_from_saved_or_new(saved) - new_token = _prompt_new_token() - return new_token, [new_token] - - -def _choose_from_saved_or_new( - saved: list[SavedToken], -) -> tuple[SavedToken, list[SavedToken]]: - """Present saved tokens and an 'Add new' option.""" - choices: list[questionary.Choice] = [ - questionary.Choice( - title=f"{t.name:<20} region={t.region} token=…{t.token[-6:]}", - value=t.name, - ) - for t in saved - ] - choices.append(questionary.Choice(title="+ Add a new token", value="_new")) - - answer: str | None = questionary.select( - "Select a saved token or add a new one:", - choices=choices, - ).ask() - - if answer is None: - raise click.Abort() - - if answer == "_new": - new_token = _prompt_new_token() - all_tokens = [t for t in saved if t.name != new_token.name] - all_tokens.append(new_token) - return new_token, all_tokens - - selected = next((t for t in saved if t.name == answer), None) - if selected is None: - raise click.ClickException(f"Token '{answer}' not found in vault.") - return selected, saved - - -def _prompt_new_token() -> SavedToken: - """Interactively gather a new token.""" - region = _prompt_region() - api_token = _prompt_token() - name = _prompt_token_name() - return SavedToken(name=name, region=region, token=api_token) - - -# ── Interactive prompts ────────────────────────────────────────── - - -def _prompt_region() -> str: - """Display region table and prompt for a choice (accepts name or number).""" - table = Table(title="Available Regions", show_header=True, header_style="bold cyan") - table.add_column("#", justify="right", style="dim") - table.add_column("Region", style="bold") - table.add_column("Description") - - region_keys = list(_REGIONS.keys()) - for idx, (key, desc) in enumerate(_REGIONS.items(), 1): - table.add_row(str(idx), key, desc) - - console.print() - console.print(table) - - while True: - choice: str = click.prompt("\nSelect region (name or #)", default="us") - choice = choice.strip().lower() - - # Accept by number - if choice.isdigit(): - idx = int(choice) - if 1 <= idx <= len(region_keys): - return region_keys[idx - 1] - - # Accept by name - if choice in region_keys: - return choice - - console.print(f"[red]Invalid choice '{choice}'. Enter a region name or number.[/red]") - - -def _prompt_token() -> str: - """Ask the user to paste their API token (hidden input).""" - token: str = click.prompt("\nPaste your SCCFM API token") - token = token.strip() - if not token: - raise click.ClickException("API token cannot be empty.") - return token - - -def _prompt_token_name() -> str: - """Ask for a label to identify this token.""" - name: str = click.prompt( - "\nName for this token (for your reference)", - default="default", - ) - return name.strip() - - -# ── .env file management ──────────────────────────────────────── - - -def _upsert_env_var(content: str, var: str, value: str) -> str: - """Replace ``export VAR=…`` in *content*, or append if not present.""" - pattern = rf"^(export\s+){var}=.*$" - replacement = f"export {var}={value}" - updated, count = re.subn(pattern, replacement, content, flags=re.MULTILINE) - if count == 0: - updated = updated.rstrip() + f"\n{replacement}\n" - return updated - - -def _write_env_file(root: Path, region: str, api_token: str) -> Path: - """Create or update the root .env with region and token. - - If ``.env`` exists, the two variables are updated in-place so that - comments and other entries are preserved. If it doesn't exist, - ``.env.example`` is used as the starting template. - """ - env_path = root / ".env" - example_path = root / _ENV_EXAMPLE - - if env_path.exists(): - content = env_path.read_text() - elif example_path.exists(): - content = example_path.read_text() - else: - content = ( - "# Auto-generated by change-tokens — do not commit\n" - "# The .env file is gitignored and loaded automatically by direnv\n\n" - ) - - content = _upsert_env_var(content, "SCCFM_REGION", region) - content = _upsert_env_var(content, "SCCFM_API_TOKEN", f'"{api_token}"') - env_path.write_text(content) - console.print(f"[green]Updated .env file:[/green] {env_path}") - return env_path - - -# ── CLI config management ──────────────────────────────────────── - - -def _update_cli_config(region: str, api_token: str, profile: str = "default") -> None: - """Update the sccfm-cli config so CLI commands use the same token.""" - from cisco_sccfm_cli.models import Config - from cisco_sccfm_cli.services import ConfigService - - config = Config(profile=profile, region=region, api_token=api_token) - ConfigService().save(config) - console.print(f"[green]Updated CLI config profile '{profile}'[/green]") - - -# ── Vault password management ──────────────────────────────────── - - -def _ensure_vault_pass(examples_path: Path) -> Path: - """Return the vault password file, creating it interactively if needed.""" - vault_pass_path = examples_path / ".vault_pass" - - if vault_pass_path.exists(): - console.print(f"\n[dim]Using existing vault password file: {vault_pass_path}[/dim]") - return vault_pass_path - - console.print("\n[yellow]No vault password file found — creating one now.[/yellow]") - password: str = click.prompt( - "Enter a vault password", - hide_input=True, - confirmation_prompt=True, - ) - if not password.strip(): - raise click.ClickException("Vault password cannot be empty.") - - vault_pass_path.write_text(password.strip() + "\n") - vault_pass_path.chmod(stat.S_IRUSR | stat.S_IWUSR) # chmod 600 - console.print(f"[green]Created vault password file:[/green] {vault_pass_path}") - return vault_pass_path - - -def _ensure_vault_pass_headless(examples_path: Path, vault_password: str | None) -> Path: - """Return the vault password file, creating it from *vault_password* - if it does not already exist. No interactive prompts. - """ - vault_pass_path = examples_path / ".vault_pass" - - if vault_pass_path.exists(): - console.print(f"[dim]Using existing vault password file: {vault_pass_path}[/dim]") - return vault_pass_path - - if not vault_password: - raise click.ClickException( - "No vault password file found and --vault-password was not supplied." - ) - - vault_pass_path.write_text(vault_password.strip() + "\n") - vault_pass_path.chmod(stat.S_IRUSR | stat.S_IWUSR) # chmod 600 - console.print(f"[green]Created vault password file:[/green] {vault_pass_path}") - return vault_pass_path - - -def _merge_token(store: VaultTokenStore, token: SavedToken) -> list[SavedToken]: - """Merge *token* into the existing saved list, replacing by name.""" - existing = store.list_tokens() - merged = [t for t in existing if t.name != token.name] - merged.append(token) - return merged - - -# ── vars.yml management ───────────────────────────────────────── - - -def _update_vars_region(examples_path: Path, region: str) -> None: - """Set sccfm_region in group_vars/all/vars.yml, preserving other content.""" - vars_path = examples_path / "group_vars" / "all" / "vars.yml" - vars_path.parent.mkdir(parents=True, exist_ok=True) - - if vars_path.exists(): - content = vars_path.read_text() - if re.search(r"^sccfm_region:.*$", content, flags=re.MULTILINE): - updated = re.sub( - r"^sccfm_region:.*$", - f"sccfm_region: {region}", - content, - flags=re.MULTILINE, - ) - else: - updated = content.rstrip() + f"\nsccfm_region: {region}\n" - vars_path.write_text(updated) - else: - vars_path.write_text( - "---\n" - "# Plain variables (not sensitive)\n" - "# These can be committed to version control\n" - "\n" - "# SCCFM connection settings\n" - f"sccfm_region: {region}\n" - ) - - console.print(f"[green]Set region to '{region}' in:[/green] {vars_path}") - - -# ── Headless logic ─────────────────────────────────────────────── - - -def _run_headless( - region: str, - api_token: str, - name: str, - profile: str, - vault_password: str | None, - path: str | None, -) -> None: - """Execute the full setup without any interactive prompts.""" - root = _project_root() - examples_path = _resolve_examples_path(path) - console.print(f"[dim]Examples directory: {examples_path}[/dim]") - - _verify_ansible_vault() - - # ── Vault password ─────────────────────────────────────────── - vault_pass_path = _ensure_vault_pass_headless(examples_path, vault_password) - - # ── Build token ────────────────────────────────────────────── - selected = SavedToken(name=name, region=region, token=api_token) - - # ── Merge with existing saved tokens ───────────────────────── - store = VaultTokenStore(examples_path) - all_tokens = _merge_token(store, selected) - - # ── Write files ────────────────────────────────────────────── - env_path = _write_env_file(root, region, api_token) - _update_vars_region(examples_path, region) - vault_path = store.save_active_and_tokens(selected, all_tokens) - console.print(f"[green]Encrypted vault file updated:[/green] {vault_path}") - _update_cli_config(region, api_token, profile=profile) - - # ── Summary ────────────────────────────────────────────────── - summary = Table(title="Setup Complete", show_header=False, border_style="green") - summary.add_column("Key", style="bold") - summary.add_column("Value") - summary.add_row("Token", name) - summary.add_row("Region", region) - summary.add_row("CLI Profile", profile) - summary.add_row(".env file", str(env_path)) - summary.add_row("Vault file", str(vault_path)) - summary.add_row("Vault password", str(vault_pass_path)) - summary.add_row("CLI config", "~/.sccfm-cli/config.json") - - console.print() - console.print(summary) - - -# ── CLI entry point ────────────────────────────────────────────── - -_VALID_REGIONS = tuple(_REGIONS) - - -@click.command( - help="Setup SCCFM API tokens, .env, and Ansible Vault.\n\n" - "Runs interactively by default. Supply --region and --api-token " - "to run in headless mode (no prompts).", -) -@click.option( - "--region", - "-r", - default=None, - type=click.Choice(_VALID_REGIONS, case_sensitive=False), - help="SCCFM region. Enables headless mode when combined with --api-token.", -) -@click.option( - "--api-token", - "-t", - default=None, - help="SCCFM API token. Enables headless mode when combined with --region.", -) -@click.option( - "--name", - "-n", - default="default", - show_default=True, - help="Label for this token in the vault store (headless only).", -) -@click.option( - "--profile", - "-p", - default="default", - show_default=True, - help="CLI config profile name to update (headless only).", -) -@click.option( - "--vault-password", - default=None, - help="Vault password — used only when .vault_pass doesn't exist yet (headless only).", -) -@click.option( - "--path", - default=None, - type=click.Path(resolve_path=True), - help=f"Path to the ansible examples directory (default: {_DEFAULT_EXAMPLES_PATH}).", -) -def main( - region: str | None, - api_token: str | None, - name: str, - profile: str, - vault_password: str | None, - path: str | None, -) -> None: - """Setup tokens — auto-detects interactive vs headless mode.""" - headless = region is not None or api_token is not None - - if headless: - if not region or not api_token: - raise click.UsageError("Headless mode requires both --region and --api-token.") - _run_headless( - region=region, - api_token=api_token, - name=name, - profile=profile, - vault_password=vault_password, - path=path, - ) - else: - try: - _run_setup(path) - except (KeyboardInterrupt, click.Abort): - console.print("\n[dim]Cancelled.[/dim]") - - -def _run_setup(path: str | None) -> None: - """Inner setup logic — separated so main() can catch exits cleanly.""" - console.print( - Panel( - "[bold]SCCFM Token Setup[/bold]\n" - "Select or create an API token, then generate\n" - ".env, vars.yml, and an encrypted vault.yml.", - border_style="cyan", - ) - ) - - root = _project_root() - examples_path = _resolve_examples_path(path) - console.print(f"[dim]Examples directory: {examples_path}[/dim]") - - _verify_ansible_vault() - - # ── Vault password (needed before we can read saved tokens) ── - vault_pass_path = _ensure_vault_pass(examples_path) - - # ── Token selection ────────────────────────────────────────── - store = VaultTokenStore(examples_path) - selected, all_tokens = _select_or_create_token(store) - - region = selected.region - api_token = selected.token - token_name = selected.name - - # ── Write files ────────────────────────────────────────────── - env_path = _write_env_file(root, region, api_token) - _update_vars_region(examples_path, region) - vault_path = store.save_active_and_tokens(selected, all_tokens) - console.print(f"[green]Encrypted vault file updated:[/green] {vault_path}") - _update_cli_config(region, api_token) - - # ── Summary ────────────────────────────────────────────────── - summary = Table(title="Setup Complete", show_header=False, border_style="green") - summary.add_column("Key", style="bold") - summary.add_column("Value") - summary.add_row("Token", token_name) - summary.add_row("Region", region) - summary.add_row(".env file", str(env_path)) - summary.add_row("Vault file", str(vault_path)) - summary.add_row("Vault password", str(vault_pass_path)) - summary.add_row("CLI config", "~/.sccfm-cli/config.json") - - console.print() - console.print(summary) - console.print( - "\n[green]You can now run playbooks with:[/green]\n" - " ansible-playbook -i examples/inventory.sccfm.yml \\\n" - " examples/show_devices.yml --vault-password-file examples/.vault_pass" - ) - - -if __name__ == "__main__": - main() diff --git a/cisco_sccfm_scripts/test_import_legacy_vault.py b/cisco_sccfm_scripts/test_import_legacy_vault.py new file mode 100644 index 00000000..cce0a207 --- /dev/null +++ b/cisco_sccfm_scripts/test_import_legacy_vault.py @@ -0,0 +1,84 @@ +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import subprocess +from pathlib import Path + +from _pytest.monkeypatch import MonkeyPatch + +from cisco_sccfm_core.models.profile import Profile +from cisco_sccfm_core.services.profile_service import ProfileService +from cisco_sccfm_scripts.import_legacy_vault import import_profiles, read_legacy_profiles + + +def test_should_read_saved_profiles_without_modifying_vault( + tmp_path: Path, monkeypatch: MonkeyPatch +) -> None: + vault_path = tmp_path / "vault.yml" + password_path = tmp_path / ".vault_pass" + vault_path.write_text("$ANSIBLE_VAULT;1.1;AES256\nunchanged\n") + password_path.write_text("password\n") + before = vault_path.read_bytes() + decrypted = """--- +sccfm_saved_tokens: + - name: default + region: us + token: token-one + - name: lab + region: eu + token: token-two +""" + monkeypatch.setattr( + subprocess, + "run", + lambda *args, **kwargs: subprocess.CompletedProcess(args[0], 0, decrypted, ""), + ) + + profiles = read_legacy_profiles(vault_path, password_path, None) + + assert profiles == [ + Profile(profile="default", region="us", api_token="token-one"), + Profile(profile="lab", region="eu", api_token="token-two"), + ] + assert vault_path.read_bytes() == before + + +def test_should_import_without_overwriting_existing_profiles(tmp_path: Path) -> None: + service = ProfileService(tmp_path / "config.json") + service.save(Profile(profile="default", region="us", api_token="existing")) + + imported, skipped = import_profiles( + [ + Profile(profile="default", region="eu", api_token="replacement"), + Profile(profile="lab", region="eu", api_token="new-token"), + ], + service, + overwrite=False, + ) + + assert imported == ["lab"] + assert skipped == ["default"] + assert service.load("default") == Profile(profile="default", region="us", api_token="existing") + + +def test_should_import_single_active_legacy_token(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + vault_path = tmp_path / "vault.yml" + password_path = tmp_path / ".vault_pass" + vars_path = tmp_path / "vars.yml" + vault_path.write_text("encrypted") + password_path.write_text("password") + vars_path.write_text("sccfm_region: apj\n") + monkeypatch.setattr( + subprocess, + "run", + lambda *args, **kwargs: subprocess.CompletedProcess( + args[0], 0, "sccfm_api_token: legacy-token\n", "" + ), + ) + + assert read_legacy_profiles(vault_path, password_path, vars_path) == [ + Profile(profile="default", region="apj", api_token="legacy-token") + ] diff --git a/cisco_sccfm_scripts/test_interactive_cli.py b/cisco_sccfm_scripts/test_interactive_cli.py new file mode 100644 index 00000000..3cf144b9 --- /dev/null +++ b/cisco_sccfm_scripts/test_interactive_cli.py @@ -0,0 +1,82 @@ +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock + +from pytest import MonkeyPatch + +from cisco_sccfm_cli.models import Config +from cisco_sccfm_cli.services import ConfigService +from cisco_sccfm_scripts import interactive_cli + + +def test_interactive_task_list_preserves_workflows_under_new_command() -> None: + task_names = [name for name, _, _ in interactive_cli._TASKS] + + assert task_names == [ + "configure-profile", + "manage-profiles", + "import-legacy-vault", + "run-cli", + "run-ansible", + "build-collection", + "generate-ansible-docs", + "generate-cli-docs", + "generate-cli-man-docs", + "install-cli-man-docs", + "setup-env", + "test", + "run-e2e", + "lint", + "format", + ] + + +def test_update_profile_uses_canonical_config_service( + tmp_path: Path, + monkeypatch: MonkeyPatch, +) -> None: + config_path = tmp_path / "config.json" + service = ConfigService(config_path) + original = Config(profile="lab", region="us", api_token="old-token") + service.save(original) + + monkeypatch.setattr(interactive_cli, "_select_profile", lambda _: original) + monkeypatch.setattr( + "cisco_sccfm_cli.services.ConfigService", + lambda: service, + ) + region_prompt = MagicMock() + region_prompt.unsafe_ask.return_value = "eu" + token_prompt = MagicMock() + token_prompt.unsafe_ask.return_value = "new-token" + monkeypatch.setattr(interactive_cli.questionary, "select", lambda *a, **k: region_prompt) + monkeypatch.setattr(interactive_cli.questionary, "password", lambda *a, **k: token_prompt) + + interactive_cli._update_profile() + + assert service.load("lab") == Config(profile="lab", region="eu", api_token="new-token") + + +def test_remove_profile_uses_canonical_config_service( + tmp_path: Path, + monkeypatch: MonkeyPatch, +) -> None: + config_path = tmp_path / "config.json" + service = ConfigService(config_path) + existing = Config(profile="lab", region="us", api_token="token") + service.save(existing) + + monkeypatch.setattr(interactive_cli, "_select_profile", lambda _: existing) + monkeypatch.setattr("cisco_sccfm_cli.services.ConfigService", lambda: service) + confirmation = MagicMock() + confirmation.unsafe_ask.return_value = True + monkeypatch.setattr(interactive_cli.questionary, "confirm", lambda *a, **k: confirmation) + + interactive_cli._remove_profile() + + assert service.load("lab") is None diff --git a/cisco_sccfm_scripts/token_store.py b/cisco_sccfm_scripts/token_store.py deleted file mode 100644 index 25ec89bc..00000000 --- a/cisco_sccfm_scripts/token_store.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2026 Cisco Systems, Inc. and its affiliates -# -# SPDX-License-Identifier: Apache-2.0 - -"""Token storage backed by the encrypted Ansible Vault file. - -Tokens are kept inside ``group_vars/all/vault.yml`` alongside the -active ``sccfm_api_token``. The vault is decrypted on read and -re-encrypted on write using ``ansible-vault`` + the ``.vault_pass`` -password file. - -Vault structure (plaintext):: - - --- - sccfm_api_token: "" - sccfm_saved_tokens: - - name: prod - region: us - token: "eyJ…" - - name: staging - region: int - token: "eyJ…" -""" - -from __future__ import annotations - -import subprocess -from dataclasses import dataclass -from pathlib import Path -from typing import cast - -import yaml - - -@dataclass(frozen=True) -class SavedToken: - """A single named API token with its associated region.""" - - name: str - region: str - token: str - - -class VaultTokenStore: - """Read/write helper for tokens stored in an encrypted vault file.""" - - def __init__(self, examples_path: Path) -> None: - self._vault_path = examples_path / "group_vars" / "all" / "vault.yml" - self._vault_pass_path = examples_path / ".vault_pass" - - # ── Public API ─────────────────────────────────────────────── - - @property - def vault_exists(self) -> bool: - """Return True if the vault file exists.""" - return self._vault_path.exists() - - @property - def vault_pass_exists(self) -> bool: - """Return True if the vault password file exists.""" - return self._vault_pass_path.exists() - - def list_tokens(self) -> list[SavedToken]: - """Return all saved tokens from the vault, sorted by name.""" - data = self._decrypt_vault() - if data is None: - return [] - raw_tokens = cast(list[dict[str, str]], data.get("sccfm_saved_tokens", [])) - tokens = [SavedToken(**entry) for entry in raw_tokens] - return sorted(tokens, key=lambda t: t.name) - - def save_active_and_tokens( - self, - active: SavedToken, - all_tokens: list[SavedToken], - ) -> Path: - """Write the active token + full saved list, then encrypt.""" - payload: dict[str, object] = { - "sccfm_api_token": active.token, - "sccfm_saved_tokens": [ - {"name": t.name, "region": t.region, "token": t.token} - for t in sorted(all_tokens, key=lambda t: t.name) - ], - } - return self._encrypt_vault(payload) - - # ── Private helpers ────────────────────────────────────────── - - def _decrypt_vault(self) -> dict[str, object] | None: - """Decrypt vault.yml and return parsed YAML, or None.""" - if not self._vault_path.exists() or not self._vault_pass_path.exists(): - return None - - result = subprocess.run( - [ - "ansible-vault", - "view", - str(self._vault_path), - "--vault-password-file", - str(self._vault_pass_path), - ], - capture_output=True, - text=True, - ) - if result.returncode != 0: - return None - - data: dict[str, object] = yaml.safe_load(result.stdout) or {} - return data - - def _encrypt_vault(self, payload: dict[str, object]) -> Path: - """Write *payload* as YAML to vault.yml, encrypting in-place.""" - self._vault_path.parent.mkdir(parents=True, exist_ok=True) - - content = "---\n" + yaml.dump(payload, default_flow_style=False, sort_keys=False) - - # Write plaintext, then encrypt in-place - self._vault_path.write_text(content) - result = subprocess.run( - [ - "ansible-vault", - "encrypt", - str(self._vault_path), - "--vault-password-file", - str(self._vault_pass_path), - ], - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise RuntimeError(f"ansible-vault encrypt failed:\n{result.stderr.strip()}") - - return self._vault_path diff --git a/dev/consistency-checklists/ci-consistency-checklist.md b/dev/consistency-checklists/ci-consistency-checklist.md index 3df6748a..0f6fa943 100644 --- a/dev/consistency-checklists/ci-consistency-checklist.md +++ b/dev/consistency-checklists/ci-consistency-checklist.md @@ -54,12 +54,12 @@ Advisory families are listed in section 6. If the PR touches one of these areas, check all related siblings and fail the review if the PR introduces or worsens drift. -### A. Region, auth, and environment contract +### A. Region and canonical profile contract Check when touching: - `cisco_sccfm_cli/commands/configure.py` - `sccfm-ansible/plugins/module_utils/config.py` -- `.env.example` +- `cisco_sccfm_core/services/profile_service.py` - `README.md` - `INSTALL.md` - Ansible module region docs @@ -67,7 +67,7 @@ Check when touching: Verify: - One canonical region vocabulary is used everywhere. - Region casing behavior is aligned across CLI and Ansible. -- `SCCFM_REGION`, `SCCFM_API_TOKEN`, and `SCCFM_CONFIG` semantics remain aligned. +- CLI and Ansible resolve the same named profile and optional `SCCFM_CONFIG` path. - Docs/examples do not advertise different region names or availability. Fail if: diff --git a/dev/consistency-checklists/claude-consistency.md b/dev/consistency-checklists/claude-consistency.md index c4508ede..044af422 100644 --- a/dev/consistency-checklists/claude-consistency.md +++ b/dev/consistency-checklists/claude-consistency.md @@ -240,14 +240,14 @@ ### 9.2 Ansible `Config` - **Canonical:** [sccfm-ansible/plugins/module_utils/config.py](sccfm-ansible/plugins/module_utils/config.py). - **Invariants:** - - Frozen dataclass with `__post_init__` env fallback (`SCCFM_REGION`, `SCCFM_API_TOKEN`). + - Frozen dataclass validates values resolved from the canonical profile store. - Allowed regions: `int, us, eu, apj, au, uae, in, ci` (`aus` is a legacy alias normalized to `au`). - - `base_argument_spec()` returns `region` + `api_token` (token has `no_log: True`). + - `base_argument_spec()` returns `profile` + `config_path`. - `create_config(module)` wraps validation in try/except → `module.fail_json(msg=...)`. -### 9.3 Environment variable names -- **Canonical set:** `SCCFM_REGION`, `SCCFM_API_TOKEN`, `SCCFM_CONFIG` (CLI only). -- **Invariants:** any new authenticated entry point reuses these — do not invent new variants. +### 9.3 Configuration path override +- **Canonical override:** `SCCFM_CONFIG`. +- **Invariants:** SCCFM region and token values come only from named profiles. --- @@ -338,7 +338,7 @@ ### 14.1 Module skeleton - **Canonical:** [sccfm-ansible/plugins/modules/execute_asa_cli.py](sccfm-ansible/plugins/modules/execute_asa_cli.py), [sccfm-ansible/plugins/modules/trigger_ftd_upgrade.py](sccfm-ansible/plugins/modules/trigger_ftd_upgrade.py), and the rest of `plugins/modules/`. - **Invariants:** - - `DOCUMENTATION` lists every option with `description`, `type`, `required`, `default`; `region`/`api_token` declare `env: [{name: SCCFM_REGION/...}]`. + - `DOCUMENTATION` lists every option with `description`, `type`, `required`, and default; shared auth options are `profile` and `config_path`. - `EXAMPLES` includes a direct call **and** a `module_defaults` (`group/cisco.sccfm.all`) example. - `RETURN` documents `changed`, `result`, and (on failure) `msg`/`error_code`/`error_details`/`status_code`. - `author: Cisco SCCFM Team`. @@ -400,7 +400,7 @@ ### 16.1 Poetry / `pyproject.toml` - **Invariants:** - Dependencies added via `poetry add`; dev deps in `[tool.poetry.group.dev.dependencies]`. - - Entry points: `sccfm-cli`, `devkit`, `build-ansible-collection`, `change-tokens`. + - Entry points: `sccfm-cli`, `sccfm-cli-interactive`, `build-ansible-collection`. - Tool configs (black, isort, mypy, pytest, coverage) all live in `pyproject.toml`. ### 16.2 Pre-commit @@ -416,14 +416,14 @@ - `cz` drives version bumps and `CHANGELOG.md` updates; do not edit version strings by hand. ### 16.5 Helper scripts -- **Canonical:** `cisco_sccfm_scripts/` (`devkit_cli.py`, `setup_environment.sh`, `setup_ci_environment.sh`, `setup_tokens.py`, `token_store.py`, `build_ansible_collection.py`, `cz.sh`). +- **Canonical:** `cisco_sccfm_scripts/` (`interactive_cli.py`, `setup_environment.sh`, `setup_ci_environment.sh`, `import_legacy_vault.py`, `build_ansible_collection.py`, `cz.sh`). - **Invariants:** any new repo-wide automation lives in `cisco_sccfm_scripts/` and is exposed via `pyproject.toml` entry points where it's user-facing. --- ## 17. Questionary Prompts -- **Canonical:** [cisco_sccfm_scripts/setup_tokens.py](cisco_sccfm_scripts/setup_tokens.py), `cisco_sccfm_cli/commands/configure.py`. +- **Canonical:** `cisco_sccfm_core/services/profile_service.py` and `cisco_sccfm_cli/commands/configure.py`. - **Invariants:** - Use `.unsafe_ask()` (sync API) — never the async variant. - `confirm()` for yes/no with explicit `default`. @@ -484,11 +484,11 @@ --- -## 22. Environment / Local Dev Files +## 22. Local Development Files - **Invariants:** - - `.env.example` committed; `.env` git-ignored. - - Variable names match the canonical set in §9.3. + - SCCFM credentials are not configured through project `.env` files. + - Profile files are owner-only and never committed. - `dev-commands.local.txt` is a developer scratchpad; don't reference it from production code. --- @@ -514,7 +514,7 @@ - [ ] **Pagination:** `--limit` 1–200 (default 50), `--offset` ≥0 (default 0); response shape `count/items/limit/offset`. - [ ] **Transactions:** poll via `TransactionService`; honor `--wait` / `--timeout`; print UID + URL even without `--wait`; exit `1` on failed wait. - [ ] **Device targets:** ASA/FTD filters mutually exclusive; `report_check_targets` JSON shape unchanged. -- [ ] **Config / env:** uses `SCCFM_REGION`, `SCCFM_API_TOKEN`, `SCCFM_CONFIG`; new region/token paths plumb through `Config` / `ConfigService`. +- [ ] **Profiles:** uses the shared `ProfileService`; optional path override is `SCCFM_CONFIG`. - [ ] **Models:** `@dataclass(frozen=True)`, full type hints, `from_dict`/`to_dict` defaults. - [ ] **Parsers:** module-level compiled regexes, return typed model, defensive on missing fields, accompanied by tests with real CLI fixtures. - [ ] **Ansible module:** `base_argument_spec()` merged, `supports_check_mode=True`, `module_defaults` example present, action group `cisco.sccfm.all`, full `RETURN` docs. diff --git a/dev/consistency-checklists/codex-consistency.md b/dev/consistency-checklists/codex-consistency.md index bc8f6cf2..f597976c 100644 --- a/dev/consistency-checklists/codex-consistency.md +++ b/dev/consistency-checklists/codex-consistency.md @@ -180,18 +180,17 @@ Primary locations: and the global `--profile` behavior. - [ ] Ansible modules use `base_argument_spec()` and `create_config(module)` before adding custom auth handling. -- [ ] Inventory plugin auth/env fallback stays aligned with module behavior. -- [ ] Region vocabulary stays aligned across CLI, Ansible, `.env.example`, +- [ ] Inventory plugin profile resolution stays aligned with module behavior. +- [ ] Region vocabulary stays aligned across the profile service, CLI, Ansible, README, install docs, and examples. -- [ ] Token/env variable names stay aligned: - `SCCFM_API_TOKEN`, `SCCFM_REGION`, `SCCFM_CONFIG`. +- [ ] `SCCFM_CONFIG` remains the only environment-based configuration override. Primary locations: - `cisco_sccfm_cli/services/config_service.py` - `cisco_sccfm_cli/commands/configure.py` - `sccfm-ansible/plugins/module_utils/config.py` - `sccfm-ansible/plugins/inventory/sccfm.py` -- `.env.example` +- `cisco_sccfm_core/services/profile_service.py` - `README.md` - `INSTALL.md` @@ -430,15 +429,15 @@ Main test locations: - Ansible unit: `sccfm-ansible/plugins/modules/tests/**` - Ansible e2e: `sccfm-ansible/e2e/**` -## 22. Devkit and discoverability +## 22. Interactive CLI and discoverability - [ ] New CLI commands remain discoverable through Click introspection, which - powers the devkit interactive runner. -- [ ] New example playbooks remain runnable through the devkit example runner. + powers the `sccfm-cli-interactive` runner. +- [ ] New example playbooks remain runnable through the interactive example runner. - [ ] Setup/lint/test/build workflows stay aligned between scripts and docs. Primary locations: -- `cisco_sccfm_scripts/devkit_cli.py` +- `cisco_sccfm_scripts/interactive_cli.py` - `cisco_sccfm_scripts/cli_commands.py` - `cisco_sccfm_scripts/setup_environment.sh` - `README.md` @@ -545,7 +544,7 @@ against them so fixed consistency issues do not reappear. policy management, object override flows, or manager/access-policy flows. - [ ] CLI group commands and helper-only modules have less direct coverage than leaf commands. Any changes to group wiring should trigger an explicit review - of command registration and devkit introspection. + of command registration and interactive CLI introspection. ## 26. Default AI review prompt fragment diff --git a/dev/inconsistency-findings/claude-inconsistencies.md b/dev/inconsistency-findings/claude-inconsistencies.md index 23a8b081..2b7de21a 100644 --- a/dev/inconsistency-findings/claude-inconsistencies.md +++ b/dev/inconsistency-findings/claude-inconsistencies.md @@ -82,7 +82,7 @@ the expected behavior, the offending site(s), and a one-line fix. - **Offending sites:** - [cisco_sccfm_scripts/build_ansible_collection.py](cisco_sccfm_scripts/build_ansible_collection.py) - [cisco_sccfm_scripts/validate_regex.py](cisco_sccfm_scripts/validate_regex.py) - - [cisco_sccfm_scripts/_test_setup_tokens.py](cisco_sccfm_scripts/_test_setup_tokens.py) + - [cisco_sccfm_scripts/test_import_legacy_vault.py](cisco_sccfm_scripts/test_import_legacy_vault.py) - [cisco_sccfm_core/__init__.py](cisco_sccfm_core/__init__.py) - **Severity:** **Low** - **Fix:** Add the import line directly after each shebang / at the top. diff --git a/dev/inconsistency-findings/codex-inconsistencies.md b/dev/inconsistency-findings/codex-inconsistencies.md index 54522559..351b7ef4 100644 --- a/dev/inconsistency-findings/codex-inconsistencies.md +++ b/dev/inconsistency-findings/codex-inconsistencies.md @@ -30,7 +30,7 @@ Evidence: - Ansible shared config uses `ALLOWED_REGIONS = ("int", "us", "eu", "apj", "aus", "uae", "in", "ci")` in `sccfm-ansible/plugins/module_utils/config.py`. -- `.env.example` also documents `aus` and `ci`. +- Resolved: region vocabulary now comes from the canonical profile configuration flow. Impact: - A region value can be presented as valid in one surface and invalid in another. @@ -815,7 +815,7 @@ Evidence: Impact: - Equivalent values such as `US` or `Eu` are normalized in the CLI flow but rejected in the Ansible flow. -- Cross-surface behavior for `SCCFM_REGION` is inconsistent. +- Resolved: CLI and Ansible now share canonical profile resolution. Recommendation: - Normalize region values to lowercase in the Ansible config path before validation, @@ -840,7 +840,7 @@ into the main priority stack because they are mostly mechanical style cleanup: It is present in much of the repo, but missing from several scripts and service/package files, including `cisco_sccfm_scripts/build_ansible_collection.py`, `cisco_sccfm_scripts/validate_regex.py`, - `cisco_sccfm_scripts/_test_setup_tokens.py`, + `cisco_sccfm_scripts/test_import_legacy_vault.py`, `cisco_sccfm_core/__init__.py`, and a number of command group / service files. - Region ordering/spelling presentation also drifts beyond the core value split. diff --git a/docs/ansible/index.md b/docs/ansible/index.md index 93a1b935..255f13e5 100644 --- a/docs/ansible/index.md +++ b/docs/ansible/index.md @@ -13,6 +13,10 @@ Generated from `ansible-doc` output. - [cisco.sccfm.sccfm](inventory/sccfm.html) +## Lookup Plugins + +- [cisco.sccfm.profile](lookup/profile.html) + ## Modules - [cisco.sccfm.add_asa_shun](modules/add_asa_shun.html) diff --git a/docs/ansible/inventory/sccfm.md b/docs/ansible/inventory/sccfm.md index 98d4611e..66d5cec8 100644 --- a/docs/ansible/inventory/sccfm.md +++ b/docs/ansible/inventory/sccfm.md @@ -20,12 +20,10 @@ $ ansible-doc -t inventory cisco.sccfm.sccfm OPTIONS (= indicates it is required): -= api_token API token for the SCCFM region. - set_via: - env: - - name: SCCFM_API_TOKEN - no_log: true - type: str +- config_path Optional path to the canonical SCCFM profile + configuration file. + default: null + type: path - group Group to place all discovered SCCFM devices into. default: sccfm @@ -43,15 +41,12 @@ OPTIONS (= indicates it is required): = plugin Ensure this plugin gets loaded. choices: [cisco.sccfm.sccfm] -- query Optional text filter applied to device names. - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str -= region SCCFM region to target (int, us, eu, apj, au, uae, in, or - ci). - set_via: - env: - - name: SCCFM_REGION +- query Optional text filter applied to device names. + default: null type: str NAME: cisco.sccfm.sccfm @@ -60,8 +55,7 @@ PLUGIN_TYPE: inventory EXAMPLES: plugin: cisco.sccfm.sccfm -region: us -api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" +profile: default limit: 100 query: "asa" group: sccfm diff --git a/docs/ansible/lookup/profile.md b/docs/ansible/lookup/profile.md new file mode 100644 index 00000000..4884edd0 --- /dev/null +++ b/docs/ansible/lookup/profile.md @@ -0,0 +1,51 @@ +--- +layout: page +title: "cisco.sccfm.profile" +--- + + + +[Back to Ansible Reference](../index.html){:.doc-button} + +{% raw %} +```text +$ ansible-doc -t lookup cisco.sccfm.profile + +> LOOKUP cisco.sccfm.profile (sccfm-ansible/plugins/lookup/profile.py) + + Reads a region or API token from the canonical SCCFM profile store. + Configure profiles with `sccfm-cli configure' before using this + lookup. + +OPTIONS (= indicates it is required): + += _terms Profile names to read. + +- config_path Optional path to the canonical SCCFM profile + configuration file. + default: null + type: path + +- field Profile field to return. + choices: [region, api_token] + default: api_token + +AUTHOR: Cisco SCCFM Team + +NAME: profile + +EXAMPLES: +- name: Use a profile token in an API request + ansible.builtin.uri: + url: https://example.invalid/api + headers: + Authorization: "Bearer {{ lookup('cisco.sccfm.profile', 'default') }}" + no_log: true + +RETURN VALUES: + +- _raw Values read from the selected SCCFM profiles. + elements: str + type: list +``` +{% endraw %} diff --git a/docs/ansible/modules/add_asa_shun.md b/docs/ansible/modules/add_asa_shun.md index 5ea3f4de..54099a56 100644 --- a/docs/ansible/modules/add_asa_shun.md +++ b/docs/ansible/modules/add_asa_shun.md @@ -31,13 +31,10 @@ $ ansible-doc -t module cisco.sccfm.add_asa_shun OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - dest_ip Destination IP of a specific connection to drop immediately. @@ -97,6 +94,10 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - protocol Protocol of the connection to drop (tcp or udp). Requires `dest_ip'. Only valid when using `source_ip' (not `entries'). @@ -110,13 +111,6 @@ OPTIONS (= indicates it is required): default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - source_ip The source IP address of the attacking host to block. Mutually exclusive with `entries'. default: null @@ -142,8 +136,7 @@ EXAMPLES: cisco.sccfm.add_asa_shun: query: "name:prod-* AND connectivityState:ONLINE" source_ip: "10.99.99.99" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Shun with connection tuple to drop an existing connection - name: Block attacker and drop active connection @@ -168,8 +161,7 @@ EXAMPLES: dest_port: 443 protocol: tcp - source_ip: "203.0.113.60" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 4: Using module_defaults (recommended) - name: Add shun entries @@ -177,8 +169,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Shun attacker IP cisco.sccfm.add_asa_shun: diff --git a/docs/ansible/modules/add_network_group_members.md b/docs/ansible/modules/add_network_group_members.md index f12da3ad..82c3df2e 100644 --- a/docs/ansible/modules/add_network_group_members.md +++ b/docs/ansible/modules/add_network_group_members.md @@ -24,31 +24,25 @@ $ ansible-doc -t module cisco.sccfm.add_network_group_members OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - name Name of the network group to update. default: null type: str +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + = referenced_objects List of existing network object names or UIDs to add to the group. Names are resolved to UIDs automatically. elements: str type: list -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uid Unique identifier (UID) of the network group to update. default: null type: str @@ -63,8 +57,7 @@ EXAMPLES: referenced_objects: - web-server-01 - web-server-02 - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Add members by UID - name: Add members to a network group by UID @@ -80,8 +73,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Add web servers to group cisco.sccfm.add_network_group_members: diff --git a/docs/ansible/modules/add_object_override.md b/docs/ansible/modules/add_object_override.md index 14849bda..f0ba879c 100644 --- a/docs/ansible/modules/add_object_override.md +++ b/docs/ansible/modules/add_object_override.md @@ -25,13 +25,10 @@ $ ansible-doc -t module cisco.sccfm.add_object_override OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path = override_value The literal value for the override. For network objects this can be an IP address (e.g., @@ -40,11 +37,8 @@ OPTIONS (= indicates it is required): URL string. type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = target_id UID of the target device for which the override applies. @@ -63,8 +57,7 @@ EXAMPLES: uid: "abc-123-def" target_id: "70bde3c9-328c-4a4b-bdc9-a4d4042bf09a" override_value: "10.10.10.10" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults to avoid repeating credentials - name: Add object overrides @@ -72,8 +65,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Override web server IP for branch device cisco.sccfm.add_object_override: diff --git a/docs/ansible/modules/apply_object_override_as_default.md b/docs/ansible/modules/apply_object_override_as_default.md index fa8f4e3f..8c085b69 100644 --- a/docs/ansible/modules/apply_object_override_as_default.md +++ b/docs/ansible/modules/apply_object_override_as_default.md @@ -21,19 +21,13 @@ $ ansible-doc -t module cisco.sccfm.apply_object_override_as_default OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = target_id UID of the target device whose override value to promote @@ -51,8 +45,7 @@ EXAMPLES: cisco.sccfm.apply_object_override_as_default: uid: "abc-123-def" target_id: "897b293f-132e-4678-9d78-0f0947629500" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults - name: Apply object override as default @@ -60,8 +53,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Apply override as default cisco.sccfm.apply_object_override_as_default: diff --git a/docs/ansible/modules/asa_ha_check.md b/docs/ansible/modules/asa_ha_check.md index fa3051cc..652d9c02 100644 --- a/docs/ansible/modules/asa_ha_check.md +++ b/docs/ansible/modules/asa_ha_check.md @@ -26,13 +26,10 @@ $ ansible-doc -t module cisco.sccfm.asa_ha_check OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. default: 50 @@ -42,19 +39,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to check. Mutually exclusive with `query'. default: null @@ -68,8 +62,7 @@ EXAMPLES: - name: Run HA checks on production ASAs cisco.sccfm.asa_ha_check: query: "name:prod-ha-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: ha_results # Example 2: Check HA status on a specific device by UID @@ -96,8 +89,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Run HA checks cisco.sccfm.asa_ha_check: diff --git a/docs/ansible/modules/change_asa_boot_image.md b/docs/ansible/modules/change_asa_boot_image.md index 9dbde793..1ee21b76 100644 --- a/docs/ansible/modules/change_asa_boot_image.md +++ b/docs/ansible/modules/change_asa_boot_image.md @@ -25,13 +25,10 @@ $ ansible-doc -t module cisco.sccfm.change_asa_boot_image OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path = image_path Full ASA image path already present on the device, such as `disk0:/asa9xxx.bin' or `boot:/asa9xxx.bin'. @@ -47,19 +44,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to target. Mutually exclusive with `query'. default: null @@ -74,8 +68,7 @@ EXAMPLES: cisco.sccfm.change_asa_boot_image: query: "name:branch-*" image_path: "disk0:/asa9-18-4-smp-k8.bin" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Change boot image on specific devices - name: Change boot image on specific ASA devices @@ -100,8 +93,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Set boot image on branch ASAs cisco.sccfm.change_asa_boot_image: diff --git a/docs/ansible/modules/change_asa_local_password.md b/docs/ansible/modules/change_asa_local_password.md index b67693e9..d829abd8 100644 --- a/docs/ansible/modules/change_asa_local_password.md +++ b/docs/ansible/modules/change_asa_local_password.md @@ -27,13 +27,10 @@ $ ansible-doc -t module cisco.sccfm.change_asa_local_password OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. @@ -49,19 +46,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to target. Mutually exclusive with `query'. default: null @@ -80,8 +74,7 @@ EXAMPLES: query: "name:branch-* AND connectivityState:ONLINE" username: admin new_password: "{{ vault_new_asa_password }}" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: password_results # Example 2: Change password on specific devices by UID @@ -100,8 +93,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Change admin password on all online ASAs cisco.sccfm.change_asa_local_password: diff --git a/docs/ansible/modules/clear_asa_shun.md b/docs/ansible/modules/clear_asa_shun.md index 5a63825c..ae296181 100644 --- a/docs/ansible/modules/clear_asa_shun.md +++ b/docs/ansible/modules/clear_asa_shun.md @@ -24,13 +24,10 @@ $ ansible-doc -t module cisco.sccfm.clear_asa_shun OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. @@ -42,19 +39,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to clear shuns on. Mutually exclusive with `query'. default: null @@ -68,8 +62,7 @@ EXAMPLES: - name: Clear all shuns on production ASAs cisco.sccfm.clear_asa_shun: query: "name:prod-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Clear shuns on specific devices by UID - name: Clear shuns on specific ASA @@ -83,8 +76,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Clear all shuns on online ASAs cisco.sccfm.clear_asa_shun: diff --git a/docs/ansible/modules/configure_manager.md b/docs/ansible/modules/configure_manager.md index 8fc2c5d7..3becec14 100644 --- a/docs/ansible/modules/configure_manager.md +++ b/docs/ansible/modules/configure_manager.md @@ -114,8 +114,7 @@ EXAMPLES: fmc_access_policy_uid: "{{ fmc_access_policy_uid }}" licenses: - BASE - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: onboard_result - name: Complete registration over SSH diff --git a/docs/ansible/modules/create_access_rule.md b/docs/ansible/modules/create_access_rule.md index 20c852b9..a7aeea54 100644 --- a/docs/ansible/modules/create_access_rule.md +++ b/docs/ansible/modules/create_access_rule.md @@ -28,13 +28,10 @@ OPTIONS (= indicates it is required): default: null type: bool -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - destination_network Destination network object name. default: null @@ -58,14 +55,11 @@ OPTIONS (= indicates it is required): default: null type: str -- protocol Protocol (e.g. tcp, udp, ip). - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION +- protocol Protocol (e.g. tcp, udp, ip). default: null type: str @@ -101,8 +95,7 @@ EXAMPLES: protocol: tcp destination_port: "443" remark: "Allow web to database" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Create a deny rule using module_defaults - name: Create access rules @@ -110,8 +103,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Create a deny rule for a source subnet cisco.sccfm.create_access_rule: @@ -125,7 +117,7 @@ EXAMPLES: destination_port: "1433" remark: "Block blocked-subnet to SQL" -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: Create an inactive permit rule cisco.sccfm.create_access_rule: access_group_uid: "{{ access_group_uid }}" diff --git a/docs/ansible/modules/create_network_group.md b/docs/ansible/modules/create_network_group.md index 80de113d..e3db78d1 100644 --- a/docs/ansible/modules/create_network_group.md +++ b/docs/ansible/modules/create_network_group.md @@ -22,13 +22,10 @@ $ ansible-doc -t module cisco.sccfm.create_network_group OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - description Optional description for the network group. default: null @@ -49,6 +46,10 @@ OPTIONS (= indicates it is required): elements: str type: list +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - referenced_objects List of existing network object names or UIDs to include in the group. Names are resolved to UIDs automatically. @@ -56,13 +57,6 @@ OPTIONS (= indicates it is required): elements: str type: list -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - tags Mapping of tag keys to lists of tag values. For example, `{"environment": ["production", "staging"]}'. default: null @@ -88,8 +82,7 @@ EXAMPLES: labels: - production - web - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Create a group with referenced objects using module_defaults - name: Create network groups @@ -97,8 +90,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Create group from existing objects cisco.sccfm.create_network_group: @@ -111,7 +103,7 @@ EXAMPLES: environment: - production -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: Create a group with URL literals cisco.sccfm.create_network_group: name: trusted-urls diff --git a/docs/ansible/modules/create_network_object.md b/docs/ansible/modules/create_network_object.md index 8ddac83f..47e33f67 100644 --- a/docs/ansible/modules/create_network_object.md +++ b/docs/ansible/modules/create_network_object.md @@ -20,13 +20,10 @@ $ ansible-doc -t module cisco.sccfm.create_network_object OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - description Optional description for the network object. default: null @@ -40,11 +37,8 @@ OPTIONS (= indicates it is required): = name Name of the network object. type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str - tags Mapping of tag keys to lists of tag values. For example, @@ -69,8 +63,7 @@ EXAMPLES: labels: - production - web - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Create a subnet network object using module_defaults - name: Create network objects @@ -78,8 +71,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Create branch office subnet cisco.sccfm.create_network_object: @@ -93,7 +85,7 @@ EXAMPLES: environment: - production -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: Create a range network object cisco.sccfm.create_network_object: name: dhcp-pool diff --git a/docs/ansible/modules/delete_access_rule.md b/docs/ansible/modules/delete_access_rule.md index 3e79e390..ae605294 100644 --- a/docs/ansible/modules/delete_access_rule.md +++ b/docs/ansible/modules/delete_access_rule.md @@ -19,19 +19,13 @@ $ ansible-doc -t module cisco.sccfm.delete_access_rule OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = uid Unique identifier (UID) of the access rule to delete. @@ -44,8 +38,7 @@ EXAMPLES: - name: Delete access rule cisco.sccfm.delete_access_rule: uid: "ac981dcd-9860-401e-a51d-c615c946b72f" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Delete using module_defaults - name: Delete access rules @@ -53,8 +46,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Delete old rule cisco.sccfm.delete_access_rule: diff --git a/docs/ansible/modules/delete_network_group.md b/docs/ansible/modules/delete_network_group.md index 9e4029d3..028e392f 100644 --- a/docs/ansible/modules/delete_network_group.md +++ b/docs/ansible/modules/delete_network_group.md @@ -20,23 +20,17 @@ $ ansible-doc -t module cisco.sccfm.delete_network_group OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - name Name of the network group to delete. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str - uid Unique identifier (UID) of the network group to delete. @@ -58,15 +52,13 @@ EXAMPLES: - name: Delete network group by UID cisco.sccfm.delete_network_group: uid: "abc-123-def-456" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Delete a network group by name - name: Delete network group by name cisco.sccfm.delete_network_group: name: "web-server-group" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 3: Delete multiple groups using module_defaults - name: Delete network groups @@ -74,8 +66,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Delete obsolete network groups cisco.sccfm.delete_network_group: @@ -84,7 +75,7 @@ EXAMPLES: - web-server-group-01 - web-subnet-group -# Example 4: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 4: Using the default configured profile - name: Delete a network group cisco.sccfm.delete_network_group: name: "temporary-group" diff --git a/docs/ansible/modules/delete_network_object.md b/docs/ansible/modules/delete_network_object.md index 01ce9f6c..54c060ad 100644 --- a/docs/ansible/modules/delete_network_object.md +++ b/docs/ansible/modules/delete_network_object.md @@ -18,23 +18,17 @@ $ ansible-doc -t module cisco.sccfm.delete_network_object OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - name Name of the network object to delete. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str - uid Unique identifier (UID) of the network object to delete. @@ -53,15 +47,13 @@ EXAMPLES: - name: Delete network object by UID cisco.sccfm.delete_network_object: uid: "abc-123-def-456" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Delete a network object by name - name: Delete network object by name cisco.sccfm.delete_network_object: name: "old-web-server" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 3: Delete multiple objects using module_defaults - name: Delete network objects @@ -69,8 +61,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Delete obsolete network objects cisco.sccfm.delete_network_object: @@ -80,7 +71,7 @@ EXAMPLES: - old-server-02 - deprecated-subnet -# Example 4: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 4: Using the default configured profile - name: Delete a network object cisco.sccfm.delete_network_object: name: "temporary-host" diff --git a/docs/ansible/modules/delete_object_override.md b/docs/ansible/modules/delete_object_override.md index e5550dc6..0898681e 100644 --- a/docs/ansible/modules/delete_object_override.md +++ b/docs/ansible/modules/delete_object_override.md @@ -21,19 +21,13 @@ $ ansible-doc -t module cisco.sccfm.delete_object_override OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = target_id UID of the target device whose override to delete. @@ -50,8 +44,7 @@ EXAMPLES: cisco.sccfm.delete_object_override: uid: "abc-123-def" target_id: "70bde3c9-328c-4a4b-bdc9-a4d4042bf09a" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults - name: Delete object overrides @@ -59,8 +52,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Delete override cisco.sccfm.delete_object_override: diff --git a/docs/ansible/modules/deploy_cdfmc_ftd.md b/docs/ansible/modules/deploy_cdfmc_ftd.md index 2489d85f..8e08a709 100644 --- a/docs/ansible/modules/deploy_cdfmc_ftd.md +++ b/docs/ansible/modules/deploy_cdfmc_ftd.md @@ -20,13 +20,10 @@ $ ansible-doc -t module cisco.sccfm.deploy_cdfmc_ftd OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - deployment_notes Notes for the deployment. default: null @@ -51,6 +48,10 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to select cdFMC-managed FTD devices. Mutually exclusive with `uids'. The query is automatically combined with @@ -58,13 +59,6 @@ OPTIONS (= indicates it is required): default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - timeout Maximum number of seconds to wait for completion when `wait=true'. default: 3600 @@ -89,8 +83,7 @@ EXAMPLES: cisco.sccfm.deploy_cdfmc_ftd: uids: - "12345678-1234-1234-1234-123456789abc" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Deploy with notes - name: Deploy FTD changes with deployment notes @@ -114,8 +107,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Deploy branch FTD changes cisco.sccfm.deploy_cdfmc_ftd: diff --git a/docs/ansible/modules/edit_object_override.md b/docs/ansible/modules/edit_object_override.md index c8094ea0..74df9144 100644 --- a/docs/ansible/modules/edit_object_override.md +++ b/docs/ansible/modules/edit_object_override.md @@ -21,13 +21,10 @@ $ ansible-doc -t module cisco.sccfm.edit_object_override OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path = override_value The new value for the override. For network objects this can be an IP address (e.g., `10.0.0.1'), a @@ -35,11 +32,8 @@ OPTIONS (= indicates it is required): For URL objects this should be the URL string. type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = target_id UID of the target device whose override value to edit. @@ -57,8 +51,7 @@ EXAMPLES: uid: "abc-123-def" target_id: "70bde3c9-328c-4a4b-bdc9-a4d4042bf09a" override_value: "10.20.30.40" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults - name: Edit object overrides @@ -66,8 +59,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Edit override cisco.sccfm.edit_object_override: diff --git a/docs/ansible/modules/execute_asa_cli.md b/docs/ansible/modules/execute_asa_cli.md index 62c756c4..31db63be 100644 --- a/docs/ansible/modules/execute_asa_cli.md +++ b/docs/ansible/modules/execute_asa_cli.md @@ -24,14 +24,6 @@ $ ansible-doc -t module cisco.sccfm.execute_asa_cli OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN - default: null - no_log: true - type: str - - command Single ASA CLI command to execute. Mutually exclusive with `commands'. Use `commands' to run more than one command. @@ -45,6 +37,11 @@ OPTIONS (= indicates it is required): elements: str type: list +- config_path Optional path to the canonical SCCFM profile + configuration file. + default: null + type: path + - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. default: 50 @@ -55,19 +52,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to execute commands on. Mutually exclusive with `query'. default: null @@ -84,8 +78,7 @@ EXAMPLES: commands: - "show version" - "show running-config" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: cli_results # Example 2: Execute commands on specific devices by UID @@ -104,8 +97,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show version on branch ASAs cisco.sccfm.execute_asa_cli: diff --git a/docs/ansible/modules/execute_ftd_cli.md b/docs/ansible/modules/execute_ftd_cli.md index 43a24b3f..d2447fe6 100644 --- a/docs/ansible/modules/execute_ftd_cli.md +++ b/docs/ansible/modules/execute_ftd_cli.md @@ -25,14 +25,6 @@ $ ansible-doc -t module cisco.sccfm.execute_ftd_cli OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN - default: null - no_log: true - type: str - - command The show command to execute on the FTD devices. Only show commands are supported (e.g. show version, show failover). @@ -48,6 +40,11 @@ OPTIONS (= indicates it is required): elements: str type: list +- config_path Optional path to the canonical SCCFM profile + configuration file. + default: null + type: path + - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. default: 50 @@ -58,6 +55,10 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter cdFMC-managed FTD devices. Mutually exclusive with `uids'. The query is automatically combined with @@ -65,13 +66,6 @@ OPTIONS (= indicates it is required): default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to execute the command on. Mutually exclusive with `query'. default: null @@ -86,8 +80,7 @@ EXAMPLES: cisco.sccfm.execute_ftd_cli: query: "name:prod-* AND connectivityState:ONLINE" command: "show failover" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: cli_results # Example 2: Execute a command on specific devices by UID @@ -105,8 +98,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show route on branch FTDs cisco.sccfm.execute_ftd_cli: diff --git a/docs/ansible/modules/get_access_group.md b/docs/ansible/modules/get_access_group.md index 7ae6cca1..c8ef9eed 100644 --- a/docs/ansible/modules/get_access_group.md +++ b/docs/ansible/modules/get_access_group.md @@ -17,19 +17,13 @@ $ ansible-doc -t module cisco.sccfm.get_access_group OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = uid Unique identifier (UID) of the access group to retrieve. @@ -42,8 +36,7 @@ EXAMPLES: - name: Get access group cisco.sccfm.get_access_group: uid: "c6fa254e-db7a-447e-a58f-95df1e09c2af" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show access group name @@ -56,8 +49,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Get access group cisco.sccfm.get_access_group: diff --git a/docs/ansible/modules/get_access_rule.md b/docs/ansible/modules/get_access_rule.md index 26909497..690d46fb 100644 --- a/docs/ansible/modules/get_access_rule.md +++ b/docs/ansible/modules/get_access_rule.md @@ -17,19 +17,13 @@ $ ansible-doc -t module cisco.sccfm.get_access_rule OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = uid Unique identifier (UID) of the access rule to retrieve. @@ -42,8 +36,7 @@ EXAMPLES: - name: Get access rule cisco.sccfm.get_access_rule: uid: "ac981dcd-9860-401e-a51d-c615c946b72f" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show rule @@ -56,8 +49,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Get access rule details cisco.sccfm.get_access_rule: diff --git a/docs/ansible/modules/get_object.md b/docs/ansible/modules/get_object.md index 96ae6035..755393d3 100644 --- a/docs/ansible/modules/get_object.md +++ b/docs/ansible/modules/get_object.md @@ -19,19 +19,13 @@ $ ansible-doc -t module cisco.sccfm.get_object OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = uid Unique identifier (UID) of the object to retrieve. @@ -44,8 +38,7 @@ EXAMPLES: - name: Get object cisco.sccfm.get_object: uid: "fd526e22-12ff-4fa0-a88d-7375c5d1e144" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: obj - name: Show object @@ -58,8 +51,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Get object details cisco.sccfm.get_object: diff --git a/docs/ansible/modules/list_access_groups.md b/docs/ansible/modules/list_access_groups.md index f2122920..6a45b62b 100644 --- a/docs/ansible/modules/list_access_groups.md +++ b/docs/ansible/modules/list_access_groups.md @@ -19,13 +19,10 @@ $ ansible-doc -t module cisco.sccfm.list_access_groups OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of results to return. default: 50 @@ -35,14 +32,11 @@ OPTIONS (= indicates it is required): default: 0 type: int -- query Optional Lucene query string to filter results. - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION +- query Optional Lucene query string to filter results. default: null type: str @@ -52,8 +46,7 @@ EXAMPLES: # List all access groups - name: List access groups cisco.sccfm.list_access_groups: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Display access groups @@ -66,8 +59,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List access groups cisco.sccfm.list_access_groups: diff --git a/docs/ansible/modules/list_access_rules.md b/docs/ansible/modules/list_access_rules.md index 090e2357..5af3cfe7 100644 --- a/docs/ansible/modules/list_access_rules.md +++ b/docs/ansible/modules/list_access_rules.md @@ -19,13 +19,10 @@ $ ansible-doc -t module cisco.sccfm.list_access_rules OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of results to return. default: 50 @@ -35,14 +32,11 @@ OPTIONS (= indicates it is required): default: 0 type: int -- query Optional Lucene query string to filter results. - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION +- query Optional Lucene query string to filter results. default: null type: str @@ -52,8 +46,7 @@ EXAMPLES: # Example 1: List all access rules - name: List all access rules cisco.sccfm.list_access_rules: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Display access rules @@ -66,8 +59,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: List first page of access rules cisco.sccfm.list_access_rules: diff --git a/docs/ansible/modules/list_asa_boot_registry.md b/docs/ansible/modules/list_asa_boot_registry.md index 287a4e84..2e4a41dd 100644 --- a/docs/ansible/modules/list_asa_boot_registry.md +++ b/docs/ansible/modules/list_asa_boot_registry.md @@ -26,13 +26,10 @@ $ ansible-doc -t module cisco.sccfm.list_asa_boot_registry OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. @@ -44,19 +41,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to query. Mutually exclusive with `query'. default: null @@ -70,8 +64,7 @@ EXAMPLES: - name: List boot registry on production ASAs cisco.sccfm.list_asa_boot_registry: query: "name:prod-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: boot_registry # Example 2: Get boot registry info for specific devices by UID @@ -88,8 +81,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List boot registry on branch ASAs cisco.sccfm.list_asa_boot_registry: diff --git a/docs/ansible/modules/list_asa_compatible_versions.md b/docs/ansible/modules/list_asa_compatible_versions.md index 3d01fb00..31008057 100644 --- a/docs/ansible/modules/list_asa_compatible_versions.md +++ b/docs/ansible/modules/list_asa_compatible_versions.md @@ -26,13 +26,10 @@ $ ansible-doc -t module cisco.sccfm.list_asa_compatible_versions OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. @@ -54,19 +51,16 @@ OPTIONS (= indicates it is required): default: false type: bool +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to query. Mutually exclusive with `query'. default: null @@ -81,8 +75,7 @@ EXAMPLES: cisco.sccfm.list_asa_compatible_versions: uids: - "12345678-1234-1234-1234-123456789abc" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: compat_versions - name: Show compatible versions @@ -121,8 +114,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Get compatible versions for branch ASAs cisco.sccfm.list_asa_compatible_versions: diff --git a/docs/ansible/modules/list_asa_disk_files.md b/docs/ansible/modules/list_asa_disk_files.md index eb3050d7..d9d09595 100644 --- a/docs/ansible/modules/list_asa_disk_files.md +++ b/docs/ansible/modules/list_asa_disk_files.md @@ -26,13 +26,10 @@ $ ansible-doc -t module cisco.sccfm.list_asa_disk_files OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. @@ -44,19 +41,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to query. Mutually exclusive with `query'. default: null @@ -70,8 +64,7 @@ EXAMPLES: - name: List disk files on production ASAs cisco.sccfm.list_asa_disk_files: query: "name:prod-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: disk_files # Example 2: List files on specific devices by UID @@ -88,8 +81,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List files on branch ASAs cisco.sccfm.list_asa_disk_files: diff --git a/docs/ansible/modules/list_asa_local_users.md b/docs/ansible/modules/list_asa_local_users.md index 8bf5c9b6..762dabbf 100644 --- a/docs/ansible/modules/list_asa_local_users.md +++ b/docs/ansible/modules/list_asa_local_users.md @@ -20,13 +20,10 @@ $ ansible-doc -t module cisco.sccfm.list_asa_local_users OPTIONS (= indicates it is required): -- api_token API token for SCCFM - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. default: 50 @@ -36,15 +33,12 @@ OPTIONS (= indicates it is required): default: 0 type: int -- query Lucene query to filter ASA devices. - Mutually exclusive with `uids'. - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION +- query Lucene query to filter ASA devices. + Mutually exclusive with `uids'. default: null type: str @@ -67,15 +61,13 @@ EXAMPLES: cisco.sccfm.list_asa_local_users: query: "name:branch-* AND connectivityState:ONLINE" region: "us" - api_token: "{{ sccfm_api_token }}" - name: List local users with shared auth hosts: localhost gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List local users on branch ASAs cisco.sccfm.list_asa_local_users: diff --git a/docs/ansible/modules/list_asa_not_on_version.md b/docs/ansible/modules/list_asa_not_on_version.md index 0d68cc3f..1b2057e2 100644 --- a/docs/ansible/modules/list_asa_not_on_version.md +++ b/docs/ansible/modules/list_asa_not_on_version.md @@ -24,13 +24,10 @@ $ ansible-doc -t module cisco.sccfm.list_asa_not_on_version OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to fetch when using `query' or no filter. @@ -43,6 +40,10 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to narrow the set of ASA devices to check. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. @@ -50,13 +51,6 @@ OPTIONS (= indicates it is required): default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to check. Mutually exclusive with `query'. If omitted, all ASA devices are checked (or those matching @@ -77,8 +71,7 @@ EXAMPLES: - name: Find ASAs not on 9.20(3)13 cisco.sccfm.list_asa_not_on_version: version: "9.20(3)13" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show devices that need upgrading @@ -108,8 +101,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Find ASAs not on target version cisco.sccfm.list_asa_not_on_version: diff --git a/docs/ansible/modules/list_cdfmc_access_policies.md b/docs/ansible/modules/list_cdfmc_access_policies.md index f602df7b..b839092a 100644 --- a/docs/ansible/modules/list_cdfmc_access_policies.md +++ b/docs/ansible/modules/list_cdfmc_access_policies.md @@ -20,13 +20,10 @@ $ ansible-doc -t module cisco.sccfm.list_cdfmc_access_policies OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path = domain_uid The FMC domain UID to query. Obtain this from the `fmc_domain_uid' field returned by the `list_managers' @@ -41,11 +38,8 @@ OPTIONS (= indicates it is required): default: 0 type: int -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str AUTHOR: Cisco SCCFM Team @@ -55,8 +49,7 @@ EXAMPLES: - name: List cdFMC access policies cisco.sccfm.list_cdfmc_access_policies: domain_uid: "e276abec-e0f2-11e3-8169-6d9ed49b625f" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show access policies @@ -69,8 +62,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List access policies for a domain cisco.sccfm.list_cdfmc_access_policies: diff --git a/docs/ansible/modules/list_ftd_compatible_versions.md b/docs/ansible/modules/list_ftd_compatible_versions.md index 9466fd73..d73e303e 100644 --- a/docs/ansible/modules/list_ftd_compatible_versions.md +++ b/docs/ansible/modules/list_ftd_compatible_versions.md @@ -22,13 +22,10 @@ $ ansible-doc -t module cisco.sccfm.list_ftd_compatible_versions OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. @@ -50,6 +47,10 @@ OPTIONS (= indicates it is required): default: false type: bool +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter FTD devices. Mutually exclusive with `uids'. The query is automatically combined with FTD device type @@ -57,13 +58,6 @@ OPTIONS (= indicates it is required): default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to query. Mutually exclusive with `query'. default: null @@ -78,8 +72,7 @@ EXAMPLES: cisco.sccfm.list_ftd_compatible_versions: uids: - "12345678-1234-1234-1234-123456789abc" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: compat_versions - name: Show compatible versions @@ -118,8 +111,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Get compatible versions for branch FTDs cisco.sccfm.list_ftd_compatible_versions: diff --git a/docs/ansible/modules/list_ftd_not_on_version.md b/docs/ansible/modules/list_ftd_not_on_version.md index 69834adb..33ec1106 100644 --- a/docs/ansible/modules/list_ftd_not_on_version.md +++ b/docs/ansible/modules/list_ftd_not_on_version.md @@ -28,13 +28,10 @@ $ ansible-doc -t module cisco.sccfm.list_ftd_not_on_version OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to fetch when using `query' or no filter. @@ -47,6 +44,10 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to narrow the set of FTD devices to check. Mutually exclusive with `uids'. The query is automatically combined with FTD device type @@ -62,13 +63,6 @@ OPTIONS (= indicates it is required): default: false type: bool -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uids List of device UIDs to check. Mutually exclusive with `query'. If omitted, all FTD devices are checked (or those matching @@ -91,8 +85,7 @@ EXAMPLES: - name: Find FTDs not on 7.4.1 cisco.sccfm.list_ftd_not_on_version: version: "7.4.1" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show devices that need upgrading @@ -124,8 +117,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Find FTDs not on target version cisco.sccfm.list_ftd_not_on_version: diff --git a/docs/ansible/modules/list_managers.md b/docs/ansible/modules/list_managers.md index 5ecf2a00..244e9d09 100644 --- a/docs/ansible/modules/list_managers.md +++ b/docs/ansible/modules/list_managers.md @@ -20,13 +20,10 @@ $ ansible-doc -t module cisco.sccfm.list_managers OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of results to return. default: 50 @@ -36,15 +33,12 @@ OPTIONS (= indicates it is required): default: 0 type: int -- query Optional Lucene query string to filter results (e.g. - `name:myFMC*'). - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION +- query Optional Lucene query string to filter results (e.g. + `name:myFMC*'). default: null type: str @@ -54,8 +48,7 @@ EXAMPLES: # Example 1: List all managers - name: List all managers cisco.sccfm.list_managers: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show managers @@ -78,8 +71,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List all managers cisco.sccfm.list_managers: diff --git a/docs/ansible/modules/list_network_groups.md b/docs/ansible/modules/list_network_groups.md index e7b041f9..5eb2f3af 100644 --- a/docs/ansible/modules/list_network_groups.md +++ b/docs/ansible/modules/list_network_groups.md @@ -21,13 +21,10 @@ $ ansible-doc -t module cisco.sccfm.list_network_groups OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of results to return. default: 50 @@ -37,27 +34,23 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Optional Lucene query string to filter results. Searchable fields include `name' and `content'. Example: `name:web*' to find groups whose name starts with "web". default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - AUTHOR: Cisco SCCFM Team EXAMPLES: # Example 1: List all network groups - name: List all network groups cisco.sccfm.list_network_groups: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Display network groups @@ -70,8 +63,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Find web-related network groups cisco.sccfm.list_network_groups: @@ -84,7 +76,7 @@ EXAMPLES: ansible.builtin.debug: msg: "Found {{ result.count }} groups" -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: List first page of network groups cisco.sccfm.list_network_groups: limit: 25 diff --git a/docs/ansible/modules/list_network_objects.md b/docs/ansible/modules/list_network_objects.md index ec7f05ba..ec4de06f 100644 --- a/docs/ansible/modules/list_network_objects.md +++ b/docs/ansible/modules/list_network_objects.md @@ -21,13 +21,10 @@ $ ansible-doc -t module cisco.sccfm.list_network_objects OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of results to return. default: 50 @@ -37,27 +34,23 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Optional Lucene query string to filter results. Searchable fields include `name' and `content'. Example: `name:web*' to find objects whose name starts with "web". default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - AUTHOR: Cisco SCCFM Team EXAMPLES: # Example 1: List all network objects - name: List all network objects cisco.sccfm.list_network_objects: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Display network objects @@ -70,8 +63,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Find web-related network objects cisco.sccfm.list_network_objects: @@ -84,7 +76,7 @@ EXAMPLES: ansible.builtin.debug: msg: "Found {{ result.count }} objects" -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: List first page of network objects cisco.sccfm.list_network_objects: limit: 25 diff --git a/docs/ansible/modules/onboard_asa.md b/docs/ansible/modules/onboard_asa.md index a4fdb88c..9b1bd9c9 100644 --- a/docs/ansible/modules/onboard_asa.md +++ b/docs/ansible/modules/onboard_asa.md @@ -17,13 +17,10 @@ $ ansible-doc -t module cisco.sccfm.onboard_asa OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - connector_name Name of the Secure Device Connector (SDC) to use (required when connector_type is SDC). @@ -53,11 +50,8 @@ OPTIONS (= indicates it is required): no_log: true type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str - ungrouped_labels List of free-form labels to assign to the device. @@ -76,8 +70,7 @@ EXAMPLES: hosts: all module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Onboard branch-asa-1 cisco.sccfm.onboard_asa: @@ -104,9 +97,8 @@ EXAMPLES: connector_type: SDC connector_name: branch-sdc-1 region: us - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: Onboard branch-asa-1 cisco.sccfm.onboard_asa: name: branch-asa-1 diff --git a/docs/ansible/modules/onboard_cdfmc_ftd.md b/docs/ansible/modules/onboard_cdfmc_ftd.md index e85a197b..d09e5625 100644 --- a/docs/ansible/modules/onboard_cdfmc_ftd.md +++ b/docs/ansible/modules/onboard_cdfmc_ftd.md @@ -21,13 +21,10 @@ $ ansible-doc -t module cisco.sccfm.onboard_cdfmc_ftd OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path = fmc_access_policy_uid UUID of the FMC access policy to apply to this device. @@ -52,11 +49,8 @@ OPTIONS (= indicates it is required): default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str - ungrouped_labels List of free-form labels to assign to the device. @@ -79,8 +73,7 @@ EXAMPLES: fmc_access_policy_uid: "7131daad-e813-4b8f-8f42-be1e241e8cdb" licenses: - BASE - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Onboard a virtual FTD with multiple licenses - name: Onboard virtual FTD @@ -112,8 +105,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Onboard branch FTD cisco.sccfm.onboard_cdfmc_ftd: diff --git a/docs/ansible/modules/onboard_cdfmc_ftd_ztp.md b/docs/ansible/modules/onboard_cdfmc_ftd_ztp.md index 6359a4f2..30764dd8 100644 --- a/docs/ansible/modules/onboard_cdfmc_ftd_ztp.md +++ b/docs/ansible/modules/onboard_cdfmc_ftd_ztp.md @@ -37,13 +37,10 @@ OPTIONS (= indicates it is required): no_log: true type: str -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - device_group_uid UUID of the device group the device will join after registration. @@ -63,11 +60,8 @@ OPTIONS (= indicates it is required): = name Human-readable name for the FTD device. type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = serial_number Serial number of the physical FTD device. @@ -84,8 +78,7 @@ EXAMPLES: licenses: - BASE fmc_access_policy_uid: "7131daad-e813-4b8f-8f42-be1e241e8cdb" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Onboard with initial password and device group - name: Onboard FTD via ZTP with password @@ -105,8 +98,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Onboard branch FTD through ZTP cisco.sccfm.onboard_cdfmc_ftd_ztp: diff --git a/docs/ansible/modules/register_cdfmc_ftd.md b/docs/ansible/modules/register_cdfmc_ftd.md index ade06e3a..7f0fe9f8 100644 --- a/docs/ansible/modules/register_cdfmc_ftd.md +++ b/docs/ansible/modules/register_cdfmc_ftd.md @@ -20,23 +20,17 @@ $ ansible-doc -t module cisco.sccfm.register_cdfmc_ftd OPTIONS (= indicates it is required): -- api_token The SCC Firewall Manager API token. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path = ftd_uid The UID of the FTD device in SCC Firewall Manager to register. type: str -- region The SCC Firewall Manager region. - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str - skip_initial_deployment If true, the initial configuration @@ -53,8 +47,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Complete the FTD registration cisco.sccfm.register_cdfmc_ftd: diff --git a/docs/ansible/modules/remove_asa_shun.md b/docs/ansible/modules/remove_asa_shun.md index 3d795bfd..79983c76 100644 --- a/docs/ansible/modules/remove_asa_shun.md +++ b/docs/ansible/modules/remove_asa_shun.md @@ -27,13 +27,10 @@ $ ansible-doc -t module cisco.sccfm.remove_asa_shun OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. @@ -45,19 +42,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - source_ip The source IP address to remove from the shun list. Mutually exclusive with `source_ips'. default: null @@ -85,8 +79,7 @@ EXAMPLES: cisco.sccfm.remove_asa_shun: query: "name:prod-* AND connectivityState:ONLINE" source_ip: "10.99.99.99" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Remove multiple shuns in a single transaction - name: Remove multiple attacker IPs in one call @@ -96,8 +89,7 @@ EXAMPLES: - "203.0.113.40" - "203.0.113.50" - "203.0.113.60" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 3: Remove a shun on specific devices by UID - name: Remove shun on specific ASA @@ -112,8 +104,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Remove shun for attacker cisco.sccfm.remove_asa_shun: diff --git a/docs/ansible/modules/remove_network_group_members.md b/docs/ansible/modules/remove_network_group_members.md index 6df409ea..0f6045da 100644 --- a/docs/ansible/modules/remove_network_group_members.md +++ b/docs/ansible/modules/remove_network_group_members.md @@ -24,31 +24,25 @@ $ ansible-doc -t module cisco.sccfm.remove_network_group_members OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - name Name of the network group to update. default: null type: str +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + = referenced_objects List of existing network object names or UIDs to remove from the group. Names are resolved to UIDs automatically. elements: str type: list -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - uid Unique identifier (UID) of the network group to update. default: null type: str @@ -63,8 +57,7 @@ EXAMPLES: referenced_objects: - web-server-01 - web-server-02 - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Remove members by UID - name: Remove members from a network group by UID @@ -80,8 +73,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Remove old web servers from group cisco.sccfm.remove_network_group_members: diff --git a/docs/ansible/modules/show_asa_shun.md b/docs/ansible/modules/show_asa_shun.md index 23eef5da..73cd9c85 100644 --- a/docs/ansible/modules/show_asa_shun.md +++ b/docs/ansible/modules/show_asa_shun.md @@ -27,13 +27,10 @@ $ ansible-doc -t module cisco.sccfm.show_asa_shun OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - limit Maximum number of devices to return when using `query'. Ignored when using `uids'. @@ -45,19 +42,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to filter ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - statistics When `true', show per-interface shun statistics instead of shun entries. default: false @@ -76,8 +70,7 @@ EXAMPLES: - name: Show shun entries on production ASAs cisco.sccfm.show_asa_shun: query: "name:prod-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: shun_entries # Example 2: Show shun entries on specific devices by UID @@ -100,8 +93,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show shun entries cisco.sccfm.show_asa_shun: diff --git a/docs/ansible/modules/trigger_asa_upgrade.md b/docs/ansible/modules/trigger_asa_upgrade.md index bef1ee58..9336c4c2 100644 --- a/docs/ansible/modules/trigger_asa_upgrade.md +++ b/docs/ansible/modules/trigger_asa_upgrade.md @@ -25,20 +25,17 @@ $ ansible-doc -t module cisco.sccfm.trigger_asa_upgrade OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN - default: null - no_log: true - type: str - - asdm_version Target ASDM version (e.g. `7.18(1.152')). At least one of `software_version' or `asdm_version' is required. default: null type: str +- config_path Optional path to the canonical SCCFM profile + configuration file. + default: null + type: path + - force_upgrade Force upgrade even if a staged upgrade already exists. default: false @@ -59,19 +56,16 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to select ASA devices. Mutually exclusive with `uids'. The query is automatically combined with `deviceType:ASA'. default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - software_version Target ASA firmware version (e.g. `9.18(4')). At least one of `software_version' or `asdm_version' is required. @@ -116,8 +110,7 @@ EXAMPLES: - "12345678-1234-1234-1234-123456789abc" software_version: "9.18(4)" asdm_version: "7.18(1.152)" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Stage-only upgrade using a query - name: Stage ASA upgrade for branch devices @@ -149,8 +142,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Stage ASA upgrade for branch devices cisco.sccfm.trigger_asa_upgrade: diff --git a/docs/ansible/modules/trigger_ftd_upgrade.md b/docs/ansible/modules/trigger_ftd_upgrade.md index ccc76e88..874f656e 100644 --- a/docs/ansible/modules/trigger_ftd_upgrade.md +++ b/docs/ansible/modules/trigger_ftd_upgrade.md @@ -25,13 +25,10 @@ $ ansible-doc -t module cisco.sccfm.trigger_ftd_upgrade OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - ignore_maintenance_window Allow upgrade outside the device maintenance window. @@ -48,6 +45,10 @@ OPTIONS (= indicates it is required): default: 0 type: int +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - query Lucene query to select FTD devices. Mutually exclusive with `uids'. The query is automatically combined with FTD device type @@ -55,13 +56,6 @@ OPTIONS (= indicates it is required): default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - = software_version Target FTD software version (e.g. `7.4.1'). The module resolves the corresponding `upgrade_package_uid' from the compatible @@ -105,8 +99,7 @@ EXAMPLES: uids: - "12345678-1234-1234-1234-123456789abc" software_version: "7.4.1" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Stage-only upgrade using a query - name: Stage FTD upgrade for branch devices @@ -130,8 +123,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Stage FTD upgrade for branch devices cisco.sccfm.trigger_ftd_upgrade: diff --git a/docs/ansible/modules/update_access_rule.md b/docs/ansible/modules/update_access_rule.md index 21cec0be..4b0005c1 100644 --- a/docs/ansible/modules/update_access_rule.md +++ b/docs/ansible/modules/update_access_rule.md @@ -24,13 +24,10 @@ OPTIONS (= indicates it is required): default: null type: bool -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - destination_network Destination network object name. default: null @@ -52,14 +49,11 @@ OPTIONS (= indicates it is required): default: null type: str -- protocol Protocol (e.g. tcp, udp, ip). - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION +- protocol Protocol (e.g. tcp, udp, ip). default: null type: str @@ -91,8 +85,7 @@ EXAMPLES: cisco.sccfm.update_access_rule: uid: "ac981dcd-9860-401e-a51d-c615c946b72f" rule_action: DENY - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Update remark and networks using module_defaults - name: Update access rules @@ -100,8 +93,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Update rule remark and source cisco.sccfm.update_access_rule: diff --git a/docs/ansible/modules/update_network_group.md b/docs/ansible/modules/update_network_group.md index 7c7e9288..dc9c1fb8 100644 --- a/docs/ansible/modules/update_network_group.md +++ b/docs/ansible/modules/update_network_group.md @@ -25,13 +25,10 @@ $ ansible-doc -t module cisco.sccfm.update_network_group OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - description New description for the network group. default: null @@ -51,6 +48,10 @@ OPTIONS (= indicates it is required): default: null type: str +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default + type: str + - referenced_objects List of existing network object names or UIDs to include in the group. Names are resolved to UIDs automatically. Replaces all existing @@ -59,13 +60,6 @@ OPTIONS (= indicates it is required): elements: str type: list -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null - type: str - - tags New mapping of tag keys to lists of tag values. For example, `{"environment": ["production", "staging"]}'. default: null @@ -85,8 +79,7 @@ EXAMPLES: referenced_objects: - web-server-01 - web-server-02 - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Rename a group and update description using module_defaults - name: Update network groups @@ -94,8 +87,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Rename and update group cisco.sccfm.update_network_group: diff --git a/docs/ansible/modules/update_network_object.md b/docs/ansible/modules/update_network_object.md index 05a07d5a..4776ce80 100644 --- a/docs/ansible/modules/update_network_object.md +++ b/docs/ansible/modules/update_network_object.md @@ -23,13 +23,10 @@ $ ansible-doc -t module cisco.sccfm.update_network_object OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path - description New description for the network object. default: null @@ -49,11 +46,8 @@ OPTIONS (= indicates it is required): default: null type: str -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str - tags New mapping of tag keys to lists of tag values. For @@ -80,16 +74,14 @@ EXAMPLES: cisco.sccfm.update_network_object: uid: "abc-123-def" value: "192.168.1.0/24" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Rename a network object by name - name: Rename a network object cisco.sccfm.update_network_object: name: old-object-name new_name: new-object-name - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 3: Update multiple fields using module_defaults - name: Update network objects @@ -97,8 +89,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Update web server object cisco.sccfm.update_network_object: diff --git a/docs/ansible/modules/update_object_default.md b/docs/ansible/modules/update_object_default.md index 55b4e69f..e7d32475 100644 --- a/docs/ansible/modules/update_object_default.md +++ b/docs/ansible/modules/update_object_default.md @@ -21,19 +21,13 @@ $ ansible-doc -t module cisco.sccfm.update_object_default OPTIONS (= indicates it is required): -- api_token API token for SCCFM. - set_via: - env: - - name: SCCFM_API_TOKEN +- config_path Optional path to the canonical SCCFM profile + configuration file. default: null - no_log: true - type: str + type: path -- region SCCFM region (int, us, eu, apj, au, uae, in, or ci). - set_via: - env: - - name: SCCFM_REGION - default: null +- profile Named SCCFM profile configured by `sccfm-cli configure'. + default: default type: str = uid Unique identifier (UID) of the object to update. @@ -53,8 +47,7 @@ EXAMPLES: cisco.sccfm.update_object_default: uid: "abc-123-def" value: "10.10.10.10" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults to avoid repeating credentials - name: Update object default values @@ -62,8 +55,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Update default value cisco.sccfm.update_object_default: @@ -81,8 +73,7 @@ EXAMPLES: gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Update shared default value cisco.sccfm.update_object_default: diff --git a/poetry.lock b/poetry.lock index 5baae9b7..b5a90640 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -1608,7 +1608,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["build", "dev"] +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -1899,4 +1899,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "5b71f343ebbea0ea086567ce5be38c6501c80021d974292dde9381f96f3d1598" +content-hash = "892343bb2171803dea35c2ac9371d1aef943fdc3223af1b8931a93d7e78c1bf9" diff --git a/pyproject.toml b/pyproject.toml index 6caf6dcf..7a727335 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,12 +41,12 @@ click-option-group = "^0.5.9" scc-firewall-manager-sdk = "^1.17.27" questionary = "^2.1.1" paramiko = "^3.5.0" +pyyaml = "^6.0.0" [tool.poetry.scripts] sccfm-cli = "cisco_sccfm_cli.cli:cli" -devkit = "cisco_sccfm_scripts.devkit_cli:main" +sccfm-cli-interactive = "cisco_sccfm_scripts.interactive_cli:main" build-ansible-collection = "cisco_sccfm_scripts.build_ansible_collection:main" -change-tokens = "cisco_sccfm_scripts.setup_tokens:main" generate-ansible-docs = "cisco_sccfm_scripts.generate_ansible_docs:main" generate-cli-docs = "cisco_sccfm_scripts.generate_cli_docs:main" generate-cli-man-docs = "cisco_sccfm_scripts.generate_cli_man_docs:main" @@ -68,9 +68,6 @@ flake8 = "^7.1.1" commitizen = "^3.27.0" reuse = "^6.2.0" -[tool.poetry.group.build.dependencies] -pyyaml = "^6.0.0" - [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/sccfm-ansible/README.md b/sccfm-ansible/README.md index 2f72bb28..1a841109 100644 --- a/sccfm-ansible/README.md +++ b/sccfm-ansible/README.md @@ -10,10 +10,8 @@ Ansible collection for managing Cisco Security Cloud Control Firewall Manager (S - [Installation](#installation) - [Local Development](#local-development) - [Trying out examples](#trying-out-examples) - - [1. Set Up Ansible Vault](#1-set-up-ansible-vault) + - [1. Configure an SCCFM Profile](#1-configure-an-sccfm-profile) - [2. Edit playbook](#2-edit-playbook) - - [2. Create Encrypted Secrets](#2-create-encrypted-secrets) - - [3. Configure Plain Variables](#3-configure-plain-variables) - [4. Run Examples](#4-run-examples) - [Test Inventory](#test-inventory) - [Host Variables](#host-variables) @@ -27,12 +25,11 @@ Ansible collection for managing Cisco Security Cloud Control Firewall Manager (S - [What is Ansible Vault?](#what-is-ansible-vault) - [Vault Commands Reference](#vault-commands-reference) - [Module Defaults Pattern](#module-defaults-pattern) -- [Authentication Methods](#authentication-methods) +- [Authentication](#authentication) - [Security Best Practices](#security-best-practices) - [Troubleshooting](#troubleshooting) - ["Decryption failed" error](#decryption-failed-error) - - ["region is required" error](#region-is-required-error) - - ["api_token is required" error](#api_token-is-required-error) + - ["profile not found" error](#profile-not-found-error) - [Inventory returns no hosts](#inventory-returns-no-hosts) - [Examples](#examples) - [Additional Resources](#additional-resources) @@ -48,8 +45,9 @@ Ansible collection for managing Cisco Security Cloud Control Firewall Manager (S - **ASA HA Health Check Module**: Validate ASA failover health and common HA issues - **ASA Boot Image Module**: Change the configured next-boot ASA image - **Device Grouping**: Automatically group devices by type (ASA, CDFMC_MANAGED_FTD, etc.) -- **Ansible Vault Integration**: Secure credential management for API tokens and device passwords -- **Module Defaults Support**: Set region/API token once for all tasks +- **Canonical SCCFM Profiles**: Share one named region/token profile with both CLI surfaces +- **Ansible Vault Integration**: Secure device passwords and other playbook-specific secrets +- **Module Defaults Support**: Select one profile for all tasks ## Installation @@ -59,7 +57,7 @@ See instructions in the [INSTALL.md](INSTALL.md) file. **Build and install (recommended):** ```bash -devkit +sccfm-cli-interactive # then select "build-collection" from the menu ``` @@ -75,37 +73,36 @@ This will: ## Trying out examples -### 1. Set Up Tokens (Recommended — interactive) +### 1. Configure an SCCFM Profile -The fastest way to configure your tokens, `.env`, vault, and region is with the devkit CLI: +The CLI and Ansible collection use the same named profile store. Configure it directly: ```bash -devkit -# then select "change-tokens" from the menu +sccfm-cli --profile default configure --region us ``` -Or run the token setup directly: +Or use the interactive flow: + ```bash -change-tokens +sccfm-cli-interactive +# select "configure-profile" ``` -This will interactively: -1. Let you pick a previously saved token or create a new one -2. Ask which SCCFM region you're connecting to (for new tokens) -3. Prompt you to paste your API token -4. Save the token for future reuse -5. Create the `.env` file with `SCCFM_REGION` and `SCCFM_API_TOKEN` -6. Create the `.vault_pass` password file (if it doesn't exist) -7. Write and encrypt `group_vars/all/vault.yml` -8. Update `group_vars/all/vars.yml` with the selected region - -You can also point the standalone command at a custom examples directory: +Profiles live at `~/.sccfm-cli/config.json`. The containing directory is restricted to +the current user (`0700`) and the file is owner read/write (`0600`). Ansible modules +and inventory load the selected profile directly; do not duplicate its API token in +environment variables or Ansible Vault. + +If you used a release that stored SCCFM tokens in Ansible Vault, import them without +modifying the source vault: + ```bash -change-tokens --path /path/to/examples +sccfm-cli-interactive +# select "import-legacy-vault" ```
-Manual setup (alternative) +Set up Ansible-specific device secrets Create a vault password file (do NOT commit this!): @@ -123,10 +120,9 @@ cp group_vars/all/vault.yml.example group_vars/all/vault.yml.temp vim group_vars/all/vault.yml.temp ``` -Add your secrets: +Add only playbook-specific secrets: ```yaml --- -sccfm_api_token: "your-actual-api-token-here" vault_asa_branch_office_01_password: "ActualPassword1" ``` @@ -139,12 +135,6 @@ ansible-vault encrypt group_vars/all/vault.yml.temp \ rm group_vars/all/vault.yml.temp ``` -Edit `group_vars/all/vars.yml`: - -```yaml -sccfm_region: us # Change to your region (int, us, eu, apj, au, uae, in, or ci) -``` -
### 2. Edit playbook @@ -155,8 +145,6 @@ Edit the `onboard_asas.yml` playbook, and change the `asas_to_onboard` list to m **Graph inventory:** ```bash -export SCCFM_REGION=int -export SCCFM_API_TOKEN=$(ansible-vault view ./examples/group_vars/all/vault.yml --vault-password-file ./examples/.vault_pass | grep sccfm_api_token | cut -d '"' -f2) ansible-inventory -i examples/inventory.sccfm.yml \ --graph \ --playbook-dir examples @@ -217,8 +205,8 @@ Onboard an ASA device to your SCCFM tenant. - `ignore_certificate`: Skip certificate validation (default: false) - `grouped_labels`: Dictionary of label groups - `ungrouped_labels`: List of labels -- `region`: SCCFM region (optional, uses vault/env) -- `api_token`: API token (optional, uses vault/env) +- `profile`: Named SCCFM profile (optional, defaults to `default`) +- `config_path`: Optional path to the canonical profile file **Example:** ```yaml @@ -226,8 +214,7 @@ Onboard an ASA device to your SCCFM tenant. hosts: localhost module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Onboard branch ASA @@ -285,7 +272,7 @@ documentation. ### What is Ansible Vault? -Ansible Vault encrypts sensitive data (API tokens, passwords) so you can safely commit them to version control. The encrypted `vault.yml` file is committed, but the `.vault_pass` password file is **never** committed. +Ansible Vault encrypts playbook-specific secrets such as managed-device passwords so you can safely commit them to version control. SCCFM API tokens belong only in the canonical profile store. The encrypted `vault.yml` file may be committed, but the `.vault_pass` password file is **never** committed. ### Vault Commands Reference @@ -331,39 +318,32 @@ head -1 group_vars/all/vault.yml ## Module Defaults Pattern -Instead of repeating `region` and `api_token` for every task, use `module_defaults`: +Select a non-default profile once with `module_defaults`: ```yaml - name: Manage SCCFM devices hosts: localhost module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: production tasks: - name: Onboard device 1 cisco.sccfm.onboard_asa: name: device-1 - # No need to specify region/api_token here! + # No need to repeat the profile here. - name: Onboard device 2 cisco.sccfm.onboard_asa: name: device-2 - # Still no region/api_token needed! + # The same profile is used here. ``` -## Authentication Methods +## Authentication -Three ways to provide credentials (in order of precedence): - -1. **Module parameters** (explicit in task) -2. **Module defaults** (recommended - set once per playbook) -3. **Environment variables**: - ```bash - export SCCFM_REGION=us - export SCCFM_API_TOKEN=your-token-here - ``` +Configure credentials once with `sccfm-cli configure`. Modules and inventory use the +`default` profile unless `profile` selects another name. `config_path` selects a custom +canonical profile file when needed. ## Security Best Practices @@ -371,7 +351,7 @@ Three ways to provide credentials (in order of precedence): 2. **Always encrypt vault files** before committing 3. **Store `.vault_pass` securely** and never commit it 4. **Use different vault passwords** for different environments (dev/prod) -5. **Rotate API tokens regularly** and update vault files accordingly +5. **Rotate API tokens regularly** with `sccfm-cli configure` 6. **Use `.gitignore`** to prevent accidental commits of sensitive files 7. **Use `no_log: true`** for password parameters in custom tasks @@ -381,20 +361,15 @@ Three ways to provide credentials (in order of precedence): - Check your vault password is correct - Ensure you're using the right vault password file -### "region is required" error -- Verify `sccfm_region` is set in `group_vars/all/vars.yml` -- Or set `SCCFM_REGION` environment variable -- Or provide `region` parameter in module defaults - -### "api_token is required" error -- Verify `sccfm_api_token` is in encrypted `group_vars/all/vault.yml` -- Or set `SCCFM_API_TOKEN` environment variable -- Or provide `api_token` parameter in module defaults +### "profile not found" error +- Run `sccfm-cli --profile configure`. +- Ensure the playbook's `profile` value matches the configured name. +- If using `config_path`, ensure it points to the same canonical profile file. ### Inventory returns no hosts - Check your API token has proper permissions - Verify the region is correct -- Test API access: `curl -H "Authorization: Bearer $SCCFM_API_TOKEN" https://.cdo.cisco.com/api/rest/v1/inventory/devices` +- Test API access without exposing the token: `sccfm-cli --profile status` ## Examples @@ -407,8 +382,8 @@ See the `examples/` directory for complete working examples: - **`execute_ftd_cli.yml`** - Execute show commands on cdFMC-managed FTD devices - **`asa_ha_check.yml`** - Run HA health checks on ASA failover devices - **`change_asa_boot_image.yml`** - Change the configured ASA boot image -- **`group_vars/all/vars.yml`** - Plain variables (region, defaults) -- **`group_vars/all/vault.yml`** - Encrypted secrets (API token, passwords) +- **`group_vars/all/vars.yml`** - Plain playbook variables +- **`group_vars/all/vault.yml`** - Encrypted playbook-specific secrets - **`group_vars/all/vault.yml.example`** - Template for vault structure ## Additional Resources diff --git a/sccfm-ansible/build.sh b/sccfm-ansible/build.sh index 3da362a2..8cd46ed3 100755 --- a/sccfm-ansible/build.sh +++ b/sccfm-ansible/build.sh @@ -17,6 +17,5 @@ ansible-galaxy collection install . --force echo "✅ Build complete!" echo "" echo "To use the collection, run:" -echo " export SCCFM_REGION=your-region" -echo " export SCCFM_API_TOKEN=your-token" +echo " sccfm-cli configure --region us # securely prompts for the token" echo " ansible-playbook -i examples/inventory.sccfm.yml examples/show_devices.yml" diff --git a/sccfm-ansible/e2e/access_rules/playbooks/cleanup.yml b/sccfm-ansible/e2e/access_rules/playbooks/cleanup.yml index 67de20db..cda11f1d 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/cleanup.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/cleanup.yml @@ -14,8 +14,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: # ── Access rule cleanup ────────────────────────────────────── diff --git a/sccfm-ansible/e2e/access_rules/playbooks/create_access_rule.yml b/sccfm-ansible/e2e/access_rules/playbooks/create_access_rule.yml index 7e07d484..77057f92 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/create_access_rule.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/create_access_rule.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached access group UID diff --git a/sccfm-ansible/e2e/access_rules/playbooks/delete_access_rule.yml b/sccfm-ansible/e2e/access_rules/playbooks/delete_access_rule.yml index 4b0d1467..055fa61e 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/delete_access_rule.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/delete_access_rule.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached rule UID diff --git a/sccfm-ansible/e2e/access_rules/playbooks/delete_idempotency.yml b/sccfm-ansible/e2e/access_rules/playbooks/delete_idempotency.yml index 7231fdf8..8ee88832 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/delete_idempotency.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/delete_idempotency.yml @@ -16,8 +16,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached rule UID diff --git a/sccfm-ansible/e2e/access_rules/playbooks/get_access_group.yml b/sccfm-ansible/e2e/access_rules/playbooks/get_access_group.yml index 7378ca54..ef7cc510 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/get_access_group.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/get_access_group.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached access group UID diff --git a/sccfm-ansible/e2e/access_rules/playbooks/list_access_groups.yml b/sccfm-ansible/e2e/access_rules/playbooks/list_access_groups.yml index e28fdad8..e3c02ed3 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/list_access_groups.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/list_access_groups.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List access groups for CI ASA device (with retry for CDO sync) diff --git a/sccfm-ansible/e2e/access_rules/playbooks/list_access_rules.yml b/sccfm-ansible/e2e/access_rules/playbooks/list_access_rules.yml index 9172eb9e..f1fa786c 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/list_access_rules.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/list_access_rules.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List access rules (first page) diff --git a/sccfm-ansible/e2e/access_rules/playbooks/provision_access_group.yml b/sccfm-ansible/e2e/access_rules/playbooks/provision_access_group.yml index c2b49482..88b122fe 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/provision_access_group.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/provision_access_group.yml @@ -13,8 +13,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Create source network object for access rule tests diff --git a/sccfm-ansible/e2e/access_rules/playbooks/update_access_rule.yml b/sccfm-ansible/e2e/access_rules/playbooks/update_access_rule.yml index cee2c076..b5b63f8a 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/update_access_rule.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/update_access_rule.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached rule UID diff --git a/sccfm-ansible/e2e/access_rules/playbooks/update_idempotency.yml b/sccfm-ansible/e2e/access_rules/playbooks/update_idempotency.yml index 339f02ac..882338f1 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/update_idempotency.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/update_idempotency.yml @@ -14,8 +14,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached rule UID diff --git a/sccfm-ansible/e2e/access_rules/playbooks/verify_create.yml b/sccfm-ansible/e2e/access_rules/playbooks/verify_create.yml index ad3f359f..531e3517 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/verify_create.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/verify_create.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached rule UID diff --git a/sccfm-ansible/e2e/access_rules/playbooks/verify_delete.yml b/sccfm-ansible/e2e/access_rules/playbooks/verify_delete.yml index 7cb6d181..322f5720 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/verify_delete.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/verify_delete.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached rule UID diff --git a/sccfm-ansible/e2e/access_rules/playbooks/verify_update.yml b/sccfm-ansible/e2e/access_rules/playbooks/verify_update.yml index 8fcd7496..bd31e40d 100644 --- a/sccfm-ansible/e2e/access_rules/playbooks/verify_update.yml +++ b/sccfm-ansible/e2e/access_rules/playbooks/verify_update.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Read cached rule UID diff --git a/sccfm-ansible/e2e/asa/playbooks/add_shun.yml b/sccfm-ansible/e2e/asa/playbooks/add_shun.yml index 42f13395..a48e4568 100644 --- a/sccfm-ansible/e2e/asa/playbooks/add_shun.yml +++ b/sccfm-ansible/e2e/asa/playbooks/add_shun.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Add shun entries on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/cleanup.yml b/sccfm-ansible/e2e/asa/playbooks/cleanup.yml index 3728de78..43aa72d9 100644 --- a/sccfm-ansible/e2e/asa/playbooks/cleanup.yml +++ b/sccfm-ansible/e2e/asa/playbooks/cleanup.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Clear all shun entries on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/clear_shun.yml b/sccfm-ansible/e2e/asa/playbooks/clear_shun.yml index 9a6c1ab5..17b84792 100644 --- a/sccfm-ansible/e2e/asa/playbooks/clear_shun.yml +++ b/sccfm-ansible/e2e/asa/playbooks/clear_shun.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Clear all shun entries on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/execute_cli_read.yml b/sccfm-ansible/e2e/asa/playbooks/execute_cli_read.yml index f51782d9..dc9aacc7 100644 --- a/sccfm-ansible/e2e/asa/playbooks/execute_cli_read.yml +++ b/sccfm-ansible/e2e/asa/playbooks/execute_cli_read.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Execute read-only CLI commands on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/ha_check_assert_structure.yml b/sccfm-ansible/e2e/asa/playbooks/ha_check_assert_structure.yml index 87834208..a9e7b989 100644 --- a/sccfm-ansible/e2e/asa/playbooks/ha_check_assert_structure.yml +++ b/sccfm-ansible/e2e/asa/playbooks/ha_check_assert_structure.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Run HA checks on CI HA-enabled ASA device diff --git a/sccfm-ansible/e2e/asa/playbooks/ha_check_by_uid.yml b/sccfm-ansible/e2e/asa/playbooks/ha_check_by_uid.yml index 4bf5d4af..5622b219 100644 --- a/sccfm-ansible/e2e/asa/playbooks/ha_check_by_uid.yml +++ b/sccfm-ansible/e2e/asa/playbooks/ha_check_by_uid.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Discover device UID via query diff --git a/sccfm-ansible/e2e/asa/playbooks/ha_check_query.yml b/sccfm-ansible/e2e/asa/playbooks/ha_check_query.yml index 89d3ca65..016db00c 100644 --- a/sccfm-ansible/e2e/asa/playbooks/ha_check_query.yml +++ b/sccfm-ansible/e2e/asa/playbooks/ha_check_query.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Run HA checks on CI HA-enabled ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/ha_check_query_with_limit.yml b/sccfm-ansible/e2e/asa/playbooks/ha_check_query_with_limit.yml index 5fc18fb1..b4470ef9 100644 --- a/sccfm-ansible/e2e/asa/playbooks/ha_check_query_with_limit.yml +++ b/sccfm-ansible/e2e/asa/playbooks/ha_check_query_with_limit.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Run HA checks with limit of 1 diff --git a/sccfm-ansible/e2e/asa/playbooks/list_boot_registry.yml b/sccfm-ansible/e2e/asa/playbooks/list_boot_registry.yml index 8ac9d2bc..19587d59 100644 --- a/sccfm-ansible/e2e/asa/playbooks/list_boot_registry.yml +++ b/sccfm-ansible/e2e/asa/playbooks/list_boot_registry.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List boot registry on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/list_compatible_versions.yml b/sccfm-ansible/e2e/asa/playbooks/list_compatible_versions.yml index bb7b0a24..8405e8c7 100644 --- a/sccfm-ansible/e2e/asa/playbooks/list_compatible_versions.yml +++ b/sccfm-ansible/e2e/asa/playbooks/list_compatible_versions.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List compatible versions for CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/list_compatible_versions_for_upgrade.yml b/sccfm-ansible/e2e/asa/playbooks/list_compatible_versions_for_upgrade.yml index 0bc7ea82..0d5d314d 100644 --- a/sccfm-ansible/e2e/asa/playbooks/list_compatible_versions_for_upgrade.yml +++ b/sccfm-ansible/e2e/asa/playbooks/list_compatible_versions_for_upgrade.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List compatible versions with per-device detail diff --git a/sccfm-ansible/e2e/asa/playbooks/list_disk_files.yml b/sccfm-ansible/e2e/asa/playbooks/list_disk_files.yml index c63930eb..56718311 100644 --- a/sccfm-ansible/e2e/asa/playbooks/list_disk_files.yml +++ b/sccfm-ansible/e2e/asa/playbooks/list_disk_files.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List disk files on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/list_local_users.yml b/sccfm-ansible/e2e/asa/playbooks/list_local_users.yml index c3790cc0..884b3b20 100644 --- a/sccfm-ansible/e2e/asa/playbooks/list_local_users.yml +++ b/sccfm-ansible/e2e/asa/playbooks/list_local_users.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List local users on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/list_not_on_version.yml b/sccfm-ansible/e2e/asa/playbooks/list_not_on_version.yml index dc77c177..aa50599c 100644 --- a/sccfm-ansible/e2e/asa/playbooks/list_not_on_version.yml +++ b/sccfm-ansible/e2e/asa/playbooks/list_not_on_version.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List ASA devices not on fake version diff --git a/sccfm-ansible/e2e/asa/playbooks/onboard_vasa.yml b/sccfm-ansible/e2e/asa/playbooks/onboard_vasa.yml index 4703262f..270801f5 100644 --- a/sccfm-ansible/e2e/asa/playbooks/onboard_vasa.yml +++ b/sccfm-ansible/e2e/asa/playbooks/onboard_vasa.yml @@ -16,8 +16,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Validate required environment variables diff --git a/sccfm-ansible/e2e/asa/playbooks/remove_shun.yml b/sccfm-ansible/e2e/asa/playbooks/remove_shun.yml index fd696534..5cf3f571 100644 --- a/sccfm-ansible/e2e/asa/playbooks/remove_shun.yml +++ b/sccfm-ansible/e2e/asa/playbooks/remove_shun.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Remove one shun entry by source IP diff --git a/sccfm-ansible/e2e/asa/playbooks/remove_vasa.yml b/sccfm-ansible/e2e/asa/playbooks/remove_vasa.yml index 45029ceb..21c34e5d 100644 --- a/sccfm-ansible/e2e/asa/playbooks/remove_vasa.yml +++ b/sccfm-ansible/e2e/asa/playbooks/remove_vasa.yml @@ -10,7 +10,7 @@ vars: sccfm_api_base: "https://ci.manage.security.cisco.com/api/rest" - sccfm_api_token: "{{ lookup('env', 'API_TOKEN') }}" + profile_token: "{{ lookup('cisco.sccfm.profile', 'default', field='api_token') }}" asa_test_query_all: "name:ci-e2e-asa-*" tasks: @@ -19,20 +19,22 @@ url: "{{ sccfm_api_base }}/v1/inventory/devices?limit=50&offset=0&q={{ asa_test_query_all | urlencode }}" method: GET headers: - Authorization: "Bearer {{ sccfm_api_token }}" + Authorization: "Bearer {{ profile_token }}" Content-Type: "application/json" status_code: [200] register: device_list + no_log: true - name: Delete each CI vASA device ansible.builtin.uri: url: "{{ sccfm_api_base }}/v1/inventory/devices/{{ item.uid }}" method: DELETE headers: - Authorization: "Bearer {{ sccfm_api_token }}" + Authorization: "Bearer {{ profile_token }}" Content-Type: "application/json" status_code: [200, 202, 204, 404] loop: "{{ device_list.json['items'] | default([]) }}" loop_control: label: "{{ item.name }} ({{ item.uid }})" when: device_list.json['items'] | default([]) | length > 0 + no_log: true diff --git a/sccfm-ansible/e2e/asa/playbooks/show_shun.yml b/sccfm-ansible/e2e/asa/playbooks/show_shun.yml index 4c3a6b44..e33a2413 100644 --- a/sccfm-ansible/e2e/asa/playbooks/show_shun.yml +++ b/sccfm-ansible/e2e/asa/playbooks/show_shun.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show shun entries on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/show_shun_statistics.yml b/sccfm-ansible/e2e/asa/playbooks/show_shun_statistics.yml index 6e963f91..d20c7a69 100644 --- a/sccfm-ansible/e2e/asa/playbooks/show_shun_statistics.yml +++ b/sccfm-ansible/e2e/asa/playbooks/show_shun_statistics.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show shun statistics on CI ASA devices diff --git a/sccfm-ansible/e2e/asa/playbooks/trigger_upgrade_stage.yml b/sccfm-ansible/e2e/asa/playbooks/trigger_upgrade_stage.yml index 6804a64e..2532ea7a 100644 --- a/sccfm-ansible/e2e/asa/playbooks/trigger_upgrade_stage.yml +++ b/sccfm-ansible/e2e/asa/playbooks/trigger_upgrade_stage.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Skip if upgrade version is not configured diff --git a/sccfm-ansible/e2e/asa/playbooks/verify_boot_registry_after_stage.yml b/sccfm-ansible/e2e/asa/playbooks/verify_boot_registry_after_stage.yml index bb4a79fb..7bdc1f03 100644 --- a/sccfm-ansible/e2e/asa/playbooks/verify_boot_registry_after_stage.yml +++ b/sccfm-ansible/e2e/asa/playbooks/verify_boot_registry_after_stage.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List boot registry after staging diff --git a/sccfm-ansible/e2e/asa/playbooks/verify_shun_cleared.yml b/sccfm-ansible/e2e/asa/playbooks/verify_shun_cleared.yml index 0162e3ea..399cd4a8 100644 --- a/sccfm-ansible/e2e/asa/playbooks/verify_shun_cleared.yml +++ b/sccfm-ansible/e2e/asa/playbooks/verify_shun_cleared.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show shun entries after clear diff --git a/sccfm-ansible/e2e/ftd/playbooks/deploy_ftd.yml b/sccfm-ansible/e2e/ftd/playbooks/deploy_ftd.yml index 40ae07e7..8bf29a87 100644 --- a/sccfm-ansible/e2e/ftd/playbooks/deploy_ftd.yml +++ b/sccfm-ansible/e2e/ftd/playbooks/deploy_ftd.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Deploy configuration to CI FTD devices diff --git a/sccfm-ansible/e2e/ftd/playbooks/list_compatible_versions.yml b/sccfm-ansible/e2e/ftd/playbooks/list_compatible_versions.yml index b99dcc71..7f85addf 100644 --- a/sccfm-ansible/e2e/ftd/playbooks/list_compatible_versions.yml +++ b/sccfm-ansible/e2e/ftd/playbooks/list_compatible_versions.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List compatible versions for CI FTD devices diff --git a/sccfm-ansible/e2e/ftd/playbooks/list_compatible_versions_for_upgrade.yml b/sccfm-ansible/e2e/ftd/playbooks/list_compatible_versions_for_upgrade.yml index 0656834b..9a6fadf8 100644 --- a/sccfm-ansible/e2e/ftd/playbooks/list_compatible_versions_for_upgrade.yml +++ b/sccfm-ansible/e2e/ftd/playbooks/list_compatible_versions_for_upgrade.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List compatible versions with per-device detail diff --git a/sccfm-ansible/e2e/ftd/playbooks/list_not_on_recommended.yml b/sccfm-ansible/e2e/ftd/playbooks/list_not_on_recommended.yml index 7cbd3d24..d6bfd5db 100644 --- a/sccfm-ansible/e2e/ftd/playbooks/list_not_on_recommended.yml +++ b/sccfm-ansible/e2e/ftd/playbooks/list_not_on_recommended.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List FTD devices not on recommended version diff --git a/sccfm-ansible/e2e/ftd/playbooks/list_not_on_version.yml b/sccfm-ansible/e2e/ftd/playbooks/list_not_on_version.yml index 34fd80e9..30559960 100644 --- a/sccfm-ansible/e2e/ftd/playbooks/list_not_on_version.yml +++ b/sccfm-ansible/e2e/ftd/playbooks/list_not_on_version.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List FTD devices not on fake version diff --git a/sccfm-ansible/e2e/ftd/playbooks/trigger_upgrade_stage.yml b/sccfm-ansible/e2e/ftd/playbooks/trigger_upgrade_stage.yml index cb13f2b4..92909284 100644 --- a/sccfm-ansible/e2e/ftd/playbooks/trigger_upgrade_stage.yml +++ b/sccfm-ansible/e2e/ftd/playbooks/trigger_upgrade_stage.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Skip if upgrade version is not configured diff --git a/sccfm-ansible/e2e/objects/playbooks/cleanup.yml b/sccfm-ansible/e2e/objects/playbooks/cleanup.yml index 366cdd1f..244df31d 100644 --- a/sccfm-ansible/e2e/objects/playbooks/cleanup.yml +++ b/sccfm-ansible/e2e/objects/playbooks/cleanup.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Delete test group diff --git a/sccfm-ansible/e2e/objects/playbooks/create_idempotency.yml b/sccfm-ansible/e2e/objects/playbooks/create_idempotency.yml index 5576d78a..a27dfa5e 100644 --- a/sccfm-ansible/e2e/objects/playbooks/create_idempotency.yml +++ b/sccfm-ansible/e2e/objects/playbooks/create_idempotency.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Re-create same objects diff --git a/sccfm-ansible/e2e/objects/playbooks/create_network_group.yml b/sccfm-ansible/e2e/objects/playbooks/create_network_group.yml index a21b0c8e..4d74ade2 100644 --- a/sccfm-ansible/e2e/objects/playbooks/create_network_group.yml +++ b/sccfm-ansible/e2e/objects/playbooks/create_network_group.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Create network group with literals diff --git a/sccfm-ansible/e2e/objects/playbooks/create_network_objects.yml b/sccfm-ansible/e2e/objects/playbooks/create_network_objects.yml index e8ddf1c5..e76d954b 100644 --- a/sccfm-ansible/e2e/objects/playbooks/create_network_objects.yml +++ b/sccfm-ansible/e2e/objects/playbooks/create_network_objects.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Create network objects diff --git a/sccfm-ansible/e2e/objects/playbooks/delete_idempotency.yml b/sccfm-ansible/e2e/objects/playbooks/delete_idempotency.yml index 1fa10707..74626391 100644 --- a/sccfm-ansible/e2e/objects/playbooks/delete_idempotency.yml +++ b/sccfm-ansible/e2e/objects/playbooks/delete_idempotency.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Re-delete group diff --git a/sccfm-ansible/e2e/objects/playbooks/delete_network_group.yml b/sccfm-ansible/e2e/objects/playbooks/delete_network_group.yml index ea930038..c0458b95 100644 --- a/sccfm-ansible/e2e/objects/playbooks/delete_network_group.yml +++ b/sccfm-ansible/e2e/objects/playbooks/delete_network_group.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Delete network group diff --git a/sccfm-ansible/e2e/objects/playbooks/delete_network_objects.yml b/sccfm-ansible/e2e/objects/playbooks/delete_network_objects.yml index 18859819..b80cd5cc 100644 --- a/sccfm-ansible/e2e/objects/playbooks/delete_network_objects.yml +++ b/sccfm-ansible/e2e/objects/playbooks/delete_network_objects.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Delete network objects diff --git a/sccfm-ansible/e2e/objects/playbooks/update_idempotency.yml b/sccfm-ansible/e2e/objects/playbooks/update_idempotency.yml index 093a1b5e..4c58d61c 100644 --- a/sccfm-ansible/e2e/objects/playbooks/update_idempotency.yml +++ b/sccfm-ansible/e2e/objects/playbooks/update_idempotency.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Re-update host diff --git a/sccfm-ansible/e2e/objects/playbooks/update_network_group.yml b/sccfm-ansible/e2e/objects/playbooks/update_network_group.yml index a155678a..4122f451 100644 --- a/sccfm-ansible/e2e/objects/playbooks/update_network_group.yml +++ b/sccfm-ansible/e2e/objects/playbooks/update_network_group.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Update group to reference test objects diff --git a/sccfm-ansible/e2e/objects/playbooks/update_network_objects.yml b/sccfm-ansible/e2e/objects/playbooks/update_network_objects.yml index f892cff7..f11eb33a 100644 --- a/sccfm-ansible/e2e/objects/playbooks/update_network_objects.yml +++ b/sccfm-ansible/e2e/objects/playbooks/update_network_objects.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Update host IP address and description diff --git a/sccfm-ansible/e2e/objects/playbooks/verify_create.yml b/sccfm-ansible/e2e/objects/playbooks/verify_create.yml index 9c5e3f65..f13e9a16 100644 --- a/sccfm-ansible/e2e/objects/playbooks/verify_create.yml +++ b/sccfm-ansible/e2e/objects/playbooks/verify_create.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List ci-test network objects diff --git a/sccfm-ansible/e2e/objects/playbooks/verify_delete.yml b/sccfm-ansible/e2e/objects/playbooks/verify_delete.yml index 561349ba..18e5e9ad 100644 --- a/sccfm-ansible/e2e/objects/playbooks/verify_delete.yml +++ b/sccfm-ansible/e2e/objects/playbooks/verify_delete.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List ci-test objects after deletion diff --git a/sccfm-ansible/e2e/objects/playbooks/verify_group.yml b/sccfm-ansible/e2e/objects/playbooks/verify_group.yml index 13bb19a2..d33607c1 100644 --- a/sccfm-ansible/e2e/objects/playbooks/verify_group.yml +++ b/sccfm-ansible/e2e/objects/playbooks/verify_group.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List ci-test network groups diff --git a/sccfm-ansible/e2e/objects/playbooks/verify_update.yml b/sccfm-ansible/e2e/objects/playbooks/verify_update.yml index e87c2ff7..da40b60e 100644 --- a/sccfm-ansible/e2e/objects/playbooks/verify_update.yml +++ b/sccfm-ansible/e2e/objects/playbooks/verify_update.yml @@ -12,8 +12,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List ci-test-host-01 diff --git a/sccfm-ansible/e2e/run_e2e.sh b/sccfm-ansible/e2e/run_e2e.sh index c5088f2e..574c11cb 100755 --- a/sccfm-ansible/e2e/run_e2e.sh +++ b/sccfm-ansible/e2e/run_e2e.sh @@ -5,7 +5,8 @@ # Generates JUnit XML for Jenkins test result reporting. # # Prerequisites: -# - Run cisco_sccfm_scripts/setup_tokens.py first (creates vault.yml and .vault_pass) +# - Configure the selected profile with sccfm-cli configure +# - Create vault.yml and .vault_pass for Ansible-specific device secrets # - Virtualenv active (source cisco_sccfm_scripts/activate.sh) # # Usage: @@ -27,12 +28,12 @@ fi # ── Preflight checks ────────────────────────────────────────────── if [[ ! -f "${VAULT_PASS}" ]]; then - echo "ERROR: ${VAULT_PASS} not found. Run cisco_sccfm_scripts/setup_tokens.py first." >&2 + echo "ERROR: ${VAULT_PASS} not found. Create it for Ansible device secrets first." >&2 exit 1 fi if [[ ! -f "${EXAMPLES_DIR}/group_vars/all/vault.yml" ]]; then - echo "ERROR: group_vars/all/vault.yml not found. Run cisco_sccfm_scripts/setup_tokens.py first." >&2 + echo "ERROR: group_vars/all/vault.yml not found. Create it for device secrets first." >&2 exit 1 fi diff --git a/sccfm-ansible/examples/access_rules.yml b/sccfm-ansible/examples/access_rules.yml index 64cc11d5..e7661762 100644 --- a/sccfm-ansible/examples/access_rules.yml +++ b/sccfm-ansible/examples/access_rules.yml @@ -11,8 +11,7 @@ # ansible-playbook examples/access_rules.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` # - A valid device UID and access group UID # - Existing source and destination network objects referenced by the rule @@ -23,8 +22,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "YOUR_DEVICE_UID" diff --git a/sccfm-ansible/examples/add_object_override.yml b/sccfm-ansible/examples/add_object_override.yml index f338ddb7..b844d290 100644 --- a/sccfm-ansible/examples/add_object_override.yml +++ b/sccfm-ansible/examples/add_object_override.yml @@ -26,8 +26,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: # ------------------------------------------------------------------------- diff --git a/sccfm-ansible/examples/asa_ha_check.yml b/sccfm-ansible/examples/asa_ha_check.yml index ea0fd0bf..908eb732 100644 --- a/sccfm-ansible/examples/asa_ha_check.yml +++ b/sccfm-ansible/examples/asa_ha_check.yml @@ -22,8 +22,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/change_asa_boot_image.yml b/sccfm-ansible/examples/change_asa_boot_image.yml index 164e2ec6..5e2a3ff9 100644 --- a/sccfm-ansible/examples/change_asa_boot_image.yml +++ b/sccfm-ansible/examples/change_asa_boot_image.yml @@ -3,8 +3,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Preview boot image change on branch ASAs diff --git a/sccfm-ansible/examples/change_asa_local_password.yml b/sccfm-ansible/examples/change_asa_local_password.yml index 884054a9..96524cfc 100644 --- a/sccfm-ansible/examples/change_asa_local_password.yml +++ b/sccfm-ansible/examples/change_asa_local_password.yml @@ -16,8 +16,7 @@ # -e new_password='' # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` # # NOTE: Config commands require the device to be in SYNCED state. # Passwords with 3+ sequential or repetitive characters (e.g. "1234", @@ -29,8 +28,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/create_network_groups.yml b/sccfm-ansible/examples/create_network_groups.yml index d8bacf43..6413b216 100644 --- a/sccfm-ansible/examples/create_network_groups.yml +++ b/sccfm-ansible/examples/create_network_groups.yml @@ -5,8 +5,7 @@ # ansible-playbook examples/create_network_groups.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` # - Run the create network_objects.yml playbook first to create any referenced network objects @@ -17,8 +16,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: network_groups: diff --git a/sccfm-ansible/examples/create_network_objects.yml b/sccfm-ansible/examples/create_network_objects.yml index 7b359cf6..d7cad233 100644 --- a/sccfm-ansible/examples/create_network_objects.yml +++ b/sccfm-ansible/examples/create_network_objects.yml @@ -9,8 +9,7 @@ # ansible-playbook examples/create_network_objects.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` - name: Create network objects in SCC Firewall Manager hosts: localhost @@ -19,8 +18,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: # Define network objects to create diff --git a/sccfm-ansible/examples/delete_network_groups.yml b/sccfm-ansible/examples/delete_network_groups.yml index cfd8ebfa..463e5ace 100644 --- a/sccfm-ansible/examples/delete_network_groups.yml +++ b/sccfm-ansible/examples/delete_network_groups.yml @@ -9,8 +9,7 @@ # ansible-playbook examples/delete_network_groups.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` - name: Delete network groups in SCC Firewall Manager hosts: localhost @@ -19,8 +18,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: # Define network groups to delete by name diff --git a/sccfm-ansible/examples/delete_network_objects.yml b/sccfm-ansible/examples/delete_network_objects.yml index aa816840..a32b1ce1 100644 --- a/sccfm-ansible/examples/delete_network_objects.yml +++ b/sccfm-ansible/examples/delete_network_objects.yml @@ -9,8 +9,7 @@ # ansible-playbook examples/delete_network_objects.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` - name: Delete network objects in SCC Firewall Manager hosts: localhost @@ -19,8 +18,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: # Define network objects to delete by name diff --git a/sccfm-ansible/examples/deploy_cdfmc_ftd.yml b/sccfm-ansible/examples/deploy_cdfmc_ftd.yml index 0dc1ec3e..7901092b 100644 --- a/sccfm-ansible/examples/deploy_cdfmc_ftd.yml +++ b/sccfm-ansible/examples/deploy_cdfmc_ftd.yml @@ -32,8 +32,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uids: [] diff --git a/sccfm-ansible/examples/execute_asa_cli.yml b/sccfm-ansible/examples/execute_asa_cli.yml index 3a968a1c..49906d9a 100644 --- a/sccfm-ansible/examples/execute_asa_cli.yml +++ b/sccfm-ansible/examples/execute_asa_cli.yml @@ -21,8 +21,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/execute_ftd_cli.yml b/sccfm-ansible/examples/execute_ftd_cli.yml index 4c4cfce6..c998ec4c 100644 --- a/sccfm-ansible/examples/execute_ftd_cli.yml +++ b/sccfm-ansible/examples/execute_ftd_cli.yml @@ -16,8 +16,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/group_vars/all/vars.yml b/sccfm-ansible/examples/group_vars/all/vars.yml index a36eeb6b..993b4186 100644 --- a/sccfm-ansible/examples/group_vars/all/vars.yml +++ b/sccfm-ansible/examples/group_vars/all/vars.yml @@ -2,9 +2,6 @@ # Plain variables (not sensitive) # These can be committed to version control -# SCCFM connection settings -sccfm_region: int - # Common ASA configuration default_asa_username: asavuser default_ignore_certificate: true diff --git a/sccfm-ansible/examples/group_vars/all/vault.yml.example b/sccfm-ansible/examples/group_vars/all/vault.yml.example index e1efeb1b..38891ebe 100644 --- a/sccfm-ansible/examples/group_vars/all/vault.yml.example +++ b/sccfm-ansible/examples/group_vars/all/vault.yml.example @@ -10,10 +10,6 @@ # 5. Encrypt it: ansible-vault encrypt vault.yml.temp --output vault.yml --vault-password-file ../../.vault_pass # 6. Remove the temp file: rm vault.yml.temp -# SCC Firewall Manager API token -# Obtain this from your CDO/SCCFM tenant -sccfm_api_token: "your-api-token-here-abc123xyz789" - # ASA device passwords - each device can have its own password # Use descriptive names that match your device naming in the playbook vault_asa_branch_office_01_password: "BranchOffice01-SecurePass!" diff --git a/sccfm-ansible/examples/inventory.sccfm.yml b/sccfm-ansible/examples/inventory.sccfm.yml index 2f2e470f..7992a806 100644 --- a/sccfm-ansible/examples/inventory.sccfm.yml +++ b/sccfm-ansible/examples/inventory.sccfm.yml @@ -1,10 +1,7 @@ -# Credentials can be provided in two ways: -# 1. Set environment variables SCCFM_REGION and SCCFM_API_TOKEN (recommended) -# - Create a .env file from .env.example and use direnv to auto-load -# 2. Use the lookup syntax below (falls back to env vars if not set) +# Configure the selected profile first: +# sccfm-cli --profile default configure --region us plugin: cisco.sccfm.sccfm -region: "{{ lookup('env', 'SCCFM_REGION') }}" -api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" +profile: default group_by_device_type: true # limit: 100 # query: "deviceType:ASA" diff --git a/sccfm-ansible/examples/list_asa_boot_registry.yml b/sccfm-ansible/examples/list_asa_boot_registry.yml index 91009e42..3ccb24fe 100644 --- a/sccfm-ansible/examples/list_asa_boot_registry.yml +++ b/sccfm-ansible/examples/list_asa_boot_registry.yml @@ -20,8 +20,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/list_asa_compatible_versions.yml b/sccfm-ansible/examples/list_asa_compatible_versions.yml index 30763485..e9edaf3a 100644 --- a/sccfm-ansible/examples/list_asa_compatible_versions.yml +++ b/sccfm-ansible/examples/list_asa_compatible_versions.yml @@ -22,8 +22,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/list_asa_disk_files.yml b/sccfm-ansible/examples/list_asa_disk_files.yml index f0b31163..9e09df17 100644 --- a/sccfm-ansible/examples/list_asa_disk_files.yml +++ b/sccfm-ansible/examples/list_asa_disk_files.yml @@ -22,8 +22,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/list_asa_local_users.yml b/sccfm-ansible/examples/list_asa_local_users.yml index 42c07e00..8e7ceb93 100644 --- a/sccfm-ansible/examples/list_asa_local_users.yml +++ b/sccfm-ansible/examples/list_asa_local_users.yml @@ -3,8 +3,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Get list of ASA local users for UIDs cisco.sccfm.list_asa_local_users: diff --git a/sccfm-ansible/examples/list_asa_not_on_version.yml b/sccfm-ansible/examples/list_asa_not_on_version.yml index 493d68fd..75c5a3da 100644 --- a/sccfm-ansible/examples/list_asa_not_on_version.yml +++ b/sccfm-ansible/examples/list_asa_not_on_version.yml @@ -26,8 +26,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: target_version: "" diff --git a/sccfm-ansible/examples/list_ftd_compatible_versions.yml b/sccfm-ansible/examples/list_ftd_compatible_versions.yml index dff09b21..085e6912 100644 --- a/sccfm-ansible/examples/list_ftd_compatible_versions.yml +++ b/sccfm-ansible/examples/list_ftd_compatible_versions.yml @@ -22,8 +22,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/list_ftd_not_on_version.yml b/sccfm-ansible/examples/list_ftd_not_on_version.yml index 72202c0b..10fad2b6 100644 --- a/sccfm-ansible/examples/list_ftd_not_on_version.yml +++ b/sccfm-ansible/examples/list_ftd_not_on_version.yml @@ -25,8 +25,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: target_version: "" diff --git a/sccfm-ansible/examples/list_network_groups.yml b/sccfm-ansible/examples/list_network_groups.yml index e335fd49..77f6c14e 100644 --- a/sccfm-ansible/examples/list_network_groups.yml +++ b/sccfm-ansible/examples/list_network_groups.yml @@ -5,8 +5,7 @@ # ansible-playbook examples/list_network_groups.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` - name: List network groups in SCC Firewall Manager hosts: localhost @@ -15,8 +14,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: # ============================================================ diff --git a/sccfm-ansible/examples/list_network_objects.yml b/sccfm-ansible/examples/list_network_objects.yml index 2791eafc..9234d961 100644 --- a/sccfm-ansible/examples/list_network_objects.yml +++ b/sccfm-ansible/examples/list_network_objects.yml @@ -5,8 +5,7 @@ # ansible-playbook examples/list_network_objects.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` - name: List network objects in SCC Firewall Manager hosts: localhost @@ -15,8 +14,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: # ============================================================ diff --git a/sccfm-ansible/examples/manage_asa_shun.yml b/sccfm-ansible/examples/manage_asa_shun.yml index fa8f2356..da3200f0 100644 --- a/sccfm-ansible/examples/manage_asa_shun.yml +++ b/sccfm-ansible/examples/manage_asa_shun.yml @@ -3,9 +3,7 @@ # # RUN: # ansible-playbook examples/manage_asa_shun.yml \ -# -e device_uid= \ -# -e sccfm_region= \ -# -e sccfm_api_token= +# -e device_uid= - name: "Manage shun entries on ASA firewall" hosts: localhost @@ -13,8 +11,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uid: "" diff --git a/sccfm-ansible/examples/manage_network_group_members.yml b/sccfm-ansible/examples/manage_network_group_members.yml index 815ff8a7..7eb0ee40 100644 --- a/sccfm-ansible/examples/manage_network_group_members.yml +++ b/sccfm-ansible/examples/manage_network_group_members.yml @@ -8,8 +8,7 @@ # ansible-playbook examples/manage_network_group_members.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` # - The network group and referenced network objects must already exist. # Run create_network_objects.yml and create_network_groups.yml first. @@ -19,8 +18,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: group_name: web-servers diff --git a/sccfm-ansible/examples/network_objects.yml b/sccfm-ansible/examples/network_objects.yml index 454ab8cc..6240f941 100644 --- a/sccfm-ansible/examples/network_objects.yml +++ b/sccfm-ansible/examples/network_objects.yml @@ -7,8 +7,7 @@ # ansible-playbook examples/network_objects.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` - name: Network object lifecycle — create, list, update, delete hosts: localhost @@ -16,8 +15,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: network_objects: diff --git a/sccfm-ansible/examples/onboard_asas.yml b/sccfm-ansible/examples/onboard_asas.yml index 674b7299..2f6c4c3b 100644 --- a/sccfm-ansible/examples/onboard_asas.yml +++ b/sccfm-ansible/examples/onboard_asas.yml @@ -6,8 +6,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: # List of ASAs to onboard diff --git a/sccfm-ansible/examples/onboard_cdfmc_ftd.yml b/sccfm-ansible/examples/onboard_cdfmc_ftd.yml index a4b19325..d23e4633 100644 --- a/sccfm-ansible/examples/onboard_cdfmc_ftd.yml +++ b/sccfm-ansible/examples/onboard_cdfmc_ftd.yml @@ -29,8 +29,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_name: "" diff --git a/sccfm-ansible/examples/onboard_cdfmc_ftd_ztp.yml b/sccfm-ansible/examples/onboard_cdfmc_ftd_ztp.yml index 29299349..16463976 100644 --- a/sccfm-ansible/examples/onboard_cdfmc_ftd_ztp.yml +++ b/sccfm-ansible/examples/onboard_cdfmc_ftd_ztp.yml @@ -42,8 +42,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_name: "" diff --git a/sccfm-ansible/examples/trigger_asa_upgrade.yml b/sccfm-ansible/examples/trigger_asa_upgrade.yml index abfc2f99..7cc458fd 100644 --- a/sccfm-ansible/examples/trigger_asa_upgrade.yml +++ b/sccfm-ansible/examples/trigger_asa_upgrade.yml @@ -37,8 +37,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uids: [] diff --git a/sccfm-ansible/examples/trigger_ftd_upgrade.yml b/sccfm-ansible/examples/trigger_ftd_upgrade.yml index cb997a10..92599023 100644 --- a/sccfm-ansible/examples/trigger_ftd_upgrade.yml +++ b/sccfm-ansible/examples/trigger_ftd_upgrade.yml @@ -31,8 +31,7 @@ module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default vars: device_uids: [] diff --git a/sccfm-ansible/examples/update_network_groups.yml b/sccfm-ansible/examples/update_network_groups.yml index 0d9d7b0e..178f396a 100644 --- a/sccfm-ansible/examples/update_network_groups.yml +++ b/sccfm-ansible/examples/update_network_groups.yml @@ -5,8 +5,7 @@ # ansible-playbook examples/update_network_groups.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` # - Run create_network_objects.yml first to create the referenced network objects # - Run create_network_groups.yml first to create the groups to update # @@ -21,8 +20,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: # ============================================================ diff --git a/sccfm-ansible/examples/update_network_objects.yml b/sccfm-ansible/examples/update_network_objects.yml index 1e34afde..b9cc770a 100644 --- a/sccfm-ansible/examples/update_network_objects.yml +++ b/sccfm-ansible/examples/update_network_objects.yml @@ -9,8 +9,7 @@ # ansible-playbook examples/update_network_objects.yml # # PREREQUISITES: -# - SCCFM_REGION and SCCFM_API_TOKEN environment variables set, OR -# - Region and API token in group_vars/all/vars.yml +# - Configure the default profile with `sccfm-cli configure` # # NOTE: # This module is idempotent. Running the same playbook twice will @@ -23,8 +22,7 @@ # Use module_defaults to avoid repeating region and api_token module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: # ============================================================ diff --git a/sccfm-ansible/plugins/inventory/sccfm.py b/sccfm-ansible/plugins/inventory/sccfm.py index 68b5f413..7c637dcf 100644 --- a/sccfm-ansible/plugins/inventory/sccfm.py +++ b/sccfm-ansible/plugins/inventory/sccfm.py @@ -4,7 +4,7 @@ from __future__ import annotations -import os +from pathlib import Path from typing import Any, Dict, List, Optional, cast from ansible.errors import AnsibleParserError @@ -12,6 +12,8 @@ from ansible.utils.display import Display from scc_firewall_manager_sdk import Device +from cisco_sccfm_core.services.profile_service import ProfileService + from ..module_utils.builders import InventoryHostBuilder from ..module_utils.config import Config from ..module_utils.loaders import InventoryLoader @@ -29,19 +31,15 @@ description: Ensure this plugin gets loaded. required: true choices: ["cisco.sccfm.sccfm"] - region: - description: SCCFM region to target (int, us, eu, apj, au, uae, in, or ci). - env: - - name: SCCFM_REGION - required: true - type: str - api_token: - description: API token for the SCCFM region. - env: - - name: SCCFM_API_TOKEN - required: true + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). + required: false type: str - no_log: true + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. + required: false + type: path limit: description: Page size to use when fetching devices. required: false @@ -65,8 +63,7 @@ EXAMPLES = r""" plugin: cisco.sccfm.sccfm -region: us -api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" +profile: default limit: 100 query: "asa" group: sccfm @@ -92,23 +89,17 @@ def parse(self, inventory: Any, loader: Any, path: str, cache: bool = True) -> N super().parse(inventory, loader, path, cache=cache) config_data: Dict[str, Any] = self._read_config_data(path) - region = self._template_string(cast(Optional[str], config_data.get("region"))) - api_token = self._template_string(cast(Optional[str], config_data.get("api_token"))) - - if region is None: - region = cast(Optional[str], os.getenv("SCCFM_REGION")) - if api_token is None: - api_token = cast(Optional[str], os.getenv("SCCFM_API_TOKEN")) - - if not region: - raise AnsibleParserError( - "SCCFM region is required. Set 'region' in the inventory file or " - "export SCCFM_REGION." - ) - if not api_token: + profile = ( + self._template_string(cast(Optional[str], config_data.get("profile"))) or "default" + ) + raw_config_path = self._template_string(cast(Optional[str], config_data.get("config_path"))) + stored = ProfileService(path=Path(raw_config_path) if raw_config_path else None).load( + profile + ) + if stored is None: raise AnsibleParserError( - "SCCFM api_token is required. Set 'api_token' in the inventory file " - "or export SCCFM_API_TOKEN." + f"SCCFM profile '{profile}' not found. " + f"Run 'sccfm-cli --profile {profile} configure' to set it up." ) limit = int(cast(int | None, config_data.get("limit")) or 100) @@ -117,7 +108,7 @@ def parse(self, inventory: Any, loader: Any, path: str, cache: bool = True) -> N group_by_device_type = bool(config_data.get("group_by_device_type", False)) try: - config = Config(region=region, api_token=api_token) + config = Config(region=stored.region, api_token=stored.api_token) except ValueError as exc: raise AnsibleParserError(str(exc)) from exc @@ -130,7 +121,7 @@ def parse(self, inventory: Any, loader: Any, path: str, cache: bool = True) -> N if group: self.inventory.add_group(group) self.inventory.set_variable(group, "sccfm_region", region) - self.inventory.set_variable(group, "sccfm_api_token", api_token) + self.inventory.set_variable(group, "sccfm_profile", profile) host_builder = InventoryHostBuilder(inventory=self.inventory, region=region) diff --git a/sccfm-ansible/plugins/lookup/profile.py b/sccfm-ansible/plugins/lookup/profile.py new file mode 100644 index 00000000..85158320 --- /dev/null +++ b/sccfm-ansible/plugins/lookup/profile.py @@ -0,0 +1,76 @@ +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase + +from cisco_sccfm_core.services.profile_service import ProfileService + +DOCUMENTATION = r""" +name: profile +author: Cisco SCCFM Team +version_added: "1.0.0" +short_description: Read a value from a configured SCCFM profile +description: + - Reads a region or API token from the canonical SCCFM profile store. + - Configure profiles with C(sccfm-cli configure) before using this lookup. +options: + _terms: + description: Profile names to read. + required: true + field: + description: Profile field to return. + choices: [region, api_token] + default: api_token + config_path: + description: Optional path to the canonical SCCFM profile configuration file. + type: path +""" + +EXAMPLES = r""" +- name: Use a profile token in an API request + ansible.builtin.uri: + url: https://example.invalid/api + headers: + Authorization: "Bearer {{ lookup('cisco.sccfm.profile', 'default') }}" + no_log: true +""" + +RETURN = r""" +_raw: + description: Values read from the selected SCCFM profiles. + type: list + elements: str +""" + + +class LookupModule(LookupBase): + """Read fields from the canonical SCCFM profile store.""" + + def run( + self, + terms: list[str], + variables: dict[str, Any] | None = None, + **kwargs: Any, + ) -> list[str]: + self.set_options(var_options=variables, direct=kwargs) + field = self.get_option("field") + raw_path = self.get_option("config_path") + service = ProfileService(path=Path(raw_path) if raw_path else None) + + values: list[str] = [] + for profile_name in terms: + profile = service.load(profile_name) + if profile is None: + raise AnsibleError( + f"SCCFM profile '{profile_name}' not found. " + f"Run 'sccfm-cli --profile {profile_name} configure' to set it up." + ) + values.append(profile.region if field == "region" else profile.api_token) + return values diff --git a/sccfm-ansible/plugins/module_utils/config.py b/sccfm-ansible/plugins/module_utils/config.py index 69ea3a16..87073266 100644 --- a/sccfm-ansible/plugins/module_utils/config.py +++ b/sccfm-ansible/plugins/module_utils/config.py @@ -4,11 +4,12 @@ from __future__ import annotations -import os from dataclasses import dataclass +from pathlib import Path from typing import TYPE_CHECKING, Any from cisco_sccfm_core.constants import SCCFM_REGIONS, normalize_sccfm_region +from cisco_sccfm_core.services.profile_service import ProfileService if TYPE_CHECKING: from ansible.module_utils.basic import AnsibleModule @@ -19,19 +20,14 @@ @dataclass(frozen=True) class Config: - """SCCFM API configuration. - - If region or api_token are empty, falls back to environment variables. - Validates region and api_token on construction. - """ + """Validated SCCFM API configuration resolved from a named profile.""" region: str = "" api_token: str = "" def __post_init__(self) -> None: - # Resolve from environment if not provided - resolved_region = normalize_sccfm_region(self.region or os.getenv("SCCFM_REGION")) - resolved_token = self.api_token or os.getenv("SCCFM_API_TOKEN") + resolved_region = normalize_sccfm_region(self.region) + resolved_token = self.api_token # Use object.__setattr__ since dataclass is frozen object.__setattr__(self, "region", resolved_region) @@ -40,30 +36,29 @@ def __post_init__(self) -> None: # Validate if not self.api_token: raise ValueError( - "api_token is required. Provide it via module parameter, module_defaults, or " - "SCCFM_API_TOKEN environment variable. " + "The selected SCCFM profile does not contain an API token. " "Generate an API token following instructions in " "https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/" "authentication/" ) if not self.region: raise ValueError( - f"region is required. Provide it via module parameter, module_defaults, or " - f"SCCFM_REGION environment variable. Allowed regions: {ALLOWED_REGIONS_TEXT}" + "The selected SCCFM profile does not contain a region. " + f"Allowed regions: {ALLOWED_REGIONS_TEXT}" ) if self.region not in ALLOWED_REGIONS: raise ValueError(f"SCCFM region must be one of: {ALLOWED_REGIONS_TEXT}") def base_argument_spec() -> dict[str, dict[str, Any]]: - """Return common argument spec for region and api_token. + """Return common argument spec for canonical SCCFM profile selection. Returns: Dictionary suitable for merging into build_argument_spec(). """ return { - "region": {"type": "str", "required": False}, - "api_token": {"type": "str", "required": False, "no_log": True}, + "profile": {"type": "str", "required": False, "default": "default"}, + "config_path": {"type": "path", "required": False}, } @@ -80,7 +75,7 @@ def identifier_argument_spec() -> dict[str, dict[str, Any]]: def create_config(module: "AnsibleModule") -> Config: - """Create a Config from module params, with error handling. + """Resolve a named profile and create a Config, with error handling. Args: module: The AnsibleModule instance. @@ -92,10 +87,15 @@ def create_config(module: "AnsibleModule") -> Config: On validation error, calls module.fail_json() and does not return. """ try: - return Config( - region=module.params.get("region") or "", - api_token=module.params.get("api_token") or "", - ) - except ValueError as e: + profile = module.params.get("profile") or "default" + raw_path = module.params.get("config_path") + stored = ProfileService(path=Path(raw_path) if raw_path else None).load(profile) + if stored is None: + raise ValueError( + f"SCCFM profile '{profile}' not found. " + f"Run 'sccfm-cli --profile {profile} configure' to set it up." + ) + return Config(region=stored.region, api_token=stored.api_token) + except (OSError, ValueError) as e: module.fail_json(msg=str(e)) raise diff --git a/sccfm-ansible/plugins/modules/add_asa_shun.py b/sccfm-ansible/plugins/modules/add_asa_shun.py index c9ce85af..e7ea2758 100644 --- a/sccfm-ansible/plugins/modules/add_asa_shun.py +++ b/sccfm-ansible/plugins/modules/add_asa_shun.py @@ -29,7 +29,8 @@ immediately. - C(source_ip) and C(entries) are mutually exclusive. - Devices can be selected by a Lucene query or by specifying a list of UIDs. - - See U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/execute-cli-command/) + - See the SCC Firewall Manager API documentation for + U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/execute-cli-command/). for API documentation. options: query: @@ -130,19 +131,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -153,8 +150,7 @@ cisco.sccfm.add_asa_shun: query: "name:prod-* AND connectivityState:ONLINE" source_ip: "10.99.99.99" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Shun with connection tuple to drop an existing connection - name: Block attacker and drop active connection @@ -179,8 +175,7 @@ dest_port: 443 protocol: tcp - source_ip: "203.0.113.60" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 4: Using module_defaults (recommended) - name: Add shun entries @@ -188,8 +183,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Shun attacker IP cisco.sccfm.add_asa_shun: diff --git a/sccfm-ansible/plugins/modules/add_network_group_members.py b/sccfm-ansible/plugins/modules/add_network_group_members.py index 36d7979a..195bc16c 100644 --- a/sccfm-ansible/plugins/modules/add_network_group_members.py +++ b/sccfm-ansible/plugins/modules/add_network_group_members.py @@ -49,19 +49,15 @@ required: true type: list elements: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -74,8 +70,7 @@ referenced_objects: - web-server-01 - web-server-02 - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Add members by UID - name: Add members to a network group by UID @@ -91,8 +86,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Add web servers to group cisco.sccfm.add_network_group_members: @@ -181,7 +175,10 @@ def run_module() -> None: ) module.exit_json( changed=False, - msg=f"Network group '{result.network_group.name}' already contains all requested members.", + msg=( + f"Network group '{result.network_group.name}' already contains all " + "requested members." + ), network_group=result.network_group.to_dict(), ) return @@ -196,7 +193,10 @@ def run_module() -> None: module.exit_json( changed=False, - msg=f"Network group '{result.network_group.name}' already contains all requested members.", + msg=( + f"Network group '{result.network_group.name}' already contains all " + "requested members." + ), network_group=result.network_group.to_dict(), ) except NotFoundError as e: diff --git a/sccfm-ansible/plugins/modules/add_object_override.py b/sccfm-ansible/plugins/modules/add_object_override.py index 5e7b007f..b3a31313 100644 --- a/sccfm-ansible/plugins/modules/add_object_override.py +++ b/sccfm-ansible/plugins/modules/add_object_override.py @@ -41,19 +41,15 @@ For URL objects this should be the URL string. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -65,8 +61,7 @@ uid: "abc-123-def" target_id: "70bde3c9-328c-4a4b-bdc9-a4d4042bf09a" override_value: "10.10.10.10" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults to avoid repeating credentials - name: Add object overrides @@ -74,8 +69,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Override web server IP for branch device cisco.sccfm.add_object_override: diff --git a/sccfm-ansible/plugins/modules/apply_object_override_as_default.py b/sccfm-ansible/plugins/modules/apply_object_override_as_default.py index 47a13f6e..7cef603c 100644 --- a/sccfm-ansible/plugins/modules/apply_object_override_as_default.py +++ b/sccfm-ansible/plugins/modules/apply_object_override_as_default.py @@ -33,19 +33,15 @@ description: UID of the target device whose override value to promote as the new default. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -56,8 +52,7 @@ cisco.sccfm.apply_object_override_as_default: uid: "abc-123-def" target_id: "897b293f-132e-4678-9d78-0f0947629500" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults - name: Apply object override as default @@ -65,8 +60,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Apply override as default cisco.sccfm.apply_object_override_as_default: @@ -123,7 +117,10 @@ def run_module() -> None: if module.check_mode: module.exit_json( changed=True, - msg=f"Would apply override as default for target '{target_id}' to default on object '{uid}'.", + msg=( + f"Would apply override as default for target '{target_id}' " + f"to default on object '{uid}'." + ), object_override={}, ) return @@ -136,7 +133,10 @@ def run_module() -> None: ) module.exit_json( changed=True, - msg=f"Successfully applied override as default for target '{target_id}' to default on object '{result.name}'.", + msg=( + f"Successfully applied override as default for target '{target_id}' " + f"to default on object '{result.name}'." + ), object_override=result.to_dict(), ) except ValueError as e: diff --git a/sccfm-ansible/plugins/modules/asa_ha_check.py b/sccfm-ansible/plugins/modules/asa_ha_check.py index 311b46ac..cf5ecad9 100644 --- a/sccfm-ansible/plugins/modules/asa_ha_check.py +++ b/sccfm-ansible/plugins/modules/asa_ha_check.py @@ -61,19 +61,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -83,8 +79,7 @@ - name: Run HA checks on production ASAs cisco.sccfm.asa_ha_check: query: "name:prod-ha-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: ha_results # Example 2: Check HA status on a specific device by UID @@ -111,8 +106,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Run HA checks cisco.sccfm.asa_ha_check: diff --git a/sccfm-ansible/plugins/modules/change_asa_boot_image.py b/sccfm-ansible/plugins/modules/change_asa_boot_image.py index 712e4ff2..1fc0aa0a 100644 --- a/sccfm-ansible/plugins/modules/change_asa_boot_image.py +++ b/sccfm-ansible/plugins/modules/change_asa_boot_image.py @@ -71,19 +71,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -94,8 +90,7 @@ cisco.sccfm.change_asa_boot_image: query: "name:branch-*" image_path: "disk0:/asa9-18-4-smp-k8.bin" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Change boot image on specific devices - name: Change boot image on specific ASA devices @@ -120,8 +115,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Set boot image on branch ASAs cisco.sccfm.change_asa_boot_image: @@ -292,7 +286,10 @@ def run_module() -> None: if isinstance(service_results, CdoTransaction): module.fail_json( - msg=f"Boot image change failed with status: {service_results.cdo_transaction_status}", + msg=( + "Boot image change failed with status: " + f"{service_results.cdo_transaction_status}" + ), transaction_uid=service_results.transaction_uid, error_message=service_results.error_message, transaction_details=service_results.transaction_details, diff --git a/sccfm-ansible/plugins/modules/change_asa_local_password.py b/sccfm-ansible/plugins/modules/change_asa_local_password.py index 777bcf15..4a95e709 100644 --- a/sccfm-ansible/plugins/modules/change_asa_local_password.py +++ b/sccfm-ansible/plugins/modules/change_asa_local_password.py @@ -27,7 +27,8 @@ password command, and verifies the user is still present afterward. - Devices can be selected by a Lucene query or by specifying a list of UIDs. - The query uses the same syntax as the Get Devices API. - - See U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/get-devices/) + - See the SCC Firewall Manager API documentation for + U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/get-devices/). for query documentation. options: query: @@ -69,19 +70,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -93,8 +90,7 @@ query: "name:branch-* AND connectivityState:ONLINE" username: admin new_password: "{{ vault_new_asa_password }}" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: password_results # Example 2: Change password on specific devices by UID @@ -113,8 +109,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Change admin password on all online ASAs cisco.sccfm.change_asa_local_password: diff --git a/sccfm-ansible/plugins/modules/clear_asa_shun.py b/sccfm-ansible/plugins/modules/clear_asa_shun.py index ea7b01bd..5fac20d1 100644 --- a/sccfm-ansible/plugins/modules/clear_asa_shun.py +++ b/sccfm-ansible/plugins/modules/clear_asa_shun.py @@ -22,7 +22,8 @@ ASA devices managed by SCC Firewall Manager. - Executes C(clear shun) on the target devices. - Devices can be selected by a Lucene query or by specifying a list of UIDs. - - See U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/execute-cli-command/) + - See the SCC Firewall Manager API documentation for + U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/execute-cli-command/). for API documentation. options: query: @@ -53,19 +54,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -75,8 +72,7 @@ - name: Clear all shuns on production ASAs cisco.sccfm.clear_asa_shun: query: "name:prod-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Clear shuns on specific devices by UID - name: Clear shuns on specific ASA @@ -90,8 +86,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Clear all shuns on online ASAs cisco.sccfm.clear_asa_shun: diff --git a/sccfm-ansible/plugins/modules/configure_manager.py b/sccfm-ansible/plugins/modules/configure_manager.py index c3b1a5b5..82431365 100644 --- a/sccfm-ansible/plugins/modules/configure_manager.py +++ b/sccfm-ansible/plugins/modules/configure_manager.py @@ -117,8 +117,7 @@ fmc_access_policy_uid: "{{ fmc_access_policy_uid }}" licenses: - BASE - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: onboard_result - name: Complete registration over SSH diff --git a/sccfm-ansible/plugins/modules/create_access_rule.py b/sccfm-ansible/plugins/modules/create_access_rule.py index f5355c05..99f86908 100644 --- a/sccfm-ansible/plugins/modules/create_access_rule.py +++ b/sccfm-ansible/plugins/modules/create_access_rule.py @@ -78,19 +78,15 @@ description: Whether the rule is active. required: false type: bool - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -108,8 +104,7 @@ protocol: tcp destination_port: "443" remark: "Allow web to database" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Create a deny rule using module_defaults - name: Create access rules @@ -117,8 +112,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Create a deny rule for a source subnet cisco.sccfm.create_access_rule: @@ -132,7 +126,7 @@ destination_port: "1433" remark: "Block blocked-subnet to SQL" -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: Create an inactive permit rule cisco.sccfm.create_access_rule: access_group_uid: "{{ access_group_uid }}" diff --git a/sccfm-ansible/plugins/modules/create_network_group.py b/sccfm-ansible/plugins/modules/create_network_group.py index 50e04e1f..6a4c5b00 100644 --- a/sccfm-ansible/plugins/modules/create_network_group.py +++ b/sccfm-ansible/plugins/modules/create_network_group.py @@ -65,19 +65,15 @@ For example, C({"environment": ["production", "staging"]}). required: false type: dict - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -94,8 +90,7 @@ labels: - production - web - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Create a group with referenced objects using module_defaults - name: Create network groups @@ -103,8 +98,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Create group from existing objects cisco.sccfm.create_network_group: @@ -117,7 +111,7 @@ environment: - production -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: Create a group with URL literals cisco.sccfm.create_network_group: name: trusted-urls diff --git a/sccfm-ansible/plugins/modules/create_network_object.py b/sccfm-ansible/plugins/modules/create_network_object.py index aab3c531..d664eb48 100644 --- a/sccfm-ansible/plugins/modules/create_network_object.py +++ b/sccfm-ansible/plugins/modules/create_network_object.py @@ -51,19 +51,15 @@ For example, C({"environment": ["production", "staging"]}). required: false type: dict - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -78,8 +74,7 @@ labels: - production - web - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Create a subnet network object using module_defaults - name: Create network objects @@ -87,8 +82,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Create branch office subnet cisco.sccfm.create_network_object: @@ -102,7 +96,7 @@ environment: - production -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: Create a range network object cisco.sccfm.create_network_object: name: dhcp-pool diff --git a/sccfm-ansible/plugins/modules/delete_access_rule.py b/sccfm-ansible/plugins/modules/delete_access_rule.py index 65d0e3c6..cb3086f1 100644 --- a/sccfm-ansible/plugins/modules/delete_access_rule.py +++ b/sccfm-ansible/plugins/modules/delete_access_rule.py @@ -27,19 +27,15 @@ description: Unique identifier (UID) of the access rule to delete. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -49,8 +45,7 @@ - name: Delete access rule cisco.sccfm.delete_access_rule: uid: "ac981dcd-9860-401e-a51d-c615c946b72f" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Delete using module_defaults - name: Delete access rules @@ -58,8 +53,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Delete old rule cisco.sccfm.delete_access_rule: diff --git a/sccfm-ansible/plugins/modules/delete_network_group.py b/sccfm-ansible/plugins/modules/delete_network_group.py index 1eb436e0..574fe742 100644 --- a/sccfm-ansible/plugins/modules/delete_network_group.py +++ b/sccfm-ansible/plugins/modules/delete_network_group.py @@ -35,23 +35,21 @@ description: Name of the network group to delete. required: false type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path notes: - Either C(uid) or C(name) must be provided, but not both. - - When using C(name), the module will search for the group and resolve it to a UID before deletion. - - Network groups are filtered by objectType to avoid accidentally matching network objects with the same name. + - When using C(name), the module searches for the group and resolves it to a UID + before deletion. + - Network groups are filtered by objectType to avoid accidentally matching + network objects with the same name. author: - Cisco SCCFM Team """ @@ -61,15 +59,13 @@ - name: Delete network group by UID cisco.sccfm.delete_network_group: uid: "abc-123-def-456" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Delete a network group by name - name: Delete network group by name cisco.sccfm.delete_network_group: name: "web-server-group" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 3: Delete multiple groups using module_defaults - name: Delete network groups @@ -77,8 +73,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Delete obsolete network groups cisco.sccfm.delete_network_group: @@ -87,7 +82,7 @@ - web-server-group-01 - web-subnet-group -# Example 4: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 4: Using the default configured profile - name: Delete a network group cisco.sccfm.delete_network_group: name: "temporary-group" @@ -117,7 +112,7 @@ def run_module() -> None: mutually_exclusive=[("uid", "name")], ) - config = create_config(module) + config: Config = create_config(module) service = NetworkGroupService(config=config) run_delete_with_idempotency( diff --git a/sccfm-ansible/plugins/modules/delete_network_object.py b/sccfm-ansible/plugins/modules/delete_network_object.py index efd88bc6..44874d20 100644 --- a/sccfm-ansible/plugins/modules/delete_network_object.py +++ b/sccfm-ansible/plugins/modules/delete_network_object.py @@ -34,22 +34,19 @@ description: Name of the network object to delete. required: false type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path notes: - Either C(uid) or C(name) must be provided, but not both. - - When using C(name), the module will search for the object and resolve it to a UID before deletion. + - When using C(name), the module searches for the object and resolves it to a UID + before deletion. author: - Cisco SCCFM Team """ @@ -59,15 +56,13 @@ - name: Delete network object by UID cisco.sccfm.delete_network_object: uid: "abc-123-def-456" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Delete a network object by name - name: Delete network object by name cisco.sccfm.delete_network_object: name: "old-web-server" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 3: Delete multiple objects using module_defaults - name: Delete network objects @@ -75,8 +70,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Delete obsolete network objects cisco.sccfm.delete_network_object: @@ -86,7 +80,7 @@ - old-server-02 - deprecated-subnet -# Example 4: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 4: Using the default configured profile - name: Delete a network object cisco.sccfm.delete_network_object: name: "temporary-host" @@ -116,7 +110,7 @@ def run_module() -> None: mutually_exclusive=[("uid", "name")], ) - config = create_config(module) + config: Config = create_config(module) service = NetworkObjectService(config=config) run_delete_with_idempotency( diff --git a/sccfm-ansible/plugins/modules/delete_object_override.py b/sccfm-ansible/plugins/modules/delete_object_override.py index b6c29fc2..c5424a07 100644 --- a/sccfm-ansible/plugins/modules/delete_object_override.py +++ b/sccfm-ansible/plugins/modules/delete_object_override.py @@ -33,19 +33,15 @@ description: UID of the target device whose override to delete. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -56,8 +52,7 @@ cisco.sccfm.delete_object_override: uid: "abc-123-def" target_id: "70bde3c9-328c-4a4b-bdc9-a4d4042bf09a" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults - name: Delete object overrides @@ -65,8 +60,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Delete override cisco.sccfm.delete_object_override: @@ -136,7 +130,10 @@ def run_module() -> None: ) module.exit_json( changed=True, - msg=f"Successfully deleted override for target '{target_id}' on object '{result.name}'.", + msg=( + f"Successfully deleted override for target '{target_id}' " + f"on object '{result.name}'." + ), object_override=result.to_dict(), ) except ValueError as e: diff --git a/sccfm-ansible/plugins/modules/deploy_cdfmc_ftd.py b/sccfm-ansible/plugins/modules/deploy_cdfmc_ftd.py index f69ae527..9d5526fe 100644 --- a/sccfm-ansible/plugins/modules/deploy_cdfmc_ftd.py +++ b/sccfm-ansible/plugins/modules/deploy_cdfmc_ftd.py @@ -7,12 +7,7 @@ from typing import Any, cast from ansible.module_utils.basic import AnsibleModule -from scc_firewall_manager_sdk import ( - ApiException, - CdoTransaction, - DevicePage, - EntityType, -) +from scc_firewall_manager_sdk import ApiException, CdoTransaction, DevicePage, EntityType from cisco_sccfm_core import InventoryService, SccApiError from cisco_sccfm_core.constants import DEFAULT_TRANSACTION_TIMEOUT_SEC @@ -88,19 +83,15 @@ required: false type: int default: 3600 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -111,8 +102,7 @@ cisco.sccfm.deploy_cdfmc_ftd: uids: - "12345678-1234-1234-1234-123456789abc" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Deploy with notes - name: Deploy FTD changes with deployment notes @@ -136,8 +126,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Deploy branch FTD changes cisco.sccfm.deploy_cdfmc_ftd: diff --git a/sccfm-ansible/plugins/modules/edit_object_override.py b/sccfm-ansible/plugins/modules/edit_object_override.py index 944d440a..18b0b2a0 100644 --- a/sccfm-ansible/plugins/modules/edit_object_override.py +++ b/sccfm-ansible/plugins/modules/edit_object_override.py @@ -41,19 +41,15 @@ For URL objects this should be the URL string. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -65,8 +61,7 @@ uid: "abc-123-def" target_id: "70bde3c9-328c-4a4b-bdc9-a4d4042bf09a" override_value: "10.20.30.40" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults - name: Edit object overrides @@ -74,8 +69,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Edit override cisco.sccfm.edit_object_override: @@ -149,7 +143,10 @@ def run_module() -> None: ) module.exit_json( changed=True, - msg=f"Successfully updated override for target '{target_id}' on object '{result.name}'.", + msg=( + f"Successfully updated override for target '{target_id}' " + f"on object '{result.name}'." + ), object_override=result.to_dict(), ) except ValueError as e: diff --git a/sccfm-ansible/plugins/modules/execute_asa_cli.py b/sccfm-ansible/plugins/modules/execute_asa_cli.py index fa6cd572..67003f90 100644 --- a/sccfm-ansible/plugins/modules/execute_asa_cli.py +++ b/sccfm-ansible/plugins/modules/execute_asa_cli.py @@ -27,7 +27,9 @@ - Execute CLI commands on one or more ASA devices managed by SCC Firewall Manager. - Devices can be selected by a Lucene query or by specifying a list of UIDs. - The query uses the same syntax as the Get Devices API. - - See U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/get-devices/) for query documentation. + - See the SCC Firewall Manager API documentation for + U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/get-devices/) + query details. options: query: description: @@ -71,19 +73,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -96,8 +94,7 @@ commands: - "show version" - "show running-config" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: cli_results # Example 2: Execute commands on specific devices by UID @@ -116,8 +113,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show version on branch ASAs cisco.sccfm.execute_asa_cli: diff --git a/sccfm-ansible/plugins/modules/execute_ftd_cli.py b/sccfm-ansible/plugins/modules/execute_ftd_cli.py index b39f8afa..810641d6 100644 --- a/sccfm-ansible/plugins/modules/execute_ftd_cli.py +++ b/sccfm-ansible/plugins/modules/execute_ftd_cli.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Any, cast +from typing import Any from ansible.module_utils.basic import AnsibleModule from scc_firewall_manager_sdk import ApiException, Device, DevicePage @@ -28,7 +28,9 @@ - Devices can be selected by a Lucene query or by specifying a list of UIDs. - Only show commands are supported (e.g. show version, show failover, show route). - The command runs via the cdFMC bulk command proxy endpoint. - - See U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/create-bulk-command/) for endpoint documentation. + - See the SCC Firewall Manager API documentation for + U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/create-bulk-command/) + endpoint details. options: query: description: @@ -72,19 +74,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -95,8 +93,7 @@ cisco.sccfm.execute_ftd_cli: query: "name:prod-* AND connectivityState:ONLINE" command: "show failover" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: cli_results # Example 2: Execute a command on specific devices by UID @@ -114,8 +111,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show route on branch FTDs cisco.sccfm.execute_ftd_cli: diff --git a/sccfm-ansible/plugins/modules/get_access_group.py b/sccfm-ansible/plugins/modules/get_access_group.py index dd1ea38e..5432b77d 100644 --- a/sccfm-ansible/plugins/modules/get_access_group.py +++ b/sccfm-ansible/plugins/modules/get_access_group.py @@ -25,19 +25,15 @@ description: Unique identifier (UID) of the access group to retrieve. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -47,8 +43,7 @@ - name: Get access group cisco.sccfm.get_access_group: uid: "c6fa254e-db7a-447e-a58f-95df1e09c2af" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show access group name @@ -61,8 +56,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Get access group cisco.sccfm.get_access_group: diff --git a/sccfm-ansible/plugins/modules/get_access_rule.py b/sccfm-ansible/plugins/modules/get_access_rule.py index 15a92e3a..6610375d 100644 --- a/sccfm-ansible/plugins/modules/get_access_rule.py +++ b/sccfm-ansible/plugins/modules/get_access_rule.py @@ -25,19 +25,15 @@ description: Unique identifier (UID) of the access rule to retrieve. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -47,8 +43,7 @@ - name: Get access rule cisco.sccfm.get_access_rule: uid: "ac981dcd-9860-401e-a51d-c615c946b72f" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show rule @@ -61,8 +56,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Get access rule details cisco.sccfm.get_access_rule: diff --git a/sccfm-ansible/plugins/modules/get_object.py b/sccfm-ansible/plugins/modules/get_object.py index ef3e8654..959289c6 100644 --- a/sccfm-ansible/plugins/modules/get_object.py +++ b/sccfm-ansible/plugins/modules/get_object.py @@ -27,19 +27,15 @@ description: Unique identifier (UID) of the object to retrieve. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -49,8 +45,7 @@ - name: Get object cisco.sccfm.get_object: uid: "fd526e22-12ff-4fa0-a88d-7375c5d1e144" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: obj - name: Show object @@ -63,8 +58,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Get object details cisco.sccfm.get_object: diff --git a/sccfm-ansible/plugins/modules/list_access_groups.py b/sccfm-ansible/plugins/modules/list_access_groups.py index ff5557e2..b9a03498 100644 --- a/sccfm-ansible/plugins/modules/list_access_groups.py +++ b/sccfm-ansible/plugins/modules/list_access_groups.py @@ -38,19 +38,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -59,8 +55,7 @@ # List all access groups - name: List access groups cisco.sccfm.list_access_groups: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Display access groups @@ -73,8 +68,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List access groups cisco.sccfm.list_access_groups: diff --git a/sccfm-ansible/plugins/modules/list_access_rules.py b/sccfm-ansible/plugins/modules/list_access_rules.py index be767f23..9cde7c53 100644 --- a/sccfm-ansible/plugins/modules/list_access_rules.py +++ b/sccfm-ansible/plugins/modules/list_access_rules.py @@ -38,19 +38,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -59,8 +55,7 @@ # Example 1: List all access rules - name: List all access rules cisco.sccfm.list_access_rules: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Display access rules @@ -73,8 +68,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: List first page of access rules cisco.sccfm.list_access_rules: diff --git a/sccfm-ansible/plugins/modules/list_asa_boot_registry.py b/sccfm-ansible/plugins/modules/list_asa_boot_registry.py index f12ab630..a62b9c87 100644 --- a/sccfm-ansible/plugins/modules/list_asa_boot_registry.py +++ b/sccfm-ansible/plugins/modules/list_asa_boot_registry.py @@ -62,19 +62,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -84,8 +80,7 @@ - name: List boot registry on production ASAs cisco.sccfm.list_asa_boot_registry: query: "name:prod-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: boot_registry # Example 2: Get boot registry info for specific devices by UID @@ -102,8 +97,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List boot registry on branch ASAs cisco.sccfm.list_asa_boot_registry: diff --git a/sccfm-ansible/plugins/modules/list_asa_compatible_versions.py b/sccfm-ansible/plugins/modules/list_asa_compatible_versions.py index 4ed90268..01397277 100644 --- a/sccfm-ansible/plugins/modules/list_asa_compatible_versions.py +++ b/sccfm-ansible/plugins/modules/list_asa_compatible_versions.py @@ -7,11 +7,7 @@ from typing import Any, cast from ansible.module_utils.basic import AnsibleModule -from scc_firewall_manager_sdk import ( - ApiException, - AsaCompatibleVersion, - DevicePage, -) +from scc_firewall_manager_sdk import ApiException, AsaCompatibleVersion, DevicePage from cisco_sccfm_core import ASA_DEVICE_TYPE_FILTER, InventoryService, SccApiError from cisco_sccfm_core.models.asa_upgrade_version import AsaGroupCompatibleVersions @@ -31,7 +27,8 @@ - Uses the C(GET /v1/inventory/devices/asas/{deviceUid}/upgrades/versions) API endpoint for each device, then returns the common set of versions that every device in the group can upgrade to. - - See U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/get-compatible-upgrade-versions-for-an-asa/) + - See the SCC Firewall Manager API documentation for + U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/get-compatible-upgrade-versions-for-an-asa/). for API documentation. options: query: @@ -72,19 +69,15 @@ required: false type: bool default: false - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -95,8 +88,7 @@ cisco.sccfm.list_asa_compatible_versions: uids: - "12345678-1234-1234-1234-123456789abc" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: compat_versions - name: Show compatible versions @@ -135,8 +127,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Get compatible versions for branch ASAs cisco.sccfm.list_asa_compatible_versions: diff --git a/sccfm-ansible/plugins/modules/list_asa_disk_files.py b/sccfm-ansible/plugins/modules/list_asa_disk_files.py index f8e9540a..b22a6074 100644 --- a/sccfm-ansible/plugins/modules/list_asa_disk_files.py +++ b/sccfm-ansible/plugins/modules/list_asa_disk_files.py @@ -62,19 +62,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -84,8 +80,7 @@ - name: List disk files on production ASAs cisco.sccfm.list_asa_disk_files: query: "name:prod-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: disk_files # Example 2: List files on specific devices by UID @@ -102,8 +97,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List files on branch ASAs cisco.sccfm.list_asa_disk_files: @@ -112,7 +106,10 @@ - name: Show only AnyConnect packages ansible.builtin.debug: - msg: "{{ disk_files.results | selectattr('file_type', 'equalto', 'ANYCONNECT_PACKAGE') | list }}" + msg: >- + {{ disk_files.results + | selectattr('file_type', 'equalto', 'ANYCONNECT_PACKAGE') + | list }} """ RETURN = r""" diff --git a/sccfm-ansible/plugins/modules/list_asa_local_users.py b/sccfm-ansible/plugins/modules/list_asa_local_users.py index 1e8c31ab..71ddcb27 100644 --- a/sccfm-ansible/plugins/modules/list_asa_local_users.py +++ b/sccfm-ansible/plugins/modules/list_asa_local_users.py @@ -54,19 +54,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -82,15 +78,13 @@ cisco.sccfm.list_asa_local_users: query: "name:branch-* AND connectivityState:ONLINE" region: "us" - api_token: "{{ sccfm_api_token }}" - name: List local users with shared auth hosts: localhost gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List local users on branch ASAs cisco.sccfm.list_asa_local_users: diff --git a/sccfm-ansible/plugins/modules/list_asa_not_on_version.py b/sccfm-ansible/plugins/modules/list_asa_not_on_version.py index a71faa23..05480e70 100644 --- a/sccfm-ansible/plugins/modules/list_asa_not_on_version.py +++ b/sccfm-ansible/plugins/modules/list_asa_not_on_version.py @@ -63,19 +63,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -85,8 +81,7 @@ - name: Find ASAs not on 9.20(3)13 cisco.sccfm.list_asa_not_on_version: version: "9.20(3)13" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show devices that need upgrading @@ -116,8 +111,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Find ASAs not on target version cisco.sccfm.list_asa_not_on_version: diff --git a/sccfm-ansible/plugins/modules/list_cdfmc_access_policies.py b/sccfm-ansible/plugins/modules/list_cdfmc_access_policies.py index c88a626a..239f2f15 100644 --- a/sccfm-ansible/plugins/modules/list_cdfmc_access_policies.py +++ b/sccfm-ansible/plugins/modules/list_cdfmc_access_policies.py @@ -38,19 +38,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -60,8 +56,7 @@ - name: List cdFMC access policies cisco.sccfm.list_cdfmc_access_policies: domain_uid: "e276abec-e0f2-11e3-8169-6d9ed49b625f" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show access policies @@ -74,8 +69,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List access policies for a domain cisco.sccfm.list_cdfmc_access_policies: diff --git a/sccfm-ansible/plugins/modules/list_ftd_compatible_versions.py b/sccfm-ansible/plugins/modules/list_ftd_compatible_versions.py index 17cbe550..40e02875 100644 --- a/sccfm-ansible/plugins/modules/list_ftd_compatible_versions.py +++ b/sccfm-ansible/plugins/modules/list_ftd_compatible_versions.py @@ -7,19 +7,14 @@ from typing import Any, cast from ansible.module_utils.basic import AnsibleModule -from scc_firewall_manager_sdk import ( - ApiException, - DevicePage, - EntityType, - FtdVersion, -) +from scc_firewall_manager_sdk import ApiException, DevicePage, FtdVersion from cisco_sccfm_core import FTD_DEVICE_TYPE_FILTER, InventoryService, SccApiError from cisco_sccfm_core.models.ftd_upgrade_version import FtdGroupCompatibleVersions from cisco_sccfm_core.services.inventory import FtdUpgradeVersionService from cisco_sccfm_core.types import ConfigLike -from ..module_utils.config import Config, base_argument_spec, create_config +from ..module_utils.config import base_argument_spec, create_config DOCUMENTATION = r""" --- @@ -71,19 +66,15 @@ required: false type: bool default: false - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -94,8 +85,7 @@ cisco.sccfm.list_ftd_compatible_versions: uids: - "12345678-1234-1234-1234-123456789abc" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: compat_versions - name: Show compatible versions @@ -134,8 +124,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Get compatible versions for branch FTDs cisco.sccfm.list_ftd_compatible_versions: diff --git a/sccfm-ansible/plugins/modules/list_ftd_not_on_version.py b/sccfm-ansible/plugins/modules/list_ftd_not_on_version.py index 477a3c0e..a1fa238a 100644 --- a/sccfm-ansible/plugins/modules/list_ftd_not_on_version.py +++ b/sccfm-ansible/plugins/modules/list_ftd_not_on_version.py @@ -8,10 +8,9 @@ from typing import Any, cast from ansible.module_utils.basic import AnsibleModule -from scc_firewall_manager_sdk import ApiException, Device, DevicePage, EntityType, FtdVersion +from scc_firewall_manager_sdk import ApiException, Device, DevicePage from cisco_sccfm_core import FTD_DEVICE_TYPE_FILTER, InventoryService, SccApiError -from cisco_sccfm_core.models.ftd_upgrade_version import FtdGroupCompatibleVersions from cisco_sccfm_core.services.inventory import FtdUpgradeVersionService from ..module_utils.config import base_argument_spec, create_config @@ -77,19 +76,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -99,8 +94,7 @@ - name: Find FTDs not on 7.4.1 cisco.sccfm.list_ftd_not_on_version: version: "7.4.1" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show devices that need upgrading @@ -116,7 +110,9 @@ - name: Show non-compliant devices ansible.builtin.debug: - msg: "{{ item.name }} is on {{ item.software_version }}, recommended: {{ item.recommended_version }}" + msg: >- + {{ item.name }} is on {{ item.software_version }}, + recommended: {{ item.recommended_version }} loop: "{{ result.devices }}" # Example 3: Filter by name pattern @@ -132,8 +128,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Find FTDs not on target version cisco.sccfm.list_ftd_not_on_version: diff --git a/sccfm-ansible/plugins/modules/list_managers.py b/sccfm-ansible/plugins/modules/list_managers.py index fdeddc6c..4d7ffc34 100644 --- a/sccfm-ansible/plugins/modules/list_managers.py +++ b/sccfm-ansible/plugins/modules/list_managers.py @@ -37,19 +37,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -58,8 +54,7 @@ # Example 1: List all managers - name: List all managers cisco.sccfm.list_managers: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Show managers @@ -82,8 +77,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: List all managers cisco.sccfm.list_managers: diff --git a/sccfm-ansible/plugins/modules/list_network_groups.py b/sccfm-ansible/plugins/modules/list_network_groups.py index 6d9c6644..5097547c 100644 --- a/sccfm-ansible/plugins/modules/list_network_groups.py +++ b/sccfm-ansible/plugins/modules/list_network_groups.py @@ -44,19 +44,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -65,8 +61,7 @@ # Example 1: List all network groups - name: List all network groups cisco.sccfm.list_network_groups: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Display network groups @@ -79,8 +74,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Find web-related network groups cisco.sccfm.list_network_groups: @@ -93,7 +87,7 @@ ansible.builtin.debug: msg: "Found {{ result.count }} groups" -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: List first page of network groups cisco.sccfm.list_network_groups: limit: 25 diff --git a/sccfm-ansible/plugins/modules/list_network_objects.py b/sccfm-ansible/plugins/modules/list_network_objects.py index 44a0fa54..844541ea 100644 --- a/sccfm-ansible/plugins/modules/list_network_objects.py +++ b/sccfm-ansible/plugins/modules/list_network_objects.py @@ -44,19 +44,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -65,8 +61,7 @@ # Example 1: List all network objects - name: List all network objects cisco.sccfm.list_network_objects: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: result - name: Display network objects @@ -79,8 +74,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Find web-related network objects cisco.sccfm.list_network_objects: @@ -93,7 +87,7 @@ ansible.builtin.debug: msg: "Found {{ result.count }} objects" -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: List first page of network objects cisco.sccfm.list_network_objects: limit: 25 diff --git a/sccfm-ansible/plugins/modules/onboard_asa.py b/sccfm-ansible/plugins/modules/onboard_asa.py index 5353374d..926ba2f2 100644 --- a/sccfm-ansible/plugins/modules/onboard_asa.py +++ b/sccfm-ansible/plugins/modules/onboard_asa.py @@ -70,19 +70,15 @@ required: false type: list elements: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -93,8 +89,7 @@ hosts: all module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Onboard branch-asa-1 cisco.sccfm.onboard_asa: @@ -121,9 +116,8 @@ connector_type: SDC connector_name: branch-sdc-1 region: us - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" -# Example 3: Using environment variables (SCCFM_REGION and SCCFM_API_TOKEN) +# Example 3: Using the default configured profile - name: Onboard branch-asa-1 cisco.sccfm.onboard_asa: name: branch-asa-1 diff --git a/sccfm-ansible/plugins/modules/onboard_cdfmc_ftd.py b/sccfm-ansible/plugins/modules/onboard_cdfmc_ftd.py index 41dbfd49..3ae7e8b0 100644 --- a/sccfm-ansible/plugins/modules/onboard_cdfmc_ftd.py +++ b/sccfm-ansible/plugins/modules/onboard_cdfmc_ftd.py @@ -70,19 +70,15 @@ required: false type: list elements: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -95,8 +91,7 @@ fmc_access_policy_uid: "7131daad-e813-4b8f-8f42-be1e241e8cdb" licenses: - BASE - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Onboard a virtual FTD with multiple licenses - name: Onboard virtual FTD @@ -128,8 +123,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Onboard branch FTD cisco.sccfm.onboard_cdfmc_ftd: diff --git a/sccfm-ansible/plugins/modules/onboard_cdfmc_ftd_ztp.py b/sccfm-ansible/plugins/modules/onboard_cdfmc_ftd_ztp.py index 491a35b3..763f4c59 100644 --- a/sccfm-ansible/plugins/modules/onboard_cdfmc_ftd_ztp.py +++ b/sccfm-ansible/plugins/modules/onboard_cdfmc_ftd_ztp.py @@ -69,19 +69,15 @@ description: UUID of the device group the device will join after registration. required: false type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -95,8 +91,7 @@ licenses: - BASE fmc_access_policy_uid: "7131daad-e813-4b8f-8f42-be1e241e8cdb" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Onboard with initial password and device group - name: Onboard FTD via ZTP with password @@ -116,8 +111,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Onboard branch FTD through ZTP cisco.sccfm.onboard_cdfmc_ftd_ztp: diff --git a/sccfm-ansible/plugins/modules/register_cdfmc_ftd.py b/sccfm-ansible/plugins/modules/register_cdfmc_ftd.py index 0d27e057..0954c839 100644 --- a/sccfm-ansible/plugins/modules/register_cdfmc_ftd.py +++ b/sccfm-ansible/plugins/modules/register_cdfmc_ftd.py @@ -34,21 +34,15 @@ required: false type: bool default: false - region: - description: - - The SCC Firewall Manager region. + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: - - The SCC Firewall Manager API token. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -59,8 +53,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Complete the FTD registration cisco.sccfm.register_cdfmc_ftd: diff --git a/sccfm-ansible/plugins/modules/remove_asa_shun.py b/sccfm-ansible/plugins/modules/remove_asa_shun.py index 84aa9289..b72e2298 100644 --- a/sccfm-ansible/plugins/modules/remove_asa_shun.py +++ b/sccfm-ansible/plugins/modules/remove_asa_shun.py @@ -25,7 +25,8 @@ - Executes C(no shun ) on the target devices for each IP. - C(source_ip) and C(source_ips) are mutually exclusive. - Devices can be selected by a Lucene query or by specifying a list of UIDs. - - See U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/execute-cli-command/) + - See the SCC Firewall Manager API documentation for + U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/execute-cli-command/). for API documentation. options: query: @@ -70,19 +71,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -93,8 +90,7 @@ cisco.sccfm.remove_asa_shun: query: "name:prod-* AND connectivityState:ONLINE" source_ip: "10.99.99.99" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Remove multiple shuns in a single transaction - name: Remove multiple attacker IPs in one call @@ -104,8 +100,7 @@ - "203.0.113.40" - "203.0.113.50" - "203.0.113.60" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 3: Remove a shun on specific devices by UID - name: Remove shun on specific ASA @@ -120,8 +115,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Remove shun for attacker cisco.sccfm.remove_asa_shun: diff --git a/sccfm-ansible/plugins/modules/remove_network_group_members.py b/sccfm-ansible/plugins/modules/remove_network_group_members.py index 10426ee5..ba96d09c 100644 --- a/sccfm-ansible/plugins/modules/remove_network_group_members.py +++ b/sccfm-ansible/plugins/modules/remove_network_group_members.py @@ -49,19 +49,15 @@ required: true type: list elements: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -74,8 +70,7 @@ referenced_objects: - web-server-01 - web-server-02 - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Remove members by UID - name: Remove members from a network group by UID @@ -91,8 +86,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Remove old web servers from group cisco.sccfm.remove_network_group_members: @@ -183,7 +177,10 @@ def run_module() -> None: ) module.exit_json( changed=False, - msg=f"Network group '{result.network_group.name}' already excludes all requested members.", + msg=( + f"Network group '{result.network_group.name}' already excludes all " + "requested members." + ), network_group=result.network_group.to_dict(), ) return @@ -191,14 +188,20 @@ def run_module() -> None: if result.changed: module.exit_json( changed=True, - msg=f"Successfully removed members from network group '{result.network_group.name}'.", + msg=( + "Successfully removed members from network group " + f"'{result.network_group.name}'." + ), network_group=result.network_group.to_dict(), ) return module.exit_json( changed=False, - msg=f"Network group '{result.network_group.name}' already excludes all requested members.", + msg=( + f"Network group '{result.network_group.name}' already excludes all " + "requested members." + ), network_group=result.network_group.to_dict(), ) except NotFoundError as e: diff --git a/sccfm-ansible/plugins/modules/show_asa_shun.py b/sccfm-ansible/plugins/modules/show_asa_shun.py index eaf5d489..6c586017 100644 --- a/sccfm-ansible/plugins/modules/show_asa_shun.py +++ b/sccfm-ansible/plugins/modules/show_asa_shun.py @@ -26,7 +26,8 @@ - When C(statistics) is set to C(true), executes C(show shun statistics) instead and returns per-interface shun/received counters. - Devices can be selected by a Lucene query or by specifying a list of UIDs. - - See U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/execute-cli-command/) + - See the SCC Firewall Manager API documentation for + U(https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/execute-cli-command/). for API documentation. options: query: @@ -63,19 +64,15 @@ required: false type: int default: 0 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -85,8 +82,7 @@ - name: Show shun entries on production ASAs cisco.sccfm.show_asa_shun: query: "name:prod-* AND connectivityState:ONLINE" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default register: shun_entries # Example 2: Show shun entries on specific devices by UID @@ -109,8 +105,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Show shun entries cisco.sccfm.show_asa_shun: @@ -233,7 +228,10 @@ def run_module() -> None: results = service.view_shun_statistics(device_uids=device_uids) if isinstance(results, CdoTransaction): module.fail_json( - msg=f"Show shun statistics failed with status: {results.cdo_transaction_status}", + msg=( + "Show shun statistics failed with status: " + f"{results.cdo_transaction_status}" + ), transaction_uid=results.transaction_uid, error_message=results.error_message, transaction_details=results.transaction_details, diff --git a/sccfm-ansible/plugins/modules/tests/_module_contract_smoke.py b/sccfm-ansible/plugins/modules/tests/_module_contract_smoke.py index a05b8780..81e6e5fa 100644 --- a/sccfm-ansible/plugins/modules/tests/_module_contract_smoke.py +++ b/sccfm-ansible/plugins/modules/tests/_module_contract_smoke.py @@ -16,5 +16,7 @@ def assert_module_contract(module_name: str) -> None: assert module.RETURN.strip() argument_spec = module.build_argument_spec() - assert "region" in argument_spec - assert "api_token" in argument_spec + assert argument_spec["profile"]["default"] == "default" + assert "config_path" in argument_spec + assert "region" not in argument_spec + assert "api_token" not in argument_spec diff --git a/sccfm-ansible/plugins/modules/tests/conftest.py b/sccfm-ansible/plugins/modules/tests/conftest.py index 86937315..1fa28240 100644 --- a/sccfm-ansible/plugins/modules/tests/conftest.py +++ b/sccfm-ansible/plugins/modules/tests/conftest.py @@ -27,6 +27,11 @@ from pathlib import Path from types import ModuleType +import pytest +from _pytest.monkeypatch import MonkeyPatch + +from cisco_sccfm_core.models.profile import Profile + # Set environment variable that Ansible uses for module argument passing os.environ.setdefault("ANSIBLE_MODULE_ARGS", "{}") @@ -79,3 +84,17 @@ operations_submodule.fields_need_update = operations_module.fields_need_update operations_submodule.__package__ = "plugins.module_utils" sys.modules["plugins.module_utils.operations"] = operations_submodule + + +@pytest.fixture(autouse=True) +def configured_sccfm_profile(monkeypatch: MonkeyPatch) -> None: + """Keep module tests isolated from the user's canonical profile file.""" + monkeypatch.setattr( + config_module.ProfileService, + "load", + lambda _service, profile: Profile( + profile=profile, + region="us", + api_token="test-token-123", + ), + ) diff --git a/sccfm-ansible/plugins/modules/tests/test_add_network_group_members.py b/sccfm-ansible/plugins/modules/tests/test_add_network_group_members.py index cb6f28d3..3b43d458 100644 --- a/sccfm-ansible/plugins/modules/tests/test_add_network_group_members.py +++ b/sccfm-ansible/plugins/modules/tests/test_add_network_group_members.py @@ -4,7 +4,6 @@ from __future__ import annotations -from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -50,8 +49,7 @@ def mock_module_instance() -> MagicMock: "uid": None, "name": "test-network-group", "referenced_objects": ["ref-uid-002"], - "region": "us", - "api_token": "test-token-123", + "profile": "default", } mock_module.check_mode = False mock_module.exit_json.side_effect = SystemExit(0) diff --git a/sccfm-ansible/plugins/modules/tests/test_asa_ha_check.py b/sccfm-ansible/plugins/modules/tests/test_asa_ha_check.py index 3f7d3414..bdc2f844 100644 --- a/sccfm-ansible/plugins/modules/tests/test_asa_ha_check.py +++ b/sccfm-ansible/plugins/modules/tests/test_asa_ha_check.py @@ -11,13 +11,7 @@ import pytest from plugins.modules import asa_ha_check # noqa: E402 -from scc_firewall_manager_sdk import ( - ConfigState, - ConnectivityState, - Device, - DevicePage, - EntityType, -) +from scc_firewall_manager_sdk import ConfigState, ConnectivityState, Device, DevicePage, EntityType from cisco_sccfm_core.models.asa_failover_status import ( AsaFailoverInterface, @@ -85,8 +79,7 @@ def base_module_params_with_uids() -> dict[str, Any]: "uids": [UID_1], "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -97,8 +90,7 @@ def base_module_params_with_query() -> dict[str, Any]: "uids": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_change_asa_boot_image.py b/sccfm-ansible/plugins/modules/tests/test_change_asa_boot_image.py index ea5653e8..011733f2 100644 --- a/sccfm-ansible/plugins/modules/tests/test_change_asa_boot_image.py +++ b/sccfm-ansible/plugins/modules/tests/test_change_asa_boot_image.py @@ -55,8 +55,7 @@ def base_module_params_with_query() -> dict[str, Any]: "image_path": IMAGE_PATH, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -68,8 +67,7 @@ def base_module_params_with_uids() -> dict[str, Any]: "image_path": IMAGE_PATH, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_change_asa_local_password.py b/sccfm-ansible/plugins/modules/tests/test_change_asa_local_password.py index 5b5d6a70..44330858 100644 --- a/sccfm-ansible/plugins/modules/tests/test_change_asa_local_password.py +++ b/sccfm-ansible/plugins/modules/tests/test_change_asa_local_password.py @@ -57,8 +57,7 @@ def base_module_params_with_query() -> dict[str, Any]: "new_password": "NewSecurePass123", "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -72,8 +71,7 @@ def base_module_params_with_uids() -> dict[str, Any]: "new_password": "NewSecurePass123", "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -259,39 +257,3 @@ def test_should_return_structured_error_on_api_exception( call_kwargs = mock_module_instance_query.fail_json.call_args[1] assert call_kwargs["msg"] == "Access denied" assert call_kwargs["error_code"] == "FORBIDDEN" - - -@patch("plugins.modules.change_asa_local_password.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance_query.params["region"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - change_asa_local_password.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.change_asa_local_password.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance_query.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - change_asa_local_password.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] diff --git a/sccfm-ansible/plugins/modules/tests/test_create_access_rule.py b/sccfm-ansible/plugins/modules/tests/test_create_access_rule.py index bb6b57e9..71014cc5 100644 --- a/sccfm-ansible/plugins/modules/tests/test_create_access_rule.py +++ b/sccfm-ansible/plugins/modules/tests/test_create_access_rule.py @@ -43,8 +43,7 @@ def base_module_params() -> dict[str, Any]: "log_level": None, "log_interval": None, "active": True, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -110,8 +109,7 @@ def test_should_create_access_rule_without_optional_fields( "log_level": None, "log_interval": None, "active": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } mock_ansible_module_class.return_value = mock_module_instance @@ -212,40 +210,6 @@ def test_should_fail_if_service_raises_exception( assert "API error: 400" in mock_module_instance.fail_json.call_args[1]["msg"] -@patch("plugins.modules.create_access_rule.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - create_access_rule.run_module() - - mock_module_instance.fail_json.assert_called_once() - assert "region is required" in mock_module_instance.fail_json.call_args[1]["msg"] - - -@patch("plugins.modules.create_access_rule.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - create_access_rule.run_module() - - mock_module_instance.fail_json.assert_called_once() - assert "api_token is required" in mock_module_instance.fail_json.call_args[1]["msg"] - - def test_build_argument_spec() -> None: """build_argument_spec should include all expected keys.""" spec = create_access_rule.build_argument_spec() @@ -256,5 +220,5 @@ def test_build_argument_spec() -> None: assert "source_network" in spec assert "destination_network" in spec assert "protocol" in spec - assert "region" in spec - assert "api_token" in spec + assert "profile" in spec + assert "config_path" in spec diff --git a/sccfm-ansible/plugins/modules/tests/test_create_network_group.py b/sccfm-ansible/plugins/modules/tests/test_create_network_group.py index 339bfa3a..72ced70e 100644 --- a/sccfm-ansible/plugins/modules/tests/test_create_network_group.py +++ b/sccfm-ansible/plugins/modules/tests/test_create_network_group.py @@ -40,8 +40,7 @@ def base_module_params() -> dict[str, Any]: "description": "Test network group description", "labels": ["production", "web"], "tags": {"environment": ["production"]}, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -104,8 +103,7 @@ def test_should_create_network_group_without_optional_fields( "description": None, "labels": None, "tags": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } mock_ansible_module_class.return_value = mock_module_instance @@ -155,42 +153,6 @@ def test_should_fail_if_service_raises_exception( assert "API error: 409 Conflict" in call_kwargs["msg"] -@patch("plugins.modules.create_network_group.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - create_network_group.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.create_network_group.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - create_network_group.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.create_network_group.Config") @patch("plugins.modules.create_network_group.NetworkGroupService") @patch("plugins.modules.create_network_group.AnsibleModule") @@ -242,8 +204,7 @@ def test_should_create_group_with_url_literals( "description": None, "labels": None, "tags": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } mock_ansible_module_class.return_value = mock_module_instance diff --git a/sccfm-ansible/plugins/modules/tests/test_create_network_object.py b/sccfm-ansible/plugins/modules/tests/test_create_network_object.py index d93e6796..254192f8 100644 --- a/sccfm-ansible/plugins/modules/tests/test_create_network_object.py +++ b/sccfm-ansible/plugins/modules/tests/test_create_network_object.py @@ -37,8 +37,7 @@ def base_module_params() -> dict[str, Any]: "description": "Test network object description", "labels": ["production", "web"], "tags": {"environment": ["production"]}, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -99,8 +98,7 @@ def test_should_create_network_object_without_optional_fields( "description": None, "labels": None, "tags": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } mock_ansible_module_class.return_value = mock_module_instance @@ -148,42 +146,6 @@ def test_should_fail_if_service_raises_exception( assert "API error: 409 Conflict" in call_kwargs["msg"] -@patch("plugins.modules.create_network_object.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - create_network_object.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.create_network_object.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - create_network_object.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.create_network_object.Config") @patch("plugins.modules.create_network_object.NetworkObjectService") @patch("plugins.modules.create_network_object.AnsibleModule") diff --git a/sccfm-ansible/plugins/modules/tests/test_delete_access_rule.py b/sccfm-ansible/plugins/modules/tests/test_delete_access_rule.py index 5a6f3b80..0870f10e 100644 --- a/sccfm-ansible/plugins/modules/tests/test_delete_access_rule.py +++ b/sccfm-ansible/plugins/modules/tests/test_delete_access_rule.py @@ -17,8 +17,7 @@ def base_module_params() -> dict[str, Any]: return { "uid": "rule-uid-123", - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -135,5 +134,5 @@ def test_build_argument_spec() -> None: """build_argument_spec should include required keys.""" spec = delete_access_rule.build_argument_spec() assert "uid" in spec - assert "region" in spec - assert "api_token" in spec + assert "profile" in spec + assert "config_path" in spec diff --git a/sccfm-ansible/plugins/modules/tests/test_delete_network_group.py b/sccfm-ansible/plugins/modules/tests/test_delete_network_group.py index d832f912..fefe6931 100644 --- a/sccfm-ansible/plugins/modules/tests/test_delete_network_group.py +++ b/sccfm-ansible/plugins/modules/tests/test_delete_network_group.py @@ -20,8 +20,7 @@ def base_module_params_with_uid() -> dict[str, Any]: return { "uid": "net-grp-uid-123", "name": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -31,8 +30,7 @@ def base_module_params_with_name() -> dict[str, Any]: return { "uid": None, "name": "test-network-group", - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -221,42 +219,6 @@ def test_should_return_structured_api_error( assert call_kwargs["status_code"] == 403 -@patch("plugins.modules.delete_network_group.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - delete_network_group.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.delete_network_group.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - delete_network_group.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.delete_network_group.Config") @patch("plugins.modules.delete_network_group.NetworkGroupService") @patch("plugins.modules.delete_network_group.AnsibleModule") @@ -296,12 +258,11 @@ def test_build_argument_spec() -> None: assert spec["name"]["required"] is False assert spec["name"]["type"] == "str" - assert "region" in spec - assert spec["region"]["required"] is False + assert "profile" in spec + assert spec["profile"]["default"] == "default" - assert "api_token" in spec - assert spec["api_token"]["required"] is False - assert spec["api_token"]["no_log"] is True + assert "config_path" in spec + assert spec["config_path"]["required"] is False @patch("plugins.modules.delete_network_group.Config") diff --git a/sccfm-ansible/plugins/modules/tests/test_delete_network_object.py b/sccfm-ansible/plugins/modules/tests/test_delete_network_object.py index 9d2852b4..0ed543ca 100644 --- a/sccfm-ansible/plugins/modules/tests/test_delete_network_object.py +++ b/sccfm-ansible/plugins/modules/tests/test_delete_network_object.py @@ -20,8 +20,7 @@ def base_module_params_with_uid() -> dict[str, Any]: return { "uid": "net-obj-uid-123", "name": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -31,8 +30,7 @@ def base_module_params_with_name() -> dict[str, Any]: return { "uid": None, "name": "test-network-object", - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -221,42 +219,6 @@ def test_should_return_structured_api_error( assert call_kwargs["status_code"] == 403 -@patch("plugins.modules.delete_network_object.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - delete_network_object.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.delete_network_object.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - delete_network_object.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.delete_network_object.Config") @patch("plugins.modules.delete_network_object.NetworkObjectService") @patch("plugins.modules.delete_network_object.AnsibleModule") @@ -296,12 +258,11 @@ def test_build_argument_spec() -> None: assert spec["name"]["required"] is False assert spec["name"]["type"] == "str" - assert "region" in spec - assert spec["region"]["required"] is False + assert "profile" in spec + assert spec["profile"]["default"] == "default" - assert "api_token" in spec - assert spec["api_token"]["required"] is False - assert spec["api_token"]["no_log"] is True + assert "config_path" in spec + assert spec["config_path"]["required"] is False @patch("plugins.modules.delete_network_object.Config") diff --git a/sccfm-ansible/plugins/modules/tests/test_deploy_cdfmc_ftd.py b/sccfm-ansible/plugins/modules/tests/test_deploy_cdfmc_ftd.py index ff0a8eea..d58ae28c 100644 --- a/sccfm-ansible/plugins/modules/tests/test_deploy_cdfmc_ftd.py +++ b/sccfm-ansible/plugins/modules/tests/test_deploy_cdfmc_ftd.py @@ -46,8 +46,7 @@ def base_params() -> dict[str, Any]: "ignore_warnings": False, "wait": False, "timeout": 3600, - "region": "us", - "api_token": "test-token", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_execute_asa_cli.py b/sccfm-ansible/plugins/modules/tests/test_execute_asa_cli.py index d69a9598..573d1290 100644 --- a/sccfm-ansible/plugins/modules/tests/test_execute_asa_cli.py +++ b/sccfm-ansible/plugins/modules/tests/test_execute_asa_cli.py @@ -57,8 +57,7 @@ def base_module_params_with_query() -> dict[str, Any]: "commands": ["show version", "show running-config"], "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -72,8 +71,7 @@ def base_module_params_with_uids() -> dict[str, Any]: "commands": ["show version"], "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -323,42 +321,6 @@ def test_should_fail_if_inventory_lookup_raises_exception( assert "API unavailable" in call_kwargs["msg"] -@patch("plugins.modules.execute_asa_cli.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance_query.params["region"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - execute_asa_cli.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.execute_asa_cli.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance_query.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - execute_asa_cli.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.execute_asa_cli.Config") @patch("plugins.modules.execute_asa_cli.AsaCommandLineService") @patch("plugins.modules.execute_asa_cli.InventoryService") diff --git a/sccfm-ansible/plugins/modules/tests/test_execute_ftd_cli.py b/sccfm-ansible/plugins/modules/tests/test_execute_ftd_cli.py index 99436f43..966e9f2e 100644 --- a/sccfm-ansible/plugins/modules/tests/test_execute_ftd_cli.py +++ b/sccfm-ansible/plugins/modules/tests/test_execute_ftd_cli.py @@ -62,8 +62,7 @@ def base_module_params_with_query() -> dict[str, Any]: "commands": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -76,8 +75,7 @@ def base_module_params_with_uids() -> dict[str, Any]: "commands": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -270,40 +268,6 @@ def test_should_fail_on_api_exception( mock_module_instance_query.fail_json.assert_called_once() -@patch("plugins.modules.execute_ftd_cli.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - del mock_module_instance_query.params["region"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - execute_ftd_cli.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.execute_ftd_cli.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - del mock_module_instance_query.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - execute_ftd_cli.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.execute_ftd_cli.Config") @patch("plugins.modules.execute_ftd_cli.FtdCommandLineService") @patch("plugins.modules.execute_ftd_cli.InventoryService") diff --git a/sccfm-ansible/plugins/modules/tests/test_get_access_group.py b/sccfm-ansible/plugins/modules/tests/test_get_access_group.py index 79165ef4..8460d40e 100644 --- a/sccfm-ansible/plugins/modules/tests/test_get_access_group.py +++ b/sccfm-ansible/plugins/modules/tests/test_get_access_group.py @@ -27,8 +27,7 @@ def sample_access_group_response() -> MagicMock: def base_module_params() -> dict[str, Any]: return { "uid": "ag-uid-123", - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -123,5 +122,5 @@ def test_build_argument_spec() -> None: """build_argument_spec should include required keys.""" spec = get_access_group.build_argument_spec() assert "uid" in spec - assert "region" in spec - assert "api_token" in spec + assert "profile" in spec + assert "config_path" in spec diff --git a/sccfm-ansible/plugins/modules/tests/test_get_access_rule.py b/sccfm-ansible/plugins/modules/tests/test_get_access_rule.py index f44552a9..58b9d6ee 100644 --- a/sccfm-ansible/plugins/modules/tests/test_get_access_rule.py +++ b/sccfm-ansible/plugins/modules/tests/test_get_access_rule.py @@ -31,8 +31,7 @@ def sample_access_rule_response() -> MagicMock: def base_module_params() -> dict[str, Any]: return { "uid": "rule-uid-123", - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -127,5 +126,5 @@ def test_build_argument_spec() -> None: """build_argument_spec should include required keys.""" spec = get_access_rule.build_argument_spec() assert "uid" in spec - assert "region" in spec - assert "api_token" in spec + assert "profile" in spec + assert "config_path" in spec diff --git a/sccfm-ansible/plugins/modules/tests/test_list_access_groups.py b/sccfm-ansible/plugins/modules/tests/test_list_access_groups.py index 07ed301f..6cfe4e66 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_access_groups.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_access_groups.py @@ -17,8 +17,7 @@ def base_module_params() -> dict[str, Any]: "query": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -128,5 +127,5 @@ def test_build_argument_spec() -> None: assert "query" in spec assert "limit" in spec assert "offset" in spec - assert "region" in spec - assert "api_token" in spec + assert "profile" in spec + assert "config_path" in spec diff --git a/sccfm-ansible/plugins/modules/tests/test_list_access_rules.py b/sccfm-ansible/plugins/modules/tests/test_list_access_rules.py index 31b3b07c..f81d82a0 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_access_rules.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_access_rules.py @@ -17,8 +17,7 @@ def base_module_params() -> dict[str, Any]: "query": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -128,5 +127,5 @@ def test_build_argument_spec() -> None: assert "query" in spec assert "limit" in spec assert "offset" in spec - assert "region" in spec - assert "api_token" in spec + assert "profile" in spec + assert "config_path" in spec diff --git a/sccfm-ansible/plugins/modules/tests/test_list_asa_boot_registry.py b/sccfm-ansible/plugins/modules/tests/test_list_asa_boot_registry.py index e904cb37..68ac4a6d 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_asa_boot_registry.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_asa_boot_registry.py @@ -75,8 +75,7 @@ def query_params() -> dict[str, Any]: "uids": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -87,8 +86,7 @@ def uids_params() -> dict[str, Any]: "uids": ["uid-1", "uid-2"], "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_list_asa_compatible_versions.py b/sccfm-ansible/plugins/modules/tests/test_list_asa_compatible_versions.py index a6a8d6d1..c3343a17 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_asa_compatible_versions.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_asa_compatible_versions.py @@ -54,8 +54,7 @@ def base_module_params_with_query() -> dict[str, Any]: "limit": 50, "offset": 0, "per_device": False, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -70,8 +69,7 @@ def base_module_params_with_uids() -> dict[str, Any]: "limit": 50, "offset": 0, "per_device": False, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -287,39 +285,3 @@ def test_should_include_per_device_when_flag_set( call_kwargs = mock_module_instance_uids.exit_json.call_args[1] assert "common_versions" in call_kwargs assert "per_device" in call_kwargs - - -@patch("plugins.modules.list_asa_compatible_versions.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance_query.params["region"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_asa_compatible_versions.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.list_asa_compatible_versions.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance_query.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_asa_compatible_versions.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] diff --git a/sccfm-ansible/plugins/modules/tests/test_list_asa_local_users.py b/sccfm-ansible/plugins/modules/tests/test_list_asa_local_users.py index 4375d7d8..d7bdf007 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_asa_local_users.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_asa_local_users.py @@ -62,8 +62,7 @@ def query_params() -> dict[str, Any]: "uids": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -74,8 +73,7 @@ def uids_params() -> dict[str, Any]: "uids": ["uid-1", "uid-2"], "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_list_asa_not_on_version.py b/sccfm-ansible/plugins/modules/tests/test_list_asa_not_on_version.py index 9ccf3735..d823322c 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_asa_not_on_version.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_asa_not_on_version.py @@ -58,8 +58,7 @@ def base_params() -> dict[str, Any]: "uids": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -465,41 +464,3 @@ def test_never_reports_changed( # ── Auth validation ─────────────────────────────────────────────── - - -@patch("plugins.modules.list_asa_not_on_version.AnsibleModule") -def test_fails_if_region_not_provided( - mock_ansible_cls: MagicMock, - base_params: dict[str, Any], -) -> None: - """fail_json is called when region is absent and not in env.""" - params = {**base_params} - del params["region"] - mock_module = _mock_module(params) - mock_ansible_cls.return_value = mock_module - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_asa_not_on_version.run_module() - - mock_module.fail_json.assert_called_once() - assert "region is required" in mock_module.fail_json.call_args[1]["msg"] - - -@patch("plugins.modules.list_asa_not_on_version.AnsibleModule") -def test_fails_if_api_token_not_provided( - mock_ansible_cls: MagicMock, - base_params: dict[str, Any], -) -> None: - """fail_json is called when api_token is absent and not in env.""" - params = {**base_params} - del params["api_token"] - mock_module = _mock_module(params) - mock_ansible_cls.return_value = mock_module - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_asa_not_on_version.run_module() - - mock_module.fail_json.assert_called_once() - assert "api_token is required" in mock_module.fail_json.call_args[1]["msg"] diff --git a/sccfm-ansible/plugins/modules/tests/test_list_cdfmc_access_policies.py b/sccfm-ansible/plugins/modules/tests/test_list_cdfmc_access_policies.py index b8b6352e..60756368 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_cdfmc_access_policies.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_cdfmc_access_policies.py @@ -23,8 +23,7 @@ def base_module_params() -> dict[str, Any]: "domain_uid": "domain-1", "limit": 5, "offset": 5, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -120,5 +119,5 @@ def test_build_argument_spec() -> None: assert "domain_uid" in spec assert "limit" in spec assert "offset" in spec - assert "region" in spec - assert "api_token" in spec + assert "profile" in spec + assert "config_path" in spec diff --git a/sccfm-ansible/plugins/modules/tests/test_list_ftd_compatible_versions.py b/sccfm-ansible/plugins/modules/tests/test_list_ftd_compatible_versions.py index d20b216d..c122bc30 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_ftd_compatible_versions.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_ftd_compatible_versions.py @@ -86,8 +86,7 @@ def base_module_params_with_query() -> dict[str, Any]: "limit": 50, "offset": 0, "per_device": False, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -102,8 +101,7 @@ def base_module_params_with_uids() -> dict[str, Any]: "limit": 50, "offset": 0, "per_device": False, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -344,39 +342,3 @@ def test_should_include_skipped_devices_when_present( call_kwargs = mock_module_instance_uids.exit_json.call_args[1] assert call_kwargs["skipped"] == {"skipped-device": "Unsupported device type"} - - -@patch("plugins.modules.list_ftd_compatible_versions.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance_query.params["region"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_ftd_compatible_versions.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.list_ftd_compatible_versions.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance_query: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance_query.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance_query - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_ftd_compatible_versions.run_module() - - mock_module_instance_query.fail_json.assert_called_once() - call_kwargs = mock_module_instance_query.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] diff --git a/sccfm-ansible/plugins/modules/tests/test_list_ftd_not_on_version.py b/sccfm-ansible/plugins/modules/tests/test_list_ftd_not_on_version.py index e889169b..250a8739 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_ftd_not_on_version.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_ftd_not_on_version.py @@ -62,8 +62,7 @@ def base_params() -> dict[str, Any]: "uids": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -605,39 +604,3 @@ def test_never_reports_changed( # ── Auth validation ────────────────────────────────────────────── - - -@patch("plugins.modules.list_ftd_not_on_version.AnsibleModule") -def test_fails_if_region_not_provided( - mock_ansible_cls: MagicMock, - base_params: dict[str, Any], -) -> None: - params = {**base_params} - del params["region"] - mock_module = _mock_module(params) - mock_ansible_cls.return_value = mock_module - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_ftd_not_on_version.run_module() - - mock_module.fail_json.assert_called_once() - assert "region is required" in mock_module.fail_json.call_args[1]["msg"] - - -@patch("plugins.modules.list_ftd_not_on_version.AnsibleModule") -def test_fails_if_api_token_not_provided( - mock_ansible_cls: MagicMock, - base_params: dict[str, Any], -) -> None: - params = {**base_params} - del params["api_token"] - mock_module = _mock_module(params) - mock_ansible_cls.return_value = mock_module - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_ftd_not_on_version.run_module() - - mock_module.fail_json.assert_called_once() - assert "api_token is required" in mock_module.fail_json.call_args[1]["msg"] diff --git a/sccfm-ansible/plugins/modules/tests/test_list_network_groups.py b/sccfm-ansible/plugins/modules/tests/test_list_network_groups.py index 523154c4..9625ca75 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_network_groups.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_network_groups.py @@ -56,8 +56,7 @@ def base_module_params() -> dict[str, Any]: "query": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -116,8 +115,7 @@ def test_should_forward_query_and_pagination( "query": "name:web*", "limit": 10, "offset": 20, - "region": "eu", - "api_token": "test-token-456", + "profile": "default", } mock_ansible_module_class.return_value = mock_module_instance @@ -159,42 +157,6 @@ def test_should_fail_if_service_raises_exception( assert "API error: 500" in call_kwargs["msg"] -@patch("plugins.modules.list_network_groups.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_network_groups.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.list_network_groups.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_network_groups.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.list_network_groups.Config") @patch("plugins.modules.list_network_groups.NetworkGroupService") @patch("plugins.modules.list_network_groups.AnsibleModule") diff --git a/sccfm-ansible/plugins/modules/tests/test_list_network_objects.py b/sccfm-ansible/plugins/modules/tests/test_list_network_objects.py index 9665c263..2c26de20 100644 --- a/sccfm-ansible/plugins/modules/tests/test_list_network_objects.py +++ b/sccfm-ansible/plugins/modules/tests/test_list_network_objects.py @@ -54,8 +54,7 @@ def base_module_params() -> dict[str, Any]: "query": None, "limit": 50, "offset": 0, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -114,8 +113,7 @@ def test_should_forward_query_and_pagination( "query": "name:web*", "limit": 10, "offset": 20, - "region": "eu", - "api_token": "test-token-456", + "profile": "default", } mock_ansible_module_class.return_value = mock_module_instance @@ -157,42 +155,6 @@ def test_should_fail_if_service_raises_exception( assert "API error: 500" in call_kwargs["msg"] -@patch("plugins.modules.list_network_objects.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_network_objects.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.list_network_objects.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - list_network_objects.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.list_network_objects.Config") @patch("plugins.modules.list_network_objects.NetworkObjectService") @patch("plugins.modules.list_network_objects.AnsibleModule") diff --git a/sccfm-ansible/plugins/modules/tests/test_module_utils_config.py b/sccfm-ansible/plugins/modules/tests/test_module_utils_config.py index cf763686..15186d56 100644 --- a/sccfm-ansible/plugins/modules/tests/test_module_utils_config.py +++ b/sccfm-ansible/plugins/modules/tests/test_module_utils_config.py @@ -4,8 +4,15 @@ from __future__ import annotations +from pathlib import Path +from unittest.mock import MagicMock + import pytest -from config import Config +from _pytest.monkeypatch import MonkeyPatch +from config import Config, base_argument_spec, create_config + +from cisco_sccfm_core.models.profile import Profile +from cisco_sccfm_core.services.profile_service import ProfileService def test_config_should_normalize_region_case_and_legacy_aliases() -> None: @@ -17,3 +24,45 @@ def test_config_should_normalize_region_case_and_legacy_aliases() -> None: def test_config_should_reject_unknown_regions() -> None: with pytest.raises(ValueError, match="SCCFM region must be one of"): Config(region="mars", api_token="token-xyz") + + +def test_base_argument_spec_should_only_expose_canonical_profile_options() -> None: + spec = base_argument_spec() + + assert spec == { + "profile": {"type": "str", "required": False, "default": "default"}, + "config_path": {"type": "path", "required": False}, + } + + +def test_create_config_should_load_named_profile(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + config_path = tmp_path / "config.json" + module = MagicMock() + module.params = {"profile": "lab", "config_path": str(config_path)} + monkeypatch.setattr( + ProfileService, + "load", + lambda _service, profile: Profile( + profile=profile, + region="eu", + api_token="profile-token", + ), + ) + + config = create_config(module) + + assert config == Config(region="eu", api_token="profile-token") + module.fail_json.assert_not_called() + + +def test_create_config_should_fail_for_missing_profile( + tmp_path: Path, monkeypatch: MonkeyPatch +) -> None: + module = MagicMock() + module.params = {"profile": "missing", "config_path": str(tmp_path / "config.json")} + monkeypatch.setattr(ProfileService, "load", lambda _service, _profile: None) + + with pytest.raises(ValueError, match="profile 'missing' not found"): + create_config(module) + + assert "sccfm-cli --profile missing configure" in module.fail_json.call_args.kwargs["msg"] diff --git a/sccfm-ansible/plugins/modules/tests/test_onboard_asa.py b/sccfm-ansible/plugins/modules/tests/test_onboard_asa.py index 6f9d8476..ec896a98 100644 --- a/sccfm-ansible/plugins/modules/tests/test_onboard_asa.py +++ b/sccfm-ansible/plugins/modules/tests/test_onboard_asa.py @@ -45,8 +45,7 @@ def base_module_params() -> dict[str, Any]: "ignore_certificate": True, "grouped_labels": {"environment": ["production"]}, "ungrouped_labels": ["asa", "firewall"], - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -183,42 +182,6 @@ def test_should_fail_if_inventory_lookup_raises_exception( assert "API unavailable" in call_kwargs["msg"] -@patch("plugins.modules.onboard_asa.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - onboard_asa.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.onboard_asa.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - onboard_asa.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - @patch("plugins.modules.onboard_asa.Config") @patch("plugins.modules.onboard_asa.AsaOnboardService") @patch("plugins.modules.onboard_asa.InventoryService") @@ -239,7 +202,10 @@ def test_should_return_structured_error_on_api_exception( # Create ApiException with structured JSON body api_error = ApiException(status=400, reason="Bad Request") - api_error.body = '{"errorMsg": "Invalid device address", "errorCode": "VALIDATION_ERROR", "details": {"field": "deviceAddress"}}' + api_error.body = ( + '{"errorMsg": "Invalid device address", "errorCode": "VALIDATION_ERROR", ' + '"details": {"field": "deviceAddress"}}' + ) mock_onboard = MagicMock() mock_onboard.onboard_asa.side_effect = api_error diff --git a/sccfm-ansible/plugins/modules/tests/test_onboard_cdfmc_ftd.py b/sccfm-ansible/plugins/modules/tests/test_onboard_cdfmc_ftd.py index 3e893ab3..e3f6ea8e 100644 --- a/sccfm-ansible/plugins/modules/tests/test_onboard_cdfmc_ftd.py +++ b/sccfm-ansible/plugins/modules/tests/test_onboard_cdfmc_ftd.py @@ -9,13 +9,7 @@ import pytest from plugins.modules import onboard_cdfmc_ftd # noqa: E402 -from scc_firewall_manager_sdk import ( - ConfigState, - ConnectivityState, - Device, - DevicePage, - EntityType, -) +from scc_firewall_manager_sdk import ConfigState, ConnectivityState, Device, DevicePage, EntityType @pytest.fixture @@ -41,8 +35,7 @@ def base_module_params() -> dict[str, Any]: "performance_tier": None, "grouped_labels": {"environment": ["production"]}, "ungrouped_labels": ["branch", "firewall"], - "region": "us", - "api_token": "test-token-123", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_onboard_cdfmc_ftd_ztp.py b/sccfm-ansible/plugins/modules/tests/test_onboard_cdfmc_ftd_ztp.py index 94b09a2a..c3aa1319 100644 --- a/sccfm-ansible/plugins/modules/tests/test_onboard_cdfmc_ftd_ztp.py +++ b/sccfm-ansible/plugins/modules/tests/test_onboard_cdfmc_ftd_ztp.py @@ -31,8 +31,7 @@ def base_module_params() -> dict[str, Any]: "fmc_access_policy_uid": "policy-uid-abc", "admin_password": None, "device_group_uid": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_register_cdfmc_ftd.py b/sccfm-ansible/plugins/modules/tests/test_register_cdfmc_ftd.py index 5b80e05c..29ce87cd 100644 --- a/sccfm-ansible/plugins/modules/tests/test_register_cdfmc_ftd.py +++ b/sccfm-ansible/plugins/modules/tests/test_register_cdfmc_ftd.py @@ -31,8 +31,7 @@ def base_module_params() -> dict[str, Any]: return { "ftd_uid": "cdfmc-ftd-uid-123", "skip_initial_deployment": False, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_remove_network_group_members.py b/sccfm-ansible/plugins/modules/tests/test_remove_network_group_members.py index 8b9b7074..26e00abc 100644 --- a/sccfm-ansible/plugins/modules/tests/test_remove_network_group_members.py +++ b/sccfm-ansible/plugins/modules/tests/test_remove_network_group_members.py @@ -49,8 +49,7 @@ def mock_module_instance() -> MagicMock: "uid": "net-grp-uid-456", "name": None, "referenced_objects": ["ref-uid-001"], - "region": "us", - "api_token": "test-token-123", + "profile": "default", } mock_module.check_mode = False mock_module.exit_json.side_effect = SystemExit(0) diff --git a/sccfm-ansible/plugins/modules/tests/test_trigger_asa_upgrade.py b/sccfm-ansible/plugins/modules/tests/test_trigger_asa_upgrade.py index 4df7f664..4238c082 100644 --- a/sccfm-ansible/plugins/modules/tests/test_trigger_asa_upgrade.py +++ b/sccfm-ansible/plugins/modules/tests/test_trigger_asa_upgrade.py @@ -72,8 +72,7 @@ def base_params() -> dict[str, Any]: "force_upgrade": False, "ignore_maintenance_window": False, "upgrade_name": None, - "region": "us", - "api_token": "test-token", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_trigger_ftd_upgrade.py b/sccfm-ansible/plugins/modules/tests/test_trigger_ftd_upgrade.py index 28d09a48..ed013f8a 100644 --- a/sccfm-ansible/plugins/modules/tests/test_trigger_ftd_upgrade.py +++ b/sccfm-ansible/plugins/modules/tests/test_trigger_ftd_upgrade.py @@ -98,8 +98,7 @@ def base_params() -> dict[str, Any]: "upgrade_name": None, "wait": False, "timeout": 3600, - "region": "us", - "api_token": "test-token", + "profile": "default", } diff --git a/sccfm-ansible/plugins/modules/tests/test_update_access_rule.py b/sccfm-ansible/plugins/modules/tests/test_update_access_rule.py index a2734f74..cebc3f0b 100644 --- a/sccfm-ansible/plugins/modules/tests/test_update_access_rule.py +++ b/sccfm-ansible/plugins/modules/tests/test_update_access_rule.py @@ -42,8 +42,7 @@ def base_module_params() -> dict[str, Any]: "log_level": None, "log_interval": None, "active": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -128,8 +127,7 @@ def test_should_fail_without_update_fields( "log_level": None, "log_interval": None, "active": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } mock_ansible_module_class.return_value = mock_module_instance @@ -173,5 +171,5 @@ def test_build_argument_spec() -> None: assert "source_network" in spec assert "destination_network" in spec assert "protocol" in spec - assert "region" in spec - assert "api_token" in spec + assert "profile" in spec + assert "config_path" in spec diff --git a/sccfm-ansible/plugins/modules/tests/test_update_network_group.py b/sccfm-ansible/plugins/modules/tests/test_update_network_group.py index 02601a1a..c0c71cc6 100644 --- a/sccfm-ansible/plugins/modules/tests/test_update_network_group.py +++ b/sccfm-ansible/plugins/modules/tests/test_update_network_group.py @@ -74,8 +74,7 @@ def base_module_params() -> dict[str, Any]: "description": None, "labels": None, "tags": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -453,47 +452,6 @@ def test_should_fail_on_service_exception( assert "API error: 400 Bad Request" in call_kwargs["msg"] -@patch("plugins.modules.update_network_group.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - update_network_group.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.update_network_group.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - update_network_group.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - -# ============================================================ -# _needs_update unit tests -# ============================================================ - - class TestNeedsUpdate: """Unit tests for the _needs_update helper function.""" diff --git a/sccfm-ansible/plugins/modules/tests/test_update_network_object.py b/sccfm-ansible/plugins/modules/tests/test_update_network_object.py index 35520a90..244d85a0 100644 --- a/sccfm-ansible/plugins/modules/tests/test_update_network_object.py +++ b/sccfm-ansible/plugins/modules/tests/test_update_network_object.py @@ -70,8 +70,7 @@ def base_module_params() -> dict[str, Any]: "description": None, "labels": None, "tags": None, - "region": "us", - "api_token": "test-token-123", + "profile": "default", } @@ -413,47 +412,6 @@ def test_should_fail_on_service_exception( assert "API error: 400 Bad Request" in call_kwargs["msg"] -@patch("plugins.modules.update_network_object.AnsibleModule") -def test_should_fail_if_region_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when region is not provided.""" - del mock_module_instance.params["region"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - update_network_object.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "region is required" in call_kwargs["msg"] - - -@patch("plugins.modules.update_network_object.AnsibleModule") -def test_should_fail_if_api_token_not_provided( - mock_ansible_module_class: MagicMock, - mock_module_instance: MagicMock, -) -> None: - """run_module should fail when api_token is not provided.""" - del mock_module_instance.params["api_token"] - mock_ansible_module_class.return_value = mock_module_instance - - with patch.dict("os.environ", {}, clear=True): - with pytest.raises(SystemExit): - update_network_object.run_module() - - mock_module_instance.fail_json.assert_called_once() - call_kwargs = mock_module_instance.fail_json.call_args[1] - assert "api_token is required" in call_kwargs["msg"] - - -# ============================================================ -# _needs_update unit tests -# ============================================================ - - class TestNeedsUpdate: """Unit tests for the _needs_update helper function.""" diff --git a/sccfm-ansible/plugins/modules/trigger_asa_upgrade.py b/sccfm-ansible/plugins/modules/trigger_asa_upgrade.py index 0973460d..ccb8e2e4 100644 --- a/sccfm-ansible/plugins/modules/trigger_asa_upgrade.py +++ b/sccfm-ansible/plugins/modules/trigger_asa_upgrade.py @@ -7,11 +7,7 @@ from typing import Any, cast from ansible.module_utils.basic import AnsibleModule -from scc_firewall_manager_sdk import ( - ApiException, - CdoTransaction, - DevicePage, -) +from scc_firewall_manager_sdk import ApiException, CdoTransaction, DevicePage from cisco_sccfm_core import ASA_DEVICE_TYPE_FILTER, InventoryService, SccApiError from cisco_sccfm_core.constants import DEFAULT_TRANSACTION_TIMEOUT_SEC @@ -120,19 +116,15 @@ required: false type: int default: 3600 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -145,8 +137,7 @@ - "12345678-1234-1234-1234-123456789abc" software_version: "9.18(4)" asdm_version: "7.18(1.152)" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Stage-only upgrade using a query - name: Stage ASA upgrade for branch devices @@ -178,8 +169,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Stage ASA upgrade for branch devices cisco.sccfm.trigger_asa_upgrade: diff --git a/sccfm-ansible/plugins/modules/trigger_ftd_upgrade.py b/sccfm-ansible/plugins/modules/trigger_ftd_upgrade.py index 567ae8d0..45b0e6a8 100644 --- a/sccfm-ansible/plugins/modules/trigger_ftd_upgrade.py +++ b/sccfm-ansible/plugins/modules/trigger_ftd_upgrade.py @@ -7,12 +7,7 @@ from typing import Any, cast from ansible.module_utils.basic import AnsibleModule -from scc_firewall_manager_sdk import ( - ApiException, - CdoTransaction, - DevicePage, - EntityType, -) +from scc_firewall_manager_sdk import ApiException, CdoTransaction, DevicePage from cisco_sccfm_core import FTD_DEVICE_TYPE_FILTER, InventoryService, SccApiError from cisco_sccfm_core.constants import DEFAULT_TRANSACTION_TIMEOUT_SEC @@ -26,7 +21,7 @@ from cisco_sccfm_core.services.transaction_service import TransactionService from cisco_sccfm_core.types import ConfigLike -from ..module_utils.config import Config, base_argument_spec, create_config +from ..module_utils.config import base_argument_spec, create_config DOCUMENTATION = r""" --- @@ -110,19 +105,15 @@ required: false type: int default: 3600 - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -134,8 +125,7 @@ uids: - "12345678-1234-1234-1234-123456789abc" software_version: "7.4.1" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Stage-only upgrade using a query - name: Stage FTD upgrade for branch devices @@ -159,8 +149,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Stage FTD upgrade for branch devices cisco.sccfm.trigger_ftd_upgrade: diff --git a/sccfm-ansible/plugins/modules/update_access_rule.py b/sccfm-ansible/plugins/modules/update_access_rule.py index d56e6f00..972f1d34 100644 --- a/sccfm-ansible/plugins/modules/update_access_rule.py +++ b/sccfm-ansible/plugins/modules/update_access_rule.py @@ -73,19 +73,15 @@ description: Whether the rule is active. required: false type: bool - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -96,8 +92,7 @@ cisco.sccfm.update_access_rule: uid: "ac981dcd-9860-401e-a51d-c615c946b72f" rule_action: DENY - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Update remark and networks using module_defaults - name: Update access rules @@ -105,8 +100,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Update rule remark and source cisco.sccfm.update_access_rule: diff --git a/sccfm-ansible/plugins/modules/update_network_group.py b/sccfm-ansible/plugins/modules/update_network_group.py index 6253f640..f4c766c5 100644 --- a/sccfm-ansible/plugins/modules/update_network_group.py +++ b/sccfm-ansible/plugins/modules/update_network_group.py @@ -68,19 +68,15 @@ For example, C({"environment": ["production", "staging"]}). required: false type: dict - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -93,8 +89,7 @@ referenced_objects: - web-server-01 - web-server-02 - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Rename a group and update description using module_defaults - name: Update network groups @@ -102,8 +97,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Rename and update group cisco.sccfm.update_network_group: diff --git a/sccfm-ansible/plugins/modules/update_network_object.py b/sccfm-ansible/plugins/modules/update_network_object.py index 3851142e..4002c281 100644 --- a/sccfm-ansible/plugins/modules/update_network_object.py +++ b/sccfm-ansible/plugins/modules/update_network_object.py @@ -67,19 +67,15 @@ For example, C({"environment": ["production", "staging"]}). required: false type: dict - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -90,16 +86,14 @@ cisco.sccfm.update_network_object: uid: "abc-123-def" value: "192.168.1.0/24" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Rename a network object by name - name: Rename a network object cisco.sccfm.update_network_object: name: old-object-name new_name: new-object-name - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 3: Update multiple fields using module_defaults - name: Update network objects @@ -107,8 +101,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Update web server object cisco.sccfm.update_network_object: diff --git a/sccfm-ansible/plugins/modules/update_object_default.py b/sccfm-ansible/plugins/modules/update_object_default.py index a74168b6..0e460f70 100644 --- a/sccfm-ansible/plugins/modules/update_object_default.py +++ b/sccfm-ansible/plugins/modules/update_object_default.py @@ -35,19 +35,15 @@ For URL objects this should be the URL string. required: true type: str - region: - description: SCCFM region (int, us, eu, apj, au, uae, in, or ci). + profile: + description: Named SCCFM profile configured by C(sccfm-cli configure). required: false type: str - env: - - name: SCCFM_REGION - api_token: - description: API token for SCCFM. + default: default + config_path: + description: Optional path to the canonical SCCFM profile configuration file. required: false - type: str - no_log: true - env: - - name: SCCFM_API_TOKEN + type: path author: - Cisco SCCFM Team """ @@ -58,8 +54,7 @@ cisco.sccfm.update_object_default: uid: "abc-123-def" value: "10.10.10.10" - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default # Example 2: Using module_defaults to avoid repeating credentials - name: Update object default values @@ -67,8 +62,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ lookup('env', 'SCCFM_API_TOKEN') }}" + profile: default tasks: - name: Update default value cisco.sccfm.update_object_default: @@ -86,8 +80,7 @@ gather_facts: false module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: default tasks: - name: Update shared default value cisco.sccfm.update_object_default: @@ -155,7 +148,10 @@ def run_module() -> None: result = service.update_default_value(uid=uid, new_value=value) module.exit_json( changed=True, - msg=f"Successfully updated default value of object '{result.name}' to '{result.default_value}'.", + msg=( + f"Successfully updated default value of object '{result.name}' " + f"to '{result.default_value}'." + ), object_default=result.to_dict(), ) except ValueError as e: diff --git a/sccfm-ansible/tests/test_profile_lookup.py b/sccfm-ansible/tests/test_profile_lookup.py new file mode 100644 index 00000000..38fdb78d --- /dev/null +++ b/sccfm-ansible/tests/test_profile_lookup.py @@ -0,0 +1,44 @@ +# Copyright 2026 Cisco Systems, Inc. and its affiliates +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest +from ansible.errors import AnsibleError + +from cisco_sccfm_core.models.profile import Profile +from cisco_sccfm_core.services.profile_service import ProfileService + +_PLUGIN_PATH = Path(__file__).resolve().parent.parent / "plugins" / "lookup" / "profile.py" +_SPEC = importlib.util.spec_from_file_location("sccfm_profile_lookup", _PLUGIN_PATH) +assert _SPEC is not None and _SPEC.loader is not None +_MODULE = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(_MODULE) +LookupModule = _MODULE.LookupModule + + +def _lookup(config_path: Path, field: str = "api_token") -> LookupModule: + lookup = LookupModule() + lookup.set_options = lambda **kwargs: None # type: ignore[method-assign] + lookup.get_option = lambda name: { # type: ignore[method-assign] + "field": field, + "config_path": str(config_path), + }[name] + return lookup + + +def test_should_read_profile_field(tmp_path: Path) -> None: + config_path = tmp_path / "config.json" + ProfileService(config_path).save(Profile(profile="lab", region="eu", api_token="test-token")) + + assert _lookup(config_path).run(["lab"]) == ["test-token"] + assert _lookup(config_path, field="region").run(["lab"]) == ["eu"] + + +def test_should_fail_for_missing_profile(tmp_path: Path) -> None: + with pytest.raises(AnsibleError, match="profile 'missing' not found"): + _lookup(tmp_path / "config.json").run(["missing"]) diff --git a/skills/sccfm-ansible/SKILL.md b/skills/sccfm-ansible/SKILL.md index 09ee5456..098de8fc 100644 --- a/skills/sccfm-ansible/SKILL.md +++ b/skills/sccfm-ansible/SKILL.md @@ -1,7 +1,7 @@ --- name: sccfm-ansible description: Use the cisco.sccfm Ansible collection for SCC Firewall Manager by discovering modules and inventory plugins with ansible-doc at runtime, validating parameters, auth, check mode, and safety before generating or running playbooks. Use for cisco.sccfm Ansible modules, inventory, vault, and playbook workflows. Do NOT use for sccfm-cli commands; use the sccfm-cli skill instead. Do not use for Jira/Confluence work, architecture design, or non-Ansible tasks. -allowed-tools: "Bash(command -v *) Bash(source cisco_sccfm_scripts/activate.sh) Bash(ansible-doc *) Bash(ansible-playbook *) Bash(ansible-inventory *) Bash(ansible-vault *) Bash(ansible-galaxy *) Bash(build-ansible-collection) Bash(devkit *) Bash(jq *) Read Grep Glob Write Edit" +allowed-tools: "Bash(command -v *) Bash(source cisco_sccfm_scripts/activate.sh) Bash(ansible-doc *) Bash(ansible-playbook *) Bash(ansible-inventory *) Bash(ansible-vault *) Bash(ansible-galaxy *) Bash(build-ansible-collection) Bash(sccfm-cli *) Bash(sccfm-cli-interactive *) Bash(jq *) Read Grep Glob Write Edit" --- # SCC Firewall Manager Ansible Collection @@ -32,8 +32,9 @@ respective operations. 3. Never improvise module names, parameters, defaults, target lists, inventory files, vault paths, or output paths. 4. Never ask the user to paste secrets into chat. -5. Use Ansible Vault, environment variables, or existing local variable files - for secrets; never put API tokens or device passwords directly in playbooks. +5. Use the canonical SCCFM profile store for API tokens and Ansible Vault for + playbook-specific secrets such as device passwords. Never put secrets directly + in playbooks. 6. Treat any task as mutating unless `ansible-doc`, examples, and source context prove it is read-only. 7. Use fully qualified collection names, such as `cisco.sccfm.`, in @@ -199,26 +200,26 @@ parameter, example, and return-value knowledge must come from the discovered ### Step C: Verify Credentials Without Exposing Secrets -Use the matched docs to identify credential options. Most modules support -`region` and `api_token`; inventory docs expose their own auth options. +Use the matched docs to identify profile options. SCCFM modules and inventory +use the canonical named profile store shared with `sccfm-cli`. Rules: -1. Prefer `module_defaults: group/cisco.sccfm.all:` for module auth. -2. Prefer Ansible Vault for API tokens and device passwords. -3. Environment variables are acceptable when `ansible-doc` documents them, such - as `SCCFM_REGION` and `SCCFM_API_TOKEN`. +1. Prefer `module_defaults: group/cisco.sccfm.all:` when selecting a non-default profile. +2. Configure SCCFM profiles with `sccfm-cli --profile configure`. +3. Use Ansible Vault for device passwords and other playbook-specific secrets, + never for the SCCFM API token. 4. Never ask for token or password contents in chat. 5. Never print decrypted vault contents. 6. Never write real secrets to tracked files. -7. If credentials are missing, generate the playbook with placeholders or tell - the user which local setup command to run. +7. If credentials are missing, tell the user which local profile configuration + command to run without asking them to paste the token into chat. 8. Use Write/Edit only for non-secret playbook, inventory, vars template, or documentation artifacts. -Use `change-tokens` for local credential setup only when the user explicitly -asks for it. It configures `.env`, CLI profile state, Ansible vars, and encrypted -vault files. +Use `sccfm-cli configure` or the `configure-profile` option in +`sccfm-cli-interactive` for local SCCFM credential setup only when the user +explicitly asks for it. ## Step 1: Match User Intent Conservatively @@ -280,17 +281,16 @@ only through documented Ansible options and discovered inventory variables. ### Auth Pattern -For SCCFM modules, prefer this shape when `region` and `api_token` are supported: +For SCCFM modules, prefer this shape when selecting a non-default profile: ```yaml module_defaults: group/cisco.sccfm.all: - region: "{{ sccfm_region }}" - api_token: "{{ sccfm_api_token }}" + profile: production ``` -Do not repeat `region` or `api_token` inside each task unless the user asks for a -self-contained snippet or the docs require task-local values. +Omit `profile` when using the configured `default` profile. Do not place a region +or SCCFM API token in a task, variable file, environment lookup, or vault. ### Play Targets @@ -475,7 +475,7 @@ When modifying or adding Ansible modules in this repository: 1. Read the matched module source and its tests. 2. Keep all module functions typed. -3. Use `base_argument_spec()` for shared `region` and `api_token` auth. +3. Use `base_argument_spec()` for shared `profile` and `config_path` options. 4. Set `supports_check_mode=True` on every module. 5. Implement a meaningful `module.check_mode` path for mutating modules. 6. Keep secrets marked `no_log=True`. @@ -496,7 +496,8 @@ When modifying or adding Ansible modules in this repository: 1. Never hardcode modules. All module knowledge comes from `ansible-doc`. 2. Never fabricate options. Only use parameters listed in the matched docs. 3. Always use FQCNs. -4. Always protect secrets with Vault, environment lookups, or placeholders. +4. Always protect playbook-specific secrets with Vault or placeholders; keep + SCCFM API tokens in the canonical profile store. 5. Never guess between ambiguous modules, targets, or regions. 6. Never rely on default local output paths for customer data. 7. Never execute mutating automation without the confirmation workflow. diff --git a/skills/sccfm-cli/SKILL.md b/skills/sccfm-cli/SKILL.md index 72ca6648..26da3918 100644 --- a/skills/sccfm-cli/SKILL.md +++ b/skills/sccfm-cli/SKILL.md @@ -147,6 +147,9 @@ Use the selected command's `auth` object: available before executing. - Profiles contain a region and API token. Tokens come from developer.cisco.com or the SCC Firewall Manager UI. +- The canonical profile store is `~/.sccfm-cli/config.json`, shared by + `sccfm-cli`, `sccfm-cli-interactive`, and the `cisco.sccfm` Ansible collection. + Do not configure SCCFM tokens through `.env`, inline Ansible values, or Ansible Vault. #### Secret Handling Rules