raijin-server 0.1.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.
- raijin_server/__init__.py +5 -0
- raijin_server/cli.py +447 -0
- raijin_server/config.py +139 -0
- raijin_server/healthchecks.py +296 -0
- raijin_server/modules/__init__.py +26 -0
- raijin_server/modules/bootstrap.py +224 -0
- raijin_server/modules/calico.py +36 -0
- raijin_server/modules/essentials.py +29 -0
- raijin_server/modules/firewall.py +27 -0
- raijin_server/modules/full_install.py +131 -0
- raijin_server/modules/grafana.py +69 -0
- raijin_server/modules/hardening.py +47 -0
- raijin_server/modules/harness.py +47 -0
- raijin_server/modules/istio.py +13 -0
- raijin_server/modules/kafka.py +34 -0
- raijin_server/modules/kong.py +19 -0
- raijin_server/modules/kubernetes.py +187 -0
- raijin_server/modules/loki.py +27 -0
- raijin_server/modules/minio.py +19 -0
- raijin_server/modules/network.py +57 -0
- raijin_server/modules/prometheus.py +30 -0
- raijin_server/modules/traefik.py +40 -0
- raijin_server/modules/velero.py +47 -0
- raijin_server/modules/vpn.py +152 -0
- raijin_server/utils.py +241 -0
- raijin_server/validators.py +230 -0
- raijin_server-0.1.0.dist-info/METADATA +219 -0
- raijin_server-0.1.0.dist-info/RECORD +32 -0
- raijin_server-0.1.0.dist-info/WHEEL +5 -0
- raijin_server-0.1.0.dist-info/entry_points.txt +2 -0
- raijin_server-0.1.0.dist-info/licenses/LICENSE +21 -0
- raijin_server-0.1.0.dist-info/top_level.txt +1 -0
raijin_server/cli.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""CLI principal do projeto Raijin Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Callable, Dict, Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich import box
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.prompt import Confirm, Prompt
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from raijin_server import __version__
|
|
17
|
+
from raijin_server.modules import (
|
|
18
|
+
calico,
|
|
19
|
+
essentials,
|
|
20
|
+
firewall,
|
|
21
|
+
grafana,
|
|
22
|
+
harness,
|
|
23
|
+
hardening,
|
|
24
|
+
istio,
|
|
25
|
+
kafka,
|
|
26
|
+
kong,
|
|
27
|
+
kubernetes,
|
|
28
|
+
loki,
|
|
29
|
+
minio,
|
|
30
|
+
network,
|
|
31
|
+
prometheus,
|
|
32
|
+
traefik,
|
|
33
|
+
velero,
|
|
34
|
+
bootstrap,
|
|
35
|
+
full_install,
|
|
36
|
+
)
|
|
37
|
+
from raijin_server.utils import ExecutionContext, logger
|
|
38
|
+
from raijin_server.validators import validate_system_requirements, check_module_dependencies
|
|
39
|
+
from raijin_server.healthchecks import run_health_check
|
|
40
|
+
from raijin_server.config import ConfigManager
|
|
41
|
+
|
|
42
|
+
app = typer.Typer(add_completion=False, help="Automacao de setup e hardening para Ubuntu Server")
|
|
43
|
+
console = Console()
|
|
44
|
+
STATE_DIR_CANDIDATES = [
|
|
45
|
+
Path("/var/lib/raijin-server/state"),
|
|
46
|
+
Path.home() / ".local/share/raijin-server/state",
|
|
47
|
+
]
|
|
48
|
+
EXIT_OPTION = "sair"
|
|
49
|
+
_STATE_DIR_CACHE: Optional[Path] = None
|
|
50
|
+
|
|
51
|
+
BANNER = r"""
|
|
52
|
+
|
|
53
|
+
___ ___ ___ ___
|
|
54
|
+
/\ \ /\ \ ___ /\ \ ___ /\__\
|
|
55
|
+
/::\ \ /::\ \ /\ \ \:\ \ /\ \ /::| |
|
|
56
|
+
/:/\:\ \ /:/\:\ \ \:\ \ ___ /::\__\ \:\ \ /:|:| |
|
|
57
|
+
/::\~\:\ \ /::\~\:\ \ /::\__\ /\ /:/\/__/ /::\__\ /:/|:| |__
|
|
58
|
+
/:/\:\ \:\__\ /:/\:\ \:\__\ __/:/\/__/ \:\/:/ / __/:/\/__/ /:/ |:| /\__\
|
|
59
|
+
\/_|::\/:/ / \/__\:\/:/ / /\/:/ / \::/ / /\/:/ / \/__|:|/:/ /
|
|
60
|
+
|:|::/ / \::/ / \::/__/ \/__/ \::/__/ |:/:/ /
|
|
61
|
+
|:|\/__/ /:/ / \:\__\ \:\__\ |::/ /
|
|
62
|
+
|:| | /:/ / \/__/ \/__/ /:/ /
|
|
63
|
+
\|__| \/__/ \/__/
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
MODULES: Dict[str, Callable[[ExecutionContext], None]] = {
|
|
68
|
+
"bootstrap": bootstrap.run,
|
|
69
|
+
"hardening": hardening.run,
|
|
70
|
+
"network": network.run,
|
|
71
|
+
"essentials": essentials.run,
|
|
72
|
+
"firewall": firewall.run,
|
|
73
|
+
"kubernetes": kubernetes.run,
|
|
74
|
+
"calico": calico.run,
|
|
75
|
+
"istio": istio.run,
|
|
76
|
+
"traefik": traefik.run,
|
|
77
|
+
"kong": kong.run,
|
|
78
|
+
"minio": minio.run,
|
|
79
|
+
"prometheus": prometheus.run,
|
|
80
|
+
"grafana": grafana.run,
|
|
81
|
+
"loki": loki.run,
|
|
82
|
+
"harness": harness.run,
|
|
83
|
+
"velero": velero.run,
|
|
84
|
+
"kafka": kafka.run,
|
|
85
|
+
"full_install": full_install.run,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
MODULE_DESCRIPTIONS: Dict[str, str] = {
|
|
89
|
+
"bootstrap": "Instala ferramentas: helm, kubectl, istioctl, velero, containerd",
|
|
90
|
+
"hardening": "Ajustes de kernel, auditd, fail2ban",
|
|
91
|
+
"network": "Netplan, hostname, DNS",
|
|
92
|
+
"essentials": "Pacotes basicos, repos, utilitarios",
|
|
93
|
+
"firewall": "Regras UFW padrao e serviços basicos",
|
|
94
|
+
"kubernetes": "Instala kubeadm/kubelet/kubectl e inicializa cluster",
|
|
95
|
+
"calico": "CNI Calico e politica default deny",
|
|
96
|
+
"istio": "Service mesh Istio via Helm",
|
|
97
|
+
"traefik": "Ingress controller Traefik com TLS",
|
|
98
|
+
"kong": "Ingress/Gateway Kong via Helm",
|
|
99
|
+
"minio": "Objeto storage S3-compat via Helm",
|
|
100
|
+
"prometheus": "Stack kube-prometheus",
|
|
101
|
+
"grafana": "Dashboards e datasource Prometheus",
|
|
102
|
+
"loki": "Logs centralizados Loki",
|
|
103
|
+
"harness": "Delegate Harness via Helm",
|
|
104
|
+
"velero": "Backup/restore de clusters",
|
|
105
|
+
"kafka": "Cluster Kafka via OCI Helm",
|
|
106
|
+
"full_install": "Instalacao completa e automatizada do ambiente",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _run_module(ctx: typer.Context, name: str, skip_validation: bool = False) -> None:
|
|
111
|
+
handler = MODULES.get(name)
|
|
112
|
+
if handler is None:
|
|
113
|
+
raise typer.BadParameter(f"Modulo '{name}' nao encontrado.")
|
|
114
|
+
exec_ctx = ctx.obj or ExecutionContext()
|
|
115
|
+
|
|
116
|
+
# Verifica dependencias do modulo
|
|
117
|
+
if not skip_validation and not check_module_dependencies(name, exec_ctx):
|
|
118
|
+
typer.secho(f"Execute primeiro os modulos dependentes antes de '{name}'", fg=typer.colors.RED)
|
|
119
|
+
raise typer.Exit(code=1)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
logger.info(f"Iniciando execucao do modulo: {name}")
|
|
123
|
+
typer.secho(f"\n{'='*60}", fg=typer.colors.CYAN)
|
|
124
|
+
typer.secho(f"Executando modulo: {name}", fg=typer.colors.CYAN, bold=True)
|
|
125
|
+
typer.secho(f"{'='*60}\n", fg=typer.colors.CYAN)
|
|
126
|
+
|
|
127
|
+
handler(exec_ctx)
|
|
128
|
+
|
|
129
|
+
# Executa health check se disponivel
|
|
130
|
+
if not exec_ctx.dry_run:
|
|
131
|
+
typer.echo("\nExecutando health check...")
|
|
132
|
+
health_ok = run_health_check(name, exec_ctx)
|
|
133
|
+
if health_ok:
|
|
134
|
+
_mark_completed(name)
|
|
135
|
+
typer.secho(f"\n✓ Modulo '{name}' concluido com sucesso!", fg=typer.colors.GREEN, bold=True)
|
|
136
|
+
logger.info(f"Modulo '{name}' concluido com sucesso")
|
|
137
|
+
else:
|
|
138
|
+
typer.secho(f"\n⚠ Modulo '{name}' executado mas health check falhou", fg=typer.colors.YELLOW, bold=True)
|
|
139
|
+
logger.warning(f"Modulo '{name}' executado mas health check falhou")
|
|
140
|
+
else:
|
|
141
|
+
typer.secho(f"\n✓ Modulo '{name}' executado em modo dry-run", fg=typer.colors.YELLOW)
|
|
142
|
+
|
|
143
|
+
# Mostra avisos e erros acumulados
|
|
144
|
+
if exec_ctx.warnings:
|
|
145
|
+
typer.secho(f"\nAvisos ({len(exec_ctx.warnings)}):", fg=typer.colors.YELLOW)
|
|
146
|
+
for warn in exec_ctx.warnings:
|
|
147
|
+
typer.echo(f" ⚠ {warn}")
|
|
148
|
+
if exec_ctx.errors:
|
|
149
|
+
typer.secho(f"\nErros ({len(exec_ctx.errors)}):", fg=typer.colors.RED)
|
|
150
|
+
for err in exec_ctx.errors:
|
|
151
|
+
typer.echo(f" ✗ {err}")
|
|
152
|
+
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
logger.warning(f"Modulo '{name}' interrompido pelo usuario")
|
|
155
|
+
typer.secho(f"\n\n⚠ Modulo '{name}' interrompido", fg=typer.colors.YELLOW)
|
|
156
|
+
raise typer.Exit(code=130)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Erro fatal no modulo '{name}': {e}", exc_info=True)
|
|
159
|
+
typer.secho(f"\n✗ Erro fatal no modulo '{name}': {e}", fg=typer.colors.RED, bold=True)
|
|
160
|
+
raise typer.Exit(code=1)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _print_banner() -> None:
|
|
164
|
+
console.print(Panel.fit(BANNER, style="bold blue"))
|
|
165
|
+
console.print("[bright_white]Automacao de setup e hardening para Ubuntu Server[/bright_white]\n")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _select_state_dir() -> Path:
|
|
169
|
+
global _STATE_DIR_CACHE
|
|
170
|
+
if _STATE_DIR_CACHE is not None:
|
|
171
|
+
return _STATE_DIR_CACHE
|
|
172
|
+
|
|
173
|
+
override = os.environ.get("RAIJIN_STATE_DIR")
|
|
174
|
+
if override:
|
|
175
|
+
cand = Path(override).expanduser()
|
|
176
|
+
cand.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
_STATE_DIR_CACHE = cand
|
|
178
|
+
console.print(f"[cyan]Usando estado em {cand} (RAIJIN_STATE_DIR)[/cyan]")
|
|
179
|
+
return cand
|
|
180
|
+
|
|
181
|
+
for cand in STATE_DIR_CANDIDATES:
|
|
182
|
+
try:
|
|
183
|
+
cand.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
test = cand / ".rwtest"
|
|
185
|
+
test.touch()
|
|
186
|
+
test.unlink()
|
|
187
|
+
_STATE_DIR_CACHE = cand
|
|
188
|
+
if cand != STATE_DIR_CANDIDATES[0]:
|
|
189
|
+
console.print(f"[yellow]Estado gravado em {cand} (fallback por permissao)[/yellow]")
|
|
190
|
+
return cand
|
|
191
|
+
except Exception:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
fallback = Path("/tmp/raijin-state")
|
|
195
|
+
fallback.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
console.print(f"[yellow]Usando fallback /tmp/raijin-state para marcar conclusao[/yellow]")
|
|
197
|
+
_STATE_DIR_CACHE = fallback
|
|
198
|
+
return fallback
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _state_file(name: str) -> Path:
|
|
202
|
+
return _select_state_dir() / f"{name}.done"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _mark_completed(name: str) -> None:
|
|
206
|
+
try:
|
|
207
|
+
path = _state_file(name)
|
|
208
|
+
path.write_text("ok\n")
|
|
209
|
+
except Exception:
|
|
210
|
+
console.print("[yellow]Nao foi possivel registrar estado (permissao negada). Considere definir RAIJIN_STATE_DIR.[/yellow]")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _is_completed(name: str) -> bool:
|
|
214
|
+
return _state_file(name).exists()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _render_menu(dry_run: bool) -> int:
|
|
218
|
+
table = Table(
|
|
219
|
+
title="Selecione um modulo para executar",
|
|
220
|
+
header_style="bold white",
|
|
221
|
+
box=box.ROUNDED,
|
|
222
|
+
expand=True,
|
|
223
|
+
)
|
|
224
|
+
table.add_column("#", justify="right", style="cyan", no_wrap=True)
|
|
225
|
+
table.add_column("Status", style="green", no_wrap=True)
|
|
226
|
+
table.add_column("Modulo", style="bold green")
|
|
227
|
+
table.add_column("Descricao", style="white")
|
|
228
|
+
for idx, name in enumerate(MODULES.keys(), start=1):
|
|
229
|
+
desc = MODULE_DESCRIPTIONS.get(name, "")
|
|
230
|
+
status = "[green]✔[/green]" if _is_completed(name) else "[dim]-[/dim]"
|
|
231
|
+
table.add_row(f"{idx}", status, name, desc)
|
|
232
|
+
|
|
233
|
+
exit_idx = len(MODULES) + 1
|
|
234
|
+
table.add_row(
|
|
235
|
+
f"{exit_idx}", "[red]↩[/red]", EXIT_OPTION, "Sair do menu",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
mode_label = "[yellow]DRY-RUN[/yellow]" if dry_run else "[bold red]APLICAR[/bold red]"
|
|
239
|
+
console.print(Panel.fit(f"Modo atual: {mode_label} | t = alternar modo | {EXIT_OPTION} = sair", style="dim"))
|
|
240
|
+
console.print(table)
|
|
241
|
+
return exit_idx
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def interactive_menu(ctx: typer.Context) -> None:
|
|
245
|
+
exec_ctx = ctx.obj or ExecutionContext()
|
|
246
|
+
current_dry_run = exec_ctx.dry_run
|
|
247
|
+
_print_banner()
|
|
248
|
+
console.print(
|
|
249
|
+
Panel.fit(
|
|
250
|
+
f"Use o numero, nome ou '{EXIT_OPTION}'.\nPressione t para alternar dry-run.",
|
|
251
|
+
style="bold magenta",
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
while True:
|
|
256
|
+
exit_idx = _render_menu(current_dry_run)
|
|
257
|
+
choice = Prompt.ask("Escolha", default=EXIT_OPTION).strip().lower()
|
|
258
|
+
|
|
259
|
+
if choice in {"q", EXIT_OPTION}:
|
|
260
|
+
return
|
|
261
|
+
if choice == "t":
|
|
262
|
+
current_dry_run = not current_dry_run
|
|
263
|
+
exec_ctx.dry_run = current_dry_run
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
if choice.isdigit():
|
|
267
|
+
idx = int(choice)
|
|
268
|
+
if idx == exit_idx:
|
|
269
|
+
return
|
|
270
|
+
if 1 <= idx <= len(MODULES):
|
|
271
|
+
name = list(MODULES.keys())[idx - 1]
|
|
272
|
+
else:
|
|
273
|
+
console.print("[red]Opcao invalida[/red]")
|
|
274
|
+
continue
|
|
275
|
+
else:
|
|
276
|
+
name = choice
|
|
277
|
+
|
|
278
|
+
if name not in MODULES:
|
|
279
|
+
console.print("[red]Opcao invalida[/red]")
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
exec_ctx = ExecutionContext(dry_run=current_dry_run)
|
|
283
|
+
ctx.obj = exec_ctx
|
|
284
|
+
_run_module(ctx, name)
|
|
285
|
+
# Loop continua e menu eh re-renderizado, refletindo status atualizado quando nao eh dry-run.
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@app.callback(invoke_without_command=True)
|
|
289
|
+
def main(
|
|
290
|
+
ctx: typer.Context,
|
|
291
|
+
module: Optional[str] = typer.Option(None, "-m", "--module", help="Modulo a executar"),
|
|
292
|
+
dry_run: bool = typer.Option(False, "-n", "--dry-run", help="Mostra comandos sem executa-los."),
|
|
293
|
+
skip_validation: bool = typer.Option(False, "--skip-validation", help="Pula validacoes de pre-requisitos"),
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Mostra um menu simples quando nenhum subcomando e informado."""
|
|
296
|
+
|
|
297
|
+
ctx.obj = ExecutionContext(dry_run=dry_run)
|
|
298
|
+
|
|
299
|
+
# Executa validacoes de pre-requisitos
|
|
300
|
+
if not skip_validation and not dry_run:
|
|
301
|
+
if not validate_system_requirements(ctx.obj, skip_root=False):
|
|
302
|
+
typer.secho("\nAbortando devido a pre-requisitos nao atendidos.", fg=typer.colors.RED)
|
|
303
|
+
typer.echo("Use --skip-validation para pular validacoes (nao recomendado).")
|
|
304
|
+
raise typer.Exit(code=1)
|
|
305
|
+
|
|
306
|
+
if ctx.invoked_subcommand:
|
|
307
|
+
return
|
|
308
|
+
if module:
|
|
309
|
+
_run_module(ctx, module, skip_validation=skip_validation)
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
interactive_menu(ctx)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@app.command()
|
|
316
|
+
def menu(ctx: typer.Context) -> None:
|
|
317
|
+
"""Abre o menu interativo colorido."""
|
|
318
|
+
|
|
319
|
+
interactive_menu(ctx)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.command()
|
|
323
|
+
def hardening(ctx: typer.Context) -> None:
|
|
324
|
+
_run_module(ctx, "hardening")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.command()
|
|
328
|
+
def network(ctx: typer.Context) -> None:
|
|
329
|
+
_run_module(ctx, "network")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@app.command()
|
|
333
|
+
def essentials(ctx: typer.Context) -> None:
|
|
334
|
+
_run_module(ctx, "essentials")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@app.command()
|
|
338
|
+
def firewall(ctx: typer.Context) -> None:
|
|
339
|
+
_run_module(ctx, "firewall")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@app.command()
|
|
343
|
+
def kubernetes(ctx: typer.Context) -> None:
|
|
344
|
+
_run_module(ctx, "kubernetes")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@app.command()
|
|
348
|
+
def calico(ctx: typer.Context) -> None:
|
|
349
|
+
_run_module(ctx, "calico")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.command()
|
|
353
|
+
def istio(ctx: typer.Context) -> None:
|
|
354
|
+
_run_module(ctx, "istio")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@app.command()
|
|
358
|
+
def traefik(ctx: typer.Context) -> None:
|
|
359
|
+
_run_module(ctx, "traefik")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@app.command()
|
|
363
|
+
def kong(ctx: typer.Context) -> None:
|
|
364
|
+
_run_module(ctx, "kong")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@app.command()
|
|
368
|
+
def minio(ctx: typer.Context) -> None:
|
|
369
|
+
_run_module(ctx, "minio")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@app.command()
|
|
373
|
+
def prometheus(ctx: typer.Context) -> None:
|
|
374
|
+
_run_module(ctx, "prometheus")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@app.command()
|
|
378
|
+
def grafana(ctx: typer.Context) -> None:
|
|
379
|
+
_run_module(ctx, "grafana")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@app.command()
|
|
383
|
+
def loki(ctx: typer.Context) -> None:
|
|
384
|
+
_run_module(ctx, "loki")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@app.command()
|
|
388
|
+
def harness(ctx: typer.Context) -> None:
|
|
389
|
+
_run_module(ctx, "harness")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@app.command()
|
|
393
|
+
def velero(ctx: typer.Context) -> None:
|
|
394
|
+
_run_module(ctx, "velero")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@app.command()
|
|
398
|
+
def kafka(ctx: typer.Context) -> None:
|
|
399
|
+
_run_module(ctx, "kafka")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@app.command(name="bootstrap")
|
|
403
|
+
def bootstrap_cmd(ctx: typer.Context) -> None:
|
|
404
|
+
"""Instala todas as ferramentas necessarias: helm, kubectl, istioctl, velero, containerd."""
|
|
405
|
+
_run_module(ctx, "bootstrap")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@app.command(name="full-install")
|
|
409
|
+
def full_install_cmd(ctx: typer.Context) -> None:
|
|
410
|
+
"""Executa instalacao completa e automatizada do ambiente de producao."""
|
|
411
|
+
_run_module(ctx, "full_install")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@app.command()
|
|
415
|
+
def version() -> None:
|
|
416
|
+
"""Mostra a versao do CLI."""
|
|
417
|
+
|
|
418
|
+
typer.echo(f"raijin-server {__version__}")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@app.command()
|
|
422
|
+
def generate_config(output: str = typer.Option("raijin-config.yaml", "--output", "-o", help="Arquivo de saida")) -> None:
|
|
423
|
+
"""Gera template de configuracao YAML/JSON."""
|
|
424
|
+
|
|
425
|
+
ConfigManager.create_template(output)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@app.command()
|
|
429
|
+
def validate(skip_root: bool = typer.Option(False, "--skip-root", help="Pula validacao de root")) -> None:
|
|
430
|
+
"""Valida pre-requisitos do sistema sem executar modulos."""
|
|
431
|
+
|
|
432
|
+
ctx = ExecutionContext(dry_run=False)
|
|
433
|
+
if validate_system_requirements(ctx, skip_root=skip_root):
|
|
434
|
+
typer.secho("\n✓ Sistema validado com sucesso!", fg=typer.colors.GREEN, bold=True)
|
|
435
|
+
else:
|
|
436
|
+
typer.secho("\n✗ Sistema nao atende pre-requisitos", fg=typer.colors.RED, bold=True)
|
|
437
|
+
raise typer.Exit(code=1)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def main_entrypoint() -> None:
|
|
441
|
+
"""Ponto de entrada para console_scripts."""
|
|
442
|
+
|
|
443
|
+
app(prog_name="raijin-server")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
main_entrypoint()
|
raijin_server/config.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Gerenciador de configuracoes via arquivo YAML/JSON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import yaml
|
|
13
|
+
YAML_AVAILABLE = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
YAML_AVAILABLE = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigManager:
|
|
19
|
+
"""Gerencia configuracoes do raijin-server via arquivo."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config_path: str | Path | None = None):
|
|
22
|
+
self.config_path = Path(config_path) if config_path else None
|
|
23
|
+
self.config: Dict[str, Any] = {}
|
|
24
|
+
if self.config_path and self.config_path.exists():
|
|
25
|
+
self.load()
|
|
26
|
+
|
|
27
|
+
def load(self) -> None:
|
|
28
|
+
"""Carrega configuracoes do arquivo."""
|
|
29
|
+
if not self.config_path or not self.config_path.exists():
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
content = self.config_path.read_text()
|
|
34
|
+
if self.config_path.suffix in [".yaml", ".yml"]:
|
|
35
|
+
if not YAML_AVAILABLE:
|
|
36
|
+
raise ImportError("PyYAML nao instalado. Instale com: pip install pyyaml")
|
|
37
|
+
self.config = yaml.safe_load(content) or {}
|
|
38
|
+
elif self.config_path.suffix == ".json":
|
|
39
|
+
self.config = json.loads(content)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f"Formato nao suportado: {self.config_path.suffix}")
|
|
42
|
+
except Exception as e:
|
|
43
|
+
typer.secho(f"Erro ao carregar config de {self.config_path}: {e}", fg=typer.colors.RED)
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
47
|
+
"""Obtem valor de configuracao."""
|
|
48
|
+
keys = key.split(".")
|
|
49
|
+
value = self.config
|
|
50
|
+
for k in keys:
|
|
51
|
+
if isinstance(value, dict):
|
|
52
|
+
value = value.get(k)
|
|
53
|
+
if value is None:
|
|
54
|
+
return default
|
|
55
|
+
else:
|
|
56
|
+
return default
|
|
57
|
+
return value if value is not None else default
|
|
58
|
+
|
|
59
|
+
def get_module_config(self, module: str) -> Dict[str, Any]:
|
|
60
|
+
"""Obtem configuracoes especificas de um modulo."""
|
|
61
|
+
return self.config.get("modules", {}).get(module, {})
|
|
62
|
+
|
|
63
|
+
def get_global(self, key: str, default: Any = None) -> Any:
|
|
64
|
+
"""Obtem configuracao global."""
|
|
65
|
+
return self.config.get("global", {}).get(key, default)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def create_template(cls, output_path: str | Path) -> None:
|
|
69
|
+
"""Cria template de configuracao."""
|
|
70
|
+
template = {
|
|
71
|
+
"global": {
|
|
72
|
+
"dry_run": False,
|
|
73
|
+
"max_retries": 3,
|
|
74
|
+
"retry_delay": 5,
|
|
75
|
+
"timeout": 300,
|
|
76
|
+
"skip_health_checks": False,
|
|
77
|
+
},
|
|
78
|
+
"modules": {
|
|
79
|
+
"network": {
|
|
80
|
+
"interface": "ens18",
|
|
81
|
+
"address": "192.168.0.10/24",
|
|
82
|
+
"gateway": "192.168.0.1",
|
|
83
|
+
"dns": "1.1.1.1,8.8.8.8",
|
|
84
|
+
},
|
|
85
|
+
"kubernetes": {
|
|
86
|
+
"pod_cidr": "10.244.0.0/16",
|
|
87
|
+
"service_cidr": "10.96.0.0/12",
|
|
88
|
+
"cluster_name": "raijin",
|
|
89
|
+
"advertise_address": "0.0.0.0",
|
|
90
|
+
},
|
|
91
|
+
"calico": {
|
|
92
|
+
"pod_cidr": "10.244.0.0/16",
|
|
93
|
+
},
|
|
94
|
+
"prometheus": {
|
|
95
|
+
"namespace": "observability",
|
|
96
|
+
"retention": "15d",
|
|
97
|
+
"storage": "20Gi",
|
|
98
|
+
},
|
|
99
|
+
"grafana": {
|
|
100
|
+
"namespace": "observability",
|
|
101
|
+
"admin_password": "changeme",
|
|
102
|
+
"ingress_host": "grafana.example.com",
|
|
103
|
+
},
|
|
104
|
+
"traefik": {
|
|
105
|
+
"namespace": "traefik",
|
|
106
|
+
"ingress_class": "traefik",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
path = Path(output_path)
|
|
112
|
+
if path.suffix in [".yaml", ".yml"]:
|
|
113
|
+
if not YAML_AVAILABLE:
|
|
114
|
+
typer.secho("PyYAML nao instalado. Salvando como JSON...", fg=typer.colors.YELLOW)
|
|
115
|
+
path = path.with_suffix(".json")
|
|
116
|
+
content = json.dumps(template, indent=2)
|
|
117
|
+
else:
|
|
118
|
+
content = yaml.dump(template, default_flow_style=False, sort_keys=False)
|
|
119
|
+
else:
|
|
120
|
+
content = json.dumps(template, indent=2)
|
|
121
|
+
|
|
122
|
+
path.write_text(content)
|
|
123
|
+
typer.secho(f"Template de configuracao criado em: {path}", fg=typer.colors.GREEN)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def prompt_or_config(
|
|
127
|
+
prompt_text: str,
|
|
128
|
+
default: str,
|
|
129
|
+
config_manager: ConfigManager | None,
|
|
130
|
+
config_key: str,
|
|
131
|
+
) -> str:
|
|
132
|
+
"""Obtem valor de prompt ou config file."""
|
|
133
|
+
if config_manager:
|
|
134
|
+
value = config_manager.get(config_key)
|
|
135
|
+
if value is not None:
|
|
136
|
+
typer.echo(f"{prompt_text} (via config): {value}")
|
|
137
|
+
return str(value)
|
|
138
|
+
|
|
139
|
+
return typer.prompt(prompt_text, default=default)
|