ftl2 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. ftl2-0.1.0/.claude/settings.local.json +13 -0
  2. ftl2-0.1.0/.ftl2-state.json +75 -0
  3. ftl2-0.1.0/.gitignore +159 -0
  4. ftl2-0.1.0/CHANGELOG.md +57 -0
  5. ftl2-0.1.0/CLAUDE.md +311 -0
  6. ftl2-0.1.0/PKG-INFO +207 -0
  7. ftl2-0.1.0/README.md +169 -0
  8. ftl2-0.1.0/REPLAY_IMPLEMENTATION.md +208 -0
  9. ftl2-0.1.0/ROADMAP.md +42 -0
  10. ftl2-0.1.0/benchmarks/RESULTS.md +117 -0
  11. ftl2-0.1.0/benchmarks/bench_ftl_modules.py +322 -0
  12. ftl2-0.1.0/benchmarks/bench_memory.py +191 -0
  13. ftl2-0.1.0/build_test_gate.py +57 -0
  14. ftl2-0.1.0/demo_crash_recovery.py +122 -0
  15. ftl2-0.1.0/docker-compose.test.yml +27 -0
  16. ftl2-0.1.0/docs/automatic-backups.md +515 -0
  17. ftl2-0.1.0/entries/2026/03/05/ftl2-improvements-for-rhel-9-compatibility.md +58 -0
  18. ftl2-0.1.0/entries/2026/03/05/ftl2-supports-python-39-on-managed-hosts.md +24 -0
  19. ftl2-0.1.0/examples/01-local-execution/README.md +173 -0
  20. ftl2-0.1.0/examples/01-local-execution/inventory.yml +20 -0
  21. ftl2-0.1.0/examples/01-local-execution/run_examples.sh +85 -0
  22. ftl2-0.1.0/examples/02-remote-ssh/README.md +326 -0
  23. ftl2-0.1.0/examples/02-remote-ssh/docker-compose.yml +54 -0
  24. ftl2-0.1.0/examples/02-remote-ssh/inventory.yml +50 -0
  25. ftl2-0.1.0/examples/02-remote-ssh/run_examples.sh +146 -0
  26. ftl2-0.1.0/examples/02-remote-ssh/setup.sh +211 -0
  27. ftl2-0.1.0/examples/03-multi-host/README.md +494 -0
  28. ftl2-0.1.0/examples/03-multi-host/docker-compose.yml +131 -0
  29. ftl2-0.1.0/examples/03-multi-host/inventory.yml +111 -0
  30. ftl2-0.1.0/examples/03-multi-host/run_examples.sh +219 -0
  31. ftl2-0.1.0/examples/03-multi-host/setup.sh +305 -0
  32. ftl2-0.1.0/examples/04-ftl-modules/README.md +154 -0
  33. ftl2-0.1.0/examples/04-ftl-modules/docker-compose.yml +39 -0
  34. ftl2-0.1.0/examples/04-ftl-modules/example_ansible_modules.py +356 -0
  35. ftl2-0.1.0/examples/04-ftl-modules/example_comparison.py +234 -0
  36. ftl2-0.1.0/examples/04-ftl-modules/example_local.py +256 -0
  37. ftl2-0.1.0/examples/04-ftl-modules/example_remote.py +253 -0
  38. ftl2-0.1.0/examples/05-event-streaming/README.md +273 -0
  39. ftl2-0.1.0/examples/05-event-streaming/docker-compose.yml +39 -0
  40. ftl2-0.1.0/examples/05-event-streaming/example_remote_streaming.py +319 -0
  41. ftl2-0.1.0/examples/05-event-streaming/example_streaming.py +328 -0
  42. ftl2-0.1.0/examples/06-automation-context/README.md +300 -0
  43. ftl2-0.1.0/examples/06-automation-context/example_dynamic_hosts.py +142 -0
  44. ftl2-0.1.0/examples/06-automation-context/example_fqcn_modules.py +203 -0
  45. ftl2-0.1.0/examples/06-automation-context/example_phase1_basic.py +173 -0
  46. ftl2-0.1.0/examples/06-automation-context/example_phase2_inventory.py +278 -0
  47. ftl2-0.1.0/examples/06-automation-context/example_phase3_secrets.py +222 -0
  48. ftl2-0.1.0/examples/06-automation-context/example_phase4_check_mode.py +237 -0
  49. ftl2-0.1.0/examples/06-automation-context/example_phase5_output.py +223 -0
  50. ftl2-0.1.0/examples/06-automation-context/example_phase6_error_handling.py +266 -0
  51. ftl2-0.1.0/examples/06-automation-context/example_ping.py +221 -0
  52. ftl2-0.1.0/examples/06-automation-context/example_secret_bindings.py +89 -0
  53. ftl2-0.1.0/examples/README.md +436 -0
  54. ftl2-0.1.0/examples/STATUS.md +123 -0
  55. ftl2-0.1.0/pyproject.toml +131 -0
  56. ftl2-0.1.0/src/ftl2/__init__.py +18 -0
  57. ftl2-0.1.0/src/ftl2/arguments.py +116 -0
  58. ftl2-0.1.0/src/ftl2/automation/__init__.py +275 -0
  59. ftl2-0.1.0/src/ftl2/automation/context.py +1769 -0
  60. ftl2-0.1.0/src/ftl2/automation/proxy.py +1292 -0
  61. ftl2-0.1.0/src/ftl2/backup.py +640 -0
  62. ftl2-0.1.0/src/ftl2/builder.py +121 -0
  63. ftl2-0.1.0/src/ftl2/cli.py +2127 -0
  64. ftl2-0.1.0/src/ftl2/config_profiles.py +258 -0
  65. ftl2-0.1.0/src/ftl2/events.py +226 -0
  66. ftl2-0.1.0/src/ftl2/exceptions.py +375 -0
  67. ftl2-0.1.0/src/ftl2/executor.py +366 -0
  68. ftl2-0.1.0/src/ftl2/ftl_gate/__init__.py +7 -0
  69. ftl2-0.1.0/src/ftl2/ftl_gate/__main__.py +1125 -0
  70. ftl2-0.1.0/src/ftl2/ftl_modules/__init__.py +162 -0
  71. ftl2-0.1.0/src/ftl2/ftl_modules/aws/__init__.py +8 -0
  72. ftl2-0.1.0/src/ftl2/ftl_modules/aws/ec2.py +33 -0
  73. ftl2-0.1.0/src/ftl2/ftl_modules/command.py +141 -0
  74. ftl2-0.1.0/src/ftl2/ftl_modules/exceptions.py +57 -0
  75. ftl2-0.1.0/src/ftl2/ftl_modules/executor.py +521 -0
  76. ftl2-0.1.0/src/ftl2/ftl_modules/file.py +372 -0
  77. ftl2-0.1.0/src/ftl2/ftl_modules/http.py +338 -0
  78. ftl2-0.1.0/src/ftl2/ftl_modules/pip.py +166 -0
  79. ftl2-0.1.0/src/ftl2/ftl_modules/swap.py +230 -0
  80. ftl2-0.1.0/src/ftl2/ftl_modules/wait_for.py +115 -0
  81. ftl2-0.1.0/src/ftl2/gate.py +496 -0
  82. ftl2-0.1.0/src/ftl2/host_filter.py +187 -0
  83. ftl2-0.1.0/src/ftl2/inventory.py +422 -0
  84. ftl2-0.1.0/src/ftl2/logging.py +403 -0
  85. ftl2-0.1.0/src/ftl2/message.py +239 -0
  86. ftl2-0.1.0/src/ftl2/module_docs.py +562 -0
  87. ftl2-0.1.0/src/ftl2/module_loading/__init__.py +75 -0
  88. ftl2-0.1.0/src/ftl2/module_loading/bundle.py +499 -0
  89. ftl2-0.1.0/src/ftl2/module_loading/dependencies.py +508 -0
  90. ftl2-0.1.0/src/ftl2/module_loading/excluded.py +189 -0
  91. ftl2-0.1.0/src/ftl2/module_loading/executor.py +933 -0
  92. ftl2-0.1.0/src/ftl2/module_loading/fqcn.py +421 -0
  93. ftl2-0.1.0/src/ftl2/module_loading/requirements.py +430 -0
  94. ftl2-0.1.0/src/ftl2/module_loading/shadowed.py +58 -0
  95. ftl2-0.1.0/src/ftl2/modules/copy.py +146 -0
  96. ftl2-0.1.0/src/ftl2/modules/file.py +161 -0
  97. ftl2-0.1.0/src/ftl2/modules/ping.py +49 -0
  98. ftl2-0.1.0/src/ftl2/modules/setup.py +80 -0
  99. ftl2-0.1.0/src/ftl2/modules/shell.py +92 -0
  100. ftl2-0.1.0/src/ftl2/policy.py +177 -0
  101. ftl2-0.1.0/src/ftl2/progress.py +631 -0
  102. ftl2-0.1.0/src/ftl2/refs.py +174 -0
  103. ftl2-0.1.0/src/ftl2/retry.py +442 -0
  104. ftl2-0.1.0/src/ftl2/runners.py +1302 -0
  105. ftl2-0.1.0/src/ftl2/safety.py +219 -0
  106. ftl2-0.1.0/src/ftl2/ssh.py +623 -0
  107. ftl2-0.1.0/src/ftl2/state/__init__.py +41 -0
  108. ftl2-0.1.0/src/ftl2/state/execution.py +304 -0
  109. ftl2-0.1.0/src/ftl2/state/file.py +93 -0
  110. ftl2-0.1.0/src/ftl2/state/merge.py +64 -0
  111. ftl2-0.1.0/src/ftl2/state/state.py +288 -0
  112. ftl2-0.1.0/src/ftl2/telemetry.py +73 -0
  113. ftl2-0.1.0/src/ftl2/types.py +311 -0
  114. ftl2-0.1.0/src/ftl2/utils.py +186 -0
  115. ftl2-0.1.0/src/ftl2/vars.py +373 -0
  116. ftl2-0.1.0/src/ftl2/vault.py +99 -0
  117. ftl2-0.1.0/src/ftl2/workflow.py +314 -0
  118. ftl2-0.1.0/test_replay.py +303 -0
  119. ftl2-0.1.0/test_replay_secrets.py +189 -0
  120. ftl2-0.1.0/tests/__init__.py +1 -0
  121. ftl2-0.1.0/tests/conftest.py +158 -0
  122. ftl2-0.1.0/tests/test_arguments.py +226 -0
  123. ftl2-0.1.0/tests/test_automation.py +1906 -0
  124. ftl2-0.1.0/tests/test_become.py +187 -0
  125. ftl2-0.1.0/tests/test_bundle.py +463 -0
  126. ftl2-0.1.0/tests/test_cli.py +2527 -0
  127. ftl2-0.1.0/tests/test_dependencies.py +511 -0
  128. ftl2-0.1.0/tests/test_events.py +203 -0
  129. ftl2-0.1.0/tests/test_excluded_modules.py +222 -0
  130. ftl2-0.1.0/tests/test_executor.py +267 -0
  131. ftl2-0.1.0/tests/test_fqcn.py +306 -0
  132. ftl2-0.1.0/tests/test_ftl_executor.py +377 -0
  133. ftl2-0.1.0/tests/test_ftl_gate.py +103 -0
  134. ftl2-0.1.0/tests/test_ftl_modules.py +217 -0
  135. ftl2-0.1.0/tests/test_ftl_modules_phase2.py +623 -0
  136. ftl2-0.1.0/tests/test_gate.py +456 -0
  137. ftl2-0.1.0/tests/test_integration.py +323 -0
  138. ftl2-0.1.0/tests/test_inventory.py +426 -0
  139. ftl2-0.1.0/tests/test_logging.py +313 -0
  140. ftl2-0.1.0/tests/test_message.py +295 -0
  141. ftl2-0.1.0/tests/test_module_loading_executor.py +1293 -0
  142. ftl2-0.1.0/tests/test_modules/test_binary.sh +14 -0
  143. ftl2-0.1.0/tests/test_modules/test_new_style.py +27 -0
  144. ftl2-0.1.0/tests/test_modules/test_want_json.py +36 -0
  145. ftl2-0.1.0/tests/test_native_file_transfer.py +513 -0
  146. ftl2-0.1.0/tests/test_progress.py +237 -0
  147. ftl2-0.1.0/tests/test_refs.py +301 -0
  148. ftl2-0.1.0/tests/test_requirements.py +565 -0
  149. ftl2-0.1.0/tests/test_runners.py +377 -0
  150. ftl2-0.1.0/tests/test_shadowed_modules.py +315 -0
  151. ftl2-0.1.0/tests/test_ssh.py +521 -0
  152. ftl2-0.1.0/tests/test_ssh_integration.py +305 -0
  153. ftl2-0.1.0/tests/test_state.py +374 -0
  154. ftl2-0.1.0/tests/test_swap_module.py +282 -0
  155. ftl2-0.1.0/tests/test_types.py +233 -0
  156. ftl2-0.1.0/tests/test_utils.py +237 -0
  157. ftl2-0.1.0/tests/test_version.py +13 -0
  158. ftl2-0.1.0/tools/ftl2_state_to_inventory.py +101 -0
  159. ftl2-0.1.0/tools/gate_debug.py +202 -0
  160. ftl2-0.1.0/tools/gate_debug_remote.py +320 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(source:*)",
5
+ "Bash(pytest:*)",
6
+ "Bash(python -m pytest:*)",
7
+ "Bash(python3 -m pytest:*)",
8
+ "Bash(git add:*)",
9
+ "Bash(git commit:*)",
10
+ "Bash(git push)"
11
+ ]
12
+ }
13
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "version": 1,
3
+ "created_at": "2026-02-11T18:06:54.971737+00:00",
4
+ "updated_at": "2026-03-08T02:49:46.865282+00:00",
5
+ "hosts": {
6
+ "web01": {
7
+ "ansible_host": "192.168.1.10",
8
+ "ansible_port": 22,
9
+ "groups": [
10
+ "ungrouped"
11
+ ],
12
+ "added_at": "2026-03-08T02:49:46.857091+00:00"
13
+ },
14
+ "db01": {
15
+ "ansible_host": "192.168.1.20",
16
+ "ansible_port": 2222,
17
+ "groups": [
18
+ "databases",
19
+ "production"
20
+ ],
21
+ "added_at": "2026-03-08T02:49:46.857669+00:00",
22
+ "ansible_user": "admin",
23
+ "db_type": "postgres"
24
+ },
25
+ "myhost.example.com": {
26
+ "ansible_host": "myhost.example.com",
27
+ "ansible_port": 22,
28
+ "groups": [
29
+ "ungrouped"
30
+ ],
31
+ "added_at": "2026-03-08T02:49:46.858186+00:00"
32
+ },
33
+ "host01": {
34
+ "ansible_host": "host01",
35
+ "ansible_port": 22,
36
+ "groups": [
37
+ "newgroup"
38
+ ],
39
+ "added_at": "2026-02-11T18:06:54.975470+00:00"
40
+ },
41
+ "web02": {
42
+ "ansible_host": "192.168.1.11",
43
+ "ansible_port": 22,
44
+ "groups": [
45
+ "webservers"
46
+ ],
47
+ "added_at": "2026-03-08T02:49:46.858685+00:00"
48
+ },
49
+ "lonely": {
50
+ "ansible_host": "10.0.0.1",
51
+ "ansible_port": 22,
52
+ "groups": [
53
+ "ungrouped"
54
+ ],
55
+ "added_at": "2026-03-08T02:49:46.859195+00:00"
56
+ },
57
+ "newhost": {
58
+ "ansible_host": "10.0.0.5",
59
+ "ansible_port": 22,
60
+ "groups": [
61
+ "ungrouped"
62
+ ],
63
+ "added_at": "2026-03-08T02:49:46.859768+00:00"
64
+ },
65
+ "localtest": {
66
+ "ansible_host": "localhost",
67
+ "ansible_port": 22,
68
+ "groups": [
69
+ "testgroup"
70
+ ],
71
+ "added_at": "2026-03-08T02:49:46.865275+00:00"
72
+ }
73
+ },
74
+ "resources": {}
75
+ }
ftl2-0.1.0/.gitignore ADDED
@@ -0,0 +1,159 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ *.manifest
31
+ *.spec
32
+
33
+ # Installer logs
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .nox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *.cover
47
+ *.py,cover
48
+ .hypothesis/
49
+ .pytest_cache/
50
+ cover/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+ db.sqlite3
60
+ db.sqlite3-journal
61
+
62
+ # Flask stuff:
63
+ instance/
64
+ .webassets-cache
65
+
66
+ # Scrapy stuff:
67
+ .scrapy
68
+
69
+ # Sphinx documentation
70
+ docs/_build/
71
+
72
+ # PyBuilder
73
+ .pybuilder/
74
+ target/
75
+
76
+ # Jupyter Notebook
77
+ .ipynb_checkpoints
78
+
79
+ # IPython
80
+ profile_default/
81
+ ipython_config.py
82
+
83
+ # pyenv
84
+ .python-version
85
+
86
+ # pipenv
87
+ Pipfile.lock
88
+
89
+ # poetry
90
+ poetry.lock
91
+
92
+ # pdm
93
+ .pdm.toml
94
+ .pdm-python
95
+ .pdm-build/
96
+
97
+ # PEP 582
98
+ __pypackages__/
99
+
100
+ # Celery stuff
101
+ celerybeat-schedule
102
+ celerybeat.pid
103
+
104
+ # SageMath parsed files
105
+ *.sage.py
106
+
107
+ # Environments
108
+ .env
109
+ .venv
110
+ env/
111
+ venv/
112
+ ENV/
113
+ env.bak/
114
+ venv.bak/
115
+
116
+ # Spyder project settings
117
+ .spyderproject
118
+ .spyproject
119
+
120
+ # Rope project settings
121
+ .ropeproject
122
+
123
+ # mkdocs documentation
124
+ /site
125
+
126
+ # mypy
127
+ .mypy_cache/
128
+ .dmypy.json
129
+ dmypy.json
130
+
131
+ # Pyre type checker
132
+ .pyre/
133
+
134
+ # pytype static type analyzer
135
+ .pytype/
136
+
137
+ # Cython debug symbols
138
+ cython_debug/
139
+
140
+ # IDEs
141
+ .vscode/
142
+ .idea/
143
+ *.swp
144
+ *.swo
145
+ *~
146
+
147
+ # OS
148
+ .DS_Store
149
+ Thumbs.db
150
+
151
+ # uv
152
+ .uv/
153
+ uv.lock
154
+ tests/ssh_test_config/
155
+
156
+ # FTL2 build artifacts
157
+ .ftl2-deps.txt
158
+ .ftl2-modules.txt
159
+ *.pyz
@@ -0,0 +1,57 @@
1
+ # FTL2 Development History
2
+
3
+ ## Foundation (Feb 5)
4
+
5
+ Core architecture: typed dataclasses for host/inventory config, Strategy-pattern runner (local vs remote), gate communication protocol for remote execution, variable reference system, argument merging, CLI with subcommands.
6
+
7
+ ## FTL Modules (Feb 6)
8
+
9
+ In-process Ansible module execution — the core performance win. FQCN parser resolves `community.general.slack` to actual module files. Dependency detector finds Python requirements from module DOCUMENTATION strings. Bundle builder packages modules + module_utils into self-contained zipapps. Async executor runs modules as Python functions instead of subprocesses. 3-17x faster than `ansible-playbook`.
10
+
11
+ ## Event Streaming (Feb 6)
12
+
13
+ Real-time event protocol between gate and controller. Event types: module start/complete, file changes (inotify), system metrics (CPU/memory/disk). Rich TUI for progress display. `await ftl.listen()` for persistent monitoring.
14
+
15
+ ## Automation Context API (Feb 6-7)
16
+
17
+ The developer-facing interface:
18
+
19
+ ```python
20
+ async with automation(inventory="hosts.yml", secret_bindings={...}) as ftl:
21
+ await ftl.file(path="/tmp/test", state="directory")
22
+ await ftl.run_on("webservers", "dnf", name="nginx", state="present")
23
+ ```
24
+
25
+ Features: secret bindings (auto-inject credentials, never visible in logs), host-scoped proxies (`ftl["hostname"].module()`), bracket notation for host names with dashes, `add_host()` for dynamic provisioning workflows, check mode, fail_fast, per-host summary printing.
26
+
27
+ ## Gate System (Feb 7)
28
+
29
+ Remote execution via SSH gate process. Gate builder creates cached zipapps with baked-in modules. FTL modules sent by name (gate has them), Ansible modules sent as bundles. Gate protocol supports debug commands (Info, ListModules). SSH subsystem registration eliminates shell startup overhead. Gates cached in `~/.ftl` per user.
30
+
31
+ ## Native Modules (Feb 7-8)
32
+
33
+ FTL-native implementations for hot-path modules: `copy` (with file transfer), `template`, `fetch`, `shell`, `swap`, `ping`, `wait_for`. Native modules skip the Ansible module machinery entirely.
34
+
35
+ ## State Management (Feb 7)
36
+
37
+ State file (`.ftl2-state.json`) tracks dynamically provisioned hosts and resources. `add_host()` persists immediately. Hosts loaded from state on context enter — enables crash recovery and idempotent provisioning. State exposed to automation scripts via `ftl.state`.
38
+
39
+ ## Dependency Management (Feb 7)
40
+
41
+ `auto_install_deps` installs missing Python packages with `uv` at runtime. `record_deps` captures requirements during execution and writes to `.ftl2-deps.txt`. Module names tracked in `.ftl2-modules.txt` for gate building.
42
+
43
+ ## Audit & Replay (Feb 8)
44
+
45
+ `record="audit.json"` captures every module execution with timestamps, durations, parameters (secrets redacted), and results. `replay="audit.json"` skips successful actions from a previous run, resuming from the first failure. Enables crash recovery without re-running completed work.
46
+
47
+ ## Policy Engine (Feb 11)
48
+
49
+ YAML-based rules evaluated before every module execution. Match conditions: `module` (fnmatch), `host`, `environment`, `param.<name>`. First matching deny rule raises `PolicyDeniedError`. Integrated into both `execute()` (local) and `_execute_on_host()` (remote). `Policy.empty()` for backward compatibility.
50
+
51
+ ## JSON Inventory & Inventory Scripts (Feb 11)
52
+
53
+ `load_inventory()` auto-detects format: executable scripts (run with `--list`), JSON (Ansible `ansible-inventory --list` format with `_meta.hostvars`), or YAML. Enables dynamic inventory from cloud providers without reimplementing inventory plugins — just shell out to `ansible-inventory` and pass the JSON to FTL2.
54
+
55
+ ## Vault Secrets (Feb 11)
56
+
57
+ HashiCorp Vault KV v2 support via `vault_secrets` parameter. Maps names to `path#field` references, resolved at context startup, accessible via `ftl.secrets["NAME"]` alongside env var secrets. Uses standard `VAULT_ADDR`/`VAULT_TOKEN` env vars. Reads grouped by path to minimize API calls. `hvac` is an optional dependency (`pip install ftl2[vault]`). Works with `secret_bindings` for auto-injection of vault-sourced credentials.
ftl2-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,311 @@
1
+ # CLAUDE.md
2
+
3
+ ## What This Is
4
+
5
+ FTL2 is a Python automation framework that runs Ansible modules in-process instead of as subprocesses. It provides an `async with automation()` context manager that gives Python scripts direct access to the entire Ansible module ecosystem — 3-17x faster than `ansible-playbook`.
6
+
7
+ ## Installation
8
+
9
+ **Preferred: Use `uv run` with PEP 723 inline metadata (no install needed):**
10
+
11
+ Add this header to any script and `uv run` handles everything:
12
+ ```python
13
+ #!/usr/bin/env python3
14
+ # /// script
15
+ # dependencies = [
16
+ # "ftl2 @ git+https://github.com/benthomasson/ftl2",
17
+ # ]
18
+ # requires-python = ">=3.13"
19
+ # ///
20
+ ```
21
+
22
+ Then run directly:
23
+ ```bash
24
+ uv run my_script.py
25
+ ```
26
+
27
+ **Alternative: Install the CLI tool globally:**
28
+ ```bash
29
+ uvx --from "git+https://github.com/benthomasson/ftl2" ftl2
30
+ ```
31
+
32
+ Both pull ftl2 and all dependencies (`ftl-module-utils`, `ftl-builtin-modules`, `ftl-collections`, `asyncssh`, `httpx`, etc.) directly from GitHub.
33
+
34
+ ## The Pattern
35
+
36
+ ```python
37
+ import asyncio
38
+ from ftl2 import automation
39
+
40
+ async def main():
41
+ async with automation(
42
+ secret_bindings={
43
+ "community.general.linode_v4": {
44
+ "access_token": "LINODE_TOKEN",
45
+ "root_pass": "LINODE_ROOT_PASS",
46
+ },
47
+ }
48
+ ) as ftl:
49
+ await ftl.file(path="/tmp/test", state="directory")
50
+
51
+ if __name__ == "__main__":
52
+ asyncio.run(main())
53
+ ```
54
+
55
+ ## Module Names
56
+
57
+ Use the same Ansible module names and parameters:
58
+
59
+ ```python
60
+ # Short names for builtin modules
61
+ await ftl.file(path="/tmp/test", state="touch")
62
+ await ftl.copy(src="config.yml", dest="/etc/app/config.yml")
63
+ await ftl.command(cmd="echo hello")
64
+ await ftl.service(name="nginx", state="restarted")
65
+ await ftl.template(src="app.conf.j2", dest="/etc/app.conf")
66
+
67
+ # FQCN for collection modules
68
+ await ftl.community.general.linode_v4(label="web01", type="g6-standard-1", ...)
69
+ await ftl.community.general.slack(channel="#ops", msg="Done!")
70
+ await ftl.ansible.posix.authorized_key(user="ben", key=ssh_key)
71
+ await ftl.ansible.posix.firewalld(port="80/tcp", state="enabled")
72
+ ```
73
+
74
+ ## Secret Bindings
75
+
76
+ Secrets are configured once and injected automatically:
77
+
78
+ ```python
79
+ async with automation(
80
+ secret_bindings={
81
+ "community.general.linode_v4": {
82
+ "access_token": "LINODE_TOKEN",
83
+ "root_pass": "LINODE_ROOT_PASS",
84
+ },
85
+ "community.general.slack": {"token": "SLACK_TOKEN"},
86
+ "uri": {"bearer_token": "API_TOKEN"},
87
+ }
88
+ ) as ftl:
89
+ # No credentials in the code - injected from environment
90
+ await ftl.local.community.general.linode_v4(label="web01", ...)
91
+
92
+ # bearer_token injected automatically, redacted in audit logs
93
+ await ftl.local.uri(
94
+ url="https://api.example.com/data",
95
+ body={"key": "value"},
96
+ body_format="json",
97
+ )
98
+ ```
99
+
100
+ ## Targeting Hosts and Groups
101
+
102
+ ```python
103
+ async with automation(inventory="inventory.yml") as ftl:
104
+ # Local execution (for cloud/API modules)
105
+ await ftl.local.community.general.linode_v4(label="web01", ...)
106
+
107
+ # Target a group
108
+ await ftl.webservers.service(name="nginx", state="restarted")
109
+
110
+ # Target a specific host
111
+ await ftl.db01.command(cmd="pg_dump mydb")
112
+ ```
113
+
114
+ ## State File Tracking
115
+
116
+ ```python
117
+ async with automation(state_file=".ftl2-state.json") as ftl:
118
+ if ftl.state.has("web01"):
119
+ resource = ftl.state.get("web01")
120
+ print(f"Server exists: {resource['ipv4'][0]}")
121
+ else:
122
+ server = await ftl.local.community.general.linode_v4(label="web01", ...)
123
+ ftl.state.add("web01", {
124
+ "provider": "linode",
125
+ "id": server["instance"]["id"],
126
+ "ipv4": server["instance"]["ipv4"],
127
+ })
128
+ ftl.add_host(
129
+ hostname="web01",
130
+ ansible_host=server["instance"]["ipv4"][0],
131
+ ansible_user="root",
132
+ groups=["webservers"],
133
+ )
134
+ ```
135
+
136
+ State operations:
137
+ ```python
138
+ ftl.state.has("web01") # Check existence
139
+ ftl.state.get("web01") # Get resource dict
140
+ ftl.state.add("web01", {...}) # Add resource (persists immediately)
141
+ ftl.state.remove("web01") # Remove resource
142
+ ftl.state.resources() # List all resource names
143
+ ftl.state.hosts() # List all host names
144
+ ```
145
+
146
+ ## Return Types
147
+
148
+ **Local execution** returns a `dict`:
149
+ ```python
150
+ server = await ftl.local.community.general.linode_v4(label="web01", ...)
151
+ ip = server["instance"]["ipv4"][0]
152
+ ```
153
+
154
+ **Remote execution** returns `list[ExecuteResult]`:
155
+ ```python
156
+ results = await ftl.webservers.command(cmd="uptime")
157
+ for result in results:
158
+ print(f"{result.host}: {result.output}")
159
+ ```
160
+
161
+ ## Safety Features
162
+
163
+ ```python
164
+ # Check mode - preview without executing
165
+ async with automation(check_mode=True) as ftl:
166
+ await ftl.file(path="/etc/important", state="absent")
167
+
168
+ # Fail fast - stop on first error
169
+ async with automation(fail_fast=True) as ftl:
170
+ await ftl.file(...)
171
+ ```
172
+
173
+ ## Gate Modules (Pre-built Remote Execution)
174
+
175
+ ```python
176
+ async with automation(
177
+ gate_modules="auto", # Read from .ftl2-modules.txt, or record on first run
178
+ record_deps=True, # Record modules to .ftl2-modules.txt
179
+ ) as ftl:
180
+ await ftl.webservers.dnf(name="nginx", state="present")
181
+ ```
182
+
183
+ First run records modules; subsequent runs bake them into the gate for faster execution.
184
+
185
+ ## Audit Recording
186
+
187
+ ```python
188
+ async with automation(record="audit.json") as ftl:
189
+ await ftl.file(path="/tmp/test", state="directory")
190
+ # Writes audit.json with all actions, timestamps, durations
191
+ # Secret-injected params are excluded
192
+ ```
193
+
194
+ ## Common Gotchas
195
+
196
+ ### Bootstrap python3-dnf on Fedora
197
+ ```python
198
+ await host.command(cmd="dnf install -y python3-dnf") # Before any dnf calls
199
+ await host.dnf(name="nginx", state="present")
200
+ ```
201
+
202
+ ### `user` module: `group` vs `groups`
203
+ ```python
204
+ # WRONG - changes primary group
205
+ await host.user(name="ben", group="wheel")
206
+ # RIGHT - adds supplementary group
207
+ await host.user(name="ben", groups=["wheel"])
208
+ ```
209
+
210
+ ### Some modules require FQCN
211
+ ```python
212
+ # WRONG - not in ansible.builtin
213
+ await host.authorized_key(user="ben", key=ssh_key)
214
+ # RIGHT
215
+ await host.ansible.posix.authorized_key(user="ben", key=ssh_key)
216
+ ```
217
+
218
+ | Module | FQCN |
219
+ |--------|------|
220
+ | `authorized_key` | `ansible.posix.authorized_key` |
221
+ | `firewalld` | `ansible.posix.firewalld` |
222
+ | `slack` | `community.general.slack` |
223
+ | `linode_v4` | `community.general.linode_v4` |
224
+
225
+ ### `swap` module: string size
226
+ ```python
227
+ await host.swap(path="/swapfile", size="1G") # String, not int
228
+ ```
229
+
230
+ ### No Jinja2 or Lookup Plugins
231
+ ```python
232
+ # WRONG
233
+ key="{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
234
+ # RIGHT
235
+ from pathlib import Path
236
+ key = (Path.home() / ".ssh" / "id_rsa.pub").read_text().strip()
237
+ ```
238
+
239
+ ### .gitignore
240
+ ```
241
+ .ftl2-state.json
242
+ .ftl2-deps.txt
243
+ .ftl2-modules.txt
244
+ *.pyz
245
+ audit.json
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Project Structure (for developers)
251
+
252
+ ```
253
+ src/ftl2/
254
+ automation/ # AutomationContext, ModuleProxy — the async with automation() API
255
+ context.py # Main context manager, execute(), secret_bindings, record
256
+ proxy.py # Host/group proxy for ftl.webservers.dnf() syntax
257
+ __init__.py # automation() wrapper function
258
+ ftl_modules/ # FTL-native module implementations (in-process, no subprocess)
259
+ http.py # ftl_uri, ftl_get_url (httpx-based)
260
+ executor.py # ExecuteResult dataclass, FTL module dispatcher
261
+ swap.py, pip.py # Other native modules
262
+ ftl_gate/ # Remote execution gate (.pyz zipapp)
263
+ __main__.py # Gate-side: receives modules over stdin, executes, returns results
264
+ state/ # State management
265
+ state.py # State class for .ftl2-state.json
266
+ execution.py # ExecutionState for CLI run tracking
267
+ gate.py # Gate builder — creates .pyz with baked-in modules
268
+ runners.py # SSH connection, gate deployment, remote module execution
269
+ cli.py # Click CLI (ftl2 command)
270
+ ssh.py # SSH host abstraction
271
+ inventory.py # Inventory loading (YAML)
272
+ builder.py # ftl-gate-builder entry point
273
+ ```
274
+
275
+ ## Key Abstractions
276
+
277
+ - **AutomationContext** (`automation/context.py`) — the core. Manages inventory, secrets, module execution, state, and recording
278
+ - **ModuleProxy** (`automation/proxy.py`) — translates `ftl.webservers.dnf()` into `context.execute("dnf", hosts, params)`
279
+ - **Gate** (`gate.py` + `ftl_gate/`) — .pyz zipapp deployed to remote hosts for module execution
280
+ - **ExecuteResult** (`ftl_modules/executor.py`) — dataclass returned from every module call
281
+
282
+ ## How Module Execution Works
283
+
284
+ 1. Script calls `await ftl.webservers.dnf(name="nginx", state="present")`
285
+ 2. ModuleProxy resolves `webservers` to a host group and `dnf` to a module name
286
+ 3. AutomationContext injects secret_bindings, captures original params for audit
287
+ 4. For local: module runs in-process via Ansible's module machinery
288
+ 5. For remote: module is sent to the gate over SSH stdin, gate executes and returns result
289
+ 6. Result is stored in `_results` list for audit recording
290
+
291
+ ## Development Commands
292
+
293
+ ```bash
294
+ pytest # Run tests
295
+ pytest tests/test_automation.py # Run specific test
296
+ ruff check src/ # Lint
297
+ ruff format src/ # Format
298
+ mypy src/ftl2 # Type check
299
+ ```
300
+
301
+ ## Dependencies
302
+
303
+ - `asyncssh` — SSH connections
304
+ - `httpx` — async HTTP (used by ftl_uri)
305
+ - `click` — CLI
306
+ - `pyyaml` — inventory parsing
307
+ - `jinja2` — template module
308
+ - `rich` — CLI output
309
+ - `ftl-module-utils` — Ansible module_utils extracted for standalone use
310
+ - `ftl-builtin-modules` — Ansible builtin modules extracted
311
+ - `ftl-collections` — Community collection module_utils (community.general, amazon.aws, etc.)