playbook

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: BSD-3-Clause Imports: 18 Imported by: 0

README

playbook

Playbook/role/task/handler engine: loops, conditionals, blocks, strategies.

Part of go-ansible — a pure-Go (CGO=0), functional-parity port of Ansible.

CI Go Reference License

Usage

data, err := os.ReadFile("site.yml")
pb, err := playbook.Parse(data)

eng := playbook.New(inv) // inv: a *github.com/go-ansible/inventory.Inventory
result, err := eng.RunPlaybook(ctx, pb)
if result.Failed() {
    // per-host summaries are in result.Hosts
}

Ties inventory+vars+template+modules+facts together with real per-host linear-strategy execution: when/loop/register, block/rescue/always with per-host recovery, notify/handlers, become, and pre_tasks/post_tasks ordering. Some playbook keys are parsed but not yet wired into the engine (roles, tags, serial, delegate_to, vars_files, include_tasks/import_tasks) — see the org's feature matrix for the current, re-checked status of each.

Documentation

Overview

Package playbook implements Ansible's playbook execution model: plays over an inventory pattern, tasks with when/loop/register/notify/ block-rescue-always, and handlers — wired to github.com/go-ansible/{inventory,vars,template,modules,facts} and github.com/go-remoteexec/transport.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConfigFilePath added in v0.4.1

func ConfigFilePath() string

ConfigFilePath returns the ansible.cfg go-ansible would read from — see findConfigFile — or "" if none of the usual locations has one. go-ansible/cli's ansible-config uses this for `view` and to report each setting's real origin (env var / config file / compiled default) in `dump`, rather than guessing from whether the resolved value merely differs from the default.

func ConfigFileValueForEnv added in v0.4.1

func ConfigFileValueForEnv(envVar string) (string, bool)

ConfigFileValueForEnv reports whether the ansible.cfg go-ansible would read from actually sets the [defaults] key matching envVar (e.g. "ANSIBLE_REMOTE_USER" -> "remote_user") — go-ansible/cli's ansible-config dump uses this to report a setting's real origin rather than guessing "cfg" just because some config file happens to exist, whether or not it sets that particular key.

func DefaultConnect

func DefaultConnect(ctx context.Context, hostName string, hostVars map[string]any) (remoteexec.Connection, error)

DefaultConnect implements Ansible's connection-variable conventions: ansible_connection ("local" or "ssh", default "ssh" — "smart" is treated as ssh, this package has no separate paramiko/openssh split to be smart about), ansible_host, ansible_port (default 22), ansible_user, ansible_password / ansible_ssh_pass, ansible_ssh_private_key_file, ansible_host_key_checking, ansible_ssh_timeout. ansible_ssh_common_args is not implemented (OpenSSH-specific flags have no equivalent in a Go SSH client). "localhost"/"127.0.0.1" (by hostName, unless ansible_connection overrides it) use Local.

Three of these settings — remote_user, host_key_checking, timeout — match real Ansible's own config precedence: an inventory/host var wins if set, otherwise an ANSIBLE_* environment variable if set, otherwise ansible.cfg's [defaults] section (see configFileValue) if it sets the matching key, otherwise the compiled-in default below. (forks is a fourth setting on the same precedence, minus the host-var layer — see Engine.Forks/ConfigDefaults — since it's a global concurrency cap, not a per-host connection detail, so it doesn't belong in this function's own resolution chain.) These four settings are the full extent of this port's ansible.cfg support — no other section or key is read at all (a real, stated gap — see go-ansible/cli's ansible-config, which reports exactly this precedence and these four settings, nothing more). Unlike real Ansible's lenient boolean parsing (yes/no/on/off/1/0/true/false, case-insensitive), ANSIBLE_HOST_KEY_CHECKING here is parsed with Go's strconv.ParseBool (true/false/1/0/t/f, case-sensitive on the word forms) — an invalid value falls back to the compiled-in default rather than erroring, matching how an unset var behaves.

Types

type ConfigSetting added in v0.2.7

type ConfigSetting struct {
	Name    string
	EnvVar  string
	Default string
	Current string
}

ConfigSetting is one entry of ConfigDefaults' report: a named setting, its ANSIBLE_* environment variable, its compiled-in default, and its current effective value (env var if set and valid, else the default) — the same precedence DefaultConnect itself uses, minus the inventory/host-var layer (which needs a specific host to resolve against, and so isn't part of this global view).

func ConfigDefaults added in v0.2.7

func ConfigDefaults() []ConfigSetting

ConfigDefaults reports every setting this package honors from the environment and ansible.cfg's [defaults] section — go-ansible/cli's ansible-config is a thin printer over this. This is the full list: go-ansible reads no other ansible.cfg section or key, and no other ANSIBLE_* variable, anywhere in the org.

type Connector

type Connector func(ctx context.Context, hostName string, hostVars map[string]any) (remoteexec.Connection, error)

Connector dials a connection to a host, given its name and fully merged variables (before task-level rendering — this only needs the ansible_* connection variables, which are ordinary inventory/group vars).

type Engine

type Engine struct {
	Inventory *inventory.Inventory
	Modules   *modules.Registry
	Template  *template.Engine
	ExtraVars map[string]any
	Connect   Connector

	// BaseDir resolves include_vars' file argument at run time (every
	// other file-referencing directive — vars_files, roles,
	// include_tasks/import_tasks, include_role/import_role — resolves
	// at parse time via ParseFile's directory instead). Defaults to "."
	// when unset, matching Parse.
	BaseDir string

	// RunTags/SkipTags filter which tasks execute, matching
	// ansible-playbook's --tags/--skip-tags: a task runs if its
	// effective tag set (its own tags unioned with every enclosing
	// block's and the play's, computed once at parse time — see
	// propagateTags) intersects RunTags, or RunTags is empty, or the
	// task carries the "always" tag; and does not intersect SkipTags.
	// Filtered-out tasks are reported Skipped (real Ansible omits them
	// from output entirely — this port reports them instead, for
	// visibility). Tag filtering does not apply to handlers.
	RunTags  []string
	SkipTags []string

	// OnResult, if set, is called synchronously as each task result is
	// produced (from whichever goroutine ran that host's task) — for
	// live progress reporting. It must not block or panic; callers
	// wanting ordering should serialize themselves.
	OnResult func(Result)

	// Forks caps how many hosts run concurrently at once — connecting/
	// gathering facts, and each task/handler fan-out (runSingleTask,
	// runFree) all respect it — matching real Ansible's own forks
	// setting (default 5; New sets this field to 5 for the same
	// reason). 0 or negative means unlimited concurrency, which was
	// this port's only behavior before Forks existed; New's default
	// changes that for anyone constructing an Engine the normal way,
	// but a caller that explicitly wants the old unbounded behavior can
	// still set Forks to 0 after New returns.
	Forks int

	// Prompt implements vars_prompt's actual interactive prompting:
	// given the fully-formatted message (already combining the prompt
	// text and "[default]" the way real Ansible's own do_var_prompt
	// does) and whether the input should be hidden, it returns what the
	// user entered. New's default (defaultPrompt) is a plain
	// bufio-over-os.Stdin read with no terminal awareness at all — it
	// works the same whether stdin is a real terminal or a pipe (never
	// hides input, and never detects a non-interactive session the way
	// real Ansible does to skip prompting and warn instead), which
	// matches a raw library caller or a piped/test invocation but not
	// real Ansible's actual interactive behavior. go-ansible/cli's
	// ansible-playbook overrides this with a real, terminal-aware
	// implementation (golang.org/x/term) for actual interactive use.
	Prompt func(msg string, private bool) (string, error)
}

Engine runs playbooks against an inventory. The zero value is not usable — use New.

func New

func New(inv *inventory.Inventory) *Engine

New returns an Engine with the built-in module registry, a fresh template engine, and DefaultConnect.

func (*Engine) RunPlaybook

func (e *Engine) RunPlaybook(ctx context.Context, pb Playbook) (*RunResult, error)

RunPlaybook runs every play in pb in order.

type HostSummary

type HostSummary struct {
	Ok, Changed, Failed, Skipped int
}

Summary counts changed/failed/skipped/ok results across the whole run, keyed by host — Ansible's PLAY RECAP.

type Play

type Play struct {
	Name         string
	Hosts        string
	GatherFacts  bool // default true
	Become       bool
	BecomeUser   string
	BecomeMethod string
	Vars         map[string]any
	VarsFiles    []string // paths, resolved relative to the playbook's directory
	Tasks        []Task
	Handlers     []Task
	Tags         []string
	Serial       int // 0 means "all hosts at once" (linear strategy default)
	Roles        []RoleRef

	// Strategy is "linear" (the default: every host finishes task N
	// before any host starts task N+1) or "free" (each host runs its
	// entire task list, and its own notified handlers, independently —
	// a slow host never holds back a fast one). Any other named
	// strategy real Ansible supports (debug, host_pinned, or a
	// strategy plugin) is rejected at parse time rather than silently
	// treated as linear.
	Strategy string

	// VarsPrompt, resolved once per play (not per host) before its
	// tasks run — see Engine.applyVarsPrompt — into an ordinary play
	// var, same scope as Vars. Real Ansible only accepts a list of
	// maps here, never a bare-string shorthand (confirmed from
	// ansible-core's own Play._load_vars_prompt/preprocess_vars: an
	// item that isn't a mapping raises a parse error there too).
	VarsPrompt []VarPrompt
}

Play runs a set of tasks against a pattern of inventory hosts.

type PlayResult

type PlayResult struct {
	Play    string
	Results []Result
	// contains filtered or unexported fields
}

PlayResult aggregates every Result from one play, in the order record was called — not necessarily task-list order, since the engine runs one goroutine per active host and every host's goroutine calls record on the same *PlayResult concurrently. mu is a pointer (not an embedded sync.Mutex) specifically so a PlayResult can still be copied by value — as RunPlaybook does, appending *pr into RunResult.Plays — without go vet's copylocks check firing; every copy keeps pointing at the one real mutex.

type Playbook

type Playbook []Play

Playbook is an ordered list of plays, as ansible-playbook reads it.

func Parse

func Parse(data []byte) (Playbook, error)

Parse parses a playbook YAML document (a top-level list of plays). File-referencing directives (vars_files, roles, include_tasks, import_tasks, include_role, import_role) resolve their paths relative to the current working directory — use ParseFile when the playbook lives elsewhere and its includes should resolve relative to it.

func ParseFile added in v0.2.0

func ParseFile(path string) (Playbook, error)

ParseFile reads and parses the playbook at path, resolving every file-referencing directive relative to path's directory (matching ansible-playbook, which resolves roles/ and included files relative to the playbook file, not the current working directory).

type Result

type Result struct {
	Host    string
	Task    string
	Module  string
	Changed bool
	Failed  bool
	Skipped bool
	Msg     string
	Extra   map[string]any
}

Result is one task's outcome on one host (one per loop iteration when a task loops).

type RoleRef added in v0.2.0

type RoleRef struct {
	Name string
	Vars map[string]any
}

RoleRef is one entry of a play's roles: list.

type RunResult

type RunResult struct {
	Plays []PlayResult
}

RunResult aggregates every play's results, in playbook order.

func (*RunResult) Failed

func (rr *RunResult) Failed() bool

Failed reports whether any result in the run failed (and was not subsequently rescued — a rescued failure still appears here, since Result records history, not final host status; check Ok for that question instead).

func (*RunResult) Summary

func (rr *RunResult) Summary() map[string]*HostSummary

type Task

type Task struct {
	Name         string
	Module       string
	Args         map[string]any
	When         string // Jinja2 expression, already normalized from a string or []string
	Loop         any    // a literal list, or a "{{ expr }}" string rendered at run time
	LoopVar      string // default "item"
	Register     string
	IgnoreErrors bool
	ChangedWhen  string
	FailedWhen   string
	Tags         []string
	Become       *bool // nil means "inherit the play's setting"
	BecomeUser   string
	Notify       []string
	Vars         map[string]any
	DelegateTo   string

	// Until/Retries/Delay implement the task retry loop: real Ansible
	// runs the task 1+Retries times (Retries nil, the "unset" state,
	// means no retry loop at all UNLESS Until is non-empty, in which
	// case real Ansible defaults Retries to 3 — mirrored in
	// runTaskOnHost, not here, since it needs to distinguish "Retries
	// explicitly 0" from "Retries unset"), re-checking Until (or, if
	// Until is empty but Retries was explicitly set, "not failed")
	// after each attempt and sleeping Delay seconds before the next one
	// if it didn't pass. See runTaskOnHost's retry loop for the exact
	// attempt-counting algorithm, including a real, deliberately
	// reproduced quirk in ansible-core's own retry loop.
	Until   string
	Retries *int
	Delay   float64

	// RunOnce restricts execution to the first currently-active host in
	// a runSingleTask call, broadcasting any Register result to every
	// other active host afterward (so a later task on ANY host can
	// still read it by bare name) — matching real Ansible's own
	// run_once, including its result-sharing, verified against a real
	// ansible-playbook run. A real, narrower limitation under strategy:
	// free: each host there calls runSingleTask with itself as the only
	// active host (see runFree), so there is no cross-host "first one"
	// to restrict to — every host still runs its own copy. Real Ansible
	// coordinates run_once across free's independent per-host lanes;
	// this port does not, and says so here rather than silently
	// re-running the task on every host without comment.
	RunOnce bool

	// Async/Poll implement async:/poll: — see runTaskOnHost's async
	// branch and modules.AsyncLaunch/AsyncCheck for the real mechanism
	// and its one disclosed limitation (no active kill on timeout).
	// Async <= 0 means synchronous, ordinary execution — the
	// overwhelming majority of tasks. Only command/shell support
	// Async > 0 at all: every other module's work happens as a
	// sequence of calls from the control node, not one remote
	// invocation that could be backgrounded on the target the way
	// async requires — a task on any other module with Async > 0 fails
	// loud rather than silently running synchronously and ignoring
	// what was explicitly asked for. Poll nil means "unset": defaults
	// to 15 seconds (real Ansible's own DEFAULT_POLL_INTERVAL) when
	// Async > 0; Poll 0 is fire-and-forget (the task returns immediately
	// once the job is launched, for a later task to check via
	// async_status); Poll > 0 waits, checking every Poll seconds, until
	// the job finishes or Async seconds pass (a timeout failure).
	Async int
	Poll  *int

	// RoleDefaults/RoleVars are set only on the synthetic block task
	// produced for a roles: entry or include_role/import_role — the
	// engine (Engine.pushRoleVars) merges them on top of the
	// RoleDefaults/RoleVars layers' current content for the duration of
	// the block, then restores the prior (pre-merge) content. Nesting
	// (a role that itself includes another role) composes correctly to
	// any depth this way: a variable the inner role doesn't define in
	// its own defaults/main.yml or vars/main.yml still resolves to
	// whatever the enclosing role(s) already had, matching real
	// Ansible's behavior of keeping every currently active role's
	// defaults/vars in scope at once.
	RoleDefaults map[string]any
	RoleVars     map[string]any

	Block  []Task
	Rescue []Task
	Always []Task
}

Task is one step of a play (or of a block's body/rescue/always). Module/Args are populated from whichever single non-reserved key the task's YAML mapping carried (e.g. `copy:` or `command:`) — empty for a block/meta task.

func (Task) IsBlock

func (t Task) IsBlock() bool

IsBlock reports whether t is a block task (block/rescue/always) rather than a module invocation.

type VarPrompt added in v0.5.0

type VarPrompt struct {
	Name    string
	Prompt  string
	Default string
	Private bool
	Confirm bool
}

VarPrompt is one vars_prompt entry. Prompt defaults to Name when empty, Private defaults to true (confirmed from ansible-core's own playbook_executor.py: private = boolean(var.get("private", True)) — prompts hide input UNLESS private: false is explicit, the opposite of what the name alone might suggest). encrypt/salt/salt_size/unsafe (hashing the prompted value, and disabling template-escaping of it) are real ansible-core vars_prompt keys this port does not implement — accepted and parsed for shape compatibility, silently no-op'd rather than erroring on an unrecognized key, since a real playbook using only the common name/prompt/default/private/confirm subset (the overwhelming majority) should not need every knob wired to run.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL