raijin-server 0.1.0__py3-none-any.whl → 0.2.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.
Files changed (32) hide show
  1. raijin_server/__init__.py +1 -1
  2. raijin_server/cli.py +58 -4
  3. raijin_server/healthchecks.py +76 -0
  4. raijin_server/modules/__init__.py +11 -2
  5. raijin_server/modules/apokolips_demo.py +378 -0
  6. raijin_server/modules/bootstrap.py +65 -0
  7. raijin_server/modules/calico.py +93 -22
  8. raijin_server/modules/cert_manager.py +127 -0
  9. raijin_server/modules/full_install.py +54 -19
  10. raijin_server/modules/network.py +96 -2
  11. raijin_server/modules/observability_dashboards.py +233 -0
  12. raijin_server/modules/observability_ingress.py +218 -0
  13. raijin_server/modules/sanitize.py +142 -0
  14. raijin_server/modules/secrets.py +109 -0
  15. raijin_server/modules/ssh_hardening.py +128 -0
  16. raijin_server/modules/traefik.py +1 -1
  17. raijin_server/modules/vpn.py +68 -3
  18. raijin_server/scripts/__init__.py +1 -0
  19. raijin_server/scripts/checklist.sh +60 -0
  20. raijin_server/scripts/install.sh +134 -0
  21. raijin_server/scripts/log_size_metric.sh +31 -0
  22. raijin_server/scripts/pre-deploy-check.sh +183 -0
  23. raijin_server/utils.py +45 -12
  24. raijin_server/validators.py +5 -0
  25. raijin_server-0.2.0.dist-info/METADATA +407 -0
  26. raijin_server-0.2.0.dist-info/RECORD +44 -0
  27. raijin_server-0.1.0.dist-info/METADATA +0 -219
  28. raijin_server-0.1.0.dist-info/RECORD +0 -32
  29. {raijin_server-0.1.0.dist-info → raijin_server-0.2.0.dist-info}/WHEEL +0 -0
  30. {raijin_server-0.1.0.dist-info → raijin_server-0.2.0.dist-info}/entry_points.txt +0 -0
  31. {raijin_server-0.1.0.dist-info → raijin_server-0.2.0.dist-info}/licenses/LICENSE +0 -0
  32. {raijin_server-0.1.0.dist-info → raijin_server-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  """Instalacao automatica de ferramentas necessarias para o raijin-server."""
2
2
 
3
3
  import shutil
4
+ import platform
4
5
  from pathlib import Path
5
6
 
6
7
  import typer
@@ -8,6 +9,35 @@ import typer
8
9
  from raijin_server.utils import ExecutionContext, apt_install, apt_update, require_root, run_cmd, write_file
9
10
 
10
11
 
12
+ def _kernel_headers_present() -> bool:
13
+ """Verifica se os headers do kernel atual estao presentes."""
14
+
15
+ release = platform.uname().release
16
+ return Path(f"/lib/modules/{release}/build").exists()
17
+
18
+
19
+ def _ensure_kernel_headers(ctx: ExecutionContext) -> None:
20
+ """Instala headers do kernel se ausentes; falha de forma clara se nao encontrar."""
21
+
22
+ if _kernel_headers_present():
23
+ return
24
+
25
+ release = platform.uname().release
26
+ header_pkg = f"linux-headers-{release}"
27
+ typer.secho(
28
+ f"Headers do kernel {release} nao encontrados. Instalando {header_pkg}...",
29
+ fg=typer.colors.YELLOW,
30
+ )
31
+ apt_install([header_pkg], ctx)
32
+
33
+ if not _kernel_headers_present():
34
+ typer.secho(
35
+ f"Headers ainda ausentes apos instalar {header_pkg}. Verifique repos ou kernel custom.",
36
+ fg=typer.colors.RED,
37
+ )
38
+ raise typer.Exit(code=1)
39
+
40
+
11
41
  # Versoes das ferramentas
12
42
  HELM_VERSION = "3.14.0"
13
43
  KUBECTL_VERSION = "1.30.0"
@@ -93,6 +123,8 @@ def _install_containerd(ctx: ExecutionContext) -> None:
93
123
  """Configura containerd como container runtime."""
94
124
  typer.echo("Configurando containerd...")
95
125
 
126
+ _ensure_kernel_headers(ctx)
127
+
96
128
  # Carrega modulos do kernel necessarios
97
129
  modules_conf = """overlay
98
130
  br_netfilter
@@ -102,6 +134,12 @@ br_netfilter
102
134
  run_cmd(["modprobe", "overlay"], ctx, check=False)
103
135
  run_cmd(["modprobe", "br_netfilter"], ctx, check=False)
104
136
 
137
+ if not Path("/proc/sys/net/bridge/bridge-nf-call-iptables").exists():
138
+ typer.secho(
139
+ "Arquivo /proc/sys/net/bridge/bridge-nf-call-iptables ausente. Verifique suporte a br_netfilter no kernel.",
140
+ fg=typer.colors.YELLOW,
141
+ )
142
+
105
143
  # Sysctl para Kubernetes
106
144
  sysctl_conf = """net.bridge.bridge-nf-call-iptables = 1
107
145
  net.bridge.bridge-nf-call-ip6tables = 1
@@ -110,6 +148,18 @@ net.ipv4.ip_forward = 1
110
148
  write_file(Path("/etc/sysctl.d/k8s.conf"), sysctl_conf, ctx)
111
149
  run_cmd(["sysctl", "--system"], ctx, check=False)
112
150
 
151
+ # Verificacao best-effort dos tunables
152
+ try:
153
+ with open("/proc/sys/net/bridge/bridge-nf-call-iptables") as f:
154
+ val = f.read().strip()
155
+ if val != "1":
156
+ typer.secho(
157
+ "bridge-nf-call-iptables nao ficou em 1 (cheque modulo br_netfilter e sysctl).",
158
+ fg=typer.colors.YELLOW,
159
+ )
160
+ except Exception:
161
+ pass
162
+
113
163
  apt_install(["containerd"], ctx)
114
164
 
115
165
  # Gera config padrao
@@ -158,6 +208,21 @@ def run(ctx: ExecutionContext) -> None:
158
208
  """Instala todas as ferramentas necessarias para o ambiente produtivo."""
159
209
  require_root(ctx)
160
210
 
211
+ # Precheck Secure Boot (pode afetar modulos DKMS e drivers)
212
+ try:
213
+ sb_path = Path("/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c")
214
+ secure_boot = False
215
+ if sb_path.exists():
216
+ data = sb_path.read_bytes()
217
+ secure_boot = len(data) >= 5 and data[4] == 1
218
+ if secure_boot:
219
+ typer.secho(
220
+ "Secure Boot detectado: modulos DKMS (WireGuard, drivers) podem exigir assinatura.",
221
+ fg=typer.colors.YELLOW,
222
+ )
223
+ except Exception:
224
+ pass
225
+
161
226
  typer.secho("\n=== Bootstrap: Instalando Ferramentas ===", fg=typer.colors.CYAN, bold=True)
162
227
 
163
228
  # Atualiza sistema
@@ -1,36 +1,107 @@
1
- """Configuracao de Calico como CNI com CIDR customizado e policy default-deny."""
1
+ """Configuracao de Calico como CNI com CIDR customizado e policies opinativas."""
2
2
 
3
3
  from pathlib import Path
4
+ from typing import Iterable
4
5
 
5
6
  import typer
6
7
 
7
- from raijin_server.utils import ExecutionContext, ensure_tool, kubectl_apply, require_root, run_cmd, write_file
8
+ from raijin_server.utils import (
9
+ ExecutionContext,
10
+ ensure_tool,
11
+ kubectl_apply,
12
+ require_root,
13
+ run_cmd,
14
+ write_file,
15
+ )
8
16
 
17
+ EGRESS_LABEL_KEY = "networking.raijin.dev/egress"
18
+ EGRESS_LABEL_VALUE = "internet"
9
19
 
10
- def run(ctx: ExecutionContext) -> None:
11
- require_root(ctx)
12
- ensure_tool("kubectl", ctx, install_hint="Instale kubectl ou habilite dry-run.")
13
- ensure_tool("curl", ctx, install_hint="Instale curl.")
14
20
 
15
- typer.echo("Aplicando Calico como CNI...")
16
- pod_cidr = typer.prompt("Pod CIDR (Calico)", default="10.244.0.0/16")
21
+ def _apply_policy(content: str, ctx: ExecutionContext, suffix: str) -> None:
22
+ path = Path(f"/tmp/raijin-{suffix}.yaml")
23
+ write_file(path, content, ctx)
24
+ kubectl_apply(str(path), ctx)
25
+ path.unlink(missing_ok=True)
26
+
27
+
28
+ def _build_default_deny(namespace: str) -> str:
29
+ return f"""apiVersion: networking.k8s.io/v1
30
+ kind: NetworkPolicy
31
+ metadata:
32
+ name: default-deny-all
33
+ namespace: {namespace}
34
+ spec:
35
+ podSelector: {{}}
36
+ policyTypes:
37
+ - Ingress
38
+ - Egress
39
+ """
17
40
 
18
- manifest_url = "https://raw.githubusercontent.com/projectcalico/calico/v3.27.2/manifests/calico.yaml"
19
- cmd = f"curl -s {manifest_url} | sed 's#192.168.0.0/16#{pod_cidr}#' | kubectl apply -f -"
20
- run_cmd(cmd, ctx, use_shell=True)
21
41
 
22
- # NetworkPolicy default-deny para workloads (excepto kube-system).
23
- default_deny = """apiVersion: networking.k8s.io/v1
42
+ def _build_allow_internet(namespace: str, cidr: str) -> str:
43
+ return f"""apiVersion: networking.k8s.io/v1
24
44
  kind: NetworkPolicy
25
45
  metadata:
26
- name: default-deny-all
27
- namespace: default
46
+ name: allow-egress-internet
47
+ namespace: {namespace}
28
48
  spec:
29
- podSelector: {}
30
- policyTypes:
31
- - Ingress
32
- - Egress
49
+ podSelector:
50
+ matchLabels:
51
+ {EGRESS_LABEL_KEY}: {EGRESS_LABEL_VALUE}
52
+ policyTypes:
53
+ - Egress
54
+ egress:
55
+ - to:
56
+ - ipBlock:
57
+ cidr: {cidr}
33
58
  """
34
- policy_path = Path("/tmp/raijin-default-deny.yaml")
35
- write_file(policy_path, default_deny, ctx)
36
- kubectl_apply(str(policy_path), ctx)
59
+
60
+
61
+ def _split_namespaces(raw_value: str) -> Iterable[str]:
62
+ return [ns.strip() for ns in raw_value.split(",") if ns.strip()]
63
+
64
+
65
+ def run(ctx: ExecutionContext) -> None:
66
+ require_root(ctx)
67
+ ensure_tool("kubectl", ctx, install_hint="Instale kubectl ou habilite dry-run.")
68
+ ensure_tool("curl", ctx, install_hint="Instale curl.")
69
+
70
+ typer.echo("Aplicando Calico como CNI...")
71
+ pod_cidr = typer.prompt("Pod CIDR (Calico)", default="10.244.0.0/16")
72
+
73
+ manifest_url = "https://raw.githubusercontent.com/projectcalico/calico/v3.27.2/manifests/calico.yaml"
74
+ cmd = f"curl -s {manifest_url} | sed 's#192.168.0.0/16#{pod_cidr}#' | kubectl apply -f -"
75
+ run_cmd(cmd, ctx, use_shell=True)
76
+
77
+ deny_namespaces_raw = typer.prompt(
78
+ "Namespaces para aplicar default-deny (CSV)",
79
+ default="default",
80
+ )
81
+ for namespace in _split_namespaces(deny_namespaces_raw):
82
+ typer.echo(f"Aplicando default-deny no namespace '{namespace}'...")
83
+ _apply_policy(_build_default_deny(namespace), ctx, f"default-deny-{namespace}")
84
+
85
+ if typer.confirm(
86
+ "Deseja liberar saida para internet (pods rotulados) em alguns namespaces?",
87
+ default=True,
88
+ ):
89
+ allow_namespaces_raw = typer.prompt(
90
+ "Namespaces com pods que precisam acessar APIs externas (CSV)",
91
+ default="default",
92
+ )
93
+ cidr = typer.prompt("CIDR liberado (ex.: 0.0.0.0/0)", default="0.0.0.0/0")
94
+ for namespace in _split_namespaces(allow_namespaces_raw):
95
+ typer.echo(
96
+ f"Criando policy allow-egress-internet em '{namespace}' para pods com "
97
+ f"label {EGRESS_LABEL_KEY}={EGRESS_LABEL_VALUE}"
98
+ )
99
+ _apply_policy(
100
+ _build_allow_internet(namespace, cidr),
101
+ ctx,
102
+ f"allow-egress-{namespace}",
103
+ )
104
+ typer.echo(
105
+ "Para habilitar egress em um workload específico execute:\n"
106
+ f" kubectl label deployment MEU-APP -n <namespace> {EGRESS_LABEL_KEY}={EGRESS_LABEL_VALUE}"
107
+ )
@@ -0,0 +1,127 @@
1
+ """Instala e configura cert-manager com emissores ACME (HTTP-01 ou DNS-01)."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from raijin_server.utils import (
8
+ ExecutionContext,
9
+ ensure_tool,
10
+ helm_upgrade_install,
11
+ kubectl_apply,
12
+ require_root,
13
+ write_file,
14
+ )
15
+
16
+ CHART_REPO = "https://charts.jetstack.io"
17
+ CHART_NAME = "cert-manager"
18
+ NAMESPACE = "cert-manager"
19
+ MANIFEST_PATH = Path("/tmp/raijin-cert-manager-issuer.yaml")
20
+
21
+
22
+ def _build_http01_issuer(name: str, email: str, ingress_class: str, staging: bool) -> str:
23
+ server = (
24
+ "https://acme-staging-v02.api.letsencrypt.org/directory"
25
+ if staging
26
+ else "https://acme-v02.api.letsencrypt.org/directory"
27
+ )
28
+ return f"""apiVersion: cert-manager.io/v1
29
+ kind: ClusterIssuer
30
+ metadata:
31
+ name: {name}
32
+ spec:
33
+ acme:
34
+ email: {email}
35
+ server: {server}
36
+ privateKeySecretRef:
37
+ name: {name}
38
+ solvers:
39
+ - http01:
40
+ ingress:
41
+ class: {ingress_class}
42
+ """
43
+
44
+
45
+ def _build_cloudflare_dns01(name: str, email: str, secret_name: str, staging: bool) -> str:
46
+ server = (
47
+ "https://acme-staging-v02.api.letsencrypt.org/directory"
48
+ if staging
49
+ else "https://acme-v02.api.letsencrypt.org/directory"
50
+ )
51
+ return f"""apiVersion: cert-manager.io/v1
52
+ kind: ClusterIssuer
53
+ metadata:
54
+ name: {name}
55
+ spec:
56
+ acme:
57
+ email: {email}
58
+ server: {server}
59
+ privateKeySecretRef:
60
+ name: {name}
61
+ solvers:
62
+ - dns01:
63
+ cloudflare:
64
+ apiTokenSecretRef:
65
+ name: {secret_name}
66
+ key: api-token
67
+ """
68
+
69
+
70
+ def _build_cloudflare_secret(secret_name: str, api_token: str) -> str:
71
+ return f"""apiVersion: v1
72
+ kind: Secret
73
+ metadata:
74
+ name: {secret_name}
75
+ namespace: {NAMESPACE}
76
+ type: Opaque
77
+ stringData:
78
+ api-token: {api_token}
79
+ """
80
+
81
+
82
+ def run(ctx: ExecutionContext) -> None:
83
+ require_root(ctx)
84
+ ensure_tool("helm", ctx, install_hint="Instale helm ou use --dry-run para simular.")
85
+ ensure_tool("kubectl", ctx, install_hint="Instale kubectl ou use --dry-run para simular.")
86
+
87
+ typer.echo("Instalando cert-manager via Helm...")
88
+ email = typer.prompt("Email para ACME (Let's Encrypt)", default="admin@example.com")
89
+ solver = typer.prompt("Tipo de desafio (http01/dns01)", default="http01")
90
+
91
+ helm_upgrade_install(
92
+ release="cert-manager",
93
+ chart=CHART_NAME,
94
+ namespace=NAMESPACE,
95
+ ctx=ctx,
96
+ repo="jetstack",
97
+ repo_url=CHART_REPO,
98
+ create_namespace=True,
99
+ extra_args=["--set", "installCRDs=true"],
100
+ )
101
+
102
+ issuer_docs = []
103
+
104
+ if solver.lower() == "dns01":
105
+ typer.secho("DNS-01 selecionado (Cloudflare)", fg=typer.colors.CYAN)
106
+ issuer_name = typer.prompt("Nome do ClusterIssuer", default="letsencrypt-dns")
107
+ staging = typer.confirm("Usar endpoint de staging? (para testes)", default=False)
108
+ secret_name = typer.prompt("Secret com API token do Cloudflare", default="cloudflare-api-token")
109
+ api_token = typer.prompt("Informe o API token do Cloudflare", hide_input=True)
110
+
111
+ issuer_docs.append(_build_cloudflare_secret(secret_name, api_token))
112
+ issuer_docs.append(_build_cloudflare_dns01(issuer_name, email, secret_name, staging))
113
+ else:
114
+ typer.secho("HTTP-01 selecionado (Ingress)", fg=typer.colors.CYAN)
115
+ issuer_name = typer.prompt("Nome do ClusterIssuer", default="letsencrypt-http")
116
+ staging = typer.confirm("Usar endpoint de staging? (para testes)", default=False)
117
+ ingress_class = typer.prompt("IngressClass para resolver HTTP-01", default="traefik")
118
+
119
+ issuer_docs.append(_build_http01_issuer(issuer_name, email, ingress_class, staging))
120
+
121
+ manifest = "---\n".join(issuer_docs)
122
+ write_file(MANIFEST_PATH, manifest, ctx)
123
+ kubectl_apply(str(MANIFEST_PATH), ctx)
124
+
125
+ typer.secho("cert-manager instalado e issuer aplicado.", fg=typer.colors.GREEN)
126
+ typer.echo("Execute um Certificate/Ingress apontando para o ClusterIssuer para emitir certificados.")
127
+
@@ -1,36 +1,49 @@
1
1
  """Instalacao completa e automatizada do ambiente produtivo."""
2
2
 
3
+ import os
4
+
3
5
  import typer
4
6
 
5
7
  from raijin_server.utils import ExecutionContext, require_root
6
8
  from raijin_server.modules import (
7
9
  bootstrap,
10
+ calico,
11
+ cert_manager,
8
12
  essentials,
9
- hardening,
10
- network,
11
13
  firewall,
12
- kubernetes,
13
- calico,
14
- prometheus,
15
14
  grafana,
15
+ hardening,
16
+ kubernetes,
16
17
  loki,
18
+ network,
19
+ observability_dashboards,
20
+ observability_ingress,
21
+ prometheus,
22
+ secrets,
23
+ sanitize,
17
24
  traefik,
18
25
  )
19
26
 
20
27
 
21
28
  # Ordem de execucao dos modulos para instalacao completa
29
+ # Modulos marcados com skip_env podem ser pulados via variavel de ambiente
22
30
  INSTALL_SEQUENCE = [
23
- ("bootstrap", bootstrap.run, "Instalacao de ferramentas (helm, kubectl, containerd, etc.)"),
24
- ("essentials", essentials.run, "Pacotes essenciais e NTP"),
25
- ("hardening", hardening.run, "Seguranca do sistema (fail2ban, sysctl, auditd)"),
26
- ("network", network.run, "Configuracao de rede (IP fixo)"),
27
- ("firewall", firewall.run, "Firewall UFW"),
28
- ("kubernetes", kubernetes.run, "Cluster Kubernetes (kubeadm)"),
29
- ("calico", calico.run, "CNI Calico + NetworkPolicy"),
30
- ("prometheus", prometheus.run, "Monitoramento Prometheus"),
31
- ("grafana", grafana.run, "Dashboards Grafana"),
32
- ("loki", loki.run, "Logs centralizados Loki"),
33
- ("traefik", traefik.run, "Ingress Controller Traefik"),
31
+ ("sanitize", sanitize.run, "Limpeza total de instalacoes anteriores", None),
32
+ ("bootstrap", bootstrap.run, "Instalacao de ferramentas (helm, kubectl, containerd, etc.)", None),
33
+ ("essentials", essentials.run, "Pacotes essenciais e NTP", None),
34
+ ("hardening", hardening.run, "Seguranca do sistema (fail2ban, sysctl, auditd)", None),
35
+ ("network", network.run, "Configuracao de rede (IP fixo) - OPCIONAL", "RAIJIN_SKIP_NETWORK"),
36
+ ("firewall", firewall.run, "Firewall UFW", None),
37
+ ("kubernetes", kubernetes.run, "Cluster Kubernetes (kubeadm)", None),
38
+ ("calico", calico.run, "CNI Calico + NetworkPolicy", None),
39
+ ("cert_manager", cert_manager.run, "cert-manager + ClusterIssuer ACME", None),
40
+ ("secrets", secrets.run, "Sealed-Secrets + External-Secrets", None),
41
+ ("prometheus", prometheus.run, "Monitoramento Prometheus", None),
42
+ ("grafana", grafana.run, "Dashboards Grafana", None),
43
+ ("loki", loki.run, "Logs centralizados Loki", None),
44
+ ("traefik", traefik.run, "Ingress Controller Traefik", None),
45
+ ("observability_ingress", observability_ingress.run, "Ingress seguro para Grafana/Prometheus/Alertmanager", None),
46
+ ("observability_dashboards", observability_dashboards.run, "Dashboards opinativos e alertas", None),
34
47
  ]
35
48
 
36
49
 
@@ -54,10 +67,20 @@ def run(ctx: ExecutionContext) -> None:
54
67
 
55
68
  # Mostra sequencia de instalacao
56
69
  typer.echo("Sequencia de instalacao:")
57
- for i, (name, _, desc) in enumerate(INSTALL_SEQUENCE, 1):
58
- typer.echo(f" {i:2}. {name:15} - {desc}")
70
+ for i, (name, _, desc, skip_env) in enumerate(INSTALL_SEQUENCE, 1):
71
+ suffix = ""
72
+ if skip_env and os.environ.get(skip_env, "").strip() in ("1", "true", "yes"):
73
+ suffix = " [SKIP]"
74
+ typer.echo(f" {i:2}. {name:25} - {desc}{suffix}")
59
75
 
60
76
  typer.echo("")
77
+ typer.secho(
78
+ "Nota: O modulo 'network' eh OPCIONAL se o IP fixo ja foi configurado\n"
79
+ " pelo provedor ISP ou durante instalacao do SO.\n"
80
+ " Set RAIJIN_SKIP_NETWORK=1 para pular automaticamente.",
81
+ fg=typer.colors.YELLOW,
82
+ )
83
+ typer.echo("")
61
84
 
62
85
  if not ctx.dry_run:
63
86
  if not typer.confirm("Deseja continuar com a instalacao completa?", default=True):
@@ -67,8 +90,15 @@ def run(ctx: ExecutionContext) -> None:
67
90
  total = len(INSTALL_SEQUENCE)
68
91
  failed = []
69
92
  succeeded = []
93
+ skipped = []
94
+
95
+ for i, (name, handler, desc, skip_env) in enumerate(INSTALL_SEQUENCE, 1):
96
+ # Verifica se modulo deve ser pulado via env
97
+ if skip_env and os.environ.get(skip_env, "").strip() in ("1", "true", "yes"):
98
+ skipped.append(name)
99
+ typer.secho(f"⏭ {name} pulado via {skip_env}=1", fg=typer.colors.YELLOW)
100
+ continue
70
101
 
71
- for i, (name, handler, desc) in enumerate(INSTALL_SEQUENCE, 1):
72
102
  typer.secho(
73
103
  f"\n{'='*60}",
74
104
  fg=typer.colors.CYAN,
@@ -108,6 +138,11 @@ def run(ctx: ExecutionContext) -> None:
108
138
  for name in succeeded:
109
139
  typer.echo(f" - {name}")
110
140
 
141
+ if skipped:
142
+ typer.secho(f"\n⏭ Modulos pulados ({len(skipped)}):", fg=typer.colors.YELLOW)
143
+ for name in skipped:
144
+ typer.echo(f" - {name}")
145
+
111
146
  if failed:
112
147
  typer.secho(f"\n✗ Modulos com falha ({len(failed)}):", fg=typer.colors.RED)
113
148
  for name, error in failed:
@@ -1,7 +1,17 @@
1
- """Configuracao de rede (IP fixo) via Netplan."""
1
+ """Configuracao de rede (IP fixo) via Netplan.
2
+
3
+ Este modulo eh OPCIONAL quando:
4
+ - IP fixo ja foi configurado no provedor ISP (ex: Ibi Internet Empresarial)
5
+ - IP estatico foi definido manualmente durante instalacao do SO
6
+ - Netplan ja possui configuracao funcional
7
+
8
+ Set RAIJIN_SKIP_NETWORK=1 para pular automaticamente em automacoes.
9
+ """
2
10
 
3
11
  import os
12
+ import subprocess
4
13
  from pathlib import Path
14
+ from typing import Optional
5
15
 
6
16
  import typer
7
17
 
@@ -18,9 +28,93 @@ def _is_wsl() -> bool:
18
28
  return False
19
29
 
20
30
 
31
+ def _get_current_ip() -> Optional[str]:
32
+ """Retorna o IP principal atual do sistema (excluindo loopback e docker)."""
33
+ try:
34
+ result = subprocess.run(
35
+ ["ip", "-4", "-o", "addr", "show", "scope", "global"],
36
+ capture_output=True,
37
+ text=True,
38
+ timeout=10,
39
+ )
40
+ for line in result.stdout.strip().split("\n"):
41
+ parts = line.split()
42
+ if len(parts) >= 4:
43
+ iface = parts[1]
44
+ # Ignora interfaces virtuais (docker, veth, br-, virbr, etc.)
45
+ if any(iface.startswith(p) for p in ("docker", "veth", "br-", "virbr", "cni", "flannel")):
46
+ continue
47
+ ip_cidr = parts[3]
48
+ return ip_cidr
49
+ except Exception:
50
+ pass
51
+ return None
52
+
53
+
54
+ def _has_static_netplan() -> bool:
55
+ """Verifica se ja existe configuracao Netplan com IP estatico."""
56
+ netplan_dir = Path("/etc/netplan")
57
+ if not netplan_dir.exists():
58
+ return False
59
+ for f in netplan_dir.glob("*.yaml"):
60
+ try:
61
+ content = f.read_text()
62
+ if "dhcp4: false" in content or "dhcp4: no" in content:
63
+ return True
64
+ except OSError:
65
+ continue
66
+ return False
67
+
68
+
21
69
  def run(ctx: ExecutionContext) -> None:
22
70
  require_root(ctx)
23
- typer.echo("Configurando IP fixo (Netplan)...")
71
+
72
+ # Permite pular via variavel de ambiente (para automacao)
73
+ if os.environ.get("RAIJIN_SKIP_NETWORK", "").strip() in ("1", "true", "yes"):
74
+ typer.secho(
75
+ "RAIJIN_SKIP_NETWORK=1 detectado. Pulando configuracao de rede.",
76
+ fg=typer.colors.YELLOW,
77
+ )
78
+ return
79
+
80
+ current_ip = _get_current_ip()
81
+ has_static = _has_static_netplan()
82
+
83
+ # Se ja tem IP estatico configurado, oferece pular
84
+ if current_ip and has_static:
85
+ typer.secho(
86
+ f"\n✓ IP estatico detectado: {current_ip}",
87
+ fg=typer.colors.GREEN,
88
+ )
89
+ typer.secho(
90
+ " Parece que a rede ja esta configurada (Netplan com dhcp4: false).",
91
+ fg=typer.colors.GREEN,
92
+ )
93
+ typer.echo("")
94
+ if not typer.confirm(
95
+ "Deseja reconfigurar a rede mesmo assim? (NAO recomendado se ja funciona)",
96
+ default=False,
97
+ ):
98
+ typer.secho("Pulando configuracao de rede.", fg=typer.colors.CYAN)
99
+ return
100
+ elif current_ip:
101
+ typer.secho(
102
+ f"\n✓ IP atual: {current_ip}",
103
+ fg=typer.colors.GREEN,
104
+ )
105
+ typer.echo(
106
+ " Se este IP foi configurado pelo seu provedor ISP ou durante a instalacao,\n"
107
+ " voce pode pular este passo."
108
+ )
109
+ typer.echo("")
110
+ if not typer.confirm(
111
+ "Deseja configurar IP estatico via Netplan?",
112
+ default=True,
113
+ ):
114
+ typer.secho("Pulando configuracao de rede.", fg=typer.colors.CYAN)
115
+ return
116
+
117
+ typer.echo("\nConfigurando IP fixo (Netplan)...")
24
118
 
25
119
  wsl = _is_wsl()
26
120
  if wsl: