@mriekki/lean 0.1.0 → 0.2.0
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.
- package/README.md +71 -67
- package/assets/components.json +33 -33
- package/assets/core/distribution.json +52 -51
- package/assets/deployment/distribution.json +34 -34
- package/assets/generated/ASSEMBLY.json +38 -33
- package/assets/generated/core/core/skills/core-sync/SKILL.md +78 -120
- package/assets/generated/core/core/skills/deploy/SKILL.md +74 -91
- package/assets/generated/core/core/skills/lean-update/SKILL.md +189 -0
- package/assets/generated/core/core/skills/upstream-fix/SKILL.md +91 -76
- package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -8
- package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -3
- package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -3
- package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -140
- package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -455
- package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +415 -415
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -208
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -100
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -271
- package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -437
- package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -121
- package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -25
- package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -5
- package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -171
- package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -204
- package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -367
- package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -443
- package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -290
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +334 -334
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +339 -339
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +85 -85
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -437
- package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -118
- package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -286
- package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -120
- package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -100
- package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -278
- package/bin/lean.mjs +9 -9
- package/package.json +33 -33
- package/src/cli.mjs +131 -131
- package/src/engine.mjs +243 -243
- package/src/package-data.mjs +18 -18
|
@@ -1,437 +1,437 @@
|
|
|
1
|
-
"""Command-line interface for the Deployment Kit."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
import json
|
|
7
|
-
import sys
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from .box_platform import (
|
|
11
|
-
HOST_PLATFORM_WRITE_PHRASE,
|
|
12
|
-
apply_platform_change,
|
|
13
|
-
plan_platform_change,
|
|
14
|
-
platform_doctor,
|
|
15
|
-
platform_status,
|
|
16
|
-
)
|
|
17
|
-
from .doctor import target_doctor, workstation_doctor
|
|
18
|
-
from .errors import DeploymentKitError
|
|
19
|
-
from .keys import add_key_to_agent, agent_status, create_dedicated_key, public_key_info
|
|
20
|
-
from .profiles import load_profile, save_profile, validate_profile
|
|
21
|
-
from .route_registry import (
|
|
22
|
-
ROUTE_REMOVE_PHRASE,
|
|
23
|
-
ROUTE_WRITE_PHRASE,
|
|
24
|
-
apply_route_change,
|
|
25
|
-
plan_route_registration,
|
|
26
|
-
plan_route_removal,
|
|
27
|
-
route_inventory,
|
|
28
|
-
)
|
|
29
|
-
from .ssh import verify_access
|
|
30
|
-
from .trust import (
|
|
31
|
-
apply_trust_plan,
|
|
32
|
-
candidate_view,
|
|
33
|
-
plan_console_verified_trust,
|
|
34
|
-
scan_host_keys,
|
|
35
|
-
trust_plan_view,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
LOCAL_WRITE_PHRASE = "[LOCAL CONFIG WRITE]"
|
|
40
|
-
KEY_WRITE_PHRASE = "[LOCAL KEY WRITE]"
|
|
41
|
-
AGENT_WRITE_PHRASE = "[SSH AGENT WRITE]"
|
|
42
|
-
TRUST_WRITE_PHRASE = "[LOCAL TRUST WRITE]"
|
|
43
|
-
TRUST_ROTATE_PHRASE = "[LOCAL TRUST ROTATION]"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _print(value: Any, *, as_json: bool) -> None:
|
|
47
|
-
if as_json:
|
|
48
|
-
print(json.dumps(value, indent=2, sort_keys=True))
|
|
49
|
-
return
|
|
50
|
-
if isinstance(value, dict):
|
|
51
|
-
for key, item in value.items():
|
|
52
|
-
if isinstance(item, (dict, list)):
|
|
53
|
-
print(f"{key}: {json.dumps(item, sort_keys=True)}")
|
|
54
|
-
else:
|
|
55
|
-
print(f"{key}: {item}")
|
|
56
|
-
elif isinstance(value, list):
|
|
57
|
-
for item in value:
|
|
58
|
-
print(json.dumps(item, sort_keys=True))
|
|
59
|
-
else:
|
|
60
|
-
print(value)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _confirm(args: argparse.Namespace, phrase: str, action: str) -> None:
|
|
64
|
-
if args.yes:
|
|
65
|
-
return
|
|
66
|
-
entered = input(f"Type {phrase} to {action}: ").strip()
|
|
67
|
-
if entered != phrase:
|
|
68
|
-
raise DeploymentKitError(f"{action.capitalize()} was not authorized.")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _configure(args: argparse.Namespace) -> int:
|
|
72
|
-
profile = {
|
|
73
|
-
"schemaVersion": 1,
|
|
74
|
-
"targetName": args.name,
|
|
75
|
-
"targetOs": args.target_os,
|
|
76
|
-
"hostName": args.host_name,
|
|
77
|
-
"sshAlias": args.ssh_alias,
|
|
78
|
-
"user": args.user,
|
|
79
|
-
"port": args.port,
|
|
80
|
-
"identityFile": args.identity_file,
|
|
81
|
-
"knownHostsFile": args.known_hosts_file,
|
|
82
|
-
"addressOverride": args.address_override,
|
|
83
|
-
}
|
|
84
|
-
print(json.dumps(profile, indent=2))
|
|
85
|
-
_confirm(args, LOCAL_WRITE_PHRASE, "write this user-scoped target profile")
|
|
86
|
-
path = save_profile(profile)
|
|
87
|
-
print(f"Wrote target profile: {path}")
|
|
88
|
-
return 0
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _set_address(args: argparse.Namespace) -> int:
|
|
92
|
-
profile = load_profile(args.name)
|
|
93
|
-
proposed = dict(profile)
|
|
94
|
-
proposed["addressOverride"] = args.address
|
|
95
|
-
proposed = validate_profile(proposed, expected_target_name=args.name)
|
|
96
|
-
evidence = verify_access(proposed)
|
|
97
|
-
_print(
|
|
98
|
-
{
|
|
99
|
-
"targetName": args.name,
|
|
100
|
-
"currentOverride": profile["addressOverride"],
|
|
101
|
-
"proposedOverride": proposed["addressOverride"],
|
|
102
|
-
"strictVerification": evidence,
|
|
103
|
-
},
|
|
104
|
-
as_json=False,
|
|
105
|
-
)
|
|
106
|
-
_confirm(args, LOCAL_WRITE_PHRASE, "store this separately verified address override")
|
|
107
|
-
save_profile(proposed)
|
|
108
|
-
print(f"Stored address override for {args.name}: {proposed['addressOverride']}")
|
|
109
|
-
return 0
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _clear_address(args: argparse.Namespace) -> int:
|
|
113
|
-
profile = load_profile(args.name)
|
|
114
|
-
if profile["addressOverride"] is None:
|
|
115
|
-
print(f"No address override is set for {args.name}.")
|
|
116
|
-
return 0
|
|
117
|
-
_print(
|
|
118
|
-
{
|
|
119
|
-
"targetName": args.name,
|
|
120
|
-
"currentOverride": profile["addressOverride"],
|
|
121
|
-
"proposedOverride": None,
|
|
122
|
-
},
|
|
123
|
-
as_json=False,
|
|
124
|
-
)
|
|
125
|
-
_confirm(args, LOCAL_WRITE_PHRASE, "clear this target address override")
|
|
126
|
-
profile["addressOverride"] = None
|
|
127
|
-
save_profile(profile)
|
|
128
|
-
print(f"Cleared address override for {args.name}.")
|
|
129
|
-
return 0
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _platform_change(args: argparse.Namespace) -> int:
|
|
133
|
-
plan = plan_platform_change(args.name, args.command)
|
|
134
|
-
_print(plan, as_json=False)
|
|
135
|
-
_confirm(
|
|
136
|
-
args,
|
|
137
|
-
HOST_PLATFORM_WRITE_PHRASE,
|
|
138
|
-
f"{args.command} the shared host platform on target {args.name}",
|
|
139
|
-
)
|
|
140
|
-
_print(apply_platform_change(plan), as_json=args.json)
|
|
141
|
-
return 0
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _route_change(args: argparse.Namespace) -> int:
|
|
145
|
-
if args.command == "register":
|
|
146
|
-
plan = plan_route_registration(
|
|
147
|
-
args.name,
|
|
148
|
-
owner_repo=args.owner_repo,
|
|
149
|
-
environment=args.environment,
|
|
150
|
-
app_slug=args.app_slug,
|
|
151
|
-
upstream_container=args.upstream_container,
|
|
152
|
-
upstream_port=args.upstream_port,
|
|
153
|
-
hostnames=args.hostname,
|
|
154
|
-
)
|
|
155
|
-
phrase = ROUTE_WRITE_PHRASE
|
|
156
|
-
action = f"register/update route {plan['route']['routeId']} on target {args.name}"
|
|
157
|
-
else:
|
|
158
|
-
plan = plan_route_removal(
|
|
159
|
-
args.name,
|
|
160
|
-
owner_repo=args.owner_repo,
|
|
161
|
-
environment=args.environment,
|
|
162
|
-
app_slug=args.app_slug,
|
|
163
|
-
)
|
|
164
|
-
phrase = ROUTE_REMOVE_PHRASE
|
|
165
|
-
action = f"remove route {plan['route']['routeId']} from target {args.name}"
|
|
166
|
-
_print(plan, as_json=False)
|
|
167
|
-
_confirm(args, phrase, action)
|
|
168
|
-
_print(apply_route_change(plan), as_json=args.json)
|
|
169
|
-
return 0
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
173
|
-
parser = argparse.ArgumentParser(prog="lean-poc-deploy")
|
|
174
|
-
subparsers = parser.add_subparsers(dest="group", required=True)
|
|
175
|
-
|
|
176
|
-
workstation = subparsers.add_parser("workstation", help="Workstation setup/readiness operations")
|
|
177
|
-
workstation_sub = workstation.add_subparsers(dest="command", required=True)
|
|
178
|
-
ws_doctor = workstation_sub.add_parser("doctor", help="Inspect workstation readiness without modifying it")
|
|
179
|
-
ws_doctor.add_argument("--json", action="store_true")
|
|
180
|
-
|
|
181
|
-
target = subparsers.add_parser("target", help="Logical deployment target operations")
|
|
182
|
-
target_sub = target.add_subparsers(dest="command", required=True)
|
|
183
|
-
|
|
184
|
-
target_doctor_parser = target_sub.add_parser("doctor", help="Inspect one target without modifying it")
|
|
185
|
-
target_doctor_parser.add_argument("name")
|
|
186
|
-
target_doctor_parser.add_argument("--json", action="store_true")
|
|
187
|
-
|
|
188
|
-
show = target_sub.add_parser("show", help="Show normalized non-secret target profile")
|
|
189
|
-
show.add_argument("name")
|
|
190
|
-
show.add_argument("--json", action="store_true")
|
|
191
|
-
|
|
192
|
-
verify = target_sub.add_parser("verify", help="Verify strict public-key SSH access without remote writes")
|
|
193
|
-
verify.add_argument("name")
|
|
194
|
-
verify.add_argument("--json", action="store_true")
|
|
195
|
-
|
|
196
|
-
configure = target_sub.add_parser("configure", help="Write a user-scoped target profile")
|
|
197
|
-
configure.add_argument("name")
|
|
198
|
-
configure.add_argument("--target-os", default="ubuntu")
|
|
199
|
-
configure.add_argument("--host-name", required=True)
|
|
200
|
-
configure.add_argument("--ssh-alias", required=True)
|
|
201
|
-
configure.add_argument("--user", required=True)
|
|
202
|
-
configure.add_argument("--port", type=int, default=22)
|
|
203
|
-
configure.add_argument("--identity-file", required=True)
|
|
204
|
-
configure.add_argument("--known-hosts-file", required=True)
|
|
205
|
-
configure.add_argument("--address-override")
|
|
206
|
-
configure.add_argument("--yes", action="store_true", help="Accept the bounded local profile write")
|
|
207
|
-
|
|
208
|
-
key_create = target_sub.add_parser(
|
|
209
|
-
"key-create",
|
|
210
|
-
help="Create the configured dedicated ed25519 key without exposing its passphrase to this tool",
|
|
211
|
-
)
|
|
212
|
-
key_create.add_argument("name")
|
|
213
|
-
key_create.add_argument("--yes", action="store_true")
|
|
214
|
-
|
|
215
|
-
key_public = target_sub.add_parser(
|
|
216
|
-
"key-public",
|
|
217
|
-
help="Show the dedicated public key and fingerprint for console-assisted installation",
|
|
218
|
-
)
|
|
219
|
-
key_public.add_argument("name")
|
|
220
|
-
key_public.add_argument("--json", action="store_true")
|
|
221
|
-
|
|
222
|
-
agent_check = target_sub.add_parser("agent-status", help="Inspect whether the dedicated key is loaded")
|
|
223
|
-
agent_check.add_argument("name")
|
|
224
|
-
agent_check.add_argument("--json", action="store_true")
|
|
225
|
-
|
|
226
|
-
agent_add = target_sub.add_parser(
|
|
227
|
-
"agent-add",
|
|
228
|
-
help="Load the configured dedicated key into an already-running SSH agent",
|
|
229
|
-
)
|
|
230
|
-
agent_add.add_argument("name")
|
|
231
|
-
agent_add.add_argument("--yes", action="store_true")
|
|
232
|
-
|
|
233
|
-
trust_candidates = target_sub.add_parser(
|
|
234
|
-
"trust-candidates",
|
|
235
|
-
help="Read candidate host-key fingerprints; candidates are never trusted by discovery",
|
|
236
|
-
)
|
|
237
|
-
trust_candidates.add_argument("name")
|
|
238
|
-
trust_candidates.add_argument(
|
|
239
|
-
"--address",
|
|
240
|
-
help="Exact current IP independently obtained from the trusted target console",
|
|
241
|
-
)
|
|
242
|
-
trust_candidates.add_argument("--json", action="store_true")
|
|
243
|
-
|
|
244
|
-
trust_enroll = target_sub.add_parser(
|
|
245
|
-
"trust-enroll",
|
|
246
|
-
help="Enroll one host key only when it matches the independently observed console fingerprint",
|
|
247
|
-
)
|
|
248
|
-
trust_enroll.add_argument("name")
|
|
249
|
-
trust_enroll.add_argument("--fingerprint", required=True)
|
|
250
|
-
trust_enroll.add_argument("--address")
|
|
251
|
-
trust_enroll.add_argument("--yes", action="store_true")
|
|
252
|
-
|
|
253
|
-
trust_rotate = target_sub.add_parser(
|
|
254
|
-
"trust-rotate",
|
|
255
|
-
help="Explicitly replace existing target trust after independent console verification",
|
|
256
|
-
)
|
|
257
|
-
trust_rotate.add_argument("name")
|
|
258
|
-
trust_rotate.add_argument("--fingerprint", required=True)
|
|
259
|
-
trust_rotate.add_argument("--address")
|
|
260
|
-
trust_rotate.add_argument("--yes", action="store_true")
|
|
261
|
-
|
|
262
|
-
address_set = target_sub.add_parser(
|
|
263
|
-
"address-set",
|
|
264
|
-
help="Store an IP override only after strict SSH verification against existing target identity",
|
|
265
|
-
)
|
|
266
|
-
address_set.add_argument("name")
|
|
267
|
-
address_set.add_argument("--address", required=True)
|
|
268
|
-
address_set.add_argument("--yes", action="store_true")
|
|
269
|
-
|
|
270
|
-
address_clear = target_sub.add_parser("address-clear", help="Clear a target's temporary address override")
|
|
271
|
-
address_clear.add_argument("name")
|
|
272
|
-
address_clear.add_argument("--yes", action="store_true")
|
|
273
|
-
|
|
274
|
-
route = subparsers.add_parser(
|
|
275
|
-
"route",
|
|
276
|
-
help="Owned application route operations on a ready shared host platform",
|
|
277
|
-
)
|
|
278
|
-
route_sub = route.add_subparsers(dest="command", required=True)
|
|
279
|
-
route_inventory_parser = route_sub.add_parser(
|
|
280
|
-
"inventory", help="Read managed route ownership plus legacy route filenames without writes"
|
|
281
|
-
)
|
|
282
|
-
route_inventory_parser.add_argument("name")
|
|
283
|
-
route_inventory_parser.add_argument("--json", action="store_true")
|
|
284
|
-
|
|
285
|
-
route_register = route_sub.add_parser(
|
|
286
|
-
"register", help="Create or update one POC/environment-owned Caddy + mDNS route"
|
|
287
|
-
)
|
|
288
|
-
route_register.add_argument("name")
|
|
289
|
-
route_register.add_argument("--owner-repo", required=True)
|
|
290
|
-
route_register.add_argument("--environment", required=True)
|
|
291
|
-
route_register.add_argument("--app-slug", required=True)
|
|
292
|
-
route_register.add_argument("--upstream-container", required=True)
|
|
293
|
-
route_register.add_argument("--upstream-port", type=int, required=True)
|
|
294
|
-
route_register.add_argument("--hostname", action="append", required=True)
|
|
295
|
-
route_register.add_argument("--json", action="store_true")
|
|
296
|
-
route_register.add_argument("--yes", action="store_true")
|
|
297
|
-
|
|
298
|
-
route_remove = route_sub.add_parser(
|
|
299
|
-
"remove", help="Remove only one owned route/alias registration, preserving app runtime and data"
|
|
300
|
-
)
|
|
301
|
-
route_remove.add_argument("name")
|
|
302
|
-
route_remove.add_argument("--owner-repo", required=True)
|
|
303
|
-
route_remove.add_argument("--environment", required=True)
|
|
304
|
-
route_remove.add_argument("--app-slug", required=True)
|
|
305
|
-
route_remove.add_argument("--json", action="store_true")
|
|
306
|
-
route_remove.add_argument("--yes", action="store_true")
|
|
307
|
-
|
|
308
|
-
platform = subparsers.add_parser(
|
|
309
|
-
"platform",
|
|
310
|
-
help="Shared Ubuntu box platform operations; never registers or removes application routes",
|
|
311
|
-
)
|
|
312
|
-
platform_sub = platform.add_subparsers(dest="command", required=True)
|
|
313
|
-
platform_status_parser = platform_sub.add_parser(
|
|
314
|
-
"status", help="Read shared host-platform state without remote writes"
|
|
315
|
-
)
|
|
316
|
-
platform_status_parser.add_argument("name")
|
|
317
|
-
platform_status_parser.add_argument("--json", action="store_true")
|
|
318
|
-
platform_doctor_parser = platform_sub.add_parser(
|
|
319
|
-
"doctor", help="Read shared host-platform readiness and next action"
|
|
320
|
-
)
|
|
321
|
-
platform_doctor_parser.add_argument("name")
|
|
322
|
-
platform_doctor_parser.add_argument("--json", action="store_true")
|
|
323
|
-
for command in ("install", "upgrade"):
|
|
324
|
-
change = platform_sub.add_parser(
|
|
325
|
-
command,
|
|
326
|
-
help=(
|
|
327
|
-
"Install shared host platform on a fresh target"
|
|
328
|
-
if command == "install"
|
|
329
|
-
else "Upgrade an already Kit-owned shared host platform"
|
|
330
|
-
),
|
|
331
|
-
)
|
|
332
|
-
change.add_argument("name")
|
|
333
|
-
change.add_argument("--json", action="store_true")
|
|
334
|
-
change.add_argument("--yes", action="store_true")
|
|
335
|
-
|
|
336
|
-
return parser
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
def main(argv: list[str] | None = None) -> int:
|
|
340
|
-
args = build_parser().parse_args(argv)
|
|
341
|
-
try:
|
|
342
|
-
if args.group == "workstation" and args.command == "doctor":
|
|
343
|
-
_print(workstation_doctor(), as_json=args.json)
|
|
344
|
-
return 0
|
|
345
|
-
if args.group == "target" and args.command == "doctor":
|
|
346
|
-
_print(target_doctor(args.name), as_json=args.json)
|
|
347
|
-
return 0
|
|
348
|
-
if args.group == "target" and args.command == "show":
|
|
349
|
-
_print(load_profile(args.name), as_json=args.json)
|
|
350
|
-
return 0
|
|
351
|
-
if args.group == "target" and args.command == "verify":
|
|
352
|
-
_print(verify_access(load_profile(args.name)), as_json=args.json)
|
|
353
|
-
return 0
|
|
354
|
-
if args.group == "target" and args.command == "configure":
|
|
355
|
-
return _configure(args)
|
|
356
|
-
if args.group == "target" and args.command == "key-create":
|
|
357
|
-
profile = load_profile(args.name)
|
|
358
|
-
_print(
|
|
359
|
-
{
|
|
360
|
-
"targetName": args.name,
|
|
361
|
-
"identityFile": profile["identityFile"],
|
|
362
|
-
"keyType": "ed25519",
|
|
363
|
-
"passphraseHandling": "entered directly into ssh-keygen",
|
|
364
|
-
},
|
|
365
|
-
as_json=False,
|
|
366
|
-
)
|
|
367
|
-
_confirm(args, KEY_WRITE_PHRASE, "create this dedicated workstation key")
|
|
368
|
-
_print(create_dedicated_key(profile), as_json=False)
|
|
369
|
-
return 0
|
|
370
|
-
if args.group == "target" and args.command == "key-public":
|
|
371
|
-
_print(public_key_info(load_profile(args.name)), as_json=args.json)
|
|
372
|
-
return 0
|
|
373
|
-
if args.group == "target" and args.command == "agent-status":
|
|
374
|
-
_print(agent_status(load_profile(args.name)), as_json=args.json)
|
|
375
|
-
return 0
|
|
376
|
-
if args.group == "target" and args.command == "agent-add":
|
|
377
|
-
profile = load_profile(args.name)
|
|
378
|
-
status = agent_status(profile)
|
|
379
|
-
_print(status, as_json=False)
|
|
380
|
-
if status["loaded"]:
|
|
381
|
-
return 0
|
|
382
|
-
if status["state"] == "agent-unavailable":
|
|
383
|
-
raise DeploymentKitError(
|
|
384
|
-
"The SSH agent is unavailable. Start/enable it as a separate workstation action; "
|
|
385
|
-
"this command will not change agent service configuration."
|
|
386
|
-
)
|
|
387
|
-
_confirm(args, AGENT_WRITE_PHRASE, "load this dedicated key into the running SSH agent")
|
|
388
|
-
_print(add_key_to_agent(profile), as_json=False)
|
|
389
|
-
return 0
|
|
390
|
-
if args.group == "target" and args.command == "trust-candidates":
|
|
391
|
-
candidates = scan_host_keys(load_profile(args.name), console_address=args.address)
|
|
392
|
-
_print([candidate_view(item) for item in candidates], as_json=args.json)
|
|
393
|
-
return 0
|
|
394
|
-
if args.group == "target" and args.command in {"trust-enroll", "trust-rotate"}:
|
|
395
|
-
profile = load_profile(args.name)
|
|
396
|
-
rotate = args.command == "trust-rotate"
|
|
397
|
-
plan = plan_console_verified_trust(
|
|
398
|
-
profile,
|
|
399
|
-
fingerprint=args.fingerprint,
|
|
400
|
-
rotate=rotate,
|
|
401
|
-
console_address=args.address,
|
|
402
|
-
)
|
|
403
|
-
_print(trust_plan_view(plan), as_json=False)
|
|
404
|
-
phrase = TRUST_ROTATE_PHRASE if rotate else TRUST_WRITE_PHRASE
|
|
405
|
-
action = (
|
|
406
|
-
"replace existing target trust with this console-verified host key"
|
|
407
|
-
if rotate
|
|
408
|
-
else "enroll this console-verified target host key"
|
|
409
|
-
)
|
|
410
|
-
_confirm(args, phrase, action)
|
|
411
|
-
_print(apply_trust_plan(profile, plan), as_json=False)
|
|
412
|
-
return 0
|
|
413
|
-
if args.group == "target" and args.command == "address-set":
|
|
414
|
-
return _set_address(args)
|
|
415
|
-
if args.group == "target" and args.command == "address-clear":
|
|
416
|
-
return _clear_address(args)
|
|
417
|
-
if args.group == "route" and args.command == "inventory":
|
|
418
|
-
_print(route_inventory(args.name), as_json=args.json)
|
|
419
|
-
return 0
|
|
420
|
-
if args.group == "route" and args.command in {"register", "remove"}:
|
|
421
|
-
return _route_change(args)
|
|
422
|
-
if args.group == "platform" and args.command == "status":
|
|
423
|
-
_print(platform_status(args.name), as_json=args.json)
|
|
424
|
-
return 0
|
|
425
|
-
if args.group == "platform" and args.command == "doctor":
|
|
426
|
-
_print(platform_doctor(args.name), as_json=args.json)
|
|
427
|
-
return 0
|
|
428
|
-
if args.group == "platform" and args.command in {"install", "upgrade"}:
|
|
429
|
-
return _platform_change(args)
|
|
430
|
-
raise DeploymentKitError("Unsupported command.")
|
|
431
|
-
except DeploymentKitError as error:
|
|
432
|
-
print(f"ERROR: {error}", file=sys.stderr)
|
|
433
|
-
return 2
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if __name__ == "__main__":
|
|
437
|
-
raise SystemExit(main())
|
|
1
|
+
"""Command-line interface for the Deployment Kit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .box_platform import (
|
|
11
|
+
HOST_PLATFORM_WRITE_PHRASE,
|
|
12
|
+
apply_platform_change,
|
|
13
|
+
plan_platform_change,
|
|
14
|
+
platform_doctor,
|
|
15
|
+
platform_status,
|
|
16
|
+
)
|
|
17
|
+
from .doctor import target_doctor, workstation_doctor
|
|
18
|
+
from .errors import DeploymentKitError
|
|
19
|
+
from .keys import add_key_to_agent, agent_status, create_dedicated_key, public_key_info
|
|
20
|
+
from .profiles import load_profile, save_profile, validate_profile
|
|
21
|
+
from .route_registry import (
|
|
22
|
+
ROUTE_REMOVE_PHRASE,
|
|
23
|
+
ROUTE_WRITE_PHRASE,
|
|
24
|
+
apply_route_change,
|
|
25
|
+
plan_route_registration,
|
|
26
|
+
plan_route_removal,
|
|
27
|
+
route_inventory,
|
|
28
|
+
)
|
|
29
|
+
from .ssh import verify_access
|
|
30
|
+
from .trust import (
|
|
31
|
+
apply_trust_plan,
|
|
32
|
+
candidate_view,
|
|
33
|
+
plan_console_verified_trust,
|
|
34
|
+
scan_host_keys,
|
|
35
|
+
trust_plan_view,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
LOCAL_WRITE_PHRASE = "[LOCAL CONFIG WRITE]"
|
|
40
|
+
KEY_WRITE_PHRASE = "[LOCAL KEY WRITE]"
|
|
41
|
+
AGENT_WRITE_PHRASE = "[SSH AGENT WRITE]"
|
|
42
|
+
TRUST_WRITE_PHRASE = "[LOCAL TRUST WRITE]"
|
|
43
|
+
TRUST_ROTATE_PHRASE = "[LOCAL TRUST ROTATION]"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _print(value: Any, *, as_json: bool) -> None:
|
|
47
|
+
if as_json:
|
|
48
|
+
print(json.dumps(value, indent=2, sort_keys=True))
|
|
49
|
+
return
|
|
50
|
+
if isinstance(value, dict):
|
|
51
|
+
for key, item in value.items():
|
|
52
|
+
if isinstance(item, (dict, list)):
|
|
53
|
+
print(f"{key}: {json.dumps(item, sort_keys=True)}")
|
|
54
|
+
else:
|
|
55
|
+
print(f"{key}: {item}")
|
|
56
|
+
elif isinstance(value, list):
|
|
57
|
+
for item in value:
|
|
58
|
+
print(json.dumps(item, sort_keys=True))
|
|
59
|
+
else:
|
|
60
|
+
print(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _confirm(args: argparse.Namespace, phrase: str, action: str) -> None:
|
|
64
|
+
if args.yes:
|
|
65
|
+
return
|
|
66
|
+
entered = input(f"Type {phrase} to {action}: ").strip()
|
|
67
|
+
if entered != phrase:
|
|
68
|
+
raise DeploymentKitError(f"{action.capitalize()} was not authorized.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _configure(args: argparse.Namespace) -> int:
|
|
72
|
+
profile = {
|
|
73
|
+
"schemaVersion": 1,
|
|
74
|
+
"targetName": args.name,
|
|
75
|
+
"targetOs": args.target_os,
|
|
76
|
+
"hostName": args.host_name,
|
|
77
|
+
"sshAlias": args.ssh_alias,
|
|
78
|
+
"user": args.user,
|
|
79
|
+
"port": args.port,
|
|
80
|
+
"identityFile": args.identity_file,
|
|
81
|
+
"knownHostsFile": args.known_hosts_file,
|
|
82
|
+
"addressOverride": args.address_override,
|
|
83
|
+
}
|
|
84
|
+
print(json.dumps(profile, indent=2))
|
|
85
|
+
_confirm(args, LOCAL_WRITE_PHRASE, "write this user-scoped target profile")
|
|
86
|
+
path = save_profile(profile)
|
|
87
|
+
print(f"Wrote target profile: {path}")
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _set_address(args: argparse.Namespace) -> int:
|
|
92
|
+
profile = load_profile(args.name)
|
|
93
|
+
proposed = dict(profile)
|
|
94
|
+
proposed["addressOverride"] = args.address
|
|
95
|
+
proposed = validate_profile(proposed, expected_target_name=args.name)
|
|
96
|
+
evidence = verify_access(proposed)
|
|
97
|
+
_print(
|
|
98
|
+
{
|
|
99
|
+
"targetName": args.name,
|
|
100
|
+
"currentOverride": profile["addressOverride"],
|
|
101
|
+
"proposedOverride": proposed["addressOverride"],
|
|
102
|
+
"strictVerification": evidence,
|
|
103
|
+
},
|
|
104
|
+
as_json=False,
|
|
105
|
+
)
|
|
106
|
+
_confirm(args, LOCAL_WRITE_PHRASE, "store this separately verified address override")
|
|
107
|
+
save_profile(proposed)
|
|
108
|
+
print(f"Stored address override for {args.name}: {proposed['addressOverride']}")
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _clear_address(args: argparse.Namespace) -> int:
|
|
113
|
+
profile = load_profile(args.name)
|
|
114
|
+
if profile["addressOverride"] is None:
|
|
115
|
+
print(f"No address override is set for {args.name}.")
|
|
116
|
+
return 0
|
|
117
|
+
_print(
|
|
118
|
+
{
|
|
119
|
+
"targetName": args.name,
|
|
120
|
+
"currentOverride": profile["addressOverride"],
|
|
121
|
+
"proposedOverride": None,
|
|
122
|
+
},
|
|
123
|
+
as_json=False,
|
|
124
|
+
)
|
|
125
|
+
_confirm(args, LOCAL_WRITE_PHRASE, "clear this target address override")
|
|
126
|
+
profile["addressOverride"] = None
|
|
127
|
+
save_profile(profile)
|
|
128
|
+
print(f"Cleared address override for {args.name}.")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _platform_change(args: argparse.Namespace) -> int:
|
|
133
|
+
plan = plan_platform_change(args.name, args.command)
|
|
134
|
+
_print(plan, as_json=False)
|
|
135
|
+
_confirm(
|
|
136
|
+
args,
|
|
137
|
+
HOST_PLATFORM_WRITE_PHRASE,
|
|
138
|
+
f"{args.command} the shared host platform on target {args.name}",
|
|
139
|
+
)
|
|
140
|
+
_print(apply_platform_change(plan), as_json=args.json)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _route_change(args: argparse.Namespace) -> int:
|
|
145
|
+
if args.command == "register":
|
|
146
|
+
plan = plan_route_registration(
|
|
147
|
+
args.name,
|
|
148
|
+
owner_repo=args.owner_repo,
|
|
149
|
+
environment=args.environment,
|
|
150
|
+
app_slug=args.app_slug,
|
|
151
|
+
upstream_container=args.upstream_container,
|
|
152
|
+
upstream_port=args.upstream_port,
|
|
153
|
+
hostnames=args.hostname,
|
|
154
|
+
)
|
|
155
|
+
phrase = ROUTE_WRITE_PHRASE
|
|
156
|
+
action = f"register/update route {plan['route']['routeId']} on target {args.name}"
|
|
157
|
+
else:
|
|
158
|
+
plan = plan_route_removal(
|
|
159
|
+
args.name,
|
|
160
|
+
owner_repo=args.owner_repo,
|
|
161
|
+
environment=args.environment,
|
|
162
|
+
app_slug=args.app_slug,
|
|
163
|
+
)
|
|
164
|
+
phrase = ROUTE_REMOVE_PHRASE
|
|
165
|
+
action = f"remove route {plan['route']['routeId']} from target {args.name}"
|
|
166
|
+
_print(plan, as_json=False)
|
|
167
|
+
_confirm(args, phrase, action)
|
|
168
|
+
_print(apply_route_change(plan), as_json=args.json)
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
173
|
+
parser = argparse.ArgumentParser(prog="lean-poc-deploy")
|
|
174
|
+
subparsers = parser.add_subparsers(dest="group", required=True)
|
|
175
|
+
|
|
176
|
+
workstation = subparsers.add_parser("workstation", help="Workstation setup/readiness operations")
|
|
177
|
+
workstation_sub = workstation.add_subparsers(dest="command", required=True)
|
|
178
|
+
ws_doctor = workstation_sub.add_parser("doctor", help="Inspect workstation readiness without modifying it")
|
|
179
|
+
ws_doctor.add_argument("--json", action="store_true")
|
|
180
|
+
|
|
181
|
+
target = subparsers.add_parser("target", help="Logical deployment target operations")
|
|
182
|
+
target_sub = target.add_subparsers(dest="command", required=True)
|
|
183
|
+
|
|
184
|
+
target_doctor_parser = target_sub.add_parser("doctor", help="Inspect one target without modifying it")
|
|
185
|
+
target_doctor_parser.add_argument("name")
|
|
186
|
+
target_doctor_parser.add_argument("--json", action="store_true")
|
|
187
|
+
|
|
188
|
+
show = target_sub.add_parser("show", help="Show normalized non-secret target profile")
|
|
189
|
+
show.add_argument("name")
|
|
190
|
+
show.add_argument("--json", action="store_true")
|
|
191
|
+
|
|
192
|
+
verify = target_sub.add_parser("verify", help="Verify strict public-key SSH access without remote writes")
|
|
193
|
+
verify.add_argument("name")
|
|
194
|
+
verify.add_argument("--json", action="store_true")
|
|
195
|
+
|
|
196
|
+
configure = target_sub.add_parser("configure", help="Write a user-scoped target profile")
|
|
197
|
+
configure.add_argument("name")
|
|
198
|
+
configure.add_argument("--target-os", default="ubuntu")
|
|
199
|
+
configure.add_argument("--host-name", required=True)
|
|
200
|
+
configure.add_argument("--ssh-alias", required=True)
|
|
201
|
+
configure.add_argument("--user", required=True)
|
|
202
|
+
configure.add_argument("--port", type=int, default=22)
|
|
203
|
+
configure.add_argument("--identity-file", required=True)
|
|
204
|
+
configure.add_argument("--known-hosts-file", required=True)
|
|
205
|
+
configure.add_argument("--address-override")
|
|
206
|
+
configure.add_argument("--yes", action="store_true", help="Accept the bounded local profile write")
|
|
207
|
+
|
|
208
|
+
key_create = target_sub.add_parser(
|
|
209
|
+
"key-create",
|
|
210
|
+
help="Create the configured dedicated ed25519 key without exposing its passphrase to this tool",
|
|
211
|
+
)
|
|
212
|
+
key_create.add_argument("name")
|
|
213
|
+
key_create.add_argument("--yes", action="store_true")
|
|
214
|
+
|
|
215
|
+
key_public = target_sub.add_parser(
|
|
216
|
+
"key-public",
|
|
217
|
+
help="Show the dedicated public key and fingerprint for console-assisted installation",
|
|
218
|
+
)
|
|
219
|
+
key_public.add_argument("name")
|
|
220
|
+
key_public.add_argument("--json", action="store_true")
|
|
221
|
+
|
|
222
|
+
agent_check = target_sub.add_parser("agent-status", help="Inspect whether the dedicated key is loaded")
|
|
223
|
+
agent_check.add_argument("name")
|
|
224
|
+
agent_check.add_argument("--json", action="store_true")
|
|
225
|
+
|
|
226
|
+
agent_add = target_sub.add_parser(
|
|
227
|
+
"agent-add",
|
|
228
|
+
help="Load the configured dedicated key into an already-running SSH agent",
|
|
229
|
+
)
|
|
230
|
+
agent_add.add_argument("name")
|
|
231
|
+
agent_add.add_argument("--yes", action="store_true")
|
|
232
|
+
|
|
233
|
+
trust_candidates = target_sub.add_parser(
|
|
234
|
+
"trust-candidates",
|
|
235
|
+
help="Read candidate host-key fingerprints; candidates are never trusted by discovery",
|
|
236
|
+
)
|
|
237
|
+
trust_candidates.add_argument("name")
|
|
238
|
+
trust_candidates.add_argument(
|
|
239
|
+
"--address",
|
|
240
|
+
help="Exact current IP independently obtained from the trusted target console",
|
|
241
|
+
)
|
|
242
|
+
trust_candidates.add_argument("--json", action="store_true")
|
|
243
|
+
|
|
244
|
+
trust_enroll = target_sub.add_parser(
|
|
245
|
+
"trust-enroll",
|
|
246
|
+
help="Enroll one host key only when it matches the independently observed console fingerprint",
|
|
247
|
+
)
|
|
248
|
+
trust_enroll.add_argument("name")
|
|
249
|
+
trust_enroll.add_argument("--fingerprint", required=True)
|
|
250
|
+
trust_enroll.add_argument("--address")
|
|
251
|
+
trust_enroll.add_argument("--yes", action="store_true")
|
|
252
|
+
|
|
253
|
+
trust_rotate = target_sub.add_parser(
|
|
254
|
+
"trust-rotate",
|
|
255
|
+
help="Explicitly replace existing target trust after independent console verification",
|
|
256
|
+
)
|
|
257
|
+
trust_rotate.add_argument("name")
|
|
258
|
+
trust_rotate.add_argument("--fingerprint", required=True)
|
|
259
|
+
trust_rotate.add_argument("--address")
|
|
260
|
+
trust_rotate.add_argument("--yes", action="store_true")
|
|
261
|
+
|
|
262
|
+
address_set = target_sub.add_parser(
|
|
263
|
+
"address-set",
|
|
264
|
+
help="Store an IP override only after strict SSH verification against existing target identity",
|
|
265
|
+
)
|
|
266
|
+
address_set.add_argument("name")
|
|
267
|
+
address_set.add_argument("--address", required=True)
|
|
268
|
+
address_set.add_argument("--yes", action="store_true")
|
|
269
|
+
|
|
270
|
+
address_clear = target_sub.add_parser("address-clear", help="Clear a target's temporary address override")
|
|
271
|
+
address_clear.add_argument("name")
|
|
272
|
+
address_clear.add_argument("--yes", action="store_true")
|
|
273
|
+
|
|
274
|
+
route = subparsers.add_parser(
|
|
275
|
+
"route",
|
|
276
|
+
help="Owned application route operations on a ready shared host platform",
|
|
277
|
+
)
|
|
278
|
+
route_sub = route.add_subparsers(dest="command", required=True)
|
|
279
|
+
route_inventory_parser = route_sub.add_parser(
|
|
280
|
+
"inventory", help="Read managed route ownership plus legacy route filenames without writes"
|
|
281
|
+
)
|
|
282
|
+
route_inventory_parser.add_argument("name")
|
|
283
|
+
route_inventory_parser.add_argument("--json", action="store_true")
|
|
284
|
+
|
|
285
|
+
route_register = route_sub.add_parser(
|
|
286
|
+
"register", help="Create or update one POC/environment-owned Caddy + mDNS route"
|
|
287
|
+
)
|
|
288
|
+
route_register.add_argument("name")
|
|
289
|
+
route_register.add_argument("--owner-repo", required=True)
|
|
290
|
+
route_register.add_argument("--environment", required=True)
|
|
291
|
+
route_register.add_argument("--app-slug", required=True)
|
|
292
|
+
route_register.add_argument("--upstream-container", required=True)
|
|
293
|
+
route_register.add_argument("--upstream-port", type=int, required=True)
|
|
294
|
+
route_register.add_argument("--hostname", action="append", required=True)
|
|
295
|
+
route_register.add_argument("--json", action="store_true")
|
|
296
|
+
route_register.add_argument("--yes", action="store_true")
|
|
297
|
+
|
|
298
|
+
route_remove = route_sub.add_parser(
|
|
299
|
+
"remove", help="Remove only one owned route/alias registration, preserving app runtime and data"
|
|
300
|
+
)
|
|
301
|
+
route_remove.add_argument("name")
|
|
302
|
+
route_remove.add_argument("--owner-repo", required=True)
|
|
303
|
+
route_remove.add_argument("--environment", required=True)
|
|
304
|
+
route_remove.add_argument("--app-slug", required=True)
|
|
305
|
+
route_remove.add_argument("--json", action="store_true")
|
|
306
|
+
route_remove.add_argument("--yes", action="store_true")
|
|
307
|
+
|
|
308
|
+
platform = subparsers.add_parser(
|
|
309
|
+
"platform",
|
|
310
|
+
help="Shared Ubuntu box platform operations; never registers or removes application routes",
|
|
311
|
+
)
|
|
312
|
+
platform_sub = platform.add_subparsers(dest="command", required=True)
|
|
313
|
+
platform_status_parser = platform_sub.add_parser(
|
|
314
|
+
"status", help="Read shared host-platform state without remote writes"
|
|
315
|
+
)
|
|
316
|
+
platform_status_parser.add_argument("name")
|
|
317
|
+
platform_status_parser.add_argument("--json", action="store_true")
|
|
318
|
+
platform_doctor_parser = platform_sub.add_parser(
|
|
319
|
+
"doctor", help="Read shared host-platform readiness and next action"
|
|
320
|
+
)
|
|
321
|
+
platform_doctor_parser.add_argument("name")
|
|
322
|
+
platform_doctor_parser.add_argument("--json", action="store_true")
|
|
323
|
+
for command in ("install", "upgrade"):
|
|
324
|
+
change = platform_sub.add_parser(
|
|
325
|
+
command,
|
|
326
|
+
help=(
|
|
327
|
+
"Install shared host platform on a fresh target"
|
|
328
|
+
if command == "install"
|
|
329
|
+
else "Upgrade an already Kit-owned shared host platform"
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
change.add_argument("name")
|
|
333
|
+
change.add_argument("--json", action="store_true")
|
|
334
|
+
change.add_argument("--yes", action="store_true")
|
|
335
|
+
|
|
336
|
+
return parser
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def main(argv: list[str] | None = None) -> int:
|
|
340
|
+
args = build_parser().parse_args(argv)
|
|
341
|
+
try:
|
|
342
|
+
if args.group == "workstation" and args.command == "doctor":
|
|
343
|
+
_print(workstation_doctor(), as_json=args.json)
|
|
344
|
+
return 0
|
|
345
|
+
if args.group == "target" and args.command == "doctor":
|
|
346
|
+
_print(target_doctor(args.name), as_json=args.json)
|
|
347
|
+
return 0
|
|
348
|
+
if args.group == "target" and args.command == "show":
|
|
349
|
+
_print(load_profile(args.name), as_json=args.json)
|
|
350
|
+
return 0
|
|
351
|
+
if args.group == "target" and args.command == "verify":
|
|
352
|
+
_print(verify_access(load_profile(args.name)), as_json=args.json)
|
|
353
|
+
return 0
|
|
354
|
+
if args.group == "target" and args.command == "configure":
|
|
355
|
+
return _configure(args)
|
|
356
|
+
if args.group == "target" and args.command == "key-create":
|
|
357
|
+
profile = load_profile(args.name)
|
|
358
|
+
_print(
|
|
359
|
+
{
|
|
360
|
+
"targetName": args.name,
|
|
361
|
+
"identityFile": profile["identityFile"],
|
|
362
|
+
"keyType": "ed25519",
|
|
363
|
+
"passphraseHandling": "entered directly into ssh-keygen",
|
|
364
|
+
},
|
|
365
|
+
as_json=False,
|
|
366
|
+
)
|
|
367
|
+
_confirm(args, KEY_WRITE_PHRASE, "create this dedicated workstation key")
|
|
368
|
+
_print(create_dedicated_key(profile), as_json=False)
|
|
369
|
+
return 0
|
|
370
|
+
if args.group == "target" and args.command == "key-public":
|
|
371
|
+
_print(public_key_info(load_profile(args.name)), as_json=args.json)
|
|
372
|
+
return 0
|
|
373
|
+
if args.group == "target" and args.command == "agent-status":
|
|
374
|
+
_print(agent_status(load_profile(args.name)), as_json=args.json)
|
|
375
|
+
return 0
|
|
376
|
+
if args.group == "target" and args.command == "agent-add":
|
|
377
|
+
profile = load_profile(args.name)
|
|
378
|
+
status = agent_status(profile)
|
|
379
|
+
_print(status, as_json=False)
|
|
380
|
+
if status["loaded"]:
|
|
381
|
+
return 0
|
|
382
|
+
if status["state"] == "agent-unavailable":
|
|
383
|
+
raise DeploymentKitError(
|
|
384
|
+
"The SSH agent is unavailable. Start/enable it as a separate workstation action; "
|
|
385
|
+
"this command will not change agent service configuration."
|
|
386
|
+
)
|
|
387
|
+
_confirm(args, AGENT_WRITE_PHRASE, "load this dedicated key into the running SSH agent")
|
|
388
|
+
_print(add_key_to_agent(profile), as_json=False)
|
|
389
|
+
return 0
|
|
390
|
+
if args.group == "target" and args.command == "trust-candidates":
|
|
391
|
+
candidates = scan_host_keys(load_profile(args.name), console_address=args.address)
|
|
392
|
+
_print([candidate_view(item) for item in candidates], as_json=args.json)
|
|
393
|
+
return 0
|
|
394
|
+
if args.group == "target" and args.command in {"trust-enroll", "trust-rotate"}:
|
|
395
|
+
profile = load_profile(args.name)
|
|
396
|
+
rotate = args.command == "trust-rotate"
|
|
397
|
+
plan = plan_console_verified_trust(
|
|
398
|
+
profile,
|
|
399
|
+
fingerprint=args.fingerprint,
|
|
400
|
+
rotate=rotate,
|
|
401
|
+
console_address=args.address,
|
|
402
|
+
)
|
|
403
|
+
_print(trust_plan_view(plan), as_json=False)
|
|
404
|
+
phrase = TRUST_ROTATE_PHRASE if rotate else TRUST_WRITE_PHRASE
|
|
405
|
+
action = (
|
|
406
|
+
"replace existing target trust with this console-verified host key"
|
|
407
|
+
if rotate
|
|
408
|
+
else "enroll this console-verified target host key"
|
|
409
|
+
)
|
|
410
|
+
_confirm(args, phrase, action)
|
|
411
|
+
_print(apply_trust_plan(profile, plan), as_json=False)
|
|
412
|
+
return 0
|
|
413
|
+
if args.group == "target" and args.command == "address-set":
|
|
414
|
+
return _set_address(args)
|
|
415
|
+
if args.group == "target" and args.command == "address-clear":
|
|
416
|
+
return _clear_address(args)
|
|
417
|
+
if args.group == "route" and args.command == "inventory":
|
|
418
|
+
_print(route_inventory(args.name), as_json=args.json)
|
|
419
|
+
return 0
|
|
420
|
+
if args.group == "route" and args.command in {"register", "remove"}:
|
|
421
|
+
return _route_change(args)
|
|
422
|
+
if args.group == "platform" and args.command == "status":
|
|
423
|
+
_print(platform_status(args.name), as_json=args.json)
|
|
424
|
+
return 0
|
|
425
|
+
if args.group == "platform" and args.command == "doctor":
|
|
426
|
+
_print(platform_doctor(args.name), as_json=args.json)
|
|
427
|
+
return 0
|
|
428
|
+
if args.group == "platform" and args.command in {"install", "upgrade"}:
|
|
429
|
+
return _platform_change(args)
|
|
430
|
+
raise DeploymentKitError("Unsupported command.")
|
|
431
|
+
except DeploymentKitError as error:
|
|
432
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
433
|
+
return 2
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
if __name__ == "__main__":
|
|
437
|
+
raise SystemExit(main())
|