veltro-cli 0.15.7__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.
- veltro_cli/__init__.py +6 -0
- veltro_cli/__main__.py +7 -0
- veltro_cli/cli.py +478 -0
- veltro_cli/lib/__init__.py +3 -0
- veltro_cli/lib/agent.py +37 -0
- veltro_cli/lib/backup.py +139 -0
- veltro_cli/lib/compose.py +276 -0
- veltro_cli/lib/datasets.py +611 -0
- veltro_cli/lib/demo.py +421 -0
- veltro_cli/lib/demo_replay.py +794 -0
- veltro_cli/lib/doctor.py +99 -0
- veltro_cli/lib/env_init.py +93 -0
- veltro_cli/lib/errors.py +40 -0
- veltro_cli/lib/host.py +346 -0
- veltro_cli/lib/install.py +400 -0
- veltro_cli/lib/k8s.py +57 -0
- veltro_cli/lib/paths.py +42 -0
- veltro_cli/lib/products.py +54 -0
- veltro_cli/lib/release.py +88 -0
- veltro_cli/lib/status.py +170 -0
- veltro_cli/lib/suite_http.py +227 -0
- veltro_cli/lib/support.py +35 -0
- veltro_cli/lib/tls.py +113 -0
- veltro_cli/lib/toml_config.py +130 -0
- veltro_cli/lib/upgrade.py +379 -0
- veltro_cli/py.typed +0 -0
- veltro_cli-0.15.7.dist-info/METADATA +40 -0
- veltro_cli-0.15.7.dist-info/RECORD +31 -0
- veltro_cli-0.15.7.dist-info/WHEEL +5 -0
- veltro_cli-0.15.7.dist-info/entry_points.txt +2 -0
- veltro_cli-0.15.7.dist-info/top_level.txt +1 -0
veltro_cli/__init__.py
ADDED
veltro_cli/__main__.py
ADDED
veltro_cli/cli.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""veltro — single operator entry point (stdlib only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
from veltro_cli import __version__
|
|
11
|
+
from veltro_cli.lib.agent import enroll_agent
|
|
12
|
+
from veltro_cli.lib.backup import cmd_backup, cmd_restore
|
|
13
|
+
from veltro_cli.lib.compose import ComposeClient
|
|
14
|
+
from veltro_cli.lib.demo import ACTIONS as DEMO_ACTIONS, run_demo
|
|
15
|
+
from veltro_cli.lib.doctor import run_doctor
|
|
16
|
+
from veltro_cli.lib.env_init import init_env
|
|
17
|
+
from veltro_cli.lib.errors import VeltroCliError, print_error
|
|
18
|
+
from veltro_cli.lib.install import install as run_install
|
|
19
|
+
from veltro_cli.lib.k8s import render as k8s_render
|
|
20
|
+
from veltro_cli.lib.paths import resolve_dir
|
|
21
|
+
from veltro_cli.lib.products import resolve_selection
|
|
22
|
+
from veltro_cli.lib.status import collect_status, format_status
|
|
23
|
+
from veltro_cli.lib.support import capture_support_bundle
|
|
24
|
+
from veltro_cli.lib.toml_config import (
|
|
25
|
+
TLS_MODES,
|
|
26
|
+
VELTRO_TOML,
|
|
27
|
+
VeltroConfig,
|
|
28
|
+
read_config,
|
|
29
|
+
)
|
|
30
|
+
from veltro_cli.lib.upgrade import execute_upgrade, plan_upgrade
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_ctx(args: argparse.Namespace) -> tuple[Path, str, bool, tuple[str, ...]]:
|
|
34
|
+
root = resolve_dir(getattr(args, "dir", None))
|
|
35
|
+
cfg = read_config(root / VELTRO_TOML)
|
|
36
|
+
products = resolve_selection(root, getattr(args, "products", None))
|
|
37
|
+
tls_mode = getattr(args, "tls", None) or (cfg.tls_mode if cfg else "off")
|
|
38
|
+
monitoring = bool(getattr(args, "monitoring", False) or (cfg.monitoring if cfg else False))
|
|
39
|
+
return root, tls_mode, monitoring, products
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _client(args: argparse.Namespace) -> ComposeClient:
|
|
43
|
+
root, tls_mode, monitoring, products = _load_ctx(args)
|
|
44
|
+
return ComposeClient(root, products=products, tls_mode=tls_mode, monitoring=monitoring)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_version(_args: argparse.Namespace) -> int:
|
|
48
|
+
print(f"veltro-cli {__version__}")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cmd_init_env(args: argparse.Namespace) -> int:
|
|
53
|
+
root = resolve_dir(args.dir)
|
|
54
|
+
generated = init_env(root, force=bool(args.force))
|
|
55
|
+
print(f"wrote {root / '.env'}")
|
|
56
|
+
print(f" generated {len(generated)} secret(s): {' '.join(generated)}")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_install(args: argparse.Namespace) -> int:
|
|
61
|
+
source = Path(args.source).resolve() if args.source else None
|
|
62
|
+
dest = run_install(
|
|
63
|
+
products=args.products,
|
|
64
|
+
directory=args.dir,
|
|
65
|
+
public_url=args.public_url,
|
|
66
|
+
tls_mode=args.tls,
|
|
67
|
+
yes=bool(args.yes),
|
|
68
|
+
source=source,
|
|
69
|
+
release_url=args.release_url,
|
|
70
|
+
release_sha256=args.release_sha256,
|
|
71
|
+
suite_version=args.suite_version,
|
|
72
|
+
monitoring=bool(args.monitoring),
|
|
73
|
+
skip_up=bool(args.skip_up),
|
|
74
|
+
)
|
|
75
|
+
print(f"installed into {dest}")
|
|
76
|
+
shown_public_url = args.public_url or (read_config(dest / VELTRO_TOML) or VeltroConfig()).public_url
|
|
77
|
+
saved = read_config(dest / VELTRO_TOML) or VeltroConfig()
|
|
78
|
+
print(f" products={saved.products} tls={args.tls} public_url={shown_public_url}")
|
|
79
|
+
if args.tls == "internal":
|
|
80
|
+
print(
|
|
81
|
+
" trust CA after startup: "
|
|
82
|
+
f"{dest / 'tls/caddy-data/caddy/pki/authorities/local/root.crt'}"
|
|
83
|
+
)
|
|
84
|
+
print(" next: veltro status")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_up(args: argparse.Namespace) -> int:
|
|
89
|
+
client = _client(args)
|
|
90
|
+
compose_args = tuple(getattr(args, "compose_args", ()))
|
|
91
|
+
if args.dry_run:
|
|
92
|
+
client.up(*compose_args, dry_run=True)
|
|
93
|
+
for call in client.calls:
|
|
94
|
+
print(call.as_shell())
|
|
95
|
+
return 0
|
|
96
|
+
from veltro_cli.lib.install import start_stack
|
|
97
|
+
|
|
98
|
+
result = start_stack(client, *compose_args)
|
|
99
|
+
if result.stdout:
|
|
100
|
+
print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
|
|
101
|
+
if result.returncode != 0:
|
|
102
|
+
raise VeltroCliError(
|
|
103
|
+
result.stderr.strip() or "compose up failed",
|
|
104
|
+
remedy="run `veltro doctor` and `veltro logs`",
|
|
105
|
+
)
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cmd_down(args: argparse.Namespace) -> int:
|
|
110
|
+
client = _client(args)
|
|
111
|
+
result = client.down(*getattr(args, "compose_args", ()), dry_run=bool(args.dry_run))
|
|
112
|
+
if args.dry_run:
|
|
113
|
+
print(client.calls[-1].as_shell())
|
|
114
|
+
return 0
|
|
115
|
+
if result.returncode != 0:
|
|
116
|
+
raise VeltroCliError(result.stderr.strip() or "compose down failed")
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cmd_restart(args: argparse.Namespace) -> int:
|
|
121
|
+
client = _client(args)
|
|
122
|
+
result = client.restart(*getattr(args, "compose_args", ()), dry_run=bool(args.dry_run))
|
|
123
|
+
if args.dry_run:
|
|
124
|
+
print(client.calls[-1].as_shell())
|
|
125
|
+
return 0
|
|
126
|
+
if result.returncode != 0:
|
|
127
|
+
raise VeltroCliError(result.stderr.strip() or "compose restart failed")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_ps(args: argparse.Namespace) -> int:
|
|
132
|
+
client = _client(args)
|
|
133
|
+
result = client.ps(*getattr(args, "compose_args", ()), dry_run=bool(args.dry_run))
|
|
134
|
+
if args.dry_run:
|
|
135
|
+
print(client.calls[-1].as_shell())
|
|
136
|
+
return 0
|
|
137
|
+
sys.stdout.write(result.stdout)
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
sys.stderr.write(result.stderr)
|
|
140
|
+
return result.returncode
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_logs(args: argparse.Namespace) -> int:
|
|
144
|
+
client = _client(args)
|
|
145
|
+
result = client.logs(*args.service, dry_run=bool(args.dry_run))
|
|
146
|
+
if args.dry_run:
|
|
147
|
+
print(client.calls[-1].as_shell())
|
|
148
|
+
return 0
|
|
149
|
+
sys.stdout.write(result.stdout)
|
|
150
|
+
if result.returncode != 0:
|
|
151
|
+
sys.stderr.write(result.stderr)
|
|
152
|
+
return result.returncode
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cmd_config(args: argparse.Namespace) -> int:
|
|
156
|
+
client = _client(args)
|
|
157
|
+
compose_args = ("--format", args.format) if args.format else ()
|
|
158
|
+
result = client.config(*compose_args, dry_run=bool(args.dry_run))
|
|
159
|
+
if args.dry_run:
|
|
160
|
+
print(client.calls[-1].as_shell())
|
|
161
|
+
return 0
|
|
162
|
+
sys.stdout.write(result.stdout)
|
|
163
|
+
if result.returncode != 0:
|
|
164
|
+
sys.stderr.write(result.stderr)
|
|
165
|
+
return result.returncode
|
|
166
|
+
|
|
167
|
+
def cmd_pull(args: argparse.Namespace) -> int:
|
|
168
|
+
client = _client(args)
|
|
169
|
+
result = client.pull(*getattr(args, "compose_args", ()), dry_run=bool(args.dry_run))
|
|
170
|
+
if args.dry_run:
|
|
171
|
+
print(client.calls[-1].as_shell())
|
|
172
|
+
return 0
|
|
173
|
+
sys.stdout.write(result.stdout)
|
|
174
|
+
if result.returncode != 0:
|
|
175
|
+
sys.stderr.write(result.stderr)
|
|
176
|
+
return result.returncode
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
180
|
+
root, tls_mode, monitoring, products = _load_ctx(args)
|
|
181
|
+
cfg = read_config(root / VELTRO_TOML)
|
|
182
|
+
public_url = args.public_url or (cfg.public_url if cfg else "http://localhost")
|
|
183
|
+
client = ComposeClient(root, products=products, tls_mode=tls_mode, monitoring=monitoring)
|
|
184
|
+
|
|
185
|
+
def fetcher(url: str, timeout: float = 3.0):
|
|
186
|
+
if args.dry_run:
|
|
187
|
+
return 200, '{"status":"ok"}'
|
|
188
|
+
from veltro_cli.lib.status import default_fetcher
|
|
189
|
+
|
|
190
|
+
return default_fetcher(url, timeout)
|
|
191
|
+
|
|
192
|
+
doc = collect_status(
|
|
193
|
+
root,
|
|
194
|
+
products=products,
|
|
195
|
+
public_url=public_url,
|
|
196
|
+
client=client,
|
|
197
|
+
fetcher=fetcher,
|
|
198
|
+
tls_mode=tls_mode,
|
|
199
|
+
check_latest=not args.dry_run,
|
|
200
|
+
)
|
|
201
|
+
sys.stdout.write(format_status(doc))
|
|
202
|
+
bad = any(p["state"] not in {"ready", "not_installed"} for p in doc.get("probes") or [])
|
|
203
|
+
return 1 if bad and not args.dry_run else 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
207
|
+
return run_doctor(
|
|
208
|
+
directory=args.dir,
|
|
209
|
+
products=args.products,
|
|
210
|
+
public_url=args.public_url,
|
|
211
|
+
http_port=int(args.http_port),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def cmd_demo(args: argparse.Namespace) -> int:
|
|
216
|
+
return run_demo(
|
|
217
|
+
args.action,
|
|
218
|
+
directory=args.dir,
|
|
219
|
+
datasets=args.dataset,
|
|
220
|
+
extra_args=args.seed_arg,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_agent(args: argparse.Namespace) -> int:
|
|
225
|
+
client = _client(args)
|
|
226
|
+
return enroll_agent(
|
|
227
|
+
directory=args.dir,
|
|
228
|
+
environment=args.environment,
|
|
229
|
+
client=client,
|
|
230
|
+
dry_run=bool(args.dry_run),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def cmd_upgrade(args: argparse.Namespace) -> int:
|
|
235
|
+
root = resolve_dir(args.dir)
|
|
236
|
+
cfg = read_config(root / VELTRO_TOML)
|
|
237
|
+
current = (cfg.installed_suite_version if cfg else "") or "0.0.0"
|
|
238
|
+
if not args.to:
|
|
239
|
+
print(f"installed: {current}")
|
|
240
|
+
print("pass --to X.Y.Z to plan/execute an upgrade")
|
|
241
|
+
return 0
|
|
242
|
+
plan = plan_upgrade(directory=root, target=args.to, current=current)
|
|
243
|
+
print(f"upgrade plan: {plan['from']} → {plan['to']}")
|
|
244
|
+
for delta in plan["module_deltas"]:
|
|
245
|
+
print(f" {delta['module']}: {delta['from']} → {delta['to']}")
|
|
246
|
+
for warning in plan["warnings"]:
|
|
247
|
+
print(f" WARNING: {warning}")
|
|
248
|
+
for step in plan["steps"]:
|
|
249
|
+
print(f" - {step}")
|
|
250
|
+
if args.dry_run:
|
|
251
|
+
return 0
|
|
252
|
+
execute_upgrade(directory=root, target=args.to, current=current)
|
|
253
|
+
print(f"upgraded to {args.to}")
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def cmd_k8s(args: argparse.Namespace) -> int:
|
|
258
|
+
if args.k8s_command != "render":
|
|
259
|
+
raise VeltroCliError(f"unknown k8s subcommand {args.k8s_command!r}")
|
|
260
|
+
products = resolve_selection(resolve_dir(getattr(args, "dir", None)), args.products)
|
|
261
|
+
out = k8s_render(
|
|
262
|
+
products=products,
|
|
263
|
+
values=args.values or (),
|
|
264
|
+
dry_run=bool(args.dry_run),
|
|
265
|
+
)
|
|
266
|
+
print(out)
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def cmd_support_bundle(args: argparse.Namespace) -> int:
|
|
271
|
+
path = capture_support_bundle(
|
|
272
|
+
directory=args.dir,
|
|
273
|
+
output=args.output,
|
|
274
|
+
base_url=args.base_url,
|
|
275
|
+
)
|
|
276
|
+
print(path)
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
281
|
+
parser = argparse.ArgumentParser(
|
|
282
|
+
prog="veltro",
|
|
283
|
+
description="Operator CLI for the Veltro security operations suite.",
|
|
284
|
+
)
|
|
285
|
+
parser.add_argument("--version", action="store_true", help="print CLI version")
|
|
286
|
+
parser.add_argument("--dir", default=None, help="install / checkout directory (default: discover or ./veltro)")
|
|
287
|
+
parser.add_argument("--dry-run", action="store_true", help="print actions without running docker")
|
|
288
|
+
|
|
289
|
+
sub = parser.add_subparsers(dest="command")
|
|
290
|
+
|
|
291
|
+
p_install = sub.add_parser("install", help="download/materialise and start Veltro")
|
|
292
|
+
p_install.add_argument("--products", default=None, help="comma-separated subset of vectorflow,chad,warden; empty for core")
|
|
293
|
+
p_install.add_argument("--dir", default="./veltro")
|
|
294
|
+
p_install.add_argument("--public-url", default=None)
|
|
295
|
+
p_install.add_argument("--tls", choices=TLS_MODES, default="off")
|
|
296
|
+
p_install.add_argument("--yes", action="store_true")
|
|
297
|
+
p_install.add_argument("--source", default=None, help="path to a Veltro git checkout to copy from")
|
|
298
|
+
p_install.add_argument("--release-url", default=None, help="URL of a release tarball")
|
|
299
|
+
p_install.add_argument("--release-sha256", default=None, help="required SHA-256 for --release-url")
|
|
300
|
+
p_install.add_argument(
|
|
301
|
+
"--suite-version",
|
|
302
|
+
default=None,
|
|
303
|
+
help="Veltro release to install, X.Y.Z (default: the newest release)",
|
|
304
|
+
)
|
|
305
|
+
p_install.add_argument("--monitoring", action="store_true")
|
|
306
|
+
p_install.add_argument("--skip-up", action="store_true", help="materialise files only (tests)")
|
|
307
|
+
p_install.set_defaults(func=cmd_install)
|
|
308
|
+
|
|
309
|
+
def _lifecycle(name: str, func, help_text: str, *, passthrough: bool = False):
|
|
310
|
+
p = sub.add_parser(name, help=help_text)
|
|
311
|
+
p.add_argument("--products", default=None)
|
|
312
|
+
p.add_argument("--tls", choices=TLS_MODES, default=None)
|
|
313
|
+
p.add_argument("--monitoring", action="store_true")
|
|
314
|
+
p.add_argument("--dir", default=None)
|
|
315
|
+
p.add_argument("--dry-run", action="store_true")
|
|
316
|
+
if passthrough:
|
|
317
|
+
p.add_argument("compose_args", nargs=argparse.REMAINDER)
|
|
318
|
+
p.set_defaults(func=func)
|
|
319
|
+
return p
|
|
320
|
+
|
|
321
|
+
_lifecycle("up", cmd_up, "start the stack (compose up -d --wait)", passthrough=True)
|
|
322
|
+
_lifecycle("down", cmd_down, "stop containers (keeps volumes)", passthrough=True)
|
|
323
|
+
_lifecycle("restart", cmd_restart, "restart services", passthrough=True)
|
|
324
|
+
_lifecycle("ps", cmd_ps, "show service status", passthrough=True)
|
|
325
|
+
_lifecycle("pull", cmd_pull, "pull pinned images", passthrough=True)
|
|
326
|
+
p_logs = _lifecycle("logs", cmd_logs, "follow logs")
|
|
327
|
+
p_logs.add_argument("service", nargs="*")
|
|
328
|
+
p_config = _lifecycle("config", cmd_config, "render resolved compose config")
|
|
329
|
+
p_config.add_argument("--format", choices=("yaml", "json"), default=None)
|
|
330
|
+
|
|
331
|
+
p_status = sub.add_parser("status", help="compose ps + HTTP health probes")
|
|
332
|
+
p_status.add_argument("--products", default=None)
|
|
333
|
+
p_status.add_argument("--tls", choices=TLS_MODES, default=None)
|
|
334
|
+
p_status.add_argument("--dir", default=None)
|
|
335
|
+
p_status.add_argument("--public-url", default=None)
|
|
336
|
+
p_status.add_argument("--dry-run", action="store_true")
|
|
337
|
+
p_status.set_defaults(func=cmd_status)
|
|
338
|
+
|
|
339
|
+
p_doctor = sub.add_parser("doctor", help="host and install preflight checks")
|
|
340
|
+
p_doctor.add_argument("--products", default=None)
|
|
341
|
+
p_doctor.add_argument("--dir", default=None)
|
|
342
|
+
p_doctor.add_argument("--public-url", default=None)
|
|
343
|
+
p_doctor.add_argument("--http-port", default="80")
|
|
344
|
+
p_doctor.set_defaults(func=cmd_doctor)
|
|
345
|
+
|
|
346
|
+
p_demo = sub.add_parser("demo", help="list, seed or reset the demo sandbox datasets")
|
|
347
|
+
p_demo.add_argument("action", choices=DEMO_ACTIONS)
|
|
348
|
+
p_demo.add_argument(
|
|
349
|
+
"--dataset",
|
|
350
|
+
action="append",
|
|
351
|
+
default=[],
|
|
352
|
+
metavar="ID",
|
|
353
|
+
help="dataset id from content/datasets (repeatable, comma-separated, or `all`); defaults to brute-force on seed",
|
|
354
|
+
)
|
|
355
|
+
p_demo.add_argument("--dir", default=None)
|
|
356
|
+
# A trailing REMAINDER positional would swallow `--dataset` itself, so the
|
|
357
|
+
# golden-path seeder's extra arguments are passed one at a time.
|
|
358
|
+
p_demo.add_argument("--seed-arg", action="append", default=[], metavar="ARG",
|
|
359
|
+
help="extra argument forwarded to scripts/seed-demo.sh (repeatable)")
|
|
360
|
+
p_demo.set_defaults(func=cmd_demo)
|
|
361
|
+
|
|
362
|
+
p_agent = sub.add_parser("agent", help="agent enrollment helpers")
|
|
363
|
+
agent_sub = p_agent.add_subparsers(dest="agent_command")
|
|
364
|
+
p_enroll = agent_sub.add_parser("enroll", help="start vf-agent profile and prepare token dir")
|
|
365
|
+
p_enroll.add_argument("--environment", default=None)
|
|
366
|
+
p_enroll.add_argument("--dir", default=None)
|
|
367
|
+
p_enroll.add_argument("--products", default=None)
|
|
368
|
+
p_enroll.add_argument("--dry-run", action="store_true")
|
|
369
|
+
p_enroll.set_defaults(func=cmd_agent)
|
|
370
|
+
|
|
371
|
+
p_upgrade = sub.add_parser("upgrade", help="upgrade to a newer suite release")
|
|
372
|
+
p_upgrade.add_argument("--to", default=None, help="Veltro release to upgrade to, X.Y.Z")
|
|
373
|
+
p_upgrade.add_argument("--dry-run", action="store_true")
|
|
374
|
+
p_upgrade.add_argument("--dir", default=None)
|
|
375
|
+
p_upgrade.set_defaults(func=cmd_upgrade)
|
|
376
|
+
|
|
377
|
+
p_backup = sub.add_parser("backup", help="create a suite backup (§1.5)")
|
|
378
|
+
p_backup.add_argument("--to", default=None)
|
|
379
|
+
p_backup.add_argument("--auto", action="store_true")
|
|
380
|
+
p_backup.add_argument("--products", default=None)
|
|
381
|
+
p_backup.add_argument("--include-env", action="store_true")
|
|
382
|
+
p_backup.add_argument("--passphrase", default=None)
|
|
383
|
+
p_backup.add_argument("--skip-optional", action="store_true")
|
|
384
|
+
p_backup.add_argument("--schedule", default=None)
|
|
385
|
+
p_backup.add_argument("--backup-dir", default=None)
|
|
386
|
+
p_backup.add_argument("--keep", type=int, default=7)
|
|
387
|
+
p_backup.add_argument("--dir", default=None)
|
|
388
|
+
p_backup.set_defaults(func=cmd_backup)
|
|
389
|
+
|
|
390
|
+
p_restore = sub.add_parser("restore", help="restore a suite backup (§1.5)")
|
|
391
|
+
p_restore.add_argument("path", nargs="?")
|
|
392
|
+
p_restore.add_argument("--yes", action="store_true")
|
|
393
|
+
p_restore.add_argument("--products", default=None)
|
|
394
|
+
p_restore.add_argument("--passphrase", default=None)
|
|
395
|
+
p_restore.add_argument("--dry-run", action="store_true")
|
|
396
|
+
p_restore.add_argument("--skip-start", action="store_true")
|
|
397
|
+
p_restore.add_argument("--dir", default=None)
|
|
398
|
+
p_restore.set_defaults(func=cmd_restore)
|
|
399
|
+
|
|
400
|
+
p_k8s = sub.add_parser("k8s", help="kubernetes helpers")
|
|
401
|
+
k8s_sub = p_k8s.add_subparsers(dest="k8s_command")
|
|
402
|
+
p_render = k8s_sub.add_parser("render", help="helm template the umbrella chart")
|
|
403
|
+
p_render.add_argument(
|
|
404
|
+
"--products",
|
|
405
|
+
default=None,
|
|
406
|
+
help="comma-separated subset of vectorflow,chad,warden on the core; empty for core only",
|
|
407
|
+
)
|
|
408
|
+
p_render.add_argument("--values", action="append", default=[])
|
|
409
|
+
p_render.add_argument("--dry-run", action="store_true")
|
|
410
|
+
p_render.set_defaults(func=cmd_k8s)
|
|
411
|
+
|
|
412
|
+
p_sb = sub.add_parser("support-bundle", help="capture a secret-free support bundle")
|
|
413
|
+
p_sb.add_argument("--dir", default=None)
|
|
414
|
+
p_sb.add_argument("--output", default=None)
|
|
415
|
+
p_sb.add_argument("--base-url", default=None)
|
|
416
|
+
p_sb.set_defaults(func=cmd_support_bundle)
|
|
417
|
+
|
|
418
|
+
p_init = sub.add_parser("init-env", help="bootstrap .env from .env.example")
|
|
419
|
+
p_init.add_argument("--dir", default=None)
|
|
420
|
+
p_init.add_argument("--force", action="store_true")
|
|
421
|
+
p_init.set_defaults(func=cmd_init_env)
|
|
422
|
+
|
|
423
|
+
return parser
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
427
|
+
argv_list = list(sys.argv[1:] if argv is None else argv)
|
|
428
|
+
parser = build_parser()
|
|
429
|
+
if any(arg == "--profile" or arg.startswith("--profile=") for arg in argv_list):
|
|
430
|
+
print("error: --profile was removed; select products with --products or VELTRO_PRODUCTS", file=sys.stderr)
|
|
431
|
+
return 2
|
|
432
|
+
try:
|
|
433
|
+
args, unknown = parser.parse_known_args(argv_list)
|
|
434
|
+
if unknown:
|
|
435
|
+
if getattr(args, "command", None) in {"up", "down", "restart", "ps", "pull"}:
|
|
436
|
+
args.compose_args = [*getattr(args, "compose_args", ()), *unknown]
|
|
437
|
+
elif getattr(args, "command", None) == "logs":
|
|
438
|
+
args.service = [*getattr(args, "service", ()), *unknown]
|
|
439
|
+
else:
|
|
440
|
+
parser.error("unrecognized arguments: " + " ".join(unknown))
|
|
441
|
+
except SystemExit as exc:
|
|
442
|
+
return int(exc.code or 0)
|
|
443
|
+
|
|
444
|
+
# Top-level --dir / --dry-run should win when a subcommand left defaults.
|
|
445
|
+
# argparse subparsers do not inherit parent option values.
|
|
446
|
+
if hasattr(args, "dir") and args.dir is None:
|
|
447
|
+
parent = argparse.ArgumentParser(add_help=False)
|
|
448
|
+
parent.add_argument("--dir", default=None)
|
|
449
|
+
parent.add_argument("--version", action="store_true")
|
|
450
|
+
parent.add_argument("--dry-run", action="store_true")
|
|
451
|
+
parent_args, _ = parent.parse_known_args(argv_list)
|
|
452
|
+
if parent_args.dir is not None:
|
|
453
|
+
args.dir = parent_args.dir
|
|
454
|
+
if parent_args.dry_run and not getattr(args, "dry_run", False):
|
|
455
|
+
args.dry_run = True
|
|
456
|
+
|
|
457
|
+
if getattr(args, "version", False) and not getattr(args, "command", None):
|
|
458
|
+
return cmd_version(args)
|
|
459
|
+
|
|
460
|
+
func = getattr(args, "func", None)
|
|
461
|
+
if func is None:
|
|
462
|
+
if getattr(args, "version", False):
|
|
463
|
+
return cmd_version(args)
|
|
464
|
+
parser.print_help()
|
|
465
|
+
return 2
|
|
466
|
+
try:
|
|
467
|
+
return int(func(args))
|
|
468
|
+
except VeltroCliError as exc:
|
|
469
|
+
return print_error(exc)
|
|
470
|
+
except KeyboardInterrupt:
|
|
471
|
+
print("interrupted", file=sys.stderr)
|
|
472
|
+
return 130
|
|
473
|
+
except Exception as exc: # noqa: BLE001
|
|
474
|
+
return print_error(exc)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
if __name__ == "__main__":
|
|
478
|
+
raise SystemExit(main())
|
veltro_cli/lib/agent.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""veltro agent enroll — agent-up + token drop ceremony."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from veltro_cli.lib.compose import ComposeClient
|
|
8
|
+
from veltro_cli.lib.errors import VeltroCliError
|
|
9
|
+
from veltro_cli.lib.paths import enrollment_token_dir, resolve_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def enroll_agent(
|
|
13
|
+
*,
|
|
14
|
+
directory: str | Path | None = None,
|
|
15
|
+
environment: str | None = None,
|
|
16
|
+
client: ComposeClient | None = None,
|
|
17
|
+
dry_run: bool = False,
|
|
18
|
+
) -> int:
|
|
19
|
+
root = resolve_dir(directory)
|
|
20
|
+
client = client or ComposeClient(root)
|
|
21
|
+
token_dir = enrollment_token_dir(root)
|
|
22
|
+
if not dry_run:
|
|
23
|
+
token_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
result = client.agent_up(dry_run=dry_run)
|
|
25
|
+
if result.returncode != 0:
|
|
26
|
+
raise VeltroCliError(
|
|
27
|
+
f"agent-up failed: {result.stderr.strip() or result.stdout.strip()}",
|
|
28
|
+
remedy="ensure the suite is up and an enrollment token will be dropped into "
|
|
29
|
+
f"{token_dir} (see README 'VectorFlow agent enrollment')",
|
|
30
|
+
)
|
|
31
|
+
# Token minting is owned by the seeder / VF API; we only start the agent profile.
|
|
32
|
+
if environment:
|
|
33
|
+
# Record intent for operators; actual enroll uses the token file.
|
|
34
|
+
marker = token_dir / "environment-name"
|
|
35
|
+
if not dry_run:
|
|
36
|
+
marker.write_text(environment + "\n", encoding="utf-8")
|
|
37
|
+
return 0
|
veltro_cli/lib/backup.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Suite backup / restore command handlers (plan §1.5).
|
|
2
|
+
|
|
3
|
+
The implementation lives in ``scripts/veltro_backup.py`` so thin shims and the
|
|
4
|
+
operator CLI share one stdlib module. This adapter translates ``veltro backup``
|
|
5
|
+
/ ``veltro restore`` argparse namespaces into that library and maps failures to
|
|
6
|
+
``VeltroCliError``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from veltro_cli.lib.errors import VeltroCliError
|
|
17
|
+
from veltro_cli.lib.paths import resolve_dir
|
|
18
|
+
|
|
19
|
+
# scripts/veltro_backup.py remains the single DR implementation. The installed
|
|
20
|
+
# CLI resolves it from the selected suite directory; the source checkout paths
|
|
21
|
+
# keep editable installs working.
|
|
22
|
+
_HERE = Path(__file__).resolve()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_veltro_backup(root: Path):
|
|
26
|
+
import importlib.util
|
|
27
|
+
|
|
28
|
+
candidates = (
|
|
29
|
+
root / "scripts",
|
|
30
|
+
_HERE.parents[3] / "scripts",
|
|
31
|
+
_HERE.parents[2] / "scripts",
|
|
32
|
+
Path.cwd() / "scripts",
|
|
33
|
+
)
|
|
34
|
+
for scripts_dir in candidates:
|
|
35
|
+
path = scripts_dir / "veltro_backup.py"
|
|
36
|
+
if path.is_file():
|
|
37
|
+
spec = importlib.util.spec_from_file_location("veltro_backup", path)
|
|
38
|
+
if spec is None or spec.loader is None:
|
|
39
|
+
continue
|
|
40
|
+
mod = importlib.util.module_from_spec(spec)
|
|
41
|
+
sys.modules["veltro_backup"] = mod
|
|
42
|
+
spec.loader.exec_module(mod)
|
|
43
|
+
return mod
|
|
44
|
+
try:
|
|
45
|
+
import veltro_backup as mod # type: ignore
|
|
46
|
+
|
|
47
|
+
return mod
|
|
48
|
+
except ImportError as exc: # pragma: no cover
|
|
49
|
+
raise VeltroCliError(
|
|
50
|
+
"veltro_backup library not found",
|
|
51
|
+
remedy="run from a Veltro checkout that contains scripts/veltro_backup.py",
|
|
52
|
+
) from exc
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_backup(args: Any) -> int:
|
|
56
|
+
"""``veltro backup [--to PATH] [--auto] [--dir DIR]``."""
|
|
57
|
+
root = resolve_dir(getattr(args, "dir", None))
|
|
58
|
+
vb = _load_veltro_backup(root)
|
|
59
|
+
products = getattr(args, "products", None)
|
|
60
|
+
include_env = bool(getattr(args, "include_env", False))
|
|
61
|
+
passphrase = getattr(args, "passphrase", None)
|
|
62
|
+
schedule = getattr(args, "schedule", None)
|
|
63
|
+
if schedule:
|
|
64
|
+
sys.stdout.write(
|
|
65
|
+
vb.schedule_snippet(
|
|
66
|
+
schedule,
|
|
67
|
+
backup_dir=getattr(args, "backup_dir", None) or "/var/lib/veltro/backups",
|
|
68
|
+
keep=int(getattr(args, "keep", 7) or 7),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
sys.stdout.write(
|
|
72
|
+
"Enable compose profile `backup` (compose/backup.yml) after writing crontab.\n"
|
|
73
|
+
)
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
output = getattr(args, "to", None)
|
|
77
|
+
if output is None and getattr(args, "auto", False):
|
|
78
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
79
|
+
output = str(root / "backups" / f"veltro-backup-auto-{stamp}.tar")
|
|
80
|
+
Path(output).parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
options = vb.BackupOptions(
|
|
83
|
+
root=root,
|
|
84
|
+
products=products,
|
|
85
|
+
output=Path(output) if output else None,
|
|
86
|
+
include_env=include_env,
|
|
87
|
+
passphrase=passphrase,
|
|
88
|
+
skip_optional=bool(getattr(args, "skip_optional", False)),
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
path = vb.create_backup(options)
|
|
92
|
+
except vb.BackupError as exc:
|
|
93
|
+
raise VeltroCliError(
|
|
94
|
+
str(exc),
|
|
95
|
+
remedy="see docs/runbooks/backup-restore.md; ensure selected products are up and VELTRO_BACKUP_PASSPHRASE is set when keyrings exist",
|
|
96
|
+
) from exc
|
|
97
|
+
print(f"wrote {path}")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_restore(args: Any) -> int:
|
|
102
|
+
"""``veltro restore PATH [--yes] [--dir DIR]``."""
|
|
103
|
+
|
|
104
|
+
archive = getattr(args, "path", None) or getattr(args, "archive", None)
|
|
105
|
+
if not archive:
|
|
106
|
+
raise VeltroCliError(
|
|
107
|
+
"restore requires an archive path",
|
|
108
|
+
remedy="veltro restore ./veltro-backup-….tar --yes",
|
|
109
|
+
)
|
|
110
|
+
root = resolve_dir(getattr(args, "dir", None))
|
|
111
|
+
vb = _load_veltro_backup(root)
|
|
112
|
+
options = vb.RestoreOptions(
|
|
113
|
+
root=root,
|
|
114
|
+
archive=Path(archive),
|
|
115
|
+
products=getattr(args, "products", None),
|
|
116
|
+
passphrase=getattr(args, "passphrase", None),
|
|
117
|
+
dry_run=bool(getattr(args, "dry_run", False)),
|
|
118
|
+
yes=bool(getattr(args, "yes", False)),
|
|
119
|
+
skip_start=bool(getattr(args, "skip_start", False)),
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
result = vb.run_restore(options)
|
|
123
|
+
except vb.BackupError as exc:
|
|
124
|
+
raise VeltroCliError(
|
|
125
|
+
str(exc),
|
|
126
|
+
remedy="run with --dry-run to inspect the plan; refuse rules are documented in docs/runbooks/backup-restore.md",
|
|
127
|
+
) from exc
|
|
128
|
+
import json
|
|
129
|
+
|
|
130
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
131
|
+
if result.get("status") == "restored":
|
|
132
|
+
return 0
|
|
133
|
+
if result.get("executable") is False:
|
|
134
|
+
return 2
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = ["cmd_backup", "cmd_restore"]
|