Multi-step workflow base class with plan/execute lifecycle and partial-completion reporting. Ansible connector stubbed — ansible-runner integration in future sprint. Credentials resolved per-host at runtime via CredentialResolver, never stored. Signed-off-by: Tyler King <tking@guildhouse.dev>
137 lines
4.8 KiB
Python
137 lines
4.8 KiB
Python
# 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,
|
|
}
|