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/authoring.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Write fleet resources into a project.
|
|
2
|
+
|
|
3
|
+
``cloudfall add`` turns the first minutes after ``cloudfall init`` into three
|
|
4
|
+
commands instead of three hand-written files: an SSH public key read from
|
|
5
|
+
disk, a server type from the bundled Debian baseline, and a server that
|
|
6
|
+
references it. Every document is schema-validated before it is written, the
|
|
7
|
+
whole project is validated afterwards, and a failed validation removes what
|
|
8
|
+
was written so the project never holds a resource it would reject.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from ipaddress import ip_address
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from cloudfall.domain import (
|
|
20
|
+
ConnectionAddress,
|
|
21
|
+
Hostname,
|
|
22
|
+
LinuxUser,
|
|
23
|
+
OpenSshPublicKey,
|
|
24
|
+
ResourceId,
|
|
25
|
+
ResourceKind,
|
|
26
|
+
TcpPort,
|
|
27
|
+
)
|
|
28
|
+
from cloudfall.validation import ConfigValidationError, SchemaCatalog, validate_config
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Sequence
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
API_VERSION = "cloudfall/v1"
|
|
35
|
+
|
|
36
|
+
BASELINE_PACKAGES: tuple[str, ...] = (
|
|
37
|
+
"acl",
|
|
38
|
+
"ca-certificates",
|
|
39
|
+
"curl",
|
|
40
|
+
"git",
|
|
41
|
+
"rsync",
|
|
42
|
+
"unzip",
|
|
43
|
+
)
|
|
44
|
+
"""Packages the Cloudfall baseline installs on every managed host."""
|
|
45
|
+
|
|
46
|
+
BASELINE_ROOT_MINIMUM_BYTES = 20_000_000_000
|
|
47
|
+
"""Smallest root filesystem the baseline server type accepts (20 GB)."""
|
|
48
|
+
|
|
49
|
+
_ERROR_KEY_FILE_MISSING = "ssh_key_file_missing"
|
|
50
|
+
_ERROR_KEY_FILE_INVALID = "ssh_key_file_invalid"
|
|
51
|
+
_ERROR_RESOURCE_EXISTS = "resource_exists"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AuthoringError(RuntimeError):
|
|
55
|
+
"""Fail-fast authoring error with a stable machine-readable code."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, code: str, message: str) -> None:
|
|
58
|
+
"""Record the failure code and human-readable detail."""
|
|
59
|
+
self.code = code
|
|
60
|
+
self.detail = message
|
|
61
|
+
super().__init__(f"{code}: {message}")
|
|
62
|
+
|
|
63
|
+
def as_dict(self) -> dict[str, object]:
|
|
64
|
+
"""Serialize the error envelope for system boundaries."""
|
|
65
|
+
return {
|
|
66
|
+
"status": "error",
|
|
67
|
+
"error": {"code": self.code, "message": self.detail},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class AddedResource:
|
|
73
|
+
"""One resource file written into the project."""
|
|
74
|
+
|
|
75
|
+
kind: ResourceKind
|
|
76
|
+
resource_id: ResourceId
|
|
77
|
+
path: str
|
|
78
|
+
|
|
79
|
+
def as_dict(self) -> dict[str, object]:
|
|
80
|
+
"""Serialize for the CLI and agent envelope."""
|
|
81
|
+
return {"kind": self.kind.value, "id": str(self.resource_id), "path": self.path}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class AddResult:
|
|
86
|
+
"""Outcome of one ``cloudfall add`` invocation."""
|
|
87
|
+
|
|
88
|
+
project: Path
|
|
89
|
+
added: tuple[AddedResource, ...]
|
|
90
|
+
|
|
91
|
+
def as_dict(self) -> dict[str, object]:
|
|
92
|
+
"""Serialize for system boundaries."""
|
|
93
|
+
return {
|
|
94
|
+
"status": "ok",
|
|
95
|
+
"project": str(self.project),
|
|
96
|
+
"added": [resource.as_dict() for resource in self.added],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class SshKeyOptions:
|
|
102
|
+
"""Inputs for ``cloudfall add ssh-key``."""
|
|
103
|
+
|
|
104
|
+
key_path: Path
|
|
105
|
+
owner: ResourceId
|
|
106
|
+
environment: ResourceId
|
|
107
|
+
resource_id: ResourceId | None = None
|
|
108
|
+
description: str | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class ServerTypeOptions:
|
|
113
|
+
"""Inputs for ``cloudfall add server-type``."""
|
|
114
|
+
|
|
115
|
+
resource_id: ResourceId
|
|
116
|
+
description: str | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class ServerOptions:
|
|
121
|
+
"""Inputs for ``cloudfall add server``."""
|
|
122
|
+
|
|
123
|
+
resource_id: ResourceId
|
|
124
|
+
address: ConnectionAddress
|
|
125
|
+
server_type: ResourceId
|
|
126
|
+
environment: ResourceId
|
|
127
|
+
ssh_user: LinuxUser
|
|
128
|
+
ssh_port: TcpPort
|
|
129
|
+
hostname: Hostname | None = None
|
|
130
|
+
description: str | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_public_key(key_path: Path) -> OpenSshPublicKey:
|
|
134
|
+
"""Read one OpenSSH public key from a file such as ``~/.ssh/id_ed25519.pub``."""
|
|
135
|
+
if not key_path.is_file():
|
|
136
|
+
message = f"SSH public key file does not exist: {key_path}"
|
|
137
|
+
raise AuthoringError(_ERROR_KEY_FILE_MISSING, message)
|
|
138
|
+
lines = [
|
|
139
|
+
line
|
|
140
|
+
for line in key_path.read_text(encoding="utf-8").splitlines()
|
|
141
|
+
if line.strip()
|
|
142
|
+
]
|
|
143
|
+
if len(lines) != 1:
|
|
144
|
+
message = f"SSH public key file must hold exactly one key: {key_path}"
|
|
145
|
+
raise AuthoringError(_ERROR_KEY_FILE_INVALID, message)
|
|
146
|
+
try:
|
|
147
|
+
return OpenSshPublicKey.from_boundary(lines[0].strip())
|
|
148
|
+
except (TypeError, ValueError) as error:
|
|
149
|
+
message = f"{key_path}: {error}"
|
|
150
|
+
raise AuthoringError(_ERROR_KEY_FILE_INVALID, message) from error
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def ssh_key_document(
|
|
154
|
+
options: SshKeyOptions, key: OpenSshPublicKey
|
|
155
|
+
) -> dict[str, object]:
|
|
156
|
+
"""Build the ``SshPublicKey`` resource for one key."""
|
|
157
|
+
resource_id = options.resource_id or options.owner
|
|
158
|
+
description = options.description or f"SSH public key of {options.owner}"
|
|
159
|
+
return {
|
|
160
|
+
"apiVersion": API_VERSION,
|
|
161
|
+
"kind": ResourceKind.SSH_PUBLIC_KEY.value,
|
|
162
|
+
"metadata": {"id": str(resource_id), "description": description},
|
|
163
|
+
"spec": {
|
|
164
|
+
"owner": str(options.owner),
|
|
165
|
+
"environment": str(options.environment),
|
|
166
|
+
"publicKey": key.value,
|
|
167
|
+
"lifecycle": "active",
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def baseline_server_type_document(options: ServerTypeOptions) -> dict[str, object]:
|
|
173
|
+
"""Build the bundled baseline: a plain Debian 13 host without software RAID."""
|
|
174
|
+
description = options.description or (
|
|
175
|
+
"Debian 13 host with the Cloudfall baseline and no software RAID"
|
|
176
|
+
)
|
|
177
|
+
return {
|
|
178
|
+
"apiVersion": API_VERSION,
|
|
179
|
+
"kind": ResourceKind.HOST_PROFILE.value,
|
|
180
|
+
"metadata": {"id": str(options.resource_id), "description": description},
|
|
181
|
+
"spec": {
|
|
182
|
+
"os": {
|
|
183
|
+
"distribution": "Debian",
|
|
184
|
+
"versions": ["13"],
|
|
185
|
+
"serviceManager": "systemd",
|
|
186
|
+
},
|
|
187
|
+
"storage": {
|
|
188
|
+
"mounts": [
|
|
189
|
+
{
|
|
190
|
+
"path": "/",
|
|
191
|
+
"filesystem": "ext4",
|
|
192
|
+
"minimumBytes": BASELINE_ROOT_MINIMUM_BYTES,
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
"packages": {
|
|
197
|
+
"required": [{"name": name} for name in BASELINE_PACKAGES],
|
|
198
|
+
"forbidden": ["telnetd"],
|
|
199
|
+
},
|
|
200
|
+
"services": {
|
|
201
|
+
"required": [
|
|
202
|
+
{"name": "ssh.service", "state": "running", "status": "enabled"},
|
|
203
|
+
{
|
|
204
|
+
"name": "apt-daily.timer",
|
|
205
|
+
"state": "running",
|
|
206
|
+
"status": "enabled",
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
"firewall": {
|
|
211
|
+
"policy": "default-deny",
|
|
212
|
+
"allowedInbound": [
|
|
213
|
+
{"port": 22, "protocol": "tcp", "description": "ssh"},
|
|
214
|
+
{"port": 80, "protocol": "tcp", "description": "http"},
|
|
215
|
+
{"port": 443, "protocol": "tcp", "description": "https"},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
"configuration": {
|
|
219
|
+
"files": [
|
|
220
|
+
{
|
|
221
|
+
"path": "/etc/ssh/sshd_config",
|
|
222
|
+
"capture": "hash",
|
|
223
|
+
"owner": "root",
|
|
224
|
+
"group": "root",
|
|
225
|
+
"mode": "0644",
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _is_ip_address(value: str) -> bool:
|
|
234
|
+
try:
|
|
235
|
+
ip_address(value)
|
|
236
|
+
except ValueError:
|
|
237
|
+
return False
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def server_document(options: ServerOptions) -> dict[str, object]:
|
|
242
|
+
"""Build the ``Server`` resource for one host."""
|
|
243
|
+
if options.hostname is not None:
|
|
244
|
+
hostname = options.hostname.value
|
|
245
|
+
elif _is_ip_address(options.address.value):
|
|
246
|
+
hostname = str(options.resource_id)
|
|
247
|
+
else:
|
|
248
|
+
hostname = Hostname(options.address.value).value
|
|
249
|
+
metadata: dict[str, object] = {"id": str(options.resource_id)}
|
|
250
|
+
if options.description is not None:
|
|
251
|
+
metadata["description"] = options.description
|
|
252
|
+
return {
|
|
253
|
+
"apiVersion": API_VERSION,
|
|
254
|
+
"kind": ResourceKind.SERVER.value,
|
|
255
|
+
"metadata": metadata,
|
|
256
|
+
"spec": {
|
|
257
|
+
"hostname": hostname,
|
|
258
|
+
"address": options.address.value,
|
|
259
|
+
"environment": str(options.environment),
|
|
260
|
+
"serverType": str(options.server_type),
|
|
261
|
+
"lifecycle": "active",
|
|
262
|
+
"ssh": {"user": options.ssh_user.value, "port": options.ssh_port.value},
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def add_ssh_key(
|
|
268
|
+
project: Path, options: SshKeyOptions, schema_directory: Path
|
|
269
|
+
) -> AddResult:
|
|
270
|
+
"""Write one ``SshPublicKey`` read from a key file."""
|
|
271
|
+
key = read_public_key(options.key_path)
|
|
272
|
+
document = ssh_key_document(options, key)
|
|
273
|
+
return _commit(project, schema_directory, [(ResourceKind.SSH_PUBLIC_KEY, document)])
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def add_server_type(
|
|
277
|
+
project: Path, options: ServerTypeOptions, schema_directory: Path
|
|
278
|
+
) -> AddResult:
|
|
279
|
+
"""Write one baseline ``ServerType``."""
|
|
280
|
+
document = baseline_server_type_document(options)
|
|
281
|
+
return _commit(project, schema_directory, [(ResourceKind.HOST_PROFILE, document)])
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def add_server(
|
|
285
|
+
project: Path, options: ServerOptions, schema_directory: Path
|
|
286
|
+
) -> AddResult:
|
|
287
|
+
"""Write one ``Server``, and its ``ServerType`` when the project lacks it."""
|
|
288
|
+
documents: list[tuple[ResourceKind, dict[str, object]]] = []
|
|
289
|
+
if not _resource_path(
|
|
290
|
+
project, ResourceKind.HOST_PROFILE, options.server_type
|
|
291
|
+
).exists():
|
|
292
|
+
server_type = ServerTypeOptions(resource_id=options.server_type)
|
|
293
|
+
documents.append(
|
|
294
|
+
(ResourceKind.HOST_PROFILE, baseline_server_type_document(server_type))
|
|
295
|
+
)
|
|
296
|
+
documents.append((ResourceKind.SERVER, server_document(options)))
|
|
297
|
+
return _commit(project, schema_directory, documents)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _resource_path(project: Path, kind: ResourceKind, resource_id: ResourceId) -> Path:
|
|
301
|
+
return project / kind.directory / f"{resource_id}.yaml"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _commit(
|
|
305
|
+
project: Path,
|
|
306
|
+
schema_directory: Path,
|
|
307
|
+
documents: Sequence[tuple[ResourceKind, dict[str, object]]],
|
|
308
|
+
) -> AddResult:
|
|
309
|
+
catalog = SchemaCatalog(schema_directory)
|
|
310
|
+
planned: list[tuple[ResourceKind, ResourceId, Path, dict[str, object]]] = []
|
|
311
|
+
for kind, document in documents:
|
|
312
|
+
catalog.validate(kind, document)
|
|
313
|
+
metadata = document["metadata"]
|
|
314
|
+
assert isinstance(metadata, dict) # noqa: S101 - built above, schema-checked
|
|
315
|
+
resource_id = ResourceId.from_boundary(metadata["id"])
|
|
316
|
+
path = _resource_path(project, kind, resource_id)
|
|
317
|
+
if path.exists():
|
|
318
|
+
message = f"{kind.value}/{resource_id} already exists: {path}"
|
|
319
|
+
raise AuthoringError(_ERROR_RESOURCE_EXISTS, message)
|
|
320
|
+
planned.append((kind, resource_id, path, document))
|
|
321
|
+
|
|
322
|
+
written: list[Path] = []
|
|
323
|
+
try:
|
|
324
|
+
for _kind, _resource_id, path, document in planned:
|
|
325
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
path.write_text(
|
|
327
|
+
f"---\n{yaml.safe_dump(document, sort_keys=False)}", encoding="utf-8"
|
|
328
|
+
)
|
|
329
|
+
written.append(path)
|
|
330
|
+
validate_config(project, schema_directory)
|
|
331
|
+
except ConfigValidationError:
|
|
332
|
+
for path in written:
|
|
333
|
+
path.unlink()
|
|
334
|
+
raise
|
|
335
|
+
return AddResult(
|
|
336
|
+
project=project,
|
|
337
|
+
added=tuple(
|
|
338
|
+
AddedResource(kind, resource_id, path.relative_to(project).as_posix())
|
|
339
|
+
for kind, resource_id, path, _document in planned
|
|
340
|
+
),
|
|
341
|
+
)
|