# Copyright 2026 Guildhouse Dev # SPDX-License-Identifier: Apache-2.0 """Ansible connector — fleet management via Ansible playbooks. The Ansible connector is an OrchestratorConnector: it plans a sequence of steps (playbook runs, ad-hoc commands, fact collection) and executes them via ansible-runner. Credential model: Ansible does NOT receive credentials from the broker. Instead, the connector passes a credential callback that ansible-runner's credential plugin calls at runtime. This callback resolves credentials via the broker's CredentialResolver for each target host at the moment the task runs — not at planning time. This means: - The Ansible inventory file contains NO passwords or keys. - Each host's credentials are resolved just-in-time from the secrets backend (Entra, Vault, etc). - If a credential expires mid-playbook, the next task for that host will resolve a fresh credential. Real integration: Library: ``ansible-runner`` (execution library) Auth: Per-host credential resolution via custom credential plugin Inventory: Dynamic inventory from Intune device cache or Bascule fleet registry Stubbed in this sprint — steps return placeholder results. """ from __future__ import annotations from typing import Any from gsap_broker.connectors.base import ConnectorContext from gsap_broker.connectors.orchestrator import ( OrchestratorConnector, WorkflowPlan, WorkflowStep, ) from gsap_broker.credentials.resolver import CredentialResolver class AnsibleConnector(OrchestratorConnector): """Fleet management via Ansible playbooks.""" connector_id = "ansible" corpus_entry_cid = "sha256:ansible-connector-v1" capability_mask = 0x7 # READ | PROPOSE | MUTATE declared_endpoints = ["ansible://*"] accord_template = "fleet-management" gsap_required = True chronicle_enabled = True def __init__(self, credential_resolver: CredentialResolver): super().__init__(credential_resolver) async def plan( self, operation: str, parameters: dict[str, Any], context: ConnectorContext ) -> WorkflowPlan: """Map operations to workflow plans. Operations: "playbook" → single step running the named playbook "adhoc" → single step running a module on targets "collect" → single step gathering facts "role" → single step applying a role """ targets = parameters.get("targets", []) if isinstance(targets, str): targets = [targets] if operation == "playbook": return WorkflowPlan(steps=[ WorkflowStep( name=f"playbook:{parameters.get('playbook', 'site.yml')}", command=parameters.get("playbook", "site.yml"), targets=targets, extra_vars=parameters.get("extra_vars", {}), ) ]) if operation == "adhoc": return WorkflowPlan(steps=[ WorkflowStep( name=f"adhoc:{parameters.get('module', 'ping')}", command=parameters.get("module", "ping"), targets=targets, extra_vars=parameters.get("args", {}), ) ]) if operation == "collect": return WorkflowPlan(steps=[ WorkflowStep( name="collect:facts", command="setup", targets=targets, required=False, # fact collection is best-effort ) ]) if operation == "role": return WorkflowPlan(steps=[ WorkflowStep( name=f"role:{parameters.get('role', '')}", command=parameters.get("role", ""), targets=targets, extra_vars=parameters.get("extra_vars", {}), ) ]) return WorkflowPlan(steps=[ WorkflowStep(name=f"unknown:{operation}", command=operation, targets=targets) ]) async def execute_step( self, step: WorkflowStep, context: ConnectorContext ) -> dict[str, Any]: """Execute a single workflow step via ansible-runner. Stubbed — actual ansible-runner integration in a future sprint. """ # TODO: use ansible_runner.run() with: # - playbook=step.command (for playbook operation) # - module=step.command, module_args=step.extra_vars (for adhoc) # - inventory from dynamic source (Intune cache, Bascule fleet) # - credential plugin that calls self._resolver.resolve() # per-host at runtime return { "success": True, "stub": True, "step": step.name, "targets": step.targets, }