cloudfall 0.2.0__py3-none-any.whl
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.
- cloudfall/__init__.py +19 -0
- cloudfall/__main__.py +5 -0
- cloudfall/_bundled/schemas/v1/alert-rule.schema.json +63 -0
- cloudfall/_bundled/schemas/v1/application.schema.json +60 -0
- cloudfall/_bundled/schemas/v1/artifact.schema.json +71 -0
- cloudfall/_bundled/schemas/v1/backup-receipt.schema.json +58 -0
- cloudfall/_bundled/schemas/v1/common.schema.json +115 -0
- cloudfall/_bundled/schemas/v1/component.schema.json +234 -0
- cloudfall/_bundled/schemas/v1/deployment-receipt.schema.json +53 -0
- cloudfall/_bundled/schemas/v1/domain.schema.json +208 -0
- cloudfall/_bundled/schemas/v1/environment-receipt.schema.json +55 -0
- cloudfall/_bundled/schemas/v1/logging-stack.schema.json +473 -0
- cloudfall/_bundled/schemas/v1/observed-domain.schema.json +158 -0
- cloudfall/_bundled/schemas/v1/observed-server.schema.json +627 -0
- cloudfall/_bundled/schemas/v1/operator-policy.schema.json +97 -0
- cloudfall/_bundled/schemas/v1/operator-proposal.schema.json +253 -0
- cloudfall/_bundled/schemas/v1/release-receipt.schema.json +66 -0
- cloudfall/_bundled/schemas/v1/server-type.schema.json +289 -0
- cloudfall/_bundled/schemas/v1/server.schema.json +146 -0
- cloudfall/_bundled/schemas/v1/service.schema.json +222 -0
- cloudfall/_bundled/schemas/v1/ssh-public-key.schema.json +55 -0
- cloudfall/agent_tools.py +868 -0
- cloudfall/arguments.py +92 -0
- cloudfall/audit.py +905 -0
- cloudfall/authoring.py +341 -0
- cloudfall/cli.py +1748 -0
- cloudfall/commands.py +257 -0
- cloudfall/cutover.py +370 -0
- cloudfall/dashboard.py +407 -0
- cloudfall/dashboard_server.py +201 -0
- cloudfall/domain.py +1157 -0
- cloudfall/importer.py +906 -0
- cloudfall/inventory.py +2116 -0
- cloudfall/lifecycle.py +874 -0
- cloudfall/mcp_server.py +704 -0
- cloudfall/migrate.py +730 -0
- cloudfall/observation.py +143 -0
- cloudfall/operations.py +1228 -0
- cloudfall/operator.py +1099 -0
- cloudfall/project.py +841 -0
- cloudfall/py.typed +0 -0
- cloudfall/render_api.py +338 -0
- cloudfall/resources.py +67 -0
- cloudfall/secrets.py +262 -0
- cloudfall/service_evidence.py +659 -0
- cloudfall/validation.py +1340 -0
- cloudfall-0.2.0.dist-info/METADATA +408 -0
- cloudfall-0.2.0.dist-info/RECORD +165 -0
- cloudfall-0.2.0.dist-info/WHEEL +4 -0
- cloudfall-0.2.0.dist-info/entry_points.txt +4 -0
- cloudfall-0.2.0.dist-info/licenses/LICENSE +661 -0
- cloudfall_engine/__init__.py +5 -0
- cloudfall_engine/__main__.py +5 -0
- cloudfall_engine/_bundled/ansible/ansible.cfg +7 -0
- cloudfall_engine/_bundled/ansible/playbooks/backup.yml +86 -0
- cloudfall_engine/_bundled/ansible/playbooks/baseline.yml +35 -0
- cloudfall_engine/_bundled/ansible/playbooks/bootstrap.yml +24 -0
- cloudfall_engine/_bundled/ansible/playbooks/data.yml +28 -0
- cloudfall_engine/_bundled/ansible/playbooks/deploy.yml +35 -0
- cloudfall_engine/_bundled/ansible/playbooks/domains.yml +21 -0
- cloudfall_engine/_bundled/ansible/playbooks/health.yml +31 -0
- cloudfall_engine/_bundled/ansible/playbooks/inspect.yml +15 -0
- cloudfall_engine/_bundled/ansible/playbooks/logging.yml +35 -0
- cloudfall_engine/_bundled/ansible/playbooks/restart.yml +31 -0
- cloudfall_engine/_bundled/ansible/playbooks/rollback.yml +33 -0
- cloudfall_engine/_bundled/ansible/playbooks/services.yml +37 -0
- cloudfall_engine/_bundled/ansible/playbooks/time.yml +9 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/defaults/main.yml +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/handlers/main.yml +5 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/meta/argument_specs.yml +34 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_access/tasks/main.yml +59 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/defaults/main.yml +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +52 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_bootstrap/tasks/main.yml +99 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/defaults/main.yml +15 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +36 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_data_migration/tasks/main.yml +201 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/defaults/main.yml +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +125 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/health.yml +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +37 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/main.yml +285 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/restart.yml +17 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/tasks/rollback.yml +52 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_deploy/templates/component.service.j2 +24 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/defaults/main.yml +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/handlers/main.yml +5 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +33 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/tasks/main.yml +51 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +31 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/defaults/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +24 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_inspect/tasks/main.yml +643 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/defaults/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/handlers/main.yml +29 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +227 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/meta/main.yml +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/tasks/main.yml +487 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +33 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +20 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +49 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +45 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +20 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +24 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/defaults/main.yml +7 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/handlers/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +122 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/meta/main.yml +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/tasks/main.yml +224 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +127 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/defaults/main.yml +4 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/handlers/main.yml +5 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +69 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/tasks/main.yml +212 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +54 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/defaults/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/handlers/main.yml +9 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +91 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/tasks/main.yml +295 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +17 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +27 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/defaults/main.yml +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/handlers/main.yml +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/meta/argument_specs.yml +68 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/tasks/main.yml +148 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.service.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +14 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +6 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +11 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +15 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_time/defaults/main.yml +2 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_time/meta/argument_specs.yml +10 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_time/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_time/tasks/main.yml +119 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +3 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +8 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +13 -0
- cloudfall_engine/_bundled/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +37 -0
- cloudfall_engine/ansible_inventory.py +253 -0
- cloudfall_engine/artifact.py +258 -0
- cloudfall_engine/cli.py +273 -0
- cloudfall_engine/playbook.py +175 -0
- cloudfall_engine/py.typed +0 -0
cloudfall_engine/cli.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Command-line boundary for the Cloudfall execution engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from cloudfall.arguments import (
|
|
12
|
+
StrictArgumentParser,
|
|
13
|
+
parse_arguments,
|
|
14
|
+
project_path_argument,
|
|
15
|
+
)
|
|
16
|
+
from cloudfall.inventory import PlatformInventory
|
|
17
|
+
from cloudfall.project import (
|
|
18
|
+
PROJECT_DIRECTORY_VARIABLE,
|
|
19
|
+
ProjectError,
|
|
20
|
+
project_context,
|
|
21
|
+
)
|
|
22
|
+
from cloudfall.resources import default_engine_directory, default_schema_directory
|
|
23
|
+
from cloudfall.validation import ConfigValidationError, validate_config
|
|
24
|
+
|
|
25
|
+
from cloudfall_engine.ansible_inventory import render_ansible_inventory
|
|
26
|
+
from cloudfall_engine.artifact import ArtifactBuildError, build_artifact
|
|
27
|
+
from cloudfall_engine.playbook import (
|
|
28
|
+
PlaybookError,
|
|
29
|
+
PlaybookRun,
|
|
30
|
+
bundled_playbooks,
|
|
31
|
+
execute_playbook,
|
|
32
|
+
resolve_playbook,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
import argparse
|
|
37
|
+
from collections.abc import Sequence
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _add_project_directory_argument(parser: argparse.ArgumentParser) -> None:
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--project",
|
|
43
|
+
type=Path,
|
|
44
|
+
default=None,
|
|
45
|
+
help=(
|
|
46
|
+
f"project directory to run in (default: ${PROJECT_DIRECTORY_VARIABLE}, "
|
|
47
|
+
"else the current directory when it is a project)"
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parser() -> StrictArgumentParser:
|
|
53
|
+
parser = StrictArgumentParser(prog="cloudfall-engine")
|
|
54
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
55
|
+
|
|
56
|
+
inventory_parser = commands.add_parser(
|
|
57
|
+
"inventory", help="generate execution inventory"
|
|
58
|
+
)
|
|
59
|
+
inventory_commands = inventory_parser.add_subparsers(
|
|
60
|
+
dest="inventory_command", required=True
|
|
61
|
+
)
|
|
62
|
+
render_parser = inventory_commands.add_parser(
|
|
63
|
+
"render", help="render Ansible JSON inventory"
|
|
64
|
+
)
|
|
65
|
+
_add_project_directory_argument(render_parser)
|
|
66
|
+
render_parser.add_argument(
|
|
67
|
+
"--schemas",
|
|
68
|
+
type=Path,
|
|
69
|
+
default=default_schema_directory(),
|
|
70
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
71
|
+
)
|
|
72
|
+
render_parser.add_argument(
|
|
73
|
+
"--output",
|
|
74
|
+
type=project_path_argument,
|
|
75
|
+
help="write inventory JSON to this file instead of stdout",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
artifact_parser = commands.add_parser("artifact", help="build release artifacts")
|
|
79
|
+
artifact_commands = artifact_parser.add_subparsers(
|
|
80
|
+
dest="artifact_command", required=True
|
|
81
|
+
)
|
|
82
|
+
build_parser = artifact_commands.add_parser(
|
|
83
|
+
"build", help="clone, package, and hash one component release"
|
|
84
|
+
)
|
|
85
|
+
_add_project_directory_argument(build_parser)
|
|
86
|
+
build_parser.add_argument("component")
|
|
87
|
+
build_parser.add_argument(
|
|
88
|
+
"--ref",
|
|
89
|
+
required=True,
|
|
90
|
+
help="git ref (branch, tag, or commit) to package",
|
|
91
|
+
)
|
|
92
|
+
build_parser.add_argument(
|
|
93
|
+
"--schemas",
|
|
94
|
+
type=Path,
|
|
95
|
+
default=default_schema_directory(),
|
|
96
|
+
help="versioned schema directory (default: bundled schemas)",
|
|
97
|
+
)
|
|
98
|
+
build_parser.add_argument(
|
|
99
|
+
"--output-dir",
|
|
100
|
+
type=project_path_argument,
|
|
101
|
+
default=Path("tmp/artifacts"),
|
|
102
|
+
help="artifact output directory (default: tmp/artifacts)",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
playbook_parser = commands.add_parser(
|
|
106
|
+
"playbook", help="run Ansible playbooks with the engine configuration"
|
|
107
|
+
)
|
|
108
|
+
playbook_commands = playbook_parser.add_subparsers(
|
|
109
|
+
dest="playbook_command", required=True
|
|
110
|
+
)
|
|
111
|
+
list_parser = playbook_commands.add_parser(
|
|
112
|
+
"list", help="list the playbooks bundled with the engine"
|
|
113
|
+
)
|
|
114
|
+
_add_engine_argument(list_parser)
|
|
115
|
+
run_parser = playbook_commands.add_parser(
|
|
116
|
+
"run", help="run one bundled playbook or a playbook file"
|
|
117
|
+
)
|
|
118
|
+
run_parser.add_argument(
|
|
119
|
+
"playbook",
|
|
120
|
+
help="bundled playbook name (see `playbook list`) or a playbook path",
|
|
121
|
+
)
|
|
122
|
+
_add_engine_argument(run_parser)
|
|
123
|
+
run_parser.add_argument(
|
|
124
|
+
"--inventory",
|
|
125
|
+
type=project_path_argument,
|
|
126
|
+
default=Path("tmp/ansible-inventory.json"),
|
|
127
|
+
help="rendered inventory path (default: tmp/ansible-inventory.json)",
|
|
128
|
+
)
|
|
129
|
+
run_parser.add_argument(
|
|
130
|
+
"--roles",
|
|
131
|
+
type=Path,
|
|
132
|
+
action="append",
|
|
133
|
+
default=[],
|
|
134
|
+
help="role directory searched before the bundled roles (repeatable)",
|
|
135
|
+
)
|
|
136
|
+
run_parser.add_argument(
|
|
137
|
+
"--extra-vars",
|
|
138
|
+
action="append",
|
|
139
|
+
default=[],
|
|
140
|
+
help="passed through to ansible-playbook unchanged (repeatable)",
|
|
141
|
+
)
|
|
142
|
+
run_parser.add_argument(
|
|
143
|
+
"--tags",
|
|
144
|
+
action="append",
|
|
145
|
+
default=[],
|
|
146
|
+
help="only run plays and tasks tagged with this value (repeatable)",
|
|
147
|
+
)
|
|
148
|
+
run_parser.add_argument("--limit", help="restrict the run to a host pattern")
|
|
149
|
+
run_parser.add_argument("--check", action="store_true", help="run in check mode")
|
|
150
|
+
run_parser.add_argument("--diff", action="store_true", help="show file diffs")
|
|
151
|
+
run_parser.add_argument(
|
|
152
|
+
"--syntax-check",
|
|
153
|
+
action="store_true",
|
|
154
|
+
help="only check the playbook syntax",
|
|
155
|
+
)
|
|
156
|
+
return parser
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _add_engine_argument(parser: argparse.ArgumentParser) -> None:
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--engine",
|
|
162
|
+
type=Path,
|
|
163
|
+
default=default_engine_directory(),
|
|
164
|
+
help="engine directory containing ansible contracts (default: bundled engine)",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _render_inventory(arguments: argparse.Namespace) -> int:
|
|
169
|
+
try:
|
|
170
|
+
state = validate_config(
|
|
171
|
+
Path(arguments.project_directory), Path(arguments.schemas)
|
|
172
|
+
)
|
|
173
|
+
except ConfigValidationError as error:
|
|
174
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
175
|
+
return 2
|
|
176
|
+
|
|
177
|
+
inventory = PlatformInventory.from_state(state)
|
|
178
|
+
rendered = render_ansible_inventory(inventory)
|
|
179
|
+
serialized = f"{json.dumps(rendered, sort_keys=True)}\n"
|
|
180
|
+
if arguments.output is None:
|
|
181
|
+
sys.stdout.write(serialized)
|
|
182
|
+
else:
|
|
183
|
+
output_path = Path(arguments.output)
|
|
184
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
output_path.write_text(serialized, encoding="utf-8")
|
|
186
|
+
result = {"output": str(output_path), "status": "ok"}
|
|
187
|
+
sys.stdout.write(f"{json.dumps(result, sort_keys=True)}\n")
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _build_artifact(arguments: argparse.Namespace) -> int:
|
|
192
|
+
try:
|
|
193
|
+
state = validate_config(
|
|
194
|
+
Path(arguments.project_directory), Path(arguments.schemas)
|
|
195
|
+
)
|
|
196
|
+
except ConfigValidationError as error:
|
|
197
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
198
|
+
return 2
|
|
199
|
+
|
|
200
|
+
inventory = PlatformInventory.from_state(state)
|
|
201
|
+
try:
|
|
202
|
+
built = build_artifact(
|
|
203
|
+
inventory,
|
|
204
|
+
arguments.component,
|
|
205
|
+
arguments.ref,
|
|
206
|
+
Path(arguments.output_dir),
|
|
207
|
+
Path(arguments.schemas),
|
|
208
|
+
)
|
|
209
|
+
except ArtifactBuildError as error:
|
|
210
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
211
|
+
return 2
|
|
212
|
+
except ConfigValidationError as error:
|
|
213
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
214
|
+
return 2
|
|
215
|
+
sys.stdout.write(f"{json.dumps(built.as_dict(), sort_keys=True)}\n")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _list_playbooks(arguments: argparse.Namespace) -> int:
|
|
220
|
+
try:
|
|
221
|
+
names = bundled_playbooks(Path(arguments.engine))
|
|
222
|
+
except PlaybookError as error:
|
|
223
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
224
|
+
return 2
|
|
225
|
+
result = {"engine": str(arguments.engine), "playbooks": list(names)}
|
|
226
|
+
sys.stdout.write(f"{json.dumps(result, sort_keys=True)}\n")
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _run_playbook(arguments: argparse.Namespace) -> int:
|
|
231
|
+
engine_directory = Path(arguments.engine)
|
|
232
|
+
try:
|
|
233
|
+
run = PlaybookRun(
|
|
234
|
+
playbook=resolve_playbook(arguments.playbook, engine_directory),
|
|
235
|
+
inventory_file=Path(arguments.inventory),
|
|
236
|
+
role_directories=tuple(Path(role) for role in arguments.roles),
|
|
237
|
+
extra_vars=tuple(arguments.extra_vars),
|
|
238
|
+
tags=tuple(arguments.tags),
|
|
239
|
+
limit=arguments.limit,
|
|
240
|
+
check=arguments.check,
|
|
241
|
+
diff=arguments.diff,
|
|
242
|
+
syntax_check=arguments.syntax_check,
|
|
243
|
+
)
|
|
244
|
+
return execute_playbook(run, engine_directory)
|
|
245
|
+
except PlaybookError as error:
|
|
246
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
247
|
+
return 2
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
251
|
+
"""Run one engine command and return a process exit code."""
|
|
252
|
+
arguments = parse_arguments(_parser(), argv)
|
|
253
|
+
if arguments.command == "playbook":
|
|
254
|
+
if arguments.playbook_command == "list":
|
|
255
|
+
return _list_playbooks(arguments)
|
|
256
|
+
return _run_playbook(arguments)
|
|
257
|
+
try:
|
|
258
|
+
with project_context(arguments.project, os.environ) as project_directory:
|
|
259
|
+
arguments.project_directory = project_directory
|
|
260
|
+
if arguments.command == "inventory":
|
|
261
|
+
return _render_inventory(arguments)
|
|
262
|
+
if arguments.command == "artifact":
|
|
263
|
+
return _build_artifact(arguments)
|
|
264
|
+
except ProjectError as error:
|
|
265
|
+
sys.stderr.write(f"{json.dumps(error.as_dict(), sort_keys=True)}\n")
|
|
266
|
+
return 2
|
|
267
|
+
message = "argparse accepted an unsupported engine command"
|
|
268
|
+
raise RuntimeError(message)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def run() -> None:
|
|
272
|
+
"""Installed engine console-script entry point."""
|
|
273
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Run Ansible playbooks under the engine's configuration.
|
|
2
|
+
|
|
3
|
+
A project keeps its own playbooks and roles next to its config.
|
|
4
|
+
This module runs those, and the bundled engine playbooks, with the
|
|
5
|
+
engine's ``ansible.cfg`` and a role search path that always ends with the
|
|
6
|
+
bundled roles, so project playbooks may reuse them without a checkout.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_PLAYBOOK_SUFFIX = ".yml"
|
|
18
|
+
_ERROR_PLAYBOOK_MISSING = "playbook_missing"
|
|
19
|
+
_ERROR_INVENTORY_MISSING = "inventory_missing"
|
|
20
|
+
_ERROR_ROLES_MISSING = "roles_missing"
|
|
21
|
+
_ERROR_ANSIBLE_MISSING = "ansible_missing"
|
|
22
|
+
_ERROR_ARGUMENT = "argument_invalid"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PlaybookError(RuntimeError):
|
|
26
|
+
"""Raised when a playbook run cannot be prepared."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, code: str, detail: str) -> None:
|
|
29
|
+
"""Record a machine-readable code alongside the human detail."""
|
|
30
|
+
super().__init__(detail)
|
|
31
|
+
self.code = code
|
|
32
|
+
self.detail = detail
|
|
33
|
+
|
|
34
|
+
def as_dict(self) -> dict[str, object]:
|
|
35
|
+
"""Return the error in the CLI's JSON error envelope."""
|
|
36
|
+
return {
|
|
37
|
+
"error": {"code": self.code, "message": self.detail},
|
|
38
|
+
"status": "error",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def playbook_directory(engine_directory: Path) -> Path:
|
|
43
|
+
"""Return the directory holding the engine's bundled playbooks."""
|
|
44
|
+
return engine_directory / "ansible" / "playbooks"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def bundled_playbooks(engine_directory: Path) -> tuple[str, ...]:
|
|
48
|
+
"""Return the names of every bundled playbook, sorted."""
|
|
49
|
+
directory = playbook_directory(engine_directory)
|
|
50
|
+
if not directory.is_dir():
|
|
51
|
+
detail = f"engine playbook directory does not exist: {directory}"
|
|
52
|
+
raise PlaybookError(_ERROR_PLAYBOOK_MISSING, detail)
|
|
53
|
+
return tuple(
|
|
54
|
+
sorted(
|
|
55
|
+
path.stem
|
|
56
|
+
for path in directory.iterdir()
|
|
57
|
+
if path.is_file() and path.suffix == _PLAYBOOK_SUFFIX
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resolve_playbook(reference: str, engine_directory: Path) -> Path:
|
|
63
|
+
"""Resolve a bare name inside the engine, or a path as given.
|
|
64
|
+
|
|
65
|
+
``inspect`` and ``inspect.yml`` both name the bundled playbook; anything
|
|
66
|
+
containing a path separator is a playbook file outside the engine.
|
|
67
|
+
"""
|
|
68
|
+
if not reference:
|
|
69
|
+
detail = "playbook reference must not be empty"
|
|
70
|
+
raise PlaybookError(_ERROR_ARGUMENT, detail)
|
|
71
|
+
candidate = Path(reference)
|
|
72
|
+
if len(candidate.parts) == 1:
|
|
73
|
+
name = reference
|
|
74
|
+
if candidate.suffix != _PLAYBOOK_SUFFIX:
|
|
75
|
+
name = f"{reference}{_PLAYBOOK_SUFFIX}"
|
|
76
|
+
candidate = playbook_directory(engine_directory) / name
|
|
77
|
+
if not candidate.is_file():
|
|
78
|
+
detail = f"playbook does not exist: {candidate}"
|
|
79
|
+
raise PlaybookError(_ERROR_PLAYBOOK_MISSING, detail)
|
|
80
|
+
return candidate
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class PlaybookRun:
|
|
85
|
+
"""One validated ``ansible-playbook`` invocation."""
|
|
86
|
+
|
|
87
|
+
playbook: Path
|
|
88
|
+
inventory_file: Path
|
|
89
|
+
role_directories: tuple[Path, ...] = ()
|
|
90
|
+
extra_vars: tuple[str, ...] = ()
|
|
91
|
+
tags: tuple[str, ...] = ()
|
|
92
|
+
limit: str | None = None
|
|
93
|
+
check: bool = False
|
|
94
|
+
diff: bool = False
|
|
95
|
+
syntax_check: bool = False
|
|
96
|
+
|
|
97
|
+
def __post_init__(self) -> None:
|
|
98
|
+
"""Reject inputs Ansible would only fail on later."""
|
|
99
|
+
if not self.playbook.is_file():
|
|
100
|
+
detail = f"playbook does not exist: {self.playbook}"
|
|
101
|
+
raise PlaybookError(_ERROR_PLAYBOOK_MISSING, detail)
|
|
102
|
+
if not self.inventory_file.is_file():
|
|
103
|
+
detail = f"inventory file does not exist: {self.inventory_file}"
|
|
104
|
+
raise PlaybookError(_ERROR_INVENTORY_MISSING, detail)
|
|
105
|
+
for directory in self.role_directories:
|
|
106
|
+
if not directory.is_dir():
|
|
107
|
+
detail = f"role directory does not exist: {directory}"
|
|
108
|
+
raise PlaybookError(_ERROR_ROLES_MISSING, detail)
|
|
109
|
+
if self.limit is not None and not self.limit:
|
|
110
|
+
detail = "limit must not be empty"
|
|
111
|
+
raise PlaybookError(_ERROR_ARGUMENT, detail)
|
|
112
|
+
if any(not value for value in self.extra_vars):
|
|
113
|
+
detail = "extra vars must not be empty"
|
|
114
|
+
raise PlaybookError(_ERROR_ARGUMENT, detail)
|
|
115
|
+
if any(not value for value in self.tags):
|
|
116
|
+
detail = "tags must not be empty"
|
|
117
|
+
raise PlaybookError(_ERROR_ARGUMENT, detail)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True, slots=True)
|
|
121
|
+
class PlaybookCommand:
|
|
122
|
+
"""The process a playbook run becomes."""
|
|
123
|
+
|
|
124
|
+
argv: tuple[str, ...]
|
|
125
|
+
environment: dict[str, str]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def playbook_command(run: PlaybookRun, engine_directory: Path) -> PlaybookCommand:
|
|
129
|
+
"""Translate a run into ``ansible-playbook`` arguments and environment."""
|
|
130
|
+
binary = shutil.which("ansible-playbook")
|
|
131
|
+
if binary is None:
|
|
132
|
+
detail = "ansible-playbook is not installed on this controller"
|
|
133
|
+
raise PlaybookError(_ERROR_ANSIBLE_MISSING, detail)
|
|
134
|
+
ansible_directory = engine_directory / "ansible"
|
|
135
|
+
configuration = ansible_directory / "ansible.cfg"
|
|
136
|
+
bundled_roles = ansible_directory / "roles"
|
|
137
|
+
for required in (configuration, bundled_roles):
|
|
138
|
+
if not required.exists():
|
|
139
|
+
detail = f"engine is incomplete, missing {required}"
|
|
140
|
+
raise PlaybookError(_ERROR_PLAYBOOK_MISSING, detail)
|
|
141
|
+
|
|
142
|
+
argv: list[str] = [binary, "--inventory", str(run.inventory_file)]
|
|
143
|
+
if run.check:
|
|
144
|
+
argv.append("--check")
|
|
145
|
+
if run.diff:
|
|
146
|
+
argv.append("--diff")
|
|
147
|
+
if run.syntax_check:
|
|
148
|
+
argv.append("--syntax-check")
|
|
149
|
+
if run.limit is not None:
|
|
150
|
+
argv.extend(("--limit", run.limit))
|
|
151
|
+
for value in run.extra_vars:
|
|
152
|
+
argv.extend(("--extra-vars", value))
|
|
153
|
+
if run.tags:
|
|
154
|
+
argv.extend(("--tags", ",".join(run.tags)))
|
|
155
|
+
argv.append(str(run.playbook))
|
|
156
|
+
|
|
157
|
+
roles = (*run.role_directories, bundled_roles)
|
|
158
|
+
environment = {
|
|
159
|
+
"ANSIBLE_CONFIG": str(configuration.resolve()),
|
|
160
|
+
"ANSIBLE_ROLES_PATH": os.pathsep.join(
|
|
161
|
+
str(directory.resolve()) for directory in roles
|
|
162
|
+
),
|
|
163
|
+
}
|
|
164
|
+
return PlaybookCommand(argv=tuple(argv), environment=environment)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def execute_playbook(run: PlaybookRun, engine_directory: Path) -> int:
|
|
168
|
+
"""Run the playbook in the foreground and return its exit code."""
|
|
169
|
+
command = playbook_command(run, engine_directory)
|
|
170
|
+
completed = subprocess.run( # noqa: S603 - fixed binary, validated args.
|
|
171
|
+
command.argv,
|
|
172
|
+
env={**os.environ, **command.environment},
|
|
173
|
+
check=False,
|
|
174
|
+
)
|
|
175
|
+
return completed.returncode
|
|
File without changes
|