Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ Commands:
| [prechange](nexus-dashboard/prechange.md) | Analyse a candidate config against current fabric state |
| [delta](nexus-dashboard/delta.md) | Compare two snapshots and report changes |
| [snapshots](nexus-dashboard/snapshots.md) | Resolve a snapshot ID (CI baseline pinning) |
| [analyze](nexus-dashboard/analyze.md) | Trigger an assurance analysis and print the snapshot it produces |
| [compliance](nexus-dashboard/compliance.md) | Report compliance rule status |
| [doctor](nexus-dashboard/doctor.md) | Check connectivity, credentials, fabric visibility |
1 change: 1 addition & 0 deletions docs/commands/nexus-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Product-level help (configuration variables and verb list) is in [Nexus Dashboar
| [prechange](prechange.md) | Gate a planned change — Terraform plan JSON or APIC MO JSON |
| [delta](delta.md) | Post-change comparison between two snapshots |
| [snapshots](snapshots.md) | Print a snapshot ID for pipeline pinning |
| [analyze](analyze.md) | Trigger an assurance analysis and print the snapshot it produces |
| [compliance](compliance.md) | Fabric compliance rules; optional `--fail-on-violations` |
| [doctor](doctor.md) | Read-only connectivity and credential check |

Expand Down
34 changes: 34 additions & 0 deletions docs/commands/nexus-dashboard/analyze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# analyze

[← Nexus Dashboard commands](README.md) · [Command reference](../README.md)

Trigger an assurance analysis on a fabric, wait for it to finish, and print the ID of the snapshot it produced. This is the API behind the GUI's "Analyze now".

Nexus Dashboard collects online fabrics on its own schedule — every 2–4 hours depending on fabric size. A pipeline that pins [`snapshots latest`](snapshots.md) before a change and compares against `latest` afterwards races that schedule: if no collection happened in between, both selectors resolve to the same record and [`delta`](delta.md) reports nothing. `analyze` removes the race by producing a snapshot on demand.

## Examples

```bash
nac-analytics nd analyze
```

Bracket a configuration push with two real snapshots:

```bash
PRE=$(nac-analytics nd analyze)
terraform apply plan.tfplan
POST=$(nac-analytics nd analyze)
nac-analytics nd delta "$PRE" "$POST"
```

Trigger without waiting — prints the **analysis job ID**, which is not a snapshot ID and cannot be passed to `delta`:

```bash
nac-analytics nd analyze --no-wait
```

`-output json` (or `yaml`) emits the whole snapshot record, including the `analysisJobId` that ties it back to the trigger.

## Notes

Run `nac-analytics nd analyze --help` for all flags and options.
2 changes: 2 additions & 0 deletions docs/commands/nexus-dashboard/snapshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Resolve a fabric snapshot selector and print its ID. Use in CI to pin baselines

Selectors: `latest`, `latest-N`, or a concrete `snapshotId`. Combine with `-since` / `-until` when the API's 50-record listing cap requires narrowing the window.

This command only reads what ND has already collected, and ND collects online fabrics every 2–4 hours. To produce a snapshot on demand instead, use [analyze](analyze.md).

## Examples

```bash
Expand Down
2 changes: 2 additions & 0 deletions docs/nexus-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Commands:
prechange Analyse a candidate configuration against a fabric's current state.
delta Compare two snapshots of a fabric and report what changed.
snapshots Resolve a fabric snapshot and print its ID (for CI baseline pinning).
analyze Trigger an assurance analysis and print the snapshot ID it produces.
compliance Report compliance rule status for a fabric (or every fabric with --all).
doctor Check connectivity, credentials, and fabric visibility.
```
Expand All @@ -49,5 +50,6 @@ Commands:
| [prechange](commands/nexus-dashboard/prechange.md) | Gate a planned change — Terraform plan JSON or APIC MO JSON |
| [delta](commands/nexus-dashboard/delta.md) | Post-change comparison between two snapshots |
| [snapshots](commands/nexus-dashboard/snapshots.md) | Print a snapshot ID for pipeline pinning |
| [analyze](commands/nexus-dashboard/analyze.md) | Trigger an assurance analysis and print the snapshot it produces |
| [compliance](commands/nexus-dashboard/compliance.md) | Fabric compliance rules; optional `--fail-on-violations` |
| [doctor](commands/nexus-dashboard/doctor.md) | Read-only connectivity and credential check |
104 changes: 102 additions & 2 deletions nac_analytics/products/nexus_dashboard/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,28 @@ def _emit_snapshot(record: dict[str, object], output: str) -> None:
typer.echo(serialize_structured(record, output))


def _emit_notices(client: NDClient) -> None:
"""Print a client's operational warnings to stderr, keeping stdout clean."""
for warning in client.notices:
typer.secho(f"warning: {warning}", fg=typer.colors.YELLOW, err=True)


def _analysis_trigger_error(exc: ApiError) -> Exception:
"""Explain a refused assurance analysis trigger.

Triggering needs the super-admin, fabric-admin or support-engineer role, so
a 403 is an RBAC fact the operator can act on rather than a transport
failure, and it exits as an auth error.
"""
if "HTTP 403" not in str(exc):
return exc
return AuthError(
"Not permitted to trigger an assurance analysis. This API requires the "
"super-admin, fabric-admin or support-engineer role; an observer "
f"account can read snapshots but cannot start one. ({exc})"
)


def _resolve_pre_post(
pre: str | None,
post: str | None,
Expand Down Expand Up @@ -815,8 +837,86 @@ def snapshots(
record = client.resolve_snapshot(
config.fabric, selector, start_date=since, end_date=until
)
for warning in client.notices:
typer.secho(f"warning: {warning}", fg=typer.colors.YELLOW, err=True)
_emit_notices(client)
_emit_snapshot(record, output)
except Exception as exc:
raise _fail(exc, verbose) from exc


@app.command()
def analyze(
host: HostOpt = None,
username: UserOpt = None,
password: PasswordOpt = None,
domain: DomainOpt = DEFAULT_DOMAIN,
fabric: FabricOpt = None,
no_wait: Annotated[
bool,
typer.Option(
"--no-wait",
help=(
"Print the analysis job ID and exit instead of waiting. This is "
"a job ID, not a snapshotId; it cannot be passed to delta."
),
),
] = False,
output: Annotated[
str,
typer.Option(
"--output",
"-o",
help="Output format: text (snapshot ID only), json, or yaml.",
),
] = "text",
timeout: TimeoutOpt = 30,
poll_interval: PollOpt = 15,
verify_ssl: VerifyOpt = True,
ca_bundle: CaBundleOpt = None,
verbose: VerboseOpt = False,
) -> None:
"""Trigger an assurance analysis and print the snapshot ID it produces."""
_configure_logging(verbose)
try:
if output not in ("text", "json", "yaml"):
raise InputError(
f"Unknown output format '{output}'. Choose from: text, json, yaml."
)
config = _build_config(
host=host,
username=username,
password=password,
domain=domain,
fabric=fabric,
verify_ssl=verify_ssl,
ca_bundle=ca_bundle,
timeout=timeout,
poll_interval=poll_interval,
)
note(_connect_message(config))
with NDClient(config) as client:
client.validate_fabric(config.fabric)
# Taken before triggering so a snapshot that already existed can
# never be mistaken for the one this run produces.
baseline = (
None if no_wait else client.latest_collection_timestamp(config.fabric)
)
note(f"Triggering an assurance analysis on {config.fabric}...")
try:
job_id = client.trigger_assurance_analysis(config.fabric)
except ApiError as exc:
raise _analysis_trigger_error(exc) from exc
if no_wait:
_emit_notices(client)
typer.echo(job_id)
return
note(
f"Waiting for analysis {job_id} to produce a snapshot "
f"(up to {timeout} minutes)..."
)
record = client.wait_for_analysis_snapshot(
config.fabric, job_id, baseline=baseline
)
_emit_notices(client)
_emit_snapshot(record, output)
except Exception as exc:
raise _fail(exc, verbose) from exc
Expand Down
Loading
Loading