raijin-server 0.2.40__py3-none-any.whl → 0.3.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.

Potentially problematic release.


This version of raijin-server might be problematic. Click here for more details.

raijin_server/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """Pacote principal do CLI Raijin Server."""
2
2
 
3
- __version__ = "0.2.40"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  __all__ = ["__version__"]
raijin_server/cli.py CHANGED
@@ -27,6 +27,7 @@ from raijin_server.modules import (
27
27
  grafana,
28
28
  harness,
29
29
  hardening,
30
+ internal_dns,
30
31
  istio,
31
32
  kafka,
32
33
  kong,
@@ -44,6 +45,7 @@ from raijin_server.modules import (
44
45
  traefik,
45
46
  velero,
46
47
  vpn,
48
+ vpn_client,
47
49
  )
48
50
  from raijin_server.utils import ExecutionContext, logger, active_log_file, available_log_files, page_text, ensure_tool
49
51
  from raijin_server.validators import validate_system_requirements, check_module_dependencies, MODULE_DEPENDENCIES
@@ -85,6 +87,8 @@ MODULES: Dict[str, Callable[[ExecutionContext], None]] = {
85
87
  "essentials": essentials.run,
86
88
  "firewall": firewall.run,
87
89
  "vpn": vpn.run,
90
+ "vpn_client": vpn_client.run,
91
+ "internal_dns": internal_dns.run,
88
92
  "kubernetes": kubernetes.run,
89
93
  "calico": calico.run,
90
94
  "metallb": metallb.run,
@@ -120,6 +124,8 @@ MODULE_DESCRIPTIONS: Dict[str, str] = {
120
124
  "essentials": "Pacotes basicos, repos, utilitarios",
121
125
  "firewall": "Regras UFW padrao e serviços basicos",
122
126
  "vpn": "Provisiona WireGuard com cliente inicial",
127
+ "vpn_client": "Gerencia clientes VPN (adicionar/remover/listar)",
128
+ "internal_dns": "Configura DNS interno para domínios privados (*.asgard.internal)",
123
129
  "kubernetes": "Instala kubeadm/kubelet/kubectl e inicializa cluster",
124
130
  "calico": "CNI Calico e politica default deny",
125
131
  "metallb": "LoadBalancer em bare metal (pool L2)",
@@ -21,6 +21,8 @@ __all__ = [
21
21
  "bootstrap",
22
22
  "ssh_hardening",
23
23
  "vpn",
24
+ "vpn_client",
25
+ "internal_dns",
24
26
  "observability_ingress",
25
27
  "observability_dashboards",
26
28
  "apokolips_demo",
@@ -32,4 +34,4 @@ __all__ = [
32
34
  from raijin_server.modules import calico, essentials, firewall, grafana, harness, hardening, istio
33
35
  from raijin_server.modules import kafka, kong, kubernetes, loki, minio, network, observability_dashboards
34
36
  from raijin_server.modules import observability_ingress, prometheus, traefik, velero, apokolips_demo, secrets, cert_manager
35
- from raijin_server.modules import bootstrap, full_install, sanitize, ssh_hardening, vpn
37
+ from raijin_server.modules import bootstrap, full_install, sanitize, ssh_hardening, vpn, vpn_client, internal_dns
@@ -1,12 +1,27 @@
1
1
  """Configuracao do Grafana via Helm com datasource e dashboards provisionados."""
2
2
 
3
+ import json
3
4
  import socket
5
+ import tempfile
6
+ import textwrap
4
7
  import time
5
8
  from pathlib import Path
6
9
 
7
10
  import typer
8
11
 
9
- from raijin_server.utils import ExecutionContext, helm_upgrade_install, require_root, run_cmd, write_file
12
+ from raijin_server.utils import (
13
+ ExecutionContext,
14
+ helm_upgrade_install,
15
+ kubectl_create_ns,
16
+ require_root,
17
+ run_cmd,
18
+ write_file,
19
+ )
20
+
21
+ LOCAL_PATH_PROVISIONER_URL = (
22
+ "https://raw.githubusercontent.com/rancher/local-path-provisioner/"
23
+ "v0.0.30/deploy/local-path-storage.yaml"
24
+ )
10
25
 
11
26
 
12
27
  def _detect_node_name(ctx: ExecutionContext) -> str:
@@ -21,6 +36,269 @@ def _detect_node_name(ctx: ExecutionContext) -> str:
21
36
  return socket.gethostname()
22
37
 
23
38
 
39
+ def _get_default_storage_class(ctx: ExecutionContext) -> str:
40
+ """Retorna o nome da StorageClass default do cluster, se existir."""
41
+ if ctx.dry_run:
42
+ return ""
43
+ result = run_cmd(
44
+ [
45
+ "kubectl",
46
+ "get",
47
+ "storageclass",
48
+ "-o",
49
+ "jsonpath={.items[?(@.metadata.annotations.storageclass\\.kubernetes\\.io/is-default-class=='true')].metadata.name}",
50
+ ],
51
+ ctx,
52
+ check=False,
53
+ )
54
+ if result.returncode == 0 and (result.stdout or "").strip():
55
+ return (result.stdout or "").strip()
56
+ return ""
57
+
58
+
59
+ def _list_storage_classes(ctx: ExecutionContext) -> list:
60
+ """Lista todas as StorageClasses disponiveis."""
61
+ result = run_cmd(
62
+ ["kubectl", "get", "storageclass", "-o", "jsonpath={.items[*].metadata.name}"],
63
+ ctx,
64
+ check=False,
65
+ )
66
+ if result.returncode == 0 and (result.stdout or "").strip():
67
+ return (result.stdout or "").strip().split()
68
+ return []
69
+
70
+
71
+ def _apply_manifest(ctx: ExecutionContext, manifest: str, description: str) -> bool:
72
+ """Aplica manifest YAML temporario com kubectl."""
73
+ tmp_path = None
74
+ try:
75
+ with tempfile.NamedTemporaryFile("w", delete=False, suffix=".yaml") as tmp:
76
+ tmp.write(manifest)
77
+ tmp.flush()
78
+ tmp_path = Path(tmp.name)
79
+ result = run_cmd(
80
+ ["kubectl", "apply", "-f", str(tmp_path)],
81
+ ctx,
82
+ check=False,
83
+ )
84
+ if result.returncode != 0:
85
+ typer.secho(f" Falha ao aplicar {description}.", fg=typer.colors.RED)
86
+ return False
87
+ typer.secho(f" ✓ {description} aplicado.", fg=typer.colors.GREEN)
88
+ return True
89
+ finally:
90
+ if tmp_path and tmp_path.exists():
91
+ tmp_path.unlink(missing_ok=True)
92
+
93
+
94
+ def _patch_local_path_provisioner_tolerations(ctx: ExecutionContext) -> None:
95
+ """Adiciona tolerations ao local-path-provisioner para rodar em control-plane."""
96
+ typer.echo(" Configurando tolerations no local-path-provisioner...")
97
+
98
+ # Patch no deployment para tolerar control-plane
99
+ patch_deployment = textwrap.dedent(
100
+ """
101
+ spec:
102
+ template:
103
+ spec:
104
+ tolerations:
105
+ - key: node-role.kubernetes.io/control-plane
106
+ operator: Exists
107
+ effect: NoSchedule
108
+ - key: node-role.kubernetes.io/master
109
+ operator: Exists
110
+ effect: NoSchedule
111
+ """
112
+ ).strip()
113
+
114
+ result = run_cmd(
115
+ [
116
+ "kubectl", "-n", "local-path-storage", "patch", "deployment",
117
+ "local-path-provisioner", "--patch", patch_deployment,
118
+ ],
119
+ ctx,
120
+ check=False,
121
+ )
122
+ if result.returncode == 0:
123
+ typer.secho(" ✓ Deployment patched com tolerations.", fg=typer.colors.GREEN)
124
+
125
+ # Patch no ConfigMap para os helper pods
126
+ helper_pod_config = {
127
+ "nodePathMap": [
128
+ {
129
+ "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
130
+ "paths": ["/opt/local-path-provisioner"]
131
+ }
132
+ ],
133
+ "setupCommand": None,
134
+ "teardownCommand": None,
135
+ "helperPod": {
136
+ "apiVersion": "v1",
137
+ "kind": "Pod",
138
+ "metadata": {},
139
+ "spec": {
140
+ "tolerations": [
141
+ {"key": "node-role.kubernetes.io/control-plane", "operator": "Exists", "effect": "NoSchedule"},
142
+ {"key": "node-role.kubernetes.io/master", "operator": "Exists", "effect": "NoSchedule"}
143
+ ],
144
+ "containers": [
145
+ {
146
+ "name": "helper-pod",
147
+ "image": "busybox:stable",
148
+ "imagePullPolicy": "IfNotPresent"
149
+ }
150
+ ]
151
+ }
152
+ }
153
+ }
154
+
155
+ config_json_str = json.dumps(helper_pod_config)
156
+ patch_data = json.dumps({"data": {"config.json": config_json_str}})
157
+
158
+ result = run_cmd(
159
+ [
160
+ "kubectl", "-n", "local-path-storage", "patch", "configmap",
161
+ "local-path-config", "--type=merge", "-p", patch_data,
162
+ ],
163
+ ctx,
164
+ check=False,
165
+ )
166
+ if result.returncode == 0:
167
+ typer.secho(" ✓ ConfigMap patched para helper pods.", fg=typer.colors.GREEN)
168
+
169
+ # Reinicia o deployment
170
+ run_cmd(
171
+ ["kubectl", "-n", "local-path-storage", "rollout", "restart", "deployment/local-path-provisioner"],
172
+ ctx,
173
+ check=False,
174
+ )
175
+
176
+ # Aguarda rollout
177
+ run_cmd(
178
+ [
179
+ "kubectl", "-n", "local-path-storage", "rollout", "status",
180
+ "deployment/local-path-provisioner", "--timeout=60s",
181
+ ],
182
+ ctx,
183
+ check=False,
184
+ )
185
+
186
+
187
+ def _install_local_path_provisioner(ctx: ExecutionContext) -> bool:
188
+ """Instala local-path-provisioner para usar storage local (NVMe/SSD)."""
189
+ typer.echo("Instalando local-path-provisioner para storage local...")
190
+
191
+ result = run_cmd(
192
+ ["kubectl", "apply", "-f", LOCAL_PATH_PROVISIONER_URL],
193
+ ctx,
194
+ check=False,
195
+ )
196
+ if result.returncode != 0:
197
+ typer.secho(" Falha ao instalar local-path-provisioner.", fg=typer.colors.RED)
198
+ return False
199
+
200
+ # Aguarda deployment ficar pronto inicialmente
201
+ typer.echo(" Aguardando local-path-provisioner ficar Ready...")
202
+ run_cmd(
203
+ [
204
+ "kubectl", "-n", "local-path-storage", "rollout", "status",
205
+ "deployment/local-path-provisioner", "--timeout=60s",
206
+ ],
207
+ ctx,
208
+ check=False,
209
+ )
210
+
211
+ # Aplica tolerations para control-plane (single-node clusters)
212
+ _patch_local_path_provisioner_tolerations(ctx)
213
+
214
+ typer.secho(" ✓ local-path-provisioner instalado e configurado.", fg=typer.colors.GREEN)
215
+ return True
216
+
217
+
218
+ def _set_default_storage_class(ctx: ExecutionContext, name: str) -> None:
219
+ """Define uma StorageClass como default."""
220
+ # Remove default de outras classes primeiro
221
+ existing = _list_storage_classes(ctx)
222
+ for sc in existing:
223
+ if sc != name:
224
+ run_cmd(
225
+ [
226
+ "kubectl", "annotate", "storageclass", sc,
227
+ "storageclass.kubernetes.io/is-default-class-",
228
+ "--overwrite",
229
+ ],
230
+ ctx,
231
+ check=False,
232
+ )
233
+
234
+ # Define a nova como default
235
+ run_cmd(
236
+ [
237
+ "kubectl", "annotate", "storageclass", name,
238
+ "storageclass.kubernetes.io/is-default-class=true",
239
+ "--overwrite",
240
+ ],
241
+ ctx,
242
+ check=True,
243
+ )
244
+ typer.secho(f" ✓ StorageClass '{name}' definida como default.", fg=typer.colors.GREEN)
245
+
246
+
247
+ def _ensure_storage_class(ctx: ExecutionContext) -> str:
248
+ """Garante que existe uma StorageClass disponivel, instalando local-path se necessario."""
249
+ if ctx.dry_run:
250
+ return "local-path" # Retorna um valor dummy para dry-run
251
+
252
+ default_sc = _get_default_storage_class(ctx)
253
+ available = _list_storage_classes(ctx)
254
+
255
+ # Se ja existir default (qualquer uma), usa ela
256
+ if default_sc:
257
+ typer.echo(f"StorageClass default detectada: {default_sc}")
258
+ # Se for local-path, garante que o provisioner tem tolerations
259
+ if default_sc == "local-path" or "local-path" in available:
260
+ _patch_local_path_provisioner_tolerations(ctx)
261
+ return default_sc
262
+
263
+ # Se local-path estiver disponivel mas nao for default, define como default
264
+ if "local-path" in available:
265
+ typer.echo("StorageClass 'local-path' detectada.")
266
+ _patch_local_path_provisioner_tolerations(ctx)
267
+ _set_default_storage_class(ctx, "local-path")
268
+ return "local-path"
269
+
270
+ # Se houver outras classes disponiveis, pergunta qual usar
271
+ if available:
272
+ typer.echo(f"StorageClasses disponiveis (sem default): {', '.join(available)}")
273
+ choice = typer.prompt(
274
+ f"Qual StorageClass usar? ({'/'.join(available)})",
275
+ default=available[0],
276
+ )
277
+ return choice
278
+
279
+ # Nenhuma StorageClass disponivel - instala local-path automaticamente
280
+ typer.secho(
281
+ "Nenhuma StorageClass encontrada no cluster.",
282
+ fg=typer.colors.YELLOW,
283
+ )
284
+ install = typer.confirm(
285
+ "Instalar local-path-provisioner para usar armazenamento local (NVMe/SSD)?",
286
+ default=True,
287
+ )
288
+ if not install:
289
+ typer.secho(
290
+ "Abortando: Grafana com PVC requer uma StorageClass.",
291
+ fg=typer.colors.RED,
292
+ )
293
+ raise typer.Exit(1)
294
+
295
+ if not _install_local_path_provisioner(ctx):
296
+ raise typer.Exit(1)
297
+
298
+ _set_default_storage_class(ctx, "local-path")
299
+ return "local-path"
300
+
301
+
24
302
  def _check_existing_grafana(ctx: ExecutionContext) -> bool:
25
303
  """Verifica se existe instalacao do Grafana."""
26
304
  result = run_cmd(
@@ -101,29 +379,84 @@ def run(ctx: ExecutionContext) -> None:
101
379
  if cleanup:
102
380
  _uninstall_grafana(ctx)
103
381
 
382
+ # Verifica se existe StorageClass default para sugerir no prompt
383
+ default_sc = _get_default_storage_class(ctx)
384
+
385
+ enable_persistence = typer.confirm(
386
+ "Habilitar PVC para persistencia do Grafana?",
387
+ default=bool(default_sc)
388
+ )
389
+
390
+ # Se habilitou PVC, garante que existe StorageClass disponivel
391
+ if enable_persistence:
392
+ default_sc = _ensure_storage_class(ctx)
393
+
104
394
  admin_password = typer.prompt("Senha admin do Grafana", default="admin")
105
- ingress_host = typer.prompt("Host para acessar o Grafana", default="grafana.local")
106
- ingress_class = typer.prompt("IngressClass", default="traefik")
107
- tls_secret = typer.prompt("Secret TLS (cert-manager)", default="grafana-tls")
108
- persistence_size = typer.prompt("Tamanho do storage", default="10Gi")
395
+
396
+ # Ingress público não é recomendado para ferramentas de observabilidade
397
+ enable_ingress = typer.confirm(
398
+ "Habilitar ingress público? (NÃO recomendado - use VPN + port-forward)",
399
+ default=False
400
+ )
401
+
402
+ ingress_host = "grafana.local"
403
+ ingress_class = "traefik"
404
+ tls_secret = "grafana-tls"
405
+
406
+ if enable_ingress:
407
+ typer.secho(
408
+ "\n⚠️ ATENÇÃO: Expor Grafana publicamente é um risco de segurança!",
409
+ fg=typer.colors.YELLOW,
410
+ bold=True,
411
+ )
412
+ typer.secho(
413
+ "Recomendação: Use VPN (raijin vpn) + port-forward para acesso seguro.\n",
414
+ fg=typer.colors.YELLOW,
415
+ )
416
+ ingress_host = typer.prompt("Host para ingress", default="grafana.local")
417
+ ingress_class = typer.prompt("IngressClass", default="traefik")
418
+ tls_secret = typer.prompt("Secret TLS (cert-manager)", default="grafana-tls")
419
+
420
+ persistence_size = "10Gi"
421
+ storage_class = ""
422
+ if enable_persistence:
423
+ storage_class = typer.prompt(
424
+ "StorageClass para PVC",
425
+ default=default_sc or "",
426
+ ).strip()
427
+ persistence_size = typer.prompt("Tamanho do storage", default="10Gi")
109
428
 
110
429
  node_name = _detect_node_name(ctx)
111
430
 
431
+ # Constroi o YAML de persistencia condicionalmente
432
+ persistence_yaml = f"""persistence:
433
+ enabled: {str(enable_persistence).lower()}"""
434
+
435
+ if enable_persistence:
436
+ persistence_yaml += f"""
437
+ size: {persistence_size}"""
438
+ if storage_class:
439
+ persistence_yaml += f"""
440
+ storageClassName: {storage_class}"""
441
+
112
442
  values_yaml = f"""adminPassword: {admin_password}
113
443
  service:
114
444
  type: ClusterIP
115
445
  ingress:
116
- enabled: true
446
+ enabled: {str(enable_ingress).lower()}"""
447
+
448
+ if enable_ingress:
449
+ values_yaml += f"""
117
450
  ingressClassName: {ingress_class}
118
451
  hosts:
119
452
  - {ingress_host}
120
453
  tls:
121
454
  - secretName: {tls_secret}
122
455
  hosts:
123
- - {ingress_host}
124
- persistence:
125
- enabled: true
126
- size: {persistence_size}
456
+ - {ingress_host}"""
457
+
458
+ values_yaml += f"""
459
+ {persistence_yaml}
127
460
  tolerations:
128
461
  - key: node-role.kubernetes.io/control-plane
129
462
  operator: Exists
@@ -181,7 +514,13 @@ dashboards:
181
514
  values_path = Path("/tmp/raijin-grafana-values.yaml")
182
515
  write_file(values_path, values_yaml, ctx)
183
516
 
184
- run_cmd(["kubectl", "create", "namespace", "observability"], ctx, check=False)
517
+ kubectl_create_ns("observability", ctx)
518
+
519
+ if not enable_persistence:
520
+ typer.secho(
521
+ "PVC desativado: Grafana usara volume efemero (dados perdidos apos restart).",
522
+ fg=typer.colors.YELLOW,
523
+ )
185
524
 
186
525
  helm_upgrade_install(
187
526
  release="grafana",
@@ -191,15 +530,25 @@ dashboards:
191
530
  repo_url="https://grafana.github.io/helm-charts",
192
531
  ctx=ctx,
193
532
  values=[],
194
- extra_args=["-f", str(values_path)],
533
+ extra_args=["-f", str(values_path), "--wait", "--timeout", "10m", "--atomic"],
195
534
  )
196
535
 
197
536
  if not ctx.dry_run:
198
537
  _wait_for_grafana_ready(ctx)
199
538
 
200
539
  typer.secho("\n✓ Grafana instalado com sucesso.", fg=typer.colors.GREEN, bold=True)
201
- typer.echo(f"\nAcesse: https://{ingress_host}")
202
- typer.echo("Usuario: admin")
540
+
541
+ if enable_ingress:
542
+ typer.echo(f"\nAcesse: https://{ingress_host}")
543
+ else:
544
+ typer.secho("\n🔒 Acesso Seguro via VPN + Port-Forward:", fg=typer.colors.CYAN, bold=True)
545
+ typer.echo("\n1. Configure VPN (se ainda não tiver):")
546
+ typer.echo(" sudo raijin vpn")
547
+ typer.echo("\n2. Conecte via WireGuard no seu Windows/Mac")
548
+ typer.echo("\n3. Faça port-forward local:")
549
+ typer.echo(" kubectl -n observability port-forward svc/grafana 3000:80")
550
+ typer.echo("\n4. Acesse no navegador:")
551
+ typer.echo(" http://localhost:3000")
552
+
553
+ typer.echo("\nUsuario: admin")
203
554
  typer.echo(f"Senha: {admin_password}")
204
- typer.echo("\nPara port-forward local:")
205
- typer.echo(" kubectl -n observability port-forward svc/grafana 3000:80")