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.
- ftl2-0.1.0/.claude/settings.local.json +13 -0
- ftl2-0.1.0/.ftl2-state.json +75 -0
- ftl2-0.1.0/.gitignore +159 -0
- ftl2-0.1.0/CHANGELOG.md +57 -0
- ftl2-0.1.0/CLAUDE.md +311 -0
- ftl2-0.1.0/PKG-INFO +207 -0
- ftl2-0.1.0/README.md +169 -0
- ftl2-0.1.0/REPLAY_IMPLEMENTATION.md +208 -0
- ftl2-0.1.0/ROADMAP.md +42 -0
- ftl2-0.1.0/benchmarks/RESULTS.md +117 -0
- ftl2-0.1.0/benchmarks/bench_ftl_modules.py +322 -0
- ftl2-0.1.0/benchmarks/bench_memory.py +191 -0
- ftl2-0.1.0/build_test_gate.py +57 -0
- ftl2-0.1.0/demo_crash_recovery.py +122 -0
- ftl2-0.1.0/docker-compose.test.yml +27 -0
- ftl2-0.1.0/docs/automatic-backups.md +515 -0
- ftl2-0.1.0/entries/2026/03/05/ftl2-improvements-for-rhel-9-compatibility.md +58 -0
- ftl2-0.1.0/entries/2026/03/05/ftl2-supports-python-39-on-managed-hosts.md +24 -0
- ftl2-0.1.0/examples/01-local-execution/README.md +173 -0
- ftl2-0.1.0/examples/01-local-execution/inventory.yml +20 -0
- ftl2-0.1.0/examples/01-local-execution/run_examples.sh +85 -0
- ftl2-0.1.0/examples/02-remote-ssh/README.md +326 -0
- ftl2-0.1.0/examples/02-remote-ssh/docker-compose.yml +54 -0
- ftl2-0.1.0/examples/02-remote-ssh/inventory.yml +50 -0
- ftl2-0.1.0/examples/02-remote-ssh/run_examples.sh +146 -0
- ftl2-0.1.0/examples/02-remote-ssh/setup.sh +211 -0
- ftl2-0.1.0/examples/03-multi-host/README.md +494 -0
- ftl2-0.1.0/examples/03-multi-host/docker-compose.yml +131 -0
- ftl2-0.1.0/examples/03-multi-host/inventory.yml +111 -0
- ftl2-0.1.0/examples/03-multi-host/run_examples.sh +219 -0
- ftl2-0.1.0/examples/03-multi-host/setup.sh +305 -0
- ftl2-0.1.0/examples/04-ftl-modules/README.md +154 -0
- ftl2-0.1.0/examples/04-ftl-modules/docker-compose.yml +39 -0
- ftl2-0.1.0/examples/04-ftl-modules/example_ansible_modules.py +356 -0
- ftl2-0.1.0/examples/04-ftl-modules/example_comparison.py +234 -0
- ftl2-0.1.0/examples/04-ftl-modules/example_local.py +256 -0
- ftl2-0.1.0/examples/04-ftl-modules/example_remote.py +253 -0
- ftl2-0.1.0/examples/05-event-streaming/README.md +273 -0
- ftl2-0.1.0/examples/05-event-streaming/docker-compose.yml +39 -0
- ftl2-0.1.0/examples/05-event-streaming/example_remote_streaming.py +319 -0
- ftl2-0.1.0/examples/05-event-streaming/example_streaming.py +328 -0
- ftl2-0.1.0/examples/06-automation-context/README.md +300 -0
- ftl2-0.1.0/examples/06-automation-context/example_dynamic_hosts.py +142 -0
- ftl2-0.1.0/examples/06-automation-context/example_fqcn_modules.py +203 -0
- ftl2-0.1.0/examples/06-automation-context/example_phase1_basic.py +173 -0
- ftl2-0.1.0/examples/06-automation-context/example_phase2_inventory.py +278 -0
- ftl2-0.1.0/examples/06-automation-context/example_phase3_secrets.py +222 -0
- ftl2-0.1.0/examples/06-automation-context/example_phase4_check_mode.py +237 -0
- ftl2-0.1.0/examples/06-automation-context/example_phase5_output.py +223 -0
- ftl2-0.1.0/examples/06-automation-context/example_phase6_error_handling.py +266 -0
- ftl2-0.1.0/examples/06-automation-context/example_ping.py +221 -0
- ftl2-0.1.0/examples/06-automation-context/example_secret_bindings.py +89 -0
- ftl2-0.1.0/examples/README.md +436 -0
- ftl2-0.1.0/examples/STATUS.md +123 -0
- ftl2-0.1.0/pyproject.toml +131 -0
- ftl2-0.1.0/src/ftl2/__init__.py +18 -0
- ftl2-0.1.0/src/ftl2/arguments.py +116 -0
- ftl2-0.1.0/src/ftl2/automation/__init__.py +275 -0
- ftl2-0.1.0/src/ftl2/automation/context.py +1769 -0
- ftl2-0.1.0/src/ftl2/automation/proxy.py +1292 -0
- ftl2-0.1.0/src/ftl2/backup.py +640 -0
- ftl2-0.1.0/src/ftl2/builder.py +121 -0
- ftl2-0.1.0/src/ftl2/cli.py +2127 -0
- ftl2-0.1.0/src/ftl2/config_profiles.py +258 -0
- ftl2-0.1.0/src/ftl2/events.py +226 -0
- ftl2-0.1.0/src/ftl2/exceptions.py +375 -0
- ftl2-0.1.0/src/ftl2/executor.py +366 -0
- ftl2-0.1.0/src/ftl2/ftl_gate/__init__.py +7 -0
- ftl2-0.1.0/src/ftl2/ftl_gate/__main__.py +1125 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/__init__.py +162 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/aws/__init__.py +8 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/aws/ec2.py +33 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/command.py +141 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/exceptions.py +57 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/executor.py +521 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/file.py +372 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/http.py +338 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/pip.py +166 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/swap.py +230 -0
- ftl2-0.1.0/src/ftl2/ftl_modules/wait_for.py +115 -0
- ftl2-0.1.0/src/ftl2/gate.py +496 -0
- ftl2-0.1.0/src/ftl2/host_filter.py +187 -0
- ftl2-0.1.0/src/ftl2/inventory.py +422 -0
- ftl2-0.1.0/src/ftl2/logging.py +403 -0
- ftl2-0.1.0/src/ftl2/message.py +239 -0
- ftl2-0.1.0/src/ftl2/module_docs.py +562 -0
- ftl2-0.1.0/src/ftl2/module_loading/__init__.py +75 -0
- ftl2-0.1.0/src/ftl2/module_loading/bundle.py +499 -0
- ftl2-0.1.0/src/ftl2/module_loading/dependencies.py +508 -0
- ftl2-0.1.0/src/ftl2/module_loading/excluded.py +189 -0
- ftl2-0.1.0/src/ftl2/module_loading/executor.py +933 -0
- ftl2-0.1.0/src/ftl2/module_loading/fqcn.py +421 -0
- ftl2-0.1.0/src/ftl2/module_loading/requirements.py +430 -0
- ftl2-0.1.0/src/ftl2/module_loading/shadowed.py +58 -0
- ftl2-0.1.0/src/ftl2/modules/copy.py +146 -0
- ftl2-0.1.0/src/ftl2/modules/file.py +161 -0
- ftl2-0.1.0/src/ftl2/modules/ping.py +49 -0
- ftl2-0.1.0/src/ftl2/modules/setup.py +80 -0
- ftl2-0.1.0/src/ftl2/modules/shell.py +92 -0
- ftl2-0.1.0/src/ftl2/policy.py +177 -0
- ftl2-0.1.0/src/ftl2/progress.py +631 -0
- ftl2-0.1.0/src/ftl2/refs.py +174 -0
- ftl2-0.1.0/src/ftl2/retry.py +442 -0
- ftl2-0.1.0/src/ftl2/runners.py +1302 -0
- ftl2-0.1.0/src/ftl2/safety.py +219 -0
- ftl2-0.1.0/src/ftl2/ssh.py +623 -0
- ftl2-0.1.0/src/ftl2/state/__init__.py +41 -0
- ftl2-0.1.0/src/ftl2/state/execution.py +304 -0
- ftl2-0.1.0/src/ftl2/state/file.py +93 -0
- ftl2-0.1.0/src/ftl2/state/merge.py +64 -0
- ftl2-0.1.0/src/ftl2/state/state.py +288 -0
- ftl2-0.1.0/src/ftl2/telemetry.py +73 -0
- ftl2-0.1.0/src/ftl2/types.py +311 -0
- ftl2-0.1.0/src/ftl2/utils.py +186 -0
- ftl2-0.1.0/src/ftl2/vars.py +373 -0
- ftl2-0.1.0/src/ftl2/vault.py +99 -0
- ftl2-0.1.0/src/ftl2/workflow.py +314 -0
- ftl2-0.1.0/test_replay.py +303 -0
- ftl2-0.1.0/test_replay_secrets.py +189 -0
- ftl2-0.1.0/tests/__init__.py +1 -0
- ftl2-0.1.0/tests/conftest.py +158 -0
- ftl2-0.1.0/tests/test_arguments.py +226 -0
- ftl2-0.1.0/tests/test_automation.py +1906 -0
- ftl2-0.1.0/tests/test_become.py +187 -0
- ftl2-0.1.0/tests/test_bundle.py +463 -0
- ftl2-0.1.0/tests/test_cli.py +2527 -0
- ftl2-0.1.0/tests/test_dependencies.py +511 -0
- ftl2-0.1.0/tests/test_events.py +203 -0
- ftl2-0.1.0/tests/test_excluded_modules.py +222 -0
- ftl2-0.1.0/tests/test_executor.py +267 -0
- ftl2-0.1.0/tests/test_fqcn.py +306 -0
- ftl2-0.1.0/tests/test_ftl_executor.py +377 -0
- ftl2-0.1.0/tests/test_ftl_gate.py +103 -0
- ftl2-0.1.0/tests/test_ftl_modules.py +217 -0
- ftl2-0.1.0/tests/test_ftl_modules_phase2.py +623 -0
- ftl2-0.1.0/tests/test_gate.py +456 -0
- ftl2-0.1.0/tests/test_integration.py +323 -0
- ftl2-0.1.0/tests/test_inventory.py +426 -0
- ftl2-0.1.0/tests/test_logging.py +313 -0
- ftl2-0.1.0/tests/test_message.py +295 -0
- ftl2-0.1.0/tests/test_module_loading_executor.py +1293 -0
- ftl2-0.1.0/tests/test_modules/test_binary.sh +14 -0
- ftl2-0.1.0/tests/test_modules/test_new_style.py +27 -0
- ftl2-0.1.0/tests/test_modules/test_want_json.py +36 -0
- ftl2-0.1.0/tests/test_native_file_transfer.py +513 -0
- ftl2-0.1.0/tests/test_progress.py +237 -0
- ftl2-0.1.0/tests/test_refs.py +301 -0
- ftl2-0.1.0/tests/test_requirements.py +565 -0
- ftl2-0.1.0/tests/test_runners.py +377 -0
- ftl2-0.1.0/tests/test_shadowed_modules.py +315 -0
- ftl2-0.1.0/tests/test_ssh.py +521 -0
- ftl2-0.1.0/tests/test_ssh_integration.py +305 -0
- ftl2-0.1.0/tests/test_state.py +374 -0
- ftl2-0.1.0/tests/test_swap_module.py +282 -0
- ftl2-0.1.0/tests/test_types.py +233 -0
- ftl2-0.1.0/tests/test_utils.py +237 -0
- ftl2-0.1.0/tests/test_version.py +13 -0
- ftl2-0.1.0/tools/ftl2_state_to_inventory.py +101 -0
- ftl2-0.1.0/tools/gate_debug.py +202 -0
- ftl2-0.1.0/tools/gate_debug_remote.py +320 -0
|
@@ -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
|
ftl2-0.1.0/CHANGELOG.md
ADDED
|
@@ -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.)
|