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.
@@ -0,0 +1,5 @@
1
+ """Pacote principal do CLI Raijin Server."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
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()
@@ -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)