raijin-server 0.2.5__py3-none-any.whl → 0.2.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.
raijin_server/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """Pacote principal do CLI Raijin Server."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.7"
4
4
 
5
5
  __all__ = ["__version__"]
raijin_server/cli.py CHANGED
@@ -6,6 +6,8 @@ import os
6
6
  from pathlib import Path
7
7
  from typing import Callable, Dict, Optional
8
8
 
9
+ import subprocess
10
+
9
11
  import typer
10
12
  from rich import box
11
13
  from rich.console import Console
@@ -42,7 +44,7 @@ from raijin_server.modules import (
42
44
  velero,
43
45
  vpn,
44
46
  )
45
- from raijin_server.utils import ExecutionContext, logger
47
+ from raijin_server.utils import ExecutionContext, logger, active_log_file, available_log_files, page_text, ensure_tool
46
48
  from raijin_server.validators import validate_system_requirements, check_module_dependencies
47
49
  from raijin_server.healthchecks import run_health_check
48
50
  from raijin_server.config import ConfigManager
@@ -131,6 +133,19 @@ MODULE_DESCRIPTIONS: Dict[str, str] = {
131
133
  }
132
134
 
133
135
 
136
+ def _capture_cmd(cmd: list[str], timeout: int = 30) -> str:
137
+ try:
138
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
139
+ if result.returncode == 0:
140
+ return result.stdout.strip() or "(sem saida)"
141
+ return (
142
+ f"✗ {' '.join(cmd)}\n"
143
+ f"{(result.stdout or '').strip()}\n{(result.stderr or '').strip()}".strip()
144
+ )
145
+ except Exception as exc:
146
+ return f"✗ {' '.join(cmd)} -> {exc}"
147
+
148
+
134
149
  def _run_module(ctx: typer.Context, name: str, skip_validation: bool = False) -> None:
135
150
  handler = MODULES.get(name)
136
151
  if handler is None:
@@ -542,6 +557,120 @@ def cert_list_issuers(ctx: typer.Context) -> None:
542
557
  pass
543
558
 
544
559
 
560
+ # ============================================================================
561
+ # Ferramentas de Depuração / Logs
562
+ # ============================================================================
563
+ debug_app = typer.Typer(help="Ferramentas de depuracao e investigacao de logs")
564
+ app.add_typer(debug_app, name="debug")
565
+
566
+
567
+ @debug_app.command(name="logs")
568
+ def debug_logs(
569
+ lines: int = typer.Option(200, "--lines", "-n", help="Quantidade de linhas ao ler"),
570
+ follow: bool = typer.Option(False, "--follow", "-f", help="Segue o log com tail -F"),
571
+ pager: bool = typer.Option(True, "--pager/--no-pager", help="Exibe com less"),
572
+ ) -> None:
573
+ """Mostra logs do raijin-server com opcao de follow."""
574
+
575
+ logs = available_log_files()
576
+ if not logs:
577
+ typer.secho("Nenhum log encontrado", fg=typer.colors.YELLOW)
578
+ return
579
+
580
+ main_log = active_log_file()
581
+ typer.echo(f"Log ativo: {main_log}")
582
+
583
+ if follow:
584
+ subprocess.run(["tail", "-n", str(lines), "-F", str(main_log)])
585
+ return
586
+
587
+ chunks = []
588
+ for path in logs:
589
+ try:
590
+ data = path.read_text()
591
+ except Exception as exc:
592
+ data = f"[erro ao ler {path}: {exc}]"
593
+ chunks.append(f"===== {path} =====\n{data}")
594
+
595
+ output = "\n\n".join(chunks)
596
+ if pager:
597
+ page_text(output)
598
+ else:
599
+ typer.echo(output)
600
+
601
+
602
+ @debug_app.command(name="kube")
603
+ def debug_kube(
604
+ ctx: typer.Context,
605
+ events: int = typer.Option(200, "--events", "-e", help="Quantas linhas finais de eventos exibir"),
606
+ namespace: Optional[str] = typer.Option(None, "--namespace", "-n", help="Filtra pods/eventos por namespace"),
607
+ pager: bool = typer.Option(True, "--pager/--no-pager", help="Exibe com less"),
608
+ ) -> None:
609
+ """Snapshot rapido de nodes, pods e eventos do cluster."""
610
+
611
+ exec_ctx = ctx.obj or ExecutionContext()
612
+ ensure_tool("kubectl", exec_ctx)
613
+
614
+ sections = []
615
+ sections.append(("kubectl get nodes -o wide", _capture_cmd(["kubectl", "get", "nodes", "-o", "wide"])))
616
+
617
+ pods_cmd: list[str] = ["kubectl", "get", "pods"]
618
+ if namespace:
619
+ pods_cmd.extend(["-n", namespace])
620
+ else:
621
+ pods_cmd.append("-A")
622
+ pods_cmd.extend(["-o", "wide"])
623
+ sections.append(("kubectl get pods", _capture_cmd(pods_cmd)))
624
+
625
+ events_cmd: list[str] = ["kubectl", "get", "events"]
626
+ if namespace:
627
+ events_cmd.extend(["-n", namespace])
628
+ else:
629
+ events_cmd.append("-A")
630
+ events_cmd.extend(["--sort-by=.lastTimestamp"])
631
+ events_output = _capture_cmd(events_cmd)
632
+ if events_output and events > 0:
633
+ events_output = "\n".join(events_output.splitlines()[-events:])
634
+ sections.append(("kubectl get events", events_output))
635
+
636
+ combined = "\n\n".join([f"[{title}]\n{body}" for title, body in sections])
637
+ if pager:
638
+ page_text(combined)
639
+ else:
640
+ typer.echo(combined)
641
+
642
+
643
+ @debug_app.command(name="journal")
644
+ def debug_journal(
645
+ ctx: typer.Context,
646
+ service: str = typer.Option("kubelet", "--service", "-s", help="Unidade systemd para inspecionar"),
647
+ lines: int = typer.Option(200, "--lines", "-n", help="Linhas a exibir"),
648
+ follow: bool = typer.Option(False, "--follow", "-f", help="Segue o journal em tempo real"),
649
+ pager: bool = typer.Option(True, "--pager/--no-pager", help="Exibe com less"),
650
+ ) -> None:
651
+ """Mostra logs de services (ex.: kubelet) via journalctl."""
652
+
653
+ exec_ctx = ctx.obj or ExecutionContext()
654
+ ensure_tool("journalctl", exec_ctx)
655
+
656
+ cmd = ["journalctl", "-u", service, "-n", str(lines)]
657
+ if follow:
658
+ cmd.append("-f")
659
+ subprocess.run(cmd)
660
+ return
661
+
662
+ cmd.append("--no-pager")
663
+ output = _capture_cmd(cmd, timeout=60)
664
+ if lines > 0:
665
+ output = "\n".join(output.splitlines()[-lines:])
666
+
667
+ text = f"[journalctl -u {service} -n {lines}]\n{output}"
668
+ if pager:
669
+ page_text(text)
670
+ else:
671
+ typer.echo(text)
672
+
673
+
545
674
  # ============================================================================
546
675
  # Comandos Existentes
547
676
  # ============================================================================
@@ -554,8 +683,24 @@ def bootstrap_cmd(ctx: typer.Context) -> None:
554
683
 
555
684
 
556
685
  @app.command(name="full-install")
557
- def full_install_cmd(ctx: typer.Context) -> None:
686
+ def full_install_cmd(
687
+ ctx: typer.Context,
688
+ steps: Optional[str] = typer.Option(None, "--steps", help="Lista de modulos, separado por virgula"),
689
+ confirm_each: bool = typer.Option(False, "--confirm-each", help="Pedir confirmacao antes de cada modulo"),
690
+ debug_mode: bool = typer.Option(False, "--debug-mode", help="Habilita snapshots e diagnose pos-modulo"),
691
+ snapshots: bool = typer.Option(False, "--snapshots", help="Habilita snapshots de cluster apos cada modulo"),
692
+ post_diagnose: bool = typer.Option(False, "--post-diagnose", help="Executa diagnose pos-modulo quando disponivel"),
693
+ select_steps: bool = typer.Option(False, "--select-steps", help="Pergunta quais modulos executar antes de iniciar"),
694
+ ) -> None:
558
695
  """Executa instalacao completa e automatizada do ambiente de producao."""
696
+ exec_ctx = ctx.obj or ExecutionContext()
697
+ if steps:
698
+ exec_ctx.selected_steps = [s.strip() for s in steps.split(",") if s.strip()]
699
+ exec_ctx.interactive_steps = select_steps
700
+ exec_ctx.confirm_each_step = confirm_each
701
+ exec_ctx.debug_snapshots = debug_mode or snapshots or exec_ctx.debug_snapshots
702
+ exec_ctx.post_diagnose = debug_mode or post_diagnose or exec_ctx.post_diagnose
703
+ ctx.obj = exec_ctx
559
704
  _run_module(ctx, "full_install")
560
705
 
561
706
 
@@ -1,7 +1,8 @@
1
1
  """Configuracao de Calico como CNI com CIDR customizado e policies opinativas."""
2
2
 
3
+ import json
3
4
  from pathlib import Path
4
- from typing import Iterable
5
+ from typing import Iterable, List
5
6
 
6
7
  import typer
7
8
 
@@ -16,6 +17,7 @@ from raijin_server.utils import (
16
17
 
17
18
  EGRESS_LABEL_KEY = "networking.raijin.dev/egress"
18
19
  EGRESS_LABEL_VALUE = "internet"
20
+ DEFAULT_WORKLOAD_NAMESPACE = "apps"
19
21
 
20
22
 
21
23
  def _apply_policy(content: str, ctx: ExecutionContext, suffix: str) -> None:
@@ -25,6 +27,23 @@ def _apply_policy(content: str, ctx: ExecutionContext, suffix: str) -> None:
25
27
  path.unlink(missing_ok=True)
26
28
 
27
29
 
30
+ def _ensure_namespace(namespace: str, ctx: ExecutionContext) -> None:
31
+ """Garante que um namespace de workloads exista com labels padrao."""
32
+ manifest = f"""apiVersion: v1
33
+ kind: Namespace
34
+ metadata:
35
+ name: {namespace}
36
+ labels:
37
+ raijin/workload-profile: production
38
+ networking.raijin.dev/default-egress: restricted
39
+ """
40
+
41
+ path = Path(f"/tmp/raijin-ns-{namespace}.yaml")
42
+ write_file(path, manifest, ctx)
43
+ kubectl_apply(str(path), ctx)
44
+ path.unlink(missing_ok=True)
45
+
46
+
28
47
  def _build_default_deny(namespace: str) -> str:
29
48
  return f"""apiVersion: networking.k8s.io/v1
30
49
  kind: NetworkPolicy
@@ -62,6 +81,43 @@ def _split_namespaces(raw_value: str) -> Iterable[str]:
62
81
  return [ns.strip() for ns in raw_value.split(",") if ns.strip()]
63
82
 
64
83
 
84
+ def _list_workloads_without_egress(namespaces: List[str], ctx: ExecutionContext) -> None:
85
+ """Lista workloads sem label de egress e apenas avisa se falhar."""
86
+ if ctx.dry_run:
87
+ typer.echo("[dry-run] Skip listagem de workloads para liberação de egress")
88
+ return
89
+
90
+ typer.secho("\nWorkloads sem liberação de egress (adicione label para liberar internet):", fg=typer.colors.CYAN)
91
+ for ns in namespaces:
92
+ result = run_cmd(
93
+ ["kubectl", "get", "deploy,statefulset,daemonset", "-n", ns, "-o", "json"],
94
+ ctx,
95
+ check=False,
96
+ )
97
+ if result.returncode != 0:
98
+ msg = (result.stderr or result.stdout or "erro desconhecido").strip()
99
+ typer.secho(f" Aviso: nao foi possivel listar workloads em '{ns}' ({msg})", fg=typer.colors.YELLOW)
100
+ continue
101
+
102
+ try:
103
+ data = json.loads(result.stdout or "{}")
104
+ items = data.get("items", [])
105
+ pending = []
106
+ for item in items:
107
+ meta = item.get("metadata", {})
108
+ labels = meta.get("labels", {}) or {}
109
+ if labels.get(EGRESS_LABEL_KEY) != EGRESS_LABEL_VALUE:
110
+ pending.append(f"{meta.get('namespace', ns)}/{meta.get('name', 'desconhecido')}")
111
+
112
+ if pending:
113
+ for name in pending:
114
+ typer.echo(f" - {name}")
115
+ else:
116
+ typer.echo(f" Nenhum workload pendente em '{ns}'")
117
+ except Exception as exc:
118
+ typer.secho(f" Aviso: falha ao processar workloads em '{ns}': {exc}", fg=typer.colors.YELLOW)
119
+
120
+
65
121
  def _check_cluster_available(ctx: ExecutionContext) -> bool:
66
122
  """Verifica se o cluster Kubernetes esta acessivel."""
67
123
  if ctx.dry_run:
@@ -99,13 +155,19 @@ def run(ctx: ExecutionContext) -> None:
99
155
  typer.echo("Aplicando Calico como CNI...")
100
156
  pod_cidr = typer.prompt("Pod CIDR (Calico)", default="10.244.0.0/16")
101
157
 
158
+ typer.secho(
159
+ f"Criando namespace padrao de workloads '{DEFAULT_WORKLOAD_NAMESPACE}' (production-ready)...",
160
+ fg=typer.colors.CYAN,
161
+ )
162
+ _ensure_namespace(DEFAULT_WORKLOAD_NAMESPACE, ctx)
163
+
102
164
  manifest_url = "https://raw.githubusercontent.com/projectcalico/calico/v3.27.2/manifests/calico.yaml"
103
165
  cmd = f"curl -s {manifest_url} | sed 's#192.168.0.0/16#{pod_cidr}#' | kubectl apply -f -"
104
166
  run_cmd(cmd, ctx, use_shell=True)
105
167
 
106
168
  deny_namespaces_raw = typer.prompt(
107
169
  "Namespaces para aplicar default-deny (CSV)",
108
- default="default",
170
+ default=DEFAULT_WORKLOAD_NAMESPACE,
109
171
  )
110
172
  for namespace in _split_namespaces(deny_namespaces_raw):
111
173
  typer.echo(f"Aplicando default-deny no namespace '{namespace}'...")
@@ -117,9 +179,12 @@ def run(ctx: ExecutionContext) -> None:
117
179
  ):
118
180
  allow_namespaces_raw = typer.prompt(
119
181
  "Namespaces com pods que precisam acessar APIs externas (CSV)",
120
- default="default",
182
+ default=DEFAULT_WORKLOAD_NAMESPACE,
121
183
  )
122
184
  cidr = typer.prompt("CIDR liberado (ex.: 0.0.0.0/0)", default="0.0.0.0/0")
185
+ namespaces = list(_split_namespaces(allow_namespaces_raw))
186
+ if namespaces:
187
+ _list_workloads_without_egress(namespaces, ctx)
123
188
  for namespace in _split_namespaces(allow_namespaces_raw):
124
189
  typer.echo(
125
190
  f"Criando policy allow-egress-internet em '{namespace}' para pods com "