raijin-server 0.2.7__tar.gz → 0.2.10__tar.gz

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.
Files changed (52) hide show
  1. {raijin_server-0.2.7/src/raijin_server.egg-info → raijin_server-0.2.10}/PKG-INFO +60 -4
  2. {raijin_server-0.2.7 → raijin_server-0.2.10}/README.md +59 -3
  3. {raijin_server-0.2.7 → raijin_server-0.2.10}/setup.cfg +1 -1
  4. raijin_server-0.2.10/src/raijin_server/__init__.py +5 -0
  5. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/cli.py +108 -3
  6. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/config.py +4 -4
  7. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/healthchecks.py +22 -0
  8. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/kubernetes.py +111 -1
  9. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/network.py +3 -3
  10. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/sanitize.py +49 -1
  11. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/traefik.py +1 -2
  12. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/validators.py +23 -22
  13. {raijin_server-0.2.7 → raijin_server-0.2.10/src/raijin_server.egg-info}/PKG-INFO +60 -4
  14. raijin_server-0.2.7/src/raijin_server/__init__.py +0 -5
  15. {raijin_server-0.2.7 → raijin_server-0.2.10}/LICENSE +0 -0
  16. {raijin_server-0.2.7 → raijin_server-0.2.10}/pyproject.toml +0 -0
  17. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/__init__.py +0 -0
  18. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/apokolips_demo.py +0 -0
  19. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/bootstrap.py +0 -0
  20. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/calico.py +0 -0
  21. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/cert_manager.py +0 -0
  22. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/essentials.py +0 -0
  23. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/firewall.py +0 -0
  24. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/full_install.py +0 -0
  25. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/grafana.py +0 -0
  26. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/hardening.py +0 -0
  27. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/harness.py +0 -0
  28. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/istio.py +0 -0
  29. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/kafka.py +0 -0
  30. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/kong.py +0 -0
  31. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/loki.py +0 -0
  32. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/minio.py +0 -0
  33. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/observability_dashboards.py +0 -0
  34. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/observability_ingress.py +0 -0
  35. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/prometheus.py +0 -0
  36. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/secrets.py +0 -0
  37. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/ssh_hardening.py +0 -0
  38. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/velero.py +0 -0
  39. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/modules/vpn.py +0 -0
  40. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/scripts/__init__.py +0 -0
  41. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/scripts/checklist.sh +0 -0
  42. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/scripts/install.sh +0 -0
  43. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/scripts/log_size_metric.sh +0 -0
  44. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/scripts/pre-deploy-check.sh +0 -0
  45. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server/utils.py +0 -0
  46. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server.egg-info/SOURCES.txt +0 -0
  47. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server.egg-info/dependency_links.txt +0 -0
  48. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server.egg-info/entry_points.txt +0 -0
  49. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server.egg-info/requires.txt +0 -0
  50. {raijin_server-0.2.7 → raijin_server-0.2.10}/src/raijin_server.egg-info/top_level.txt +0 -0
  51. {raijin_server-0.2.7 → raijin_server-0.2.10}/tests/test_full_install_sequence.py +0 -0
  52. {raijin_server-0.2.7 → raijin_server-0.2.10}/tests/test_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: raijin-server
3
- Version: 0.2.7
3
+ Version: 0.2.10
4
4
  Summary: CLI para automacao de setup e hardening de servidores Ubuntu Server.
5
5
  Home-page: https://example.com/raijin-server
6
6
  Author: Equipe Raijin
@@ -44,6 +44,8 @@ CLI em Python (Typer) para automatizar setup e hardening de servidores Ubuntu Se
44
44
  - Arquitetura: [ARCHITECTURE.md](ARCHITECTURE.md)
45
45
  - Auditoria: [AUDIT.md](AUDIT.md)
46
46
  - Segurança: [SECURITY.md](SECURITY.md)
47
+ - Acesso SSH (Windows): [docs/SSH_WINDOWS.md](docs/SSH_WINDOWS.md)
48
+ - VPN para acesso remoto (WireGuard): [docs/VPN_REMOTE_ACCESS.md](docs/VPN_REMOTE_ACCESS.md)
47
49
 
48
50
  ## Destaques
49
51
 
@@ -60,6 +62,12 @@ CLI em Python (Typer) para automatizar setup e hardening de servidores Ubuntu Se
60
62
  - ✅ **Modo Dry-run**: Simula execução sem aplicar mudanças
61
63
 
62
64
  ## Requisitos
65
+ Ubuntu Server 20.04+ com Python 3 disponível. Se precisar instalar/atualizar no host alvo:
66
+
67
+ ```bash
68
+ sudo apt update
69
+ sudo apt install -y python3 python3-venv python3-pip
70
+ ```
63
71
 
64
72
  ## Instalação (sempre em venv midgard)
65
73
 
@@ -110,6 +118,10 @@ sudo -E ~/.venvs/midgard/bin/raijin-server validate
110
118
  sudo -E ~/.venvs/midgard/bin/raijin-server menu
111
119
  ```
112
120
 
121
+ ### Rollback de módulos
122
+ - No menu, após escolher o módulo, selecione `r` para rollback; se houver dependentes já executados, será pedido confirmacao para rollback em cascata dos dependentes antes.
123
+ - Linha de comando: `sudo -E ~/.venvs/midgard/bin/raijin-server rollback <modulo> --cascade/--no-cascade` (best-effort; alguns módulos podem exigir limpeza manual). Caso seja apenas para revisar o efeito, use `-n/--dry-run` no comando principal para não aplicar.
124
+
113
125
  ### Execução Direta de Módulos
114
126
  ```bash
115
127
  # Executar módulo específico
@@ -173,6 +185,9 @@ sudo -E ~/.venvs/midgard/bin/raijin-server debug journal --service containerd --
173
185
  - **[AUDIT.md](AUDIT.md)**: Relatório completo de auditoria e melhorias implementadas
174
186
  - **[ARCHITECTURE.md](ARCHITECTURE.md)**: Arquitetura técnica do ambiente
175
187
  - **[SECURITY.md](SECURITY.md)**: Políticas de segurança e reporte de vulnerabilidades
188
+ - **Publicação PyPI**: ver seção "Publicar no PyPI" abaixo
189
+ - **CNI automático**: Calico aplicado automaticamente no passo Kubernetes (override com `RAIJIN_CNI=none`)
190
+ - Para reaplicar CNI (forçar mesmo se já houver): `RAIJIN_FORCE_CNI=1`
176
191
 
177
192
  ## Fluxo de Execução Recomendado
178
193
 
@@ -329,6 +344,46 @@ bash "$SCRIPT_PATH"
329
344
 
330
345
  O helper garante o caminho absoluto correto independentemente de onde o pacote foi instalado.
331
346
 
347
+ ## Publicar no PyPI
348
+
349
+ Use o venv local do repositório (`.venv`) para garantir dependências corretas:
350
+
351
+ ```bash
352
+ cd /home/rafael/github/raijin-server
353
+ python3 -m venv .venv
354
+ source .venv/bin/activate
355
+ python -m pip install -U pip build twine
356
+ ```
357
+
358
+ Gerar artefatos limpos:
359
+
360
+ ```bash
361
+ rm -rf dist build
362
+ python -m build --sdist --wheel --outdir dist
363
+ ```
364
+
365
+ Publicar no PyPI (requere token):
366
+
367
+ ```bash
368
+ export TWINE_USERNAME="__token__"
369
+ export TWINE_PASSWORD="pypi-xxxxx" # token do PyPI
370
+ python -m twine upload dist/*
371
+ ```
372
+
373
+ Opcional: validar no TestPyPI antes de publicar (precisa token de TestPyPI):
374
+
375
+ ```bash
376
+ export TWINE_USERNAME="__token__"
377
+ export TWINE_PASSWORD="pypi-xxxxx" # token do TestPyPI
378
+ python -m twine upload --repository testpypi dist/*
379
+ ```
380
+
381
+ Depois de publicar, atualize/instale:
382
+
383
+ ```bash
384
+ pip install -U raijin-server
385
+ ```
386
+
332
387
  ## Teste de ingress (Apokolips)
333
388
 
334
389
  O módulo [src/raijin_server/modules/apokolips_demo.py](src/raijin_server/modules/apokolips_demo.py) cria um namespace dedicado, ConfigMap com HTML, Deployment NGINX, Service e Ingress Traefik com uma landing page "Apokolips" para validar o tráfego externo.
@@ -437,17 +492,18 @@ O Twine é a ferramenta oficial para enviar pacotes Python ao PyPI com upload se
437
492
  Passo a passo:
438
493
  ```bash
439
494
  # 1) Gere artefatos
440
- python -m build --sdist --wheel --outdir dist/
495
+ python3 -m pip install --user build
496
+ python3 -m build --sdist --wheel --outdir dist/
441
497
 
442
498
  # 2) Configure o token (crie em https://pypi.org/manage/account/token/)
443
499
  export TWINE_USERNAME=__token__
444
500
  export TWINE_PASSWORD="<seu-token>"
445
501
 
446
502
  # 3) Envie para o PyPI
447
- python -m twine upload dist/*
503
+ python3 -m twine upload dist/*
448
504
 
449
505
  # 4) Verifique instalação
450
- python -m pip install -U raijin-server
506
+ python3 -m pip install -U raijin-server
451
507
  raijin-server --version
452
508
  ```
453
509
 
@@ -11,6 +11,8 @@ CLI em Python (Typer) para automatizar setup e hardening de servidores Ubuntu Se
11
11
  - Arquitetura: [ARCHITECTURE.md](ARCHITECTURE.md)
12
12
  - Auditoria: [AUDIT.md](AUDIT.md)
13
13
  - Segurança: [SECURITY.md](SECURITY.md)
14
+ - Acesso SSH (Windows): [docs/SSH_WINDOWS.md](docs/SSH_WINDOWS.md)
15
+ - VPN para acesso remoto (WireGuard): [docs/VPN_REMOTE_ACCESS.md](docs/VPN_REMOTE_ACCESS.md)
14
16
 
15
17
  ## Destaques
16
18
 
@@ -27,6 +29,12 @@ CLI em Python (Typer) para automatizar setup e hardening de servidores Ubuntu Se
27
29
  - ✅ **Modo Dry-run**: Simula execução sem aplicar mudanças
28
30
 
29
31
  ## Requisitos
32
+ Ubuntu Server 20.04+ com Python 3 disponível. Se precisar instalar/atualizar no host alvo:
33
+
34
+ ```bash
35
+ sudo apt update
36
+ sudo apt install -y python3 python3-venv python3-pip
37
+ ```
30
38
 
31
39
  ## Instalação (sempre em venv midgard)
32
40
 
@@ -77,6 +85,10 @@ sudo -E ~/.venvs/midgard/bin/raijin-server validate
77
85
  sudo -E ~/.venvs/midgard/bin/raijin-server menu
78
86
  ```
79
87
 
88
+ ### Rollback de módulos
89
+ - No menu, após escolher o módulo, selecione `r` para rollback; se houver dependentes já executados, será pedido confirmacao para rollback em cascata dos dependentes antes.
90
+ - Linha de comando: `sudo -E ~/.venvs/midgard/bin/raijin-server rollback <modulo> --cascade/--no-cascade` (best-effort; alguns módulos podem exigir limpeza manual). Caso seja apenas para revisar o efeito, use `-n/--dry-run` no comando principal para não aplicar.
91
+
80
92
  ### Execução Direta de Módulos
81
93
  ```bash
82
94
  # Executar módulo específico
@@ -140,6 +152,9 @@ sudo -E ~/.venvs/midgard/bin/raijin-server debug journal --service containerd --
140
152
  - **[AUDIT.md](AUDIT.md)**: Relatório completo de auditoria e melhorias implementadas
141
153
  - **[ARCHITECTURE.md](ARCHITECTURE.md)**: Arquitetura técnica do ambiente
142
154
  - **[SECURITY.md](SECURITY.md)**: Políticas de segurança e reporte de vulnerabilidades
155
+ - **Publicação PyPI**: ver seção "Publicar no PyPI" abaixo
156
+ - **CNI automático**: Calico aplicado automaticamente no passo Kubernetes (override com `RAIJIN_CNI=none`)
157
+ - Para reaplicar CNI (forçar mesmo se já houver): `RAIJIN_FORCE_CNI=1`
143
158
 
144
159
  ## Fluxo de Execução Recomendado
145
160
 
@@ -296,6 +311,46 @@ bash "$SCRIPT_PATH"
296
311
 
297
312
  O helper garante o caminho absoluto correto independentemente de onde o pacote foi instalado.
298
313
 
314
+ ## Publicar no PyPI
315
+
316
+ Use o venv local do repositório (`.venv`) para garantir dependências corretas:
317
+
318
+ ```bash
319
+ cd /home/rafael/github/raijin-server
320
+ python3 -m venv .venv
321
+ source .venv/bin/activate
322
+ python -m pip install -U pip build twine
323
+ ```
324
+
325
+ Gerar artefatos limpos:
326
+
327
+ ```bash
328
+ rm -rf dist build
329
+ python -m build --sdist --wheel --outdir dist
330
+ ```
331
+
332
+ Publicar no PyPI (requere token):
333
+
334
+ ```bash
335
+ export TWINE_USERNAME="__token__"
336
+ export TWINE_PASSWORD="pypi-xxxxx" # token do PyPI
337
+ python -m twine upload dist/*
338
+ ```
339
+
340
+ Opcional: validar no TestPyPI antes de publicar (precisa token de TestPyPI):
341
+
342
+ ```bash
343
+ export TWINE_USERNAME="__token__"
344
+ export TWINE_PASSWORD="pypi-xxxxx" # token do TestPyPI
345
+ python -m twine upload --repository testpypi dist/*
346
+ ```
347
+
348
+ Depois de publicar, atualize/instale:
349
+
350
+ ```bash
351
+ pip install -U raijin-server
352
+ ```
353
+
299
354
  ## Teste de ingress (Apokolips)
300
355
 
301
356
  O módulo [src/raijin_server/modules/apokolips_demo.py](src/raijin_server/modules/apokolips_demo.py) cria um namespace dedicado, ConfigMap com HTML, Deployment NGINX, Service e Ingress Traefik com uma landing page "Apokolips" para validar o tráfego externo.
@@ -404,17 +459,18 @@ O Twine é a ferramenta oficial para enviar pacotes Python ao PyPI com upload se
404
459
  Passo a passo:
405
460
  ```bash
406
461
  # 1) Gere artefatos
407
- python -m build --sdist --wheel --outdir dist/
462
+ python3 -m pip install --user build
463
+ python3 -m build --sdist --wheel --outdir dist/
408
464
 
409
465
  # 2) Configure o token (crie em https://pypi.org/manage/account/token/)
410
466
  export TWINE_USERNAME=__token__
411
467
  export TWINE_PASSWORD="<seu-token>"
412
468
 
413
469
  # 3) Envie para o PyPI
414
- python -m twine upload dist/*
470
+ python3 -m twine upload dist/*
415
471
 
416
472
  # 4) Verifique instalação
417
- python -m pip install -U raijin-server
473
+ python3 -m pip install -U raijin-server
418
474
  raijin-server --version
419
475
  ```
420
476
 
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = raijin-server
3
- version = 0.2.7
3
+ version = 0.2.10
4
4
  description = CLI para automacao de setup e hardening de servidores Ubuntu Server.
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -0,0 +1,5 @@
1
+ """Pacote principal do CLI Raijin Server."""
2
+
3
+ __version__ = "0.2.10"
4
+
5
+ __all__ = ["__version__"]
@@ -45,7 +45,7 @@ from raijin_server.modules import (
45
45
  vpn,
46
46
  )
47
47
  from raijin_server.utils import ExecutionContext, logger, active_log_file, available_log_files, page_text, ensure_tool
48
- from raijin_server.validators import validate_system_requirements, check_module_dependencies
48
+ from raijin_server.validators import validate_system_requirements, check_module_dependencies, MODULE_DEPENDENCIES
49
49
  from raijin_server.healthchecks import run_health_check
50
50
  from raijin_server.config import ConfigManager
51
51
 
@@ -85,9 +85,9 @@ MODULES: Dict[str, Callable[[ExecutionContext], None]] = {
85
85
  "vpn": vpn.run,
86
86
  "kubernetes": kubernetes.run,
87
87
  "calico": calico.run,
88
+ "traefik": traefik.run, # mover antes do cert_manager para refletir dependencia
88
89
  "cert_manager": cert_manager.run,
89
90
  "istio": istio.run,
90
- "traefik": traefik.run,
91
91
  "kong": kong.run,
92
92
  "minio": minio.run,
93
93
  "prometheus": prometheus.run,
@@ -103,6 +103,11 @@ MODULES: Dict[str, Callable[[ExecutionContext], None]] = {
103
103
  "full_install": full_install.run,
104
104
  }
105
105
 
106
+ # Rollbacks sao opcionais; por padrao apenas removem marcador de conclusao e avisam
107
+ ROLLBACK_HANDLERS: Dict[str, Callable[[ExecutionContext], None]] = {
108
+ # Exemplos para futuras customizacoes: "traefik": traefik.rollback
109
+ }
110
+
106
111
  MODULE_DESCRIPTIONS: Dict[str, str] = {
107
112
  "sanitize": "Remove instalacoes antigas de Kubernetes e prepara ambiente",
108
113
  "bootstrap": "Instala ferramentas: helm, kubectl, istioctl, velero, containerd",
@@ -253,6 +258,79 @@ def _is_completed(name: str) -> bool:
253
258
  return _state_file(name).exists()
254
259
 
255
260
 
261
+ def _clear_completed(name: str) -> None:
262
+ try:
263
+ path = _state_file(name)
264
+ if path.exists():
265
+ path.unlink()
266
+ typer.secho(f"Estado removido: {name}", fg=typer.colors.YELLOW)
267
+ except Exception as exc:
268
+ console.print(f"[yellow]Nao foi possivel limpar estado de {name}: {exc}[/yellow]")
269
+
270
+
271
+ def _dependents_of(module: str) -> list[str]:
272
+ dependents = []
273
+ for mod, deps in MODULE_DEPENDENCIES.items():
274
+ if module in deps:
275
+ dependents.append(mod)
276
+ return dependents
277
+
278
+
279
+ def _default_rollback(exec_ctx: ExecutionContext, name: str) -> None:
280
+ """Rollback padrao: nao aplica alteracoes, apenas sinaliza ausencia de implementacao."""
281
+ if exec_ctx.dry_run:
282
+ typer.secho(f"[dry-run] Rollback para '{name}' nao automatizado (necessario reverter manualmente).", fg=typer.colors.YELLOW)
283
+ else:
284
+ typer.secho(
285
+ f"Rollback para '{name}' ainda nao foi automatizado. Reverta recursos manualmente e reexecute se necessario.",
286
+ fg=typer.colors.YELLOW,
287
+ )
288
+
289
+
290
+ def _get_rollback_handler(name: str) -> Callable[[ExecutionContext], None]:
291
+ return ROLLBACK_HANDLERS.get(name, lambda ctx: _default_rollback(ctx, name))
292
+
293
+
294
+ def _rollback_module(
295
+ ctx: typer.Context,
296
+ name: str,
297
+ *,
298
+ cascade_prompt: bool = True,
299
+ visited: set[str] | None = None,
300
+ ) -> None:
301
+ handler = _get_rollback_handler(name)
302
+ exec_ctx = ctx.obj or ExecutionContext()
303
+
304
+ visited = visited or set()
305
+ if name in visited:
306
+ return
307
+ visited.add(name)
308
+
309
+ dependents = _dependents_of(name)
310
+ completed_dependents = [dep for dep in dependents if _is_completed(dep)]
311
+
312
+ if completed_dependents and cascade_prompt:
313
+ typer.secho(
314
+ "Dependencias detectadas apos este modulo: " + ", ".join(completed_dependents),
315
+ fg=typer.colors.YELLOW,
316
+ )
317
+ typer.secho(
318
+ "Rollback em cascata vai tentar reverter esses modulos primeiro para evitar estado inconsistente.",
319
+ fg=typer.colors.YELLOW,
320
+ )
321
+ if not typer.confirm("Prosseguir com rollback em cascata?", default=False):
322
+ typer.secho("Rollback cancelado.", fg=typer.colors.RED)
323
+ return
324
+
325
+ for dep in completed_dependents:
326
+ _rollback_module(ctx, dep, cascade_prompt=False, visited=visited)
327
+
328
+ typer.secho(f"\n[ROLLBACK] {name}", fg=typer.colors.CYAN, bold=True)
329
+ handler(exec_ctx)
330
+ _clear_completed(name)
331
+ typer.secho(f"Rollback finalizado (best-effort) para {name}\n", fg=typer.colors.GREEN)
332
+
333
+
256
334
  def _render_menu(dry_run: bool) -> int:
257
335
  table = Table(
258
336
  title="Selecione um modulo para executar",
@@ -326,9 +404,21 @@ def interactive_menu(ctx: typer.Context) -> None:
326
404
  console.print("[red]Opcao invalida[/red]")
327
405
  continue
328
406
 
407
+ action = Prompt.ask(
408
+ "Acao (e=executar, r=rollback, c=cancelar)",
409
+ choices=["e", "r", "c"],
410
+ default="e",
411
+ )
412
+
329
413
  exec_ctx = ExecutionContext(dry_run=current_dry_run)
330
414
  ctx.obj = exec_ctx
331
- _run_module(ctx, name)
415
+
416
+ if action == "e":
417
+ _run_module(ctx, name)
418
+ elif action == "r":
419
+ _rollback_module(ctx, name)
420
+ else:
421
+ console.print("[yellow]Acao cancelada[/yellow]")
332
422
  # Loop continua e menu eh re-renderizado, refletindo status atualizado quando nao eh dry-run.
333
423
 
334
424
 
@@ -375,6 +465,21 @@ def menu(ctx: typer.Context) -> None:
375
465
  interactive_menu(ctx)
376
466
 
377
467
 
468
+ @app.command()
469
+ def rollback(
470
+ ctx: typer.Context,
471
+ module: str = typer.Argument(..., help="Modulo a reverter"),
472
+ cascade: bool = typer.Option(
473
+ True,
474
+ "--cascade/--no-cascade",
475
+ help="Quando habilitado, pergunta e aplica rollback em dependentes concluidos primeiro",
476
+ ),
477
+ ) -> None:
478
+ """Executa rollback best-effort de um modulo (com aviso sobre dependencias)."""
479
+
480
+ _rollback_module(ctx, module, cascade_prompt=cascade)
481
+
482
+
378
483
  @app.command()
379
484
  def hardening(ctx: typer.Context) -> None:
380
485
  _run_module(ctx, "hardening")
@@ -78,15 +78,15 @@ class ConfigManager:
78
78
  "modules": {
79
79
  "network": {
80
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",
81
+ "address": "192.168.1.81/24",
82
+ "gateway": "192.168.1.254",
83
+ "dns": "177.128.80.44,177.128.80.45",
84
84
  },
85
85
  "kubernetes": {
86
86
  "pod_cidr": "10.244.0.0/16",
87
87
  "service_cidr": "10.96.0.0/12",
88
88
  "cluster_name": "raijin",
89
- "advertise_address": "0.0.0.0",
89
+ "advertise_address": "192.168.1.81",
90
90
  },
91
91
  "calico": {
92
92
  "pod_cidr": "10.244.0.0/16",
@@ -124,6 +124,21 @@ def check_k8s_pods_in_namespace(namespace: str, ctx: ExecutionContext, timeout:
124
124
  )
125
125
 
126
126
 
127
+ def check_swap_disabled(ctx: ExecutionContext) -> tuple[bool, str]:
128
+ """Confirma que nao ha swap ativa (requisito kubeadm/kubelet)."""
129
+ if ctx.dry_run:
130
+ return True, "dry-run"
131
+ try:
132
+ with open("/proc/swaps") as f:
133
+ lines = f.read().strip().splitlines()
134
+ # /proc/swaps tem header + linhas; se so header, swap esta off
135
+ if len(lines) <= 1:
136
+ return True, "swap desativada"
137
+ return False, "swap ativa (remova entradas do fstab e execute swapoff -a)"
138
+ except Exception as exc:
139
+ return False, f"falha ao verificar swap: {exc}"
140
+
141
+
127
142
  def check_helm_release(release: str, namespace: str, ctx: ExecutionContext) -> Tuple[bool, str]:
128
143
  """Verifica status de um release Helm."""
129
144
  if ctx.dry_run:
@@ -217,6 +232,13 @@ def verify_kubernetes(ctx: ExecutionContext) -> bool:
217
232
  services = ["kubelet", "containerd"]
218
233
  all_ok = True
219
234
 
235
+ swap_ok, swap_msg = check_swap_disabled(ctx)
236
+ if swap_ok:
237
+ typer.secho(f" ✓ Swap: {swap_msg}", fg=typer.colors.GREEN)
238
+ else:
239
+ typer.secho(f" ✗ Swap: {swap_msg}", fg=typer.colors.RED)
240
+ all_ok = False
241
+
220
242
  for service in services:
221
243
  ok, status = check_systemd_service(service, ctx)
222
244
  if ok:
@@ -17,6 +17,12 @@ from raijin_server.utils import (
17
17
  )
18
18
 
19
19
 
20
+ CALICO_VERSION = "v3.28.0"
21
+ CALICO_URL = f"https://raw.githubusercontent.com/projectcalico/calico/{CALICO_VERSION}/manifests/calico.yaml"
22
+ DEFAULT_CNI = os.environ.get("RAIJIN_CNI", "calico").lower() # calico|none
23
+ FORCE_CNI = os.environ.get("RAIJIN_FORCE_CNI", "0") == "1"
24
+
25
+
20
26
  def _cleanup_old_repo(ctx: ExecutionContext) -> None:
21
27
  """Remove repo legado apt.kubernetes.io se existir para evitar erro 404."""
22
28
 
@@ -49,6 +55,69 @@ def _reset_cluster(ctx: ExecutionContext) -> None:
49
55
  typer.secho("✓ Limpeza concluida.", fg=typer.colors.GREEN)
50
56
 
51
57
 
58
+ def _cni_present(ctx: ExecutionContext) -> bool:
59
+ """Detecta se ja existe um CNI aplicado (qualquer DaemonSet tipico)."""
60
+
61
+ result = run_cmd(
62
+ [
63
+ "kubectl",
64
+ "get",
65
+ "daemonset",
66
+ "-n",
67
+ "kube-system",
68
+ "-o",
69
+ "jsonpath={.items[*].metadata.name}",
70
+ ],
71
+ ctx,
72
+ check=False,
73
+ )
74
+ if result.returncode != 0:
75
+ return False
76
+ names = (result.stdout or "").split()
77
+ for name in names:
78
+ if any(token in name for token in ("calico", "cilium", "flannel", "weave", "canal")):
79
+ return True
80
+ return False
81
+
82
+
83
+ def _apply_calico(pod_cidr: str, ctx: ExecutionContext) -> None:
84
+ """Aplica Calico com CIDR alinhado ao podSubnet informado."""
85
+
86
+ typer.echo(f"Aplicando Calico ({CALICO_VERSION}) com pod CIDR {pod_cidr}...")
87
+
88
+ if ctx.dry_run:
89
+ typer.echo("[dry-run] kubectl apply -f <calico.yaml>")
90
+ return
91
+
92
+ cmd = (
93
+ f"curl -fsSL --retry 3 --retry-delay 2 {CALICO_URL} "
94
+ f"| sed 's#192.168.0.0/16#{pod_cidr}#g' "
95
+ f"| kubectl apply -f -"
96
+ )
97
+ run_cmd(cmd, ctx, use_shell=True)
98
+
99
+ # Aguarda o daemonset subir para evitar Node NotReady por falta de CNI
100
+ run_cmd(
101
+ ["kubectl", "-n", "kube-system", "rollout", "status", "daemonset/calico-node", "--timeout", "300s"],
102
+ ctx,
103
+ check=False,
104
+ )
105
+ run_cmd(
106
+ [
107
+ "kubectl",
108
+ "-n",
109
+ "kube-system",
110
+ "rollout",
111
+ "status",
112
+ "deployment/calico-kube-controllers",
113
+ "--timeout",
114
+ "300s",
115
+ ],
116
+ ctx,
117
+ check=False,
118
+ )
119
+
120
+
52
121
  def run(ctx: ExecutionContext) -> None:
53
122
  require_root(ctx)
54
123
  typer.echo("Instalando e preparando Kubernetes (kubeadm/kubelet/kubectl)...")
@@ -146,6 +215,11 @@ def run(ctx: ExecutionContext) -> None:
146
215
  enable_service("containerd", ctx)
147
216
  enable_service("kubelet", ctx)
148
217
 
218
+ # Garante swap off antes de prosseguir (requisito kubeadm)
219
+ typer.echo("Desabilitando swap (requisito Kubernetes)...")
220
+ run_cmd(["swapoff", "-a"], ctx, check=False)
221
+ run_cmd("sed -i '/swap/d' /etc/fstab", ctx, use_shell=True, check=False)
222
+
149
223
  # kubeadm exige ip_forward=1; sobrepoe ajuste de hardening para fase de cluster.
150
224
  # Desabilita IPv6 completamente para evitar erros de preflight e simplificar rede
151
225
  sysctl_k8s = """# Kubernetes network settings
@@ -164,7 +238,19 @@ net.ipv6.conf.lo.disable_ipv6=1
164
238
  pod_cidr = typer.prompt("Pod CIDR", default="10.244.0.0/16")
165
239
  service_cidr = typer.prompt("Service CIDR", default="10.96.0.0/12")
166
240
  cluster_name = typer.prompt("Nome do cluster", default="raijin")
167
- advertise_address = typer.prompt("API advertise address", default="0.0.0.0")
241
+ default_adv = "192.168.1.81"
242
+ advertise_address = typer.prompt("API advertise address", default=default_adv)
243
+ if advertise_address != default_adv:
244
+ typer.secho(
245
+ f"⚠ Para ambiente atual use {default_adv} (IP LAN, evita NAT).", fg=typer.colors.YELLOW
246
+ )
247
+ if not typer.confirm(f"Deseja forcar {default_adv}?", default=True):
248
+ typer.secho(
249
+ f"Usando valor informado: {advertise_address}. Certifique-se que todos os nos alcancem esse IP.",
250
+ fg=typer.colors.YELLOW,
251
+ )
252
+ else:
253
+ advertise_address = default_adv
168
254
 
169
255
  kubeadm_config = f"""apiVersion: kubeadm.k8s.io/v1beta3
170
256
  kind: ClusterConfiguration
@@ -221,3 +307,27 @@ cgroupDriver: systemd
221
307
 
222
308
  typer.echo("Comando de join para workers:")
223
309
  run_cmd(["kubeadm", "token", "create", "--print-join-command"], ctx, check=False)
310
+
311
+ # CNI padrao: Calico (pode ser desabilitado via RAIJIN_CNI=none)
312
+ cni_choice = DEFAULT_CNI
313
+ if cni_choice == "none":
314
+ typer.secho(
315
+ "CNI nao aplicado (RAIJIN_CNI=none). Node permanecera NotReady ate aplicar um CNI manual.",
316
+ fg=typer.colors.YELLOW,
317
+ )
318
+ else:
319
+ if _cni_present(ctx) and not FORCE_CNI:
320
+ typer.secho("CNI ja detectado em kube-system; pulando aplicacao automatica (defina RAIJIN_FORCE_CNI=1 para reaplicar).", fg=typer.colors.YELLOW)
321
+ else:
322
+ _apply_calico(pod_cidr, ctx)
323
+
324
+ # Pequeno health check basico para sinalizar ao usuario
325
+ typer.echo("Validando node apos CNI...")
326
+ run_cmd([
327
+ "kubectl",
328
+ "wait",
329
+ "--for=condition=Ready",
330
+ "nodes",
331
+ "--all",
332
+ "--timeout=180s",
333
+ ], ctx, check=False)
@@ -124,9 +124,9 @@ def run(ctx: ExecutionContext) -> None:
124
124
  )
125
125
 
126
126
  iface = typer.prompt("Interface", default="ens18")
127
- address = typer.prompt("Endereco CIDR", default="192.168.0.10/24")
128
- gateway = typer.prompt("Gateway", default="192.168.0.1")
129
- dns = typer.prompt("DNS (separe por virgula)", default="1.1.1.1,8.8.8.8")
127
+ address = typer.prompt("Endereco CIDR", default="192.168.1.81/24")
128
+ gateway = typer.prompt("Gateway", default="192.168.1.254")
129
+ dns = typer.prompt("DNS (separe por virgula)", default="177.128.80.44,177.128.80.45")
130
130
 
131
131
  dns_list = ",".join([item.strip() for item in dns.split(",") if item.strip()])
132
132
  netplan_content = f"""network:
@@ -7,7 +7,14 @@ from pathlib import Path
7
7
 
8
8
  import typer
9
9
 
10
- from raijin_server.utils import ExecutionContext, require_root, run_cmd
10
+ from raijin_server.utils import ExecutionContext, require_root, run_cmd, write_file
11
+
12
+ # Defaults alinhados com configuracao de rede solicitada
13
+ NETPLAN_IFACE = "ens18"
14
+ NETPLAN_ADDRESS = "192.168.1.81/24"
15
+ NETPLAN_GATEWAY = "192.168.1.254"
16
+ NETPLAN_DNS = "177.128.80.44,177.128.80.45"
17
+ NETPLAN_PATH = Path("/etc/netplan/01-raijin-static.yaml")
11
18
 
12
19
  SYSTEMD_SERVICES = [
13
20
  "kubelet",
@@ -48,6 +55,44 @@ APT_MARKERS = [
48
55
  ]
49
56
 
50
57
 
58
+ def _ensure_netplan(ctx: ExecutionContext) -> None:
59
+ """Garante que o netplan esteja com IP fixo esperado; se ja estiver, mostra OK."""
60
+
61
+ desired = f"""network:
62
+ version: 2
63
+ renderer: networkd
64
+ ethernets:
65
+ {NETPLAN_IFACE}:
66
+ dhcp4: false
67
+ addresses: [{NETPLAN_ADDRESS}]
68
+ gateway4: {NETPLAN_GATEWAY}
69
+ nameservers:
70
+ addresses: [{NETPLAN_DNS}]
71
+ """
72
+
73
+ existing = None
74
+ if NETPLAN_PATH.exists():
75
+ try:
76
+ existing = NETPLAN_PATH.read_text()
77
+ except Exception:
78
+ existing = None
79
+
80
+ if existing and all(x in existing for x in (NETPLAN_ADDRESS, NETPLAN_GATEWAY, NETPLAN_DNS)):
81
+ typer.secho(
82
+ f"\n✓ Netplan ja configurado com {NETPLAN_ADDRESS} / gw {NETPLAN_GATEWAY} / dns {NETPLAN_DNS}",
83
+ fg=typer.colors.GREEN,
84
+ )
85
+ return
86
+
87
+ typer.echo("Aplicando netplan padrao antes da limpeza...")
88
+ write_file(NETPLAN_PATH, desired, ctx)
89
+ run_cmd(["netplan", "apply"], ctx, check=False)
90
+ typer.secho(
91
+ f"✓ Netplan ajustado para {NETPLAN_ADDRESS} (gw {NETPLAN_GATEWAY}, dns {NETPLAN_DNS})",
92
+ fg=typer.colors.GREEN,
93
+ )
94
+
95
+
51
96
  def _stop_services(ctx: ExecutionContext) -> None:
52
97
  typer.echo("Parando serviços relacionados (kubelet, containerd)...")
53
98
  for service in SYSTEMD_SERVICES:
@@ -131,6 +176,9 @@ def run(ctx: ExecutionContext) -> None:
131
176
  typer.echo("Sanitizacao cancelada pelo usuario.")
132
177
  return
133
178
 
179
+ # Primeiro passo: garantir netplan consistente, sem quebrar ao limpar
180
+ _ensure_netplan(ctx)
181
+
134
182
  _stop_services(ctx)
135
183
  _kubeadm_reset(ctx)
136
184
  _flush_iptables(ctx)
@@ -15,8 +15,7 @@ def run(ctx: ExecutionContext) -> None:
15
15
  values = [
16
16
  "ingressClass.enabled=true",
17
17
  "ingressClass.isDefaultClass=true",
18
- "ports.web.redirectTo=websecure=true",
19
- "ports.websecure.tls.enabled=true",
18
+ "ports.web.redirectTo=websecure", # valor esperado é o nome da porta de destino
20
19
  "service.type=LoadBalancer",
21
20
  f"certificatesResolvers.letsencrypt.acme.email={acme_email}",
22
21
  "certificatesResolvers.letsencrypt.acme.storage=/data/acme.json",
@@ -15,6 +15,27 @@ import typer
15
15
 
16
16
  from raijin_server.utils import ExecutionContext, logger
17
17
 
18
+ # Grafo de dependencias entre modulos (usado por validacoes e funcoes de rollback)
19
+ MODULE_DEPENDENCIES = {
20
+ "kubernetes": ["essentials", "network", "firewall"],
21
+ "calico": ["kubernetes"],
22
+ "cert_manager": ["kubernetes", "traefik"],
23
+ "istio": ["kubernetes", "calico"],
24
+ "traefik": ["kubernetes"],
25
+ "kong": ["kubernetes"],
26
+ "minio": ["kubernetes"],
27
+ "prometheus": ["kubernetes"],
28
+ "grafana": ["kubernetes", "prometheus"],
29
+ "loki": ["kubernetes"],
30
+ "secrets": ["kubernetes"],
31
+ "harness": ["kubernetes"],
32
+ "velero": ["kubernetes"],
33
+ "kafka": ["kubernetes"],
34
+ "observability_ingress": ["traefik", "prometheus", "grafana"],
35
+ "observability_dashboards": ["prometheus", "grafana"],
36
+ "apokolips_demo": ["kubernetes", "traefik"],
37
+ }
38
+
18
39
 
19
40
  class ValidationError(Exception):
20
41
  """Erro de validacao de pre-requisitos."""
@@ -215,30 +236,10 @@ def check_module_dependencies(module: str, ctx: ExecutionContext) -> bool:
215
236
  Returns:
216
237
  True se todas as dependencias foram satisfeitas
217
238
  """
218
- dependencies = {
219
- "kubernetes": ["essentials", "network", "firewall"],
220
- "calico": ["kubernetes"],
221
- "cert_manager": ["kubernetes", "traefik"],
222
- "istio": ["kubernetes", "calico"],
223
- "traefik": ["kubernetes"],
224
- "kong": ["kubernetes"],
225
- "minio": ["kubernetes"],
226
- "prometheus": ["kubernetes"],
227
- "grafana": ["kubernetes", "prometheus"],
228
- "loki": ["kubernetes"],
229
- "secrets": ["kubernetes"],
230
- "harness": ["kubernetes"],
231
- "velero": ["kubernetes"],
232
- "kafka": ["kubernetes"],
233
- "observability_ingress": ["traefik", "prometheus", "grafana"],
234
- "observability_dashboards": ["prometheus", "grafana"],
235
- "apokolips_demo": ["kubernetes", "traefik"],
236
- }
237
-
238
- if module not in dependencies:
239
+ if module not in MODULE_DEPENDENCIES:
239
240
  return True
240
241
 
241
- required = dependencies[module]
242
+ required = MODULE_DEPENDENCIES[module]
242
243
  missing = []
243
244
 
244
245
  # Verifica arquivos de estado
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: raijin-server
3
- Version: 0.2.7
3
+ Version: 0.2.10
4
4
  Summary: CLI para automacao de setup e hardening de servidores Ubuntu Server.
5
5
  Home-page: https://example.com/raijin-server
6
6
  Author: Equipe Raijin
@@ -44,6 +44,8 @@ CLI em Python (Typer) para automatizar setup e hardening de servidores Ubuntu Se
44
44
  - Arquitetura: [ARCHITECTURE.md](ARCHITECTURE.md)
45
45
  - Auditoria: [AUDIT.md](AUDIT.md)
46
46
  - Segurança: [SECURITY.md](SECURITY.md)
47
+ - Acesso SSH (Windows): [docs/SSH_WINDOWS.md](docs/SSH_WINDOWS.md)
48
+ - VPN para acesso remoto (WireGuard): [docs/VPN_REMOTE_ACCESS.md](docs/VPN_REMOTE_ACCESS.md)
47
49
 
48
50
  ## Destaques
49
51
 
@@ -60,6 +62,12 @@ CLI em Python (Typer) para automatizar setup e hardening de servidores Ubuntu Se
60
62
  - ✅ **Modo Dry-run**: Simula execução sem aplicar mudanças
61
63
 
62
64
  ## Requisitos
65
+ Ubuntu Server 20.04+ com Python 3 disponível. Se precisar instalar/atualizar no host alvo:
66
+
67
+ ```bash
68
+ sudo apt update
69
+ sudo apt install -y python3 python3-venv python3-pip
70
+ ```
63
71
 
64
72
  ## Instalação (sempre em venv midgard)
65
73
 
@@ -110,6 +118,10 @@ sudo -E ~/.venvs/midgard/bin/raijin-server validate
110
118
  sudo -E ~/.venvs/midgard/bin/raijin-server menu
111
119
  ```
112
120
 
121
+ ### Rollback de módulos
122
+ - No menu, após escolher o módulo, selecione `r` para rollback; se houver dependentes já executados, será pedido confirmacao para rollback em cascata dos dependentes antes.
123
+ - Linha de comando: `sudo -E ~/.venvs/midgard/bin/raijin-server rollback <modulo> --cascade/--no-cascade` (best-effort; alguns módulos podem exigir limpeza manual). Caso seja apenas para revisar o efeito, use `-n/--dry-run` no comando principal para não aplicar.
124
+
113
125
  ### Execução Direta de Módulos
114
126
  ```bash
115
127
  # Executar módulo específico
@@ -173,6 +185,9 @@ sudo -E ~/.venvs/midgard/bin/raijin-server debug journal --service containerd --
173
185
  - **[AUDIT.md](AUDIT.md)**: Relatório completo de auditoria e melhorias implementadas
174
186
  - **[ARCHITECTURE.md](ARCHITECTURE.md)**: Arquitetura técnica do ambiente
175
187
  - **[SECURITY.md](SECURITY.md)**: Políticas de segurança e reporte de vulnerabilidades
188
+ - **Publicação PyPI**: ver seção "Publicar no PyPI" abaixo
189
+ - **CNI automático**: Calico aplicado automaticamente no passo Kubernetes (override com `RAIJIN_CNI=none`)
190
+ - Para reaplicar CNI (forçar mesmo se já houver): `RAIJIN_FORCE_CNI=1`
176
191
 
177
192
  ## Fluxo de Execução Recomendado
178
193
 
@@ -329,6 +344,46 @@ bash "$SCRIPT_PATH"
329
344
 
330
345
  O helper garante o caminho absoluto correto independentemente de onde o pacote foi instalado.
331
346
 
347
+ ## Publicar no PyPI
348
+
349
+ Use o venv local do repositório (`.venv`) para garantir dependências corretas:
350
+
351
+ ```bash
352
+ cd /home/rafael/github/raijin-server
353
+ python3 -m venv .venv
354
+ source .venv/bin/activate
355
+ python -m pip install -U pip build twine
356
+ ```
357
+
358
+ Gerar artefatos limpos:
359
+
360
+ ```bash
361
+ rm -rf dist build
362
+ python -m build --sdist --wheel --outdir dist
363
+ ```
364
+
365
+ Publicar no PyPI (requere token):
366
+
367
+ ```bash
368
+ export TWINE_USERNAME="__token__"
369
+ export TWINE_PASSWORD="pypi-xxxxx" # token do PyPI
370
+ python -m twine upload dist/*
371
+ ```
372
+
373
+ Opcional: validar no TestPyPI antes de publicar (precisa token de TestPyPI):
374
+
375
+ ```bash
376
+ export TWINE_USERNAME="__token__"
377
+ export TWINE_PASSWORD="pypi-xxxxx" # token do TestPyPI
378
+ python -m twine upload --repository testpypi dist/*
379
+ ```
380
+
381
+ Depois de publicar, atualize/instale:
382
+
383
+ ```bash
384
+ pip install -U raijin-server
385
+ ```
386
+
332
387
  ## Teste de ingress (Apokolips)
333
388
 
334
389
  O módulo [src/raijin_server/modules/apokolips_demo.py](src/raijin_server/modules/apokolips_demo.py) cria um namespace dedicado, ConfigMap com HTML, Deployment NGINX, Service e Ingress Traefik com uma landing page "Apokolips" para validar o tráfego externo.
@@ -437,17 +492,18 @@ O Twine é a ferramenta oficial para enviar pacotes Python ao PyPI com upload se
437
492
  Passo a passo:
438
493
  ```bash
439
494
  # 1) Gere artefatos
440
- python -m build --sdist --wheel --outdir dist/
495
+ python3 -m pip install --user build
496
+ python3 -m build --sdist --wheel --outdir dist/
441
497
 
442
498
  # 2) Configure o token (crie em https://pypi.org/manage/account/token/)
443
499
  export TWINE_USERNAME=__token__
444
500
  export TWINE_PASSWORD="<seu-token>"
445
501
 
446
502
  # 3) Envie para o PyPI
447
- python -m twine upload dist/*
503
+ python3 -m twine upload dist/*
448
504
 
449
505
  # 4) Verifique instalação
450
- python -m pip install -U raijin-server
506
+ python3 -m pip install -U raijin-server
451
507
  raijin-server --version
452
508
  ```
453
509
 
@@ -1,5 +0,0 @@
1
- """Pacote principal do CLI Raijin Server."""
2
-
3
- __version__ = "0.2.7"
4
-
5
- __all__ = ["__version__"]
File without changes