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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Configuracao do Prometheus Stack via Helm."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from raijin_server.utils import ExecutionContext, helm_upgrade_install, require_root
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(ctx: ExecutionContext) -> None:
|
|
9
|
+
require_root(ctx)
|
|
10
|
+
typer.echo("Instalando kube-prometheus-stack via Helm...")
|
|
11
|
+
|
|
12
|
+
values = [
|
|
13
|
+
"grafana.enabled=false",
|
|
14
|
+
"prometheus.prometheusSpec.retention=15d",
|
|
15
|
+
"prometheus.prometheusSpec.enableAdminAPI=true",
|
|
16
|
+
"prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false",
|
|
17
|
+
"prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=20Gi",
|
|
18
|
+
"alertmanager.alertmanagerSpec.storage.volumeClaimTemplate.spec.resources.requests.storage=10Gi",
|
|
19
|
+
"defaultRules.create=true",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
helm_upgrade_install(
|
|
23
|
+
release="kube-prometheus-stack",
|
|
24
|
+
chart="kube-prometheus-stack",
|
|
25
|
+
namespace="observability",
|
|
26
|
+
repo="prometheus-community",
|
|
27
|
+
repo_url="https://prometheus-community.github.io/helm-charts",
|
|
28
|
+
ctx=ctx,
|
|
29
|
+
values=values,
|
|
30
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Configuracao do Traefik via Helm com TLS/ACME e ingressClass."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from raijin_server.utils import ExecutionContext, helm_upgrade_install, require_root
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(ctx: ExecutionContext) -> None:
|
|
9
|
+
require_root(ctx)
|
|
10
|
+
typer.echo("Instalando Traefik via Helm...")
|
|
11
|
+
|
|
12
|
+
acme_email = typer.prompt("Email para ACME/Let's Encrypt", default="admin@example.com")
|
|
13
|
+
dashboard_host = typer.prompt("Host para dashboard (opcional)", default="traefik.local")
|
|
14
|
+
|
|
15
|
+
values = [
|
|
16
|
+
"ingressClass.enabled=true",
|
|
17
|
+
"ingressClass.isDefaultClass=true",
|
|
18
|
+
"ports.web.redirectTo=websecure=true",
|
|
19
|
+
"ports.websecure.tls.enabled=true",
|
|
20
|
+
"service.type=LoadBalancer",
|
|
21
|
+
f"certificatesResolvers.letsencrypt.acme.email={acme_email}",
|
|
22
|
+
"certificatesResolvers.letsencrypt.acme.storage=/data/acme.json",
|
|
23
|
+
"certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web",
|
|
24
|
+
"logs.general.level=INFO",
|
|
25
|
+
f"providers.kubernetesIngress.ingressClass=traefik",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
if dashboard_host:
|
|
29
|
+
values.append("ingressRoute.dashboard.enabled=true")
|
|
30
|
+
values.append(f"ingressRoute.dashboard.match=Host(`{dashboard_host}`)")
|
|
31
|
+
|
|
32
|
+
helm_upgrade_install(
|
|
33
|
+
release="traefik",
|
|
34
|
+
chart="traefik",
|
|
35
|
+
namespace="traefik",
|
|
36
|
+
repo="traefik",
|
|
37
|
+
repo_url="https://traefik.github.io/charts",
|
|
38
|
+
ctx=ctx,
|
|
39
|
+
values=values,
|
|
40
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Backup e restore com Velero."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from raijin_server.utils import ExecutionContext, ensure_tool, require_root, run_cmd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(ctx: ExecutionContext) -> None:
|
|
9
|
+
require_root(ctx)
|
|
10
|
+
ensure_tool("velero", ctx, install_hint="Instale o binario do Velero.")
|
|
11
|
+
|
|
12
|
+
typer.echo("Instalando Velero no cluster...")
|
|
13
|
+
provider = typer.prompt("Provider", default="aws")
|
|
14
|
+
bucket = typer.prompt("Bucket", default="velero-backups")
|
|
15
|
+
region = typer.prompt("Region", default="us-east-1")
|
|
16
|
+
s3_url = typer.prompt("S3 URL", default="https://s3.amazonaws.com")
|
|
17
|
+
secret_file = typer.prompt("Arquivo de credenciais (secret-file)", default="/etc/velero/credentials")
|
|
18
|
+
schedule = typer.prompt("Schedule cron para backups (ex: '0 2 * * *')", default="0 2 * * *")
|
|
19
|
+
|
|
20
|
+
run_cmd(
|
|
21
|
+
[
|
|
22
|
+
"velero",
|
|
23
|
+
"install",
|
|
24
|
+
"--provider",
|
|
25
|
+
provider,
|
|
26
|
+
"--bucket",
|
|
27
|
+
bucket,
|
|
28
|
+
"--secret-file",
|
|
29
|
+
secret_file,
|
|
30
|
+
"--backup-location-config",
|
|
31
|
+
f"region={region},s3Url={s3_url}",
|
|
32
|
+
"--use-restic",
|
|
33
|
+
],
|
|
34
|
+
ctx,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
typer.echo("Criando schedule de backup padrao...")
|
|
38
|
+
run_cmd([
|
|
39
|
+
"velero",
|
|
40
|
+
"create",
|
|
41
|
+
"schedule",
|
|
42
|
+
"raijin-daily",
|
|
43
|
+
"--schedule",
|
|
44
|
+
schedule,
|
|
45
|
+
"--include-namespaces",
|
|
46
|
+
"*",
|
|
47
|
+
], ctx, check=False)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Configuracao de servidor WireGuard e cliente inicial."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import textwrap
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from raijin_server.utils import (
|
|
14
|
+
ExecutionContext,
|
|
15
|
+
apt_install,
|
|
16
|
+
logger,
|
|
17
|
+
require_root,
|
|
18
|
+
run_cmd,
|
|
19
|
+
write_file,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
WIREGUARD_DIR = Path("/etc/wireguard")
|
|
23
|
+
CLIENTS_DIR = WIREGUARD_DIR / "clients"
|
|
24
|
+
SYSCTL_FILE = Path("/etc/sysctl.d/99-wireguard.conf")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _generate_keypair(
|
|
28
|
+
label: str,
|
|
29
|
+
private_path: Path,
|
|
30
|
+
public_path: Path,
|
|
31
|
+
ctx: ExecutionContext,
|
|
32
|
+
) -> tuple[str, str]:
|
|
33
|
+
"""Gera (ou reutiliza) chaves WireGuard, respeitando dry-run."""
|
|
34
|
+
|
|
35
|
+
if ctx.dry_run:
|
|
36
|
+
typer.echo(f"[dry-run] Geraria chaves WireGuard para {label}")
|
|
37
|
+
return ("DRY_RUN_PRIVATE_KEY", "DRY_RUN_PUBLIC_KEY")
|
|
38
|
+
|
|
39
|
+
if private_path.exists() and public_path.exists():
|
|
40
|
+
return (private_path.read_text().strip(), public_path.read_text().strip())
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
result = subprocess.run(["wg", "genkey"], capture_output=True, text=True, check=True)
|
|
44
|
+
private_key = result.stdout.strip()
|
|
45
|
+
pub_result = subprocess.run(
|
|
46
|
+
["wg", "pubkey"],
|
|
47
|
+
input=private_key,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
check=True,
|
|
51
|
+
)
|
|
52
|
+
public_key = pub_result.stdout.strip()
|
|
53
|
+
except subprocess.CalledProcessError as exc:
|
|
54
|
+
msg = f"Falha ao gerar chaves WireGuard ({label}): {exc.stderr or exc}"
|
|
55
|
+
ctx.errors.append(msg)
|
|
56
|
+
raise typer.Exit(code=1) from exc
|
|
57
|
+
|
|
58
|
+
private_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
private_path.write_text(private_key + "\n")
|
|
60
|
+
public_path.write_text(public_key + "\n")
|
|
61
|
+
os.chmod(private_path, 0o600)
|
|
62
|
+
os.chmod(public_path, 0o600)
|
|
63
|
+
logger.info("Chaves WireGuard gravadas em %s e %s", private_path, public_path)
|
|
64
|
+
|
|
65
|
+
return private_key, public_key
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run(ctx: ExecutionContext) -> None:
|
|
69
|
+
require_root(ctx)
|
|
70
|
+
typer.echo("Configurando WireGuard (VPN site-to-site)...")
|
|
71
|
+
|
|
72
|
+
apt_install(["wireguard", "wireguard-tools", "qrencode"], ctx)
|
|
73
|
+
|
|
74
|
+
interface = typer.prompt("Interface WireGuard", default="wg0")
|
|
75
|
+
vpn_cidr = typer.prompt("Rede VPN (CIDR)", default="10.8.0.0/24")
|
|
76
|
+
network = ipaddress.ip_network(vpn_cidr)
|
|
77
|
+
hosts = list(network.hosts())
|
|
78
|
+
default_server_ip = str(hosts[0]) if hosts else str(network.network_address + 1)
|
|
79
|
+
default_client_ip = str(hosts[1]) if len(hosts) > 1 else str(network.network_address + 2)
|
|
80
|
+
|
|
81
|
+
server_ip = typer.prompt("IP do servidor na VPN", default=default_server_ip)
|
|
82
|
+
server_address = f"{server_ip}/{network.prefixlen}"
|
|
83
|
+
client_name = typer.prompt("Nome do cliente inicial", default="admin")
|
|
84
|
+
client_ip = typer.prompt("IP do cliente inicial", default=default_client_ip)
|
|
85
|
+
client_address = typer.prompt("Endereco/CIDR do cliente", default=f"{client_ip}/32")
|
|
86
|
+
|
|
87
|
+
listen_port = typer.prompt("Porta WireGuard", default="51820")
|
|
88
|
+
public_endpoint = typer.prompt("IP/Dominio publico de acesso", default="vpn.example.com")
|
|
89
|
+
egress_iface = typer.prompt("Interface de saida (para NAT)", default="eth0")
|
|
90
|
+
dns_servers = typer.prompt("DNS entregues aos clientes", default="1.1.1.1,8.8.8.8")
|
|
91
|
+
keepalive = typer.prompt("PersistentKeepalive (segundos)", default="25")
|
|
92
|
+
|
|
93
|
+
server_private_path = WIREGUARD_DIR / f"{interface}.key"
|
|
94
|
+
server_public_path = WIREGUARD_DIR / f"{interface}.pub"
|
|
95
|
+
client_private_path = CLIENTS_DIR / f"{client_name}.key"
|
|
96
|
+
client_public_path = CLIENTS_DIR / f"{client_name}.pub"
|
|
97
|
+
|
|
98
|
+
server_private, server_public = _generate_keypair("servidor", server_private_path, server_public_path, ctx)
|
|
99
|
+
client_private, client_public = _generate_keypair("cliente", client_private_path, client_public_path, ctx)
|
|
100
|
+
|
|
101
|
+
server_conf_path = WIREGUARD_DIR / f"{interface}.conf"
|
|
102
|
+
client_conf_path = CLIENTS_DIR / f"{client_name}.conf"
|
|
103
|
+
|
|
104
|
+
server_config = textwrap.dedent(
|
|
105
|
+
f"""
|
|
106
|
+
[Interface]
|
|
107
|
+
Address = {server_address}
|
|
108
|
+
ListenPort = {listen_port}
|
|
109
|
+
PrivateKey = {server_private}
|
|
110
|
+
SaveConfig = true
|
|
111
|
+
PostUp = iptables -A FORWARD -i {interface} -j ACCEPT; iptables -A FORWARD -o {interface} -j ACCEPT; iptables -t nat -A POSTROUTING -o {egress_iface} -j MASQUERADE
|
|
112
|
+
PostDown = iptables -D FORWARD -i {interface} -j ACCEPT; iptables -D FORWARD -o {interface} -j ACCEPT; iptables -t nat -D POSTROUTING -o {egress_iface} -j MASQUERADE
|
|
113
|
+
|
|
114
|
+
[Peer]
|
|
115
|
+
# {client_name}
|
|
116
|
+
PublicKey = {client_public}
|
|
117
|
+
AllowedIPs = {client_address}
|
|
118
|
+
"""
|
|
119
|
+
).strip() + "\n"
|
|
120
|
+
|
|
121
|
+
client_config = textwrap.dedent(
|
|
122
|
+
f"""
|
|
123
|
+
[Interface]
|
|
124
|
+
PrivateKey = {client_private}
|
|
125
|
+
Address = {client_address}
|
|
126
|
+
DNS = {dns_servers}
|
|
127
|
+
|
|
128
|
+
[Peer]
|
|
129
|
+
PublicKey = {server_public}
|
|
130
|
+
AllowedIPs = {network.with_prefixlen}
|
|
131
|
+
Endpoint = {public_endpoint}:{listen_port}
|
|
132
|
+
PersistentKeepalive = {keepalive}
|
|
133
|
+
"""
|
|
134
|
+
).strip() + "\n"
|
|
135
|
+
|
|
136
|
+
sysctl_content = "net.ipv4.ip_forward=1\nnet.ipv6.conf.all.forwarding=1\n"
|
|
137
|
+
|
|
138
|
+
write_file(server_conf_path, server_config, ctx, mode=0o600)
|
|
139
|
+
write_file(client_conf_path, client_config, ctx, mode=0o600)
|
|
140
|
+
write_file(SYSCTL_FILE, sysctl_content, ctx)
|
|
141
|
+
|
|
142
|
+
run_cmd(["sysctl", "-p", str(SYSCTL_FILE)], ctx)
|
|
143
|
+
run_cmd(["ufw", "allow", f"{listen_port}/udp"], ctx, check=False)
|
|
144
|
+
run_cmd(["systemctl", "enable", "--now", f"wg-quick@{interface}"], ctx)
|
|
145
|
+
|
|
146
|
+
typer.secho("\n✓ WireGuard configurado com sucesso!", fg=typer.colors.GREEN, bold=True)
|
|
147
|
+
typer.echo(f"Configuracao do servidor: {server_conf_path}")
|
|
148
|
+
typer.echo(f"Cliente inicial salvo em: {client_conf_path}")
|
|
149
|
+
typer.echo("Para gerar QR code no terminal: qrencode -t ansiutf8 < {client_conf_path}")
|
|
150
|
+
typer.echo("Para novos clientes, gere chaves com 'wg genkey' e adicione entradas em ambos os arquivos.")
|
|
151
|
+
typer.echo("Clientes Linux/macOS: sudo wg-quick up ./cliente.conf | Windows: importe o arquivo no app WireGuard.")
|
|
152
|
+
```}```} CPU? Need to ensure string quotes? there is newline with braces? we used f string in echo referencing braces? we inserted literal braces within string? `typer.echo(
|
raijin_server/utils.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Utilitarios compartilhados para execucao dos modulos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import shlex
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Iterable, Mapping, MutableMapping, Sequence
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
# Configuracao de logging estruturado
|
|
20
|
+
LOG_DIR = Path("/var/log/raijin-server")
|
|
21
|
+
try:
|
|
22
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
LOG_FILE = LOG_DIR / "raijin-server.log"
|
|
24
|
+
except PermissionError:
|
|
25
|
+
LOG_FILE = Path.home() / ".raijin-server.log"
|
|
26
|
+
|
|
27
|
+
logging.basicConfig(
|
|
28
|
+
level=logging.INFO,
|
|
29
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
30
|
+
handlers=[
|
|
31
|
+
logging.FileHandler(LOG_FILE),
|
|
32
|
+
logging.StreamHandler()
|
|
33
|
+
]
|
|
34
|
+
)
|
|
35
|
+
logger = logging.getLogger('raijin-server')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ExecutionContext:
|
|
40
|
+
"""Contexto de execucao compartilhado entre modulos."""
|
|
41
|
+
|
|
42
|
+
dry_run: bool = False
|
|
43
|
+
assume_yes: bool = True
|
|
44
|
+
max_retries: int = 3
|
|
45
|
+
retry_delay: int = 5
|
|
46
|
+
timeout: int = 300
|
|
47
|
+
errors: list = field(default_factory=list)
|
|
48
|
+
warnings: list = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _format_cmd(cmd: Sequence[str] | str) -> str:
|
|
52
|
+
if isinstance(cmd, str):
|
|
53
|
+
return cmd
|
|
54
|
+
return " ".join(shlex.quote(str(part)) for part in cmd)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_cmd(
|
|
58
|
+
cmd: Sequence[str] | str,
|
|
59
|
+
ctx: ExecutionContext,
|
|
60
|
+
*,
|
|
61
|
+
env: Mapping[str, str] | None = None,
|
|
62
|
+
cwd: str | None = None,
|
|
63
|
+
check: bool = True,
|
|
64
|
+
use_shell: bool = False,
|
|
65
|
+
mask_output: bool = False,
|
|
66
|
+
display_override: str | None = None,
|
|
67
|
+
retries: int | None = None,
|
|
68
|
+
) -> subprocess.CompletedProcess:
|
|
69
|
+
"""Executa comando exibindo (ou mascarando) a linha usada.
|
|
70
|
+
|
|
71
|
+
Quando `dry_run` esta ativo, apenas mostra a linha sem executar.
|
|
72
|
+
Suporta retry automatico para comandos que podem falhar temporariamente.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
display = display_override or _format_cmd(cmd)
|
|
76
|
+
prefix = "[dry-run] " if ctx.dry_run else ""
|
|
77
|
+
if mask_output:
|
|
78
|
+
logger.info("Executando comando com argumentos sensiveis (masked)")
|
|
79
|
+
typer.echo(f"{prefix}[masked] comando executado (argumentos sensiveis ocultos)")
|
|
80
|
+
else:
|
|
81
|
+
logger.info(f"Executando: {display}")
|
|
82
|
+
typer.echo(f"{prefix}$ {display}")
|
|
83
|
+
|
|
84
|
+
if ctx.dry_run:
|
|
85
|
+
return subprocess.CompletedProcess(args=cmd, returncode=0)
|
|
86
|
+
|
|
87
|
+
merged_env: MutableMapping[str, str] = os.environ.copy()
|
|
88
|
+
if env:
|
|
89
|
+
merged_env.update(env)
|
|
90
|
+
|
|
91
|
+
max_attempts = retries if retries is not None else (ctx.max_retries if check else 1)
|
|
92
|
+
last_error = None
|
|
93
|
+
|
|
94
|
+
for attempt in range(1, max_attempts + 1):
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
cmd,
|
|
98
|
+
shell=use_shell,
|
|
99
|
+
check=check,
|
|
100
|
+
cwd=cwd,
|
|
101
|
+
env=merged_env,
|
|
102
|
+
timeout=ctx.timeout,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
)
|
|
106
|
+
if result.returncode == 0 or not check:
|
|
107
|
+
return result
|
|
108
|
+
except subprocess.TimeoutExpired as e:
|
|
109
|
+
last_error = e
|
|
110
|
+
msg = f"Comando timeout apos {ctx.timeout}s (tentativa {attempt}/{max_attempts})"
|
|
111
|
+
logger.warning(msg)
|
|
112
|
+
ctx.warnings.append(msg)
|
|
113
|
+
if attempt < max_attempts:
|
|
114
|
+
time.sleep(ctx.retry_delay)
|
|
115
|
+
except subprocess.CalledProcessError as e:
|
|
116
|
+
last_error = e
|
|
117
|
+
msg = f"Comando falhou com codigo {e.returncode} (tentativa {attempt}/{max_attempts})"
|
|
118
|
+
logger.error(f"{msg}: {e.stderr if hasattr(e, 'stderr') else ''}")
|
|
119
|
+
if attempt < max_attempts:
|
|
120
|
+
typer.secho(f"Tentando novamente em {ctx.retry_delay}s...", fg=typer.colors.YELLOW)
|
|
121
|
+
time.sleep(ctx.retry_delay)
|
|
122
|
+
else:
|
|
123
|
+
ctx.errors.append(msg)
|
|
124
|
+
if check:
|
|
125
|
+
raise
|
|
126
|
+
except Exception as e:
|
|
127
|
+
last_error = e
|
|
128
|
+
msg = f"Erro inesperado: {type(e).__name__}: {e}"
|
|
129
|
+
logger.error(msg)
|
|
130
|
+
ctx.errors.append(msg)
|
|
131
|
+
if check:
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
if check and last_error:
|
|
135
|
+
raise last_error
|
|
136
|
+
|
|
137
|
+
return subprocess.CompletedProcess(args=cmd, returncode=1)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def require_root(ctx: ExecutionContext) -> None:
|
|
141
|
+
"""Encerra se o usuario atual nao for root."""
|
|
142
|
+
|
|
143
|
+
if ctx.dry_run:
|
|
144
|
+
typer.echo("[dry-run] Validacao de root ignorada.")
|
|
145
|
+
return
|
|
146
|
+
if os.geteuid() != 0:
|
|
147
|
+
typer.secho("Este comando precisa ser executado como root (sudo).", fg=typer.colors.RED)
|
|
148
|
+
raise typer.Exit(code=1)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def ensure_tool(name: str, ctx: ExecutionContext, install_hint: str = "") -> None:
|
|
152
|
+
"""Valida que um binario esta disponivel no PATH (ignora quando dry-run)."""
|
|
153
|
+
|
|
154
|
+
if ctx.dry_run:
|
|
155
|
+
return
|
|
156
|
+
if shutil.which(name) is None:
|
|
157
|
+
hint = f" {install_hint}" if install_hint else ""
|
|
158
|
+
typer.secho(f"Ferramenta '{name}' nao encontrada.{hint}", fg=typer.colors.RED)
|
|
159
|
+
raise typer.Exit(code=1)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def apt_update(ctx: ExecutionContext) -> None:
|
|
163
|
+
run_cmd(["apt-get", "update"], ctx)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def apt_install(packages: Iterable[str], ctx: ExecutionContext) -> None:
|
|
167
|
+
pkgs = list(packages)
|
|
168
|
+
if not pkgs:
|
|
169
|
+
return
|
|
170
|
+
run_cmd(
|
|
171
|
+
["apt-get", "install", "-y", *pkgs],
|
|
172
|
+
ctx,
|
|
173
|
+
env={"DEBIAN_FRONTEND": "noninteractive"},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def enable_service(name: str, ctx: ExecutionContext) -> None:
|
|
178
|
+
run_cmd(["systemctl", "enable", "--now", name], ctx)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def write_file(path: Path, content: str, ctx: ExecutionContext, *, mode: int = 0o644) -> None:
|
|
182
|
+
"""Escreve conteudo em arquivo respeitando dry-run."""
|
|
183
|
+
|
|
184
|
+
if ctx.dry_run:
|
|
185
|
+
typer.echo(f"[dry-run] escrever {path} (mode {oct(mode)}):\n{content}")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
path.write_text(content)
|
|
190
|
+
os.chmod(path, mode)
|
|
191
|
+
typer.echo(f"Arquivo escrito: {path}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def helm_repo_add(name: str, url: str, ctx: ExecutionContext) -> None:
|
|
195
|
+
run_cmd(["helm", "repo", "add", name, url], ctx)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def helm_repo_update(ctx: ExecutionContext) -> None:
|
|
199
|
+
run_cmd(["helm", "repo", "update"], ctx)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def helm_upgrade_install(
|
|
203
|
+
release: str,
|
|
204
|
+
chart: str,
|
|
205
|
+
namespace: str,
|
|
206
|
+
ctx: ExecutionContext,
|
|
207
|
+
*,
|
|
208
|
+
repo: str | None = None,
|
|
209
|
+
repo_url: str | None = None,
|
|
210
|
+
values: list[str] | None = None,
|
|
211
|
+
create_namespace: bool = True,
|
|
212
|
+
extra_args: list[str] | None = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Executa helm upgrade --install com opcoes comuns."""
|
|
215
|
+
|
|
216
|
+
ensure_tool("helm", ctx, install_hint="Instale helm ou habilite dry-run para so visualizar.")
|
|
217
|
+
if repo and repo_url:
|
|
218
|
+
helm_repo_add(repo, repo_url, ctx)
|
|
219
|
+
helm_repo_update(ctx)
|
|
220
|
+
chart_ref = f"{repo}/{chart}"
|
|
221
|
+
else:
|
|
222
|
+
chart_ref = chart
|
|
223
|
+
|
|
224
|
+
cmd = ["helm", "upgrade", "--install", release, chart_ref, "-n", namespace]
|
|
225
|
+
if create_namespace:
|
|
226
|
+
cmd.append("--create-namespace")
|
|
227
|
+
for value in values or []:
|
|
228
|
+
cmd.extend(["--set", value])
|
|
229
|
+
if extra_args:
|
|
230
|
+
cmd.extend(extra_args)
|
|
231
|
+
run_cmd(cmd, ctx)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def kubectl_apply(target: str, ctx: ExecutionContext) -> None:
|
|
235
|
+
ensure_tool("kubectl", ctx, install_hint="Instale kubectl ou habilite dry-run.")
|
|
236
|
+
run_cmd(["kubectl", "apply", "-f", target], ctx)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def kubectl_create_ns(namespace: str, ctx: ExecutionContext) -> None:
|
|
240
|
+
ensure_tool("kubectl", ctx, install_hint="Instale kubectl ou habilite dry-run.")
|
|
241
|
+
run_cmd(["kubectl", "create", "namespace", namespace], ctx, check=False)
|