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
5 changes: 5 additions & 0 deletions .changeset/0009-performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pan-scm-cli": minor
---

Major performance improvements. CLI startup (`scm --help`, `scm --version`) dropped from ~0.42s to ~0.11s via lazy command loading — modules import only when their command is dispatched. OAuth tokens are now cached per context in `~/.scm-cli/cache/` (0600 permissions), eliminating the token + signing-key roundtrips on every invocation; the cache auto-invalidates on rejection or credential change (disable with `SCM_NO_TOKEN_CACHE=1`). Bulk `scm load` commands now apply objects concurrently (5 workers, tune with `SCM_BULK_WORKERS`); position-sensitive rule types still load sequentially to preserve order.
27 changes: 27 additions & 0 deletions docs-site/docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,33 @@ scm context test staging --mock
scm context test
```

### Token Cache

OAuth tokens are cached per context in `~/.scm-cli/cache/` (file mode 0600) and
reused until shortly before expiry, so consecutive commands skip the token and
signing-key roundtrips. The cache is invalidated automatically when the API
rejects a cached token or when the cached credentials no longer match the
active context.

```bash
# Disable the token cache entirely
export SCM_NO_TOKEN_CACHE=1

# Clear it manually
rm -rf ~/.scm-cli/cache
```

### Bulk Load Concurrency

`scm load ...` commands apply objects concurrently (default 5 workers).
Position-sensitive types (security/NAT/PBF/QoS rules) always load sequentially
to preserve rule order.

```bash
# Tune or serialize bulk loads
export SCM_BULK_WORKERS=1
```

### Context Storage

Contexts are stored in `~/.scm-cli/contexts/` as individual YAML files:
Expand Down
11 changes: 8 additions & 3 deletions src/scm_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""pan-scm-cli: CLI for Palo Alto Networks Strata Cloud Manager."""

from importlib.metadata import version

__version__ = version("pan-scm-cli")
def __getattr__(name: str):
"""Resolve __version__ lazily — importlib.metadata costs ~30ms at import."""
if name == "__version__":
from importlib.metadata import version

from .main import app
return version("pan-scm-cli")
raise AttributeError(name)


def main():
"""Entry point for the scm command."""
from .main import app

app()
55 changes: 33 additions & 22 deletions src/scm_cli/commands/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import typer
import yaml

from ..utils.bulk import run_bulk
from ..utils.config import load_from_yaml
from ..utils.decorators import handle_command_errors
from ..utils.output import OUTPUT_OPTION, OutputFormat, emit, error, info, success
Expand Down Expand Up @@ -163,24 +164,28 @@ def load_bandwidth_allocation(
typer.echo(yaml.dump(config["bandwidth_allocations"]))
return None

# Apply each allocation
results = []
for allocation_data in config["bandwidth_allocations"]:
def _apply(allocation_data: dict):
# Extract description before validation since it's not in the model
description = allocation_data.pop("description", "")

# Validate using the Pydantic model
allocation = BandwidthAllocation(**allocation_data)

# Call the SDK client to create the bandwidth allocation
result = scm_client.create_bandwidth_allocation(
return scm_client.create_bandwidth_allocation(
name=allocation.name,
bandwidth=allocation.bandwidth,
spn_name_list=allocation.spn_name_list,
description=description,
tags=allocation.tags,
)

# Apply each allocation concurrently, reporting outcomes in input order
results = []
for _allocation_data, result, exc in run_bulk(config["bandwidth_allocations"], _apply):
if exc is not None:
raise exc # abort on first error, matching the previous sequential loop

results.append(result)
# Output details about each allocation
bandwidth_value = result.get("allocated_bandwidth", result.get("bandwidth", "N/A"))
Expand Down Expand Up @@ -403,17 +408,18 @@ def load_service_connection(
typer.echo(yaml.dump(config["service_connections"]))
return None

# Apply each connection
results = []
for connection_data in config["service_connections"]:
def _apply(connection_data: dict):
# Validate using the Pydantic model
connection = ServiceConnection(**connection_data)

# Convert to SDK model format
sdk_data = connection.to_sdk_model()

# Call the SDK client to create the service connection
result = scm_client.create_service_connection(**sdk_data)
return scm_client.create_service_connection(**connection.to_sdk_model())

# Apply each connection concurrently, reporting outcomes in input order
results = []
for _connection_data, result, exc in run_bulk(config["service_connections"], _apply):
if exc is not None:
raise exc # abort on first error, matching the previous sequential loop

results.append(result)
# Show appropriate message based on action taken
Expand Down Expand Up @@ -660,17 +666,18 @@ def load_remote_network(
typer.echo(yaml.dump(config["remote_networks"]))
return None

# Apply each network
results = []
for network_data in config["remote_networks"]:
def _apply(network_data: dict):
# Validate using the Pydantic model
network = RemoteNetwork(**network_data)

# Convert to SDK model format
sdk_data = network.to_sdk_model()

# Call the SDK client to create the remote network
result = scm_client.create_remote_network(**sdk_data)
return scm_client.create_remote_network(**network.to_sdk_model())

# Apply each network concurrently, reporting outcomes in input order
results = []
for _network_data, result, exc in run_bulk(config["remote_networks"], _apply):
if exc is not None:
raise exc # abort on first error, matching the previous sequential loop

results.append(result)
# Show appropriate message based on action taken
Expand Down Expand Up @@ -1016,11 +1023,15 @@ def load_internal_dns_server(
typer.echo(yaml.dump(config["internal_dns_servers"]))
return None

results = []
for server_data in config["internal_dns_servers"]:
def _apply(server_data: dict):
server = InternalDNSServer(**server_data)
sdk_data = server.to_sdk_model()
result = scm_client.create_internal_dns_server(**sdk_data)
return scm_client.create_internal_dns_server(**server.to_sdk_model())

# Apply each server concurrently, reporting outcomes in input order
results = []
for _server_data, result, exc in run_bulk(config["internal_dns_servers"], _apply):
if exc is not None:
raise exc # abort on first error, matching the previous sequential loop

results.append(result)
action = result.get("__action__", "created")
Expand Down
Loading
Loading