mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 07:21:13 +00:00
[AZ-326] [AZ-327] C12 operator-tool CLI + companion SSH bringup
AZ-326 (3pt): operator-tool Click CLI shell at src/gps_denied_onboard/components/c12_operator_tooling/cli.py with six subcommands (download, build-cache, upload-pending, reloc-confirm, verify-ready, set-sector); SectorClassificationStore (atomic-write JSON under ~/.azaion/onboard/sector-classifications.json); freshness-table lookup driving AC-NEW-6; EXIT_* constants; AZ-266 structured-JSON log wiring to a rotating ~/.azaion/onboard/c12-tooling.log handler; operator-tool console-script entry in pyproject.toml. AZ-327 (3pt): CompanionBringup orchestrator at src/gps_denied_onboard/components/c12_operator_tooling/companion_bringup.py that opens an SSH session against the companion (paramiko per project pin), checks the four pre-flight artifacts (Manifest, expected engines, sha256 sidecars, calibration), and returns a ReadinessReport per description.md S2; CompanionUnreachableError + ContentHashMismatchError with operator-friendly remediation hints; ParamikoSshSessionFactory + RemoteSidecarVerifier (sha256sum + cat over SSH, no bytes pulled to the workstation); paramiko>=3.4,<4.0 dep added. NFR-perf-cold-start fix: PEP 562 lazy __getattr__ in c12_operator_tooling/__init__.py and flights_api/__init__.py defers HttpxFlightsApiClient (httpx), ParamikoSshSession[Factory] (paramiko + cryptography), bbox_from_waypoints / takeoff_origin_from_flight (numpy + pyproj). cli.py imports from leaf flights_api modules. operator-tool --help cold start: ~870ms -> <200ms typical, <500ms p99. Includes 73 unit tests (incl. paramiko-version-drift smoke per AZ-327 Risk 1) + console-script integration test. All 1494 repo-wide unit tests pass; 80 skips are pre-existing environment gates. Batch report: _docs/03_implementation/batch_42_cycle1_report.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,37 +1,83 @@
|
||||
"""Composition-root factory for C12 operator-tooling services (AZ-489).
|
||||
"""Composition-root factories for C12 operator-tooling services.
|
||||
|
||||
Currently exposes :func:`build_flights_api_client` — the
|
||||
:class:`FlightsApiClient` used by C12's pre-flight cache-build workflow
|
||||
(see AZ-326 / AZ-328 for downstream consumers).
|
||||
* :func:`build_flights_api_client` — AZ-489 ``FlightsApiClient`` (online +
|
||||
offline path).
|
||||
* :func:`build_sector_classification_store` — AZ-326 persistent area →
|
||||
classification map.
|
||||
* :func:`build_companion_bringup` — AZ-327 SSH-based pre-flight
|
||||
verification of the companion's four artifacts.
|
||||
* :func:`build_operator_tool` — aggregator that returns the
|
||||
:class:`OperatorToolServices` dataclass the AZ-326 CLI consumes.
|
||||
|
||||
The factory is intentionally tiny: there is only one concrete strategy
|
||||
(``HttpxFlightsApiClient``) and httpx already defaults to TLS verify ON
|
||||
and the system trust store, so the factory's job is to assemble the
|
||||
client without re-implementing those defaults.
|
||||
|
||||
The richer ``OperatorToolServices`` dataclass that aggregates this
|
||||
client with the rest of C12 (``CacheBuildWorkflow``,
|
||||
``OperatorReLocService``, etc.) is owned by AZ-328 and intentionally
|
||||
NOT created here per scope discipline.
|
||||
Each ``build_*`` function is intentionally tiny — there is one
|
||||
production strategy per service today and the CLI wiring just plugs
|
||||
the concrete instance into the same composition root method. Sibling
|
||||
tasks AZ-328 / AZ-329 / AZ-330 will each add a single field to
|
||||
:class:`OperatorToolServices` without renaming or moving the
|
||||
dataclass.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from gps_denied_onboard.components.c12_operator_tooling.companion_bringup import (
|
||||
CompanionBringup,
|
||||
)
|
||||
from gps_denied_onboard.components.c12_operator_tooling.flights_api import (
|
||||
FlightsApiClient,
|
||||
HttpxFlightsApiClient,
|
||||
)
|
||||
from gps_denied_onboard.components.c12_operator_tooling.paramiko_ssh_session import (
|
||||
ParamikoSshSessionFactory,
|
||||
)
|
||||
from gps_denied_onboard.components.c12_operator_tooling.remote_sidecar_verifier import (
|
||||
RemoteSidecarVerifier,
|
||||
)
|
||||
from gps_denied_onboard.components.c12_operator_tooling.sector_classification_store import (
|
||||
SectorClassificationStore,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gps_denied_onboard.components.c12_operator_tooling.config import (
|
||||
C12Config,
|
||||
)
|
||||
from gps_denied_onboard.config import Config
|
||||
|
||||
__all__ = ["build_flights_api_client"]
|
||||
__all__ = [
|
||||
"OperatorToolServices",
|
||||
"build_companion_bringup",
|
||||
"build_flights_api_client",
|
||||
"build_operator_tool",
|
||||
"build_sector_classification_store",
|
||||
]
|
||||
|
||||
|
||||
_C12_LOGGER_NAME = "c12_operator_tooling"
|
||||
_COMPANION_LOGGER_NAME = "c12_operator_tooling.companion_bringup"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OperatorToolServices:
|
||||
"""Aggregated service handles the operator-tool CLI consumes (AZ-326).
|
||||
|
||||
AZ-326 introduced the dataclass and now owns three services
|
||||
(``flights_api_client``, ``sector_classification_store``,
|
||||
``companion_bringup``). Sibling tasks AZ-328 (orchestrator),
|
||||
AZ-329 (post-landing upload), and AZ-330 (operator reloc service)
|
||||
extend this dataclass in-place by appending their own service
|
||||
field — they MUST NOT rename, move, or split it.
|
||||
"""
|
||||
|
||||
flights_api_client: FlightsApiClient
|
||||
sector_classification_store: SectorClassificationStore
|
||||
companion_bringup: CompanionBringup
|
||||
|
||||
|
||||
def build_flights_api_client(config: Config) -> FlightsApiClient:
|
||||
"""Return the operator-tier :class:`FlightsApiClient`.
|
||||
"""Return the operator-tier :class:`FlightsApiClient` (AZ-489).
|
||||
|
||||
The current implementation is the production
|
||||
:class:`HttpxFlightsApiClient` with httpx defaults (TLS verify ON,
|
||||
@@ -42,3 +88,68 @@ def build_flights_api_client(config: Config) -> FlightsApiClient:
|
||||
"""
|
||||
_ = config # reserved for future composition-time tuning
|
||||
return HttpxFlightsApiClient()
|
||||
|
||||
|
||||
def build_sector_classification_store(
|
||||
config: Config, *, logger: logging.Logger | None = None
|
||||
) -> SectorClassificationStore:
|
||||
"""Build the AZ-326 :class:`SectorClassificationStore` from config."""
|
||||
c12_config = _resolve_c12_config(config)
|
||||
return SectorClassificationStore(
|
||||
store_path=c12_config.sector_classification_store_path,
|
||||
logger=logger or logging.getLogger(_C12_LOGGER_NAME),
|
||||
)
|
||||
|
||||
|
||||
def build_companion_bringup(
|
||||
config: Config,
|
||||
*,
|
||||
logger: logging.Logger | None = None,
|
||||
) -> CompanionBringup:
|
||||
"""Build the AZ-327 :class:`CompanionBringup` from config."""
|
||||
from gps_denied_onboard.config.schema import ConfigError
|
||||
|
||||
c12_config = _resolve_c12_config(config)
|
||||
companion = c12_config.companion
|
||||
if not str(companion.ssh_keyfile):
|
||||
raise ConfigError(
|
||||
"C12CompanionConfig.ssh_keyfile is empty; operator wiring requires "
|
||||
"a real SSH private key path"
|
||||
)
|
||||
factory = ParamikoSshSessionFactory(
|
||||
ssh_user=companion.ssh_user,
|
||||
ssh_keyfile=companion.ssh_keyfile,
|
||||
host_key_policy=companion.host_key_policy,
|
||||
)
|
||||
verifier = RemoteSidecarVerifier(timeout_s=companion.sha256sum_timeout_s)
|
||||
return CompanionBringup(
|
||||
ssh_factory=factory,
|
||||
sidecar_verifier=verifier,
|
||||
logger=logger or logging.getLogger(_COMPANION_LOGGER_NAME),
|
||||
config=companion,
|
||||
)
|
||||
|
||||
|
||||
def build_operator_tool(config: Config) -> OperatorToolServices:
|
||||
"""Aggregate the three AZ-326 / AZ-327 / AZ-489 service handles."""
|
||||
return OperatorToolServices(
|
||||
flights_api_client=build_flights_api_client(config),
|
||||
sector_classification_store=build_sector_classification_store(config),
|
||||
companion_bringup=build_companion_bringup(config),
|
||||
)
|
||||
|
||||
|
||||
def _resolve_c12_config(config: Config) -> C12Config:
|
||||
from gps_denied_onboard.components.c12_operator_tooling.config import (
|
||||
C12Config,
|
||||
)
|
||||
|
||||
block = config.components.get("c12_operator_tooling")
|
||||
if block is None:
|
||||
return C12Config()
|
||||
if not isinstance(block, C12Config):
|
||||
raise TypeError(
|
||||
"config.components['c12_operator_tooling'] must be a C12Config; got "
|
||||
f"{type(block).__name__}"
|
||||
)
|
||||
return block
|
||||
|
||||
Reference in New Issue
Block a user