|
1 | 1 | # Spage |
2 | 2 |
|
3 | 3 | This projects aims to function 'as' Ansible, but hugely more performant. By taking an Ansible playbook + inventory as input, it will generate a Go program that can be compiled for a specific host. |
4 | | -The end result is a generated .go file that can be compiled and shipped to the target host. |
| 4 | +The end result is a generated `.go` file that can be compiled and shipped to the target host. |
5 | 5 |
|
6 | | -To create such a program, this project ships the `spage` binary, with which you can target Ansible playbooks + inventories. Output looks like this: |
| 6 | +To create such a program, this project ships the `spage` binary, with which you can target Ansible playbooks + inventories. |
| 7 | + |
| 8 | +Key benefits: |
| 9 | +- **(Almost full) Ansible compatibility** - Any playbook that works with Ansible works with Spage |
| 10 | +- **Significantly faster execution** - Compiles to native Go code instead of interpreting Python |
| 11 | +- **No Python dependency** - Single binary that can run anywhere |
| 12 | +- **Extended features** - Built-in parallel execution, automatic rollback on failure, variable usage detection, and support for external executors (like `temporal`) |
| 13 | +- **Same syntax, but extra keywords** - Uses identical YAML playbook format and module parameters, with extra options for parallel execution with `before`/`after` |
| 14 | + |
| 15 | +Spage works by: |
| 16 | +1. Taking your existing Ansible playbooks and inventory files |
| 17 | +2. Generating Go code that implements the same logic |
| 18 | +3. Compiling this into a single binary for your target environment |
| 19 | +4. Executing tasks with native Go modules where possible, falling back to Python for full compatibility |
| 20 | + |
| 21 | +## Usage |
| 22 | + |
| 23 | +You can use Spage in two ways: |
| 24 | +1. Generate the Go code that you can then compile and run (using the `spage generate` command) |
| 25 | +2. Run directly across an inventory (using the `spage run` command) |
7 | 26 |
|
8 | 27 | ```bash |
9 | | -$ spage generate -p playbook.yaml |
10 | | -Processing node pkg.TaskNode "ensure were in arch iso" "shell": &{Execute:lsblk -f | grep "/run/archiso/bootmnt" && exit 0 || exit 1 Revert: ModuleInput:<nil>} |
11 | | -Processing node pkg.TaskNode "create ssh dir" "shell": &{Execute:mkdir -p .ssh Revert: ModuleInput:<nil>} |
12 | | -Processing node pkg.TaskNode "copy ssh key" "shell": &{Execute:curl -sSL https://github.com/AlexanderGrooff.keys > .ssh/authorized_keys Revert: ModuleInput:<nil>} |
13 | | -Compiling graph to code: |
14 | | -- Step 0: |
15 | | - - ensure were in arch iso |
16 | | - - create ssh dir |
17 | | -- Step 1: |
18 | | - - copy ssh key |
19 | | -Required inputs: |
20 | | -Processing node pkg.TaskNode "ensure were in arch iso" "shell": &{Execute:lsblk -f | grep "/run/archiso/bootmnt" && exit 0 || exit 1 Revert: ModuleInput:<nil>} |
21 | | -Processing node pkg.TaskNode "create ssh dir" "shell": &{Execute:mkdir -p .ssh Revert: ModuleInput:<nil>} |
22 | | -Processing node pkg.TaskNode "copy ssh key" "shell": &{Execute:curl -sSL https://github.com/AlexanderGrooff.keys > .ssh/authorized_keys Revert: ModuleInput:<nil>} |
23 | | -Compiling graph to code: |
24 | | -- Step 0: |
25 | | - - ensure were in arch iso |
26 | | - - create ssh dir |
27 | | -- Step 1: |
28 | | - - copy ssh key |
29 | | -Required inputs: |
30 | | -``` |
| 28 | +# Generate the Go code that you can then compile and run |
| 29 | +spage generate -p playbook.yaml -o generated_tasks.go |
| 30 | +go build -o spage_playbook generated_tasks.go |
| 31 | +./spage_playbook -inventory inventory.yaml |
31 | 32 |
|
32 | | -This will generate a `generated/tasks.go` file, which can be compiled for a specific host. That file looks like this: |
33 | | - |
34 | | -```go |
35 | | -package generated |
36 | | - |
37 | | -import ( |
38 | | - "github.com/AlexanderGrooff/spage/pkg" |
39 | | - "github.com/AlexanderGrooff/spage/pkg/modules" |
40 | | -) |
41 | | - |
42 | | -var GeneratedGraph = pkg.Graph{ |
43 | | - RequiredInputs: []string{ |
44 | | - }, |
45 | | - Tasks: [][]pkg.GraphNode{ |
46 | | - []pkg.GraphNode{ |
47 | | - pkg.Task{Name: "ensure were in arch iso", Module: "shell", Register: "", Params: modules.ShellInput{Execute: "lsblk -f | grep \"/run/archiso/bootmnt\" && exit 0 || exit 1", Revert: ""}, RunAs: "", When: ""}, |
48 | | - pkg.Task{Name: "create ssh dir", Module: "shell", Register: "", Params: modules.ShellInput{Execute: "mkdir -p .ssh", Revert: ""}, RunAs: "", When: ""}, |
49 | | - }, |
50 | | - []pkg.GraphNode{ |
51 | | - pkg.Task{Name: "copy ssh key", Module: "shell", Register: "", Params: modules.ShellInput{Execute: "curl -sSL https://github.com/AlexanderGrooff.keys > .ssh/authorized_keys", Revert: ""}, RunAs: "", When: ""}, |
52 | | - }, |
53 | | - }, |
54 | | -} |
| 33 | +# Or run directly across an inventory |
| 34 | +spage run -i inventory.yaml -p playbook.yaml |
55 | 35 | ``` |
56 | 36 |
|
57 | | -## Project structure |
| 37 | +## FAQ |
58 | 38 |
|
59 | | -`spage` makes use of modules to execute tasks, just like Ansible. Modules are located in the `pkg/modules` directory. This includes modules such as `shell`, `template`, `systemd`, etc. |
| 39 | +### Q: What is Spage? |
60 | 40 |
|
61 | | -## Ansible vs Spage |
| 41 | +**A: Spage is a high-performance drop-in replacement for Ansible** that compiles your |
| 42 | +playbooks into Go programs. You can then utilize the Golang toolchain to compile and run the playbook, either locally or on the target host. |
62 | 43 |
|
63 | | -By default, Spage will generate a program that is functionally identical to Ansible. However, Spage also allows for more complex behavior, such as conditional tasks, multiple hosts, and more. |
64 | | -Spage acts as a drop-in replacement for Ansible, so any playbook that can be run with Ansible can also be run with Spage. There are extra features that Spage offers: |
| 44 | +### Q: Why is it called Spage? |
65 | 45 |
|
66 | | -- Revert functionality: Spage will automatically revert any changes made by a task if the task fails. You can specify a revert task for each task in the playbook. |
67 | | -- Parallel execution: Spage will automatically parallelize tasks across all hosts, and you can control the flow with `before`/`after`. |
68 | | -- No Python dependency: Spage is a single binary that can be run on any system. |
| 46 | +**A: It's a reference to [Factorio: Space Age](https://www.factorio.com/space-age/buy).** I build Spage when Space Age was |
| 47 | +not yet released, and I wanted something to do. So while waiting for the release, I |
| 48 | +found a very funny Reddit comment calling it Spage, and thus Spage was born. |
69 | 49 |
|
70 | | -## Usage |
| 50 | +### Q: I have module `x.y.z` from an Ansible Galaxy collection. Is this supported in Spage? |
71 | 51 |
|
72 | | -```bash |
73 | | -go generate |
74 | | -# OR |
75 | | -go run . generate -p playbook.yaml |
76 | | -# OR |
77 | | -go run generate_tasks.go -file playbook.yaml |
78 | | - |
79 | | -# Run across an inventory |
80 | | -go run generated/tasks.go -i inventory.yaml |
81 | | -# Or compile for a specific host and run |
82 | | -go run generated/tasks.go -i inventory.yaml |
| 52 | +**A: Yes, absolutely!** Spage supports **any** Ansible module, including those from Ansible Galaxy collections, through its Python fallback mechanism. See the [Python Fallback Mechanism](#python-fallback-mechanism) section for more details. |
| 53 | + |
| 54 | +## Python Fallback Mechanism |
| 55 | + |
| 56 | +Spage includes a sophisticated Python fallback mechanism that allows it to execute any Ansible module, even those not natively implemented in Go. This ensures 100% compatibility with the Ansible ecosystem while maintaining performance benefits. |
| 57 | + |
| 58 | +### When Python Fallback is Used |
| 59 | + |
| 60 | +The Python fallback automatically activates when: |
| 61 | +- A module name is not found in Spage's native Go modules |
| 62 | +- You explicitly use the `ansible_python` module type |
| 63 | +- Community collections or custom modules are referenced (e.g., `community.general.setup`, `custom.namespace.module`) |
| 64 | + |
| 65 | +### How It Works |
| 66 | + |
| 67 | +1. **Module Detection**: Spage first attempts to find a native Go implementation of the requested module |
| 68 | +2. **Fallback Activation**: If no native module exists, the Python fallback mechanism engages |
| 69 | +3. **Collection Management**: Required Ansible collections are automatically installed on target hosts if missing |
| 70 | +4. **Bundle Transfer**: Essential Ansible core files (~1.4MB) are transferred to the target host |
| 71 | +5. **Python Execution**: The module is executed using the same Python infrastructure as standard Ansible |
| 72 | + |
| 73 | +### Performance Optimizations |
| 74 | + |
| 75 | +The Python fallback includes several performance optimizations: |
| 76 | + |
| 77 | +- **Collection Caching**: Collections are installed once per host and cached for the session |
| 78 | +- **Multi-Level Bundle Caching**: |
| 79 | + - **Permanent Cache**: Long-term cache at `/tmp/spage-ansible-cached` (when permissions allow) |
| 80 | + - **Session Cache**: Per-session cache at `/tmp/spage-ansible-session` for repeated module calls |
| 81 | + - **Fresh Transfer**: Only occurs once per session when caches are unavailable |
| 82 | +- **Minimal Bundles**: Only essential Ansible core files are transferred, not the entire Ansible installation |
| 83 | +- **Smart Collection Detection**: Existing collections are detected before attempting installation |
| 84 | + |
| 85 | +### Usage Examples |
| 86 | + |
| 87 | +```yaml |
| 88 | +# Explicit Python fallback |
| 89 | +- name: Use Python fallback for ping module |
| 90 | + ansible_python: |
| 91 | + module_name: ping |
| 92 | + args: |
| 93 | + data: pong |
| 94 | + |
| 95 | +# Community collection module (automatically uses Python fallback) |
| 96 | +- name: Get Python requirements info |
| 97 | + community.general.python_requirements_info: |
| 98 | + dependencies: [] |
| 99 | + |
| 100 | +# Custom namespace module |
| 101 | +- name: Use custom module |
| 102 | + my_company.custom_collection.special_module: |
| 103 | + param1: value1 |
| 104 | + param2: value2 |
83 | 105 | ``` |
84 | 106 |
|
| 107 | +### Local vs Remote Execution |
| 108 | +
|
| 109 | +- **Local Execution**: Uses the local Ansible installation directly with `ansible-playbook` |
| 110 | +- **Remote Execution**: Transfers minimal Ansible bundle and executes via Python with proper `PYTHONPATH` configuration |
| 111 | + |
| 112 | +### Limitations |
| 113 | + |
| 114 | +- Requires Python 3 and pip on target hosts for collection installation |
| 115 | +- Some complex Ansible plugins may have additional dependencies |
| 116 | +- Performance is slower than native Go modules but still benefits from caching optimizations |
| 117 | +- Network transfers are required for initial bundle deployment per session |
| 118 | + |
| 119 | +## Differences between Spage and Ansible |
| 120 | + |
| 121 | +Spage is a drop-in replacement for Ansible, but with some notable differences: |
| 122 | + |
| 123 | +- Playbooks are allowed to start without `- tasks:`. It assumes `hosts: localhost` and runs locally. |
| 124 | +- Tasks are executed in parallel by default based on variable usage. |
| 125 | +- New keywords `before`/`after` are available to control the flow of parallel tasks. |
| 126 | +- The `shell` module has two new parameters: `execute` and `revert`. If you don't specify these options and just use it as you would with Ansible, it will not do anything on revert. |
| 127 | + |
85 | 128 | TODO: |
86 | 129 |
|
87 | 130 | - Add revert conditions `revert_when` |
88 | 131 | - Should we compile assets (templates, files) along with the code? |
89 | 132 | - Read `ansible.cfg` variables such as `[defaults] roles_path = roles/:shared_roles/` and `[privilege_escalation] become_flags = -H -S` |
90 | 133 | - `vars_prompt` on play |
91 | 134 | - `gather_facts` on play |
92 | | -- `vars` on play |
93 | 135 | - Logic for `no_log` |
94 | 136 | - Don't allow interactive commands in `temporal` executor, or define an option that allows for signals/disallows interactivity. |
95 | 137 | - Plugin support |
96 | 138 | - Callback support |
97 | | - |
98 | | -## Differences between Spage and Ansible |
99 | | - |
100 | | -- Playbooks are allowed to start without `- tasks:`. It assumes `hosts: localhost` and runs locally. |
101 | | -- Parallel execution mode and reverts tasks by default. |
|
0 commit comments