raijin-server 0.1.0__py3-none-any.whl → 0.2.1__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 +1 -1
- raijin_server/cli.py +76 -5
- raijin_server/healthchecks.py +76 -0
- raijin_server/modules/__init__.py +11 -2
- raijin_server/modules/apokolips_demo.py +378 -0
- raijin_server/modules/bootstrap.py +65 -0
- raijin_server/modules/calico.py +93 -22
- raijin_server/modules/cert_manager.py +127 -0
- raijin_server/modules/full_install.py +54 -19
- raijin_server/modules/network.py +96 -2
- raijin_server/modules/observability_dashboards.py +233 -0
- raijin_server/modules/observability_ingress.py +218 -0
- raijin_server/modules/sanitize.py +142 -0
- raijin_server/modules/secrets.py +109 -0
- raijin_server/modules/ssh_hardening.py +128 -0
- raijin_server/modules/traefik.py +1 -1
- raijin_server/modules/vpn.py +68 -3
- raijin_server/scripts/__init__.py +1 -0
- raijin_server/scripts/checklist.sh +60 -0
- raijin_server/scripts/install.sh +134 -0
- raijin_server/scripts/log_size_metric.sh +31 -0
- raijin_server/scripts/pre-deploy-check.sh +183 -0
- raijin_server/utils.py +45 -12
- raijin_server/validators.py +43 -4
- raijin_server-0.2.1.dist-info/METADATA +407 -0
- raijin_server-0.2.1.dist-info/RECORD +44 -0
- raijin_server-0.1.0.dist-info/METADATA +0 -219
- raijin_server-0.1.0.dist-info/RECORD +0 -32
- {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/WHEEL +0 -0
- {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/entry_points.txt +0 -0
- {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.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
|
raijin_server/modules/calico.py
CHANGED
|
@@ -1,36 +1,107 @@
|
|
|
1
|
-
"""Configuracao de Calico como CNI com CIDR customizado e
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
46
|
+
name: allow-egress-internet
|
|
47
|
+
namespace: {namespace}
|
|
28
48
|
spec:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
("
|
|
24
|
-
("
|
|
25
|
-
("
|
|
26
|
-
("
|
|
27
|
-
("
|
|
28
|
-
("
|
|
29
|
-
("
|
|
30
|
-
("
|
|
31
|
-
("
|
|
32
|
-
("
|
|
33
|
-
("
|
|
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
|
-
|
|
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:
|
raijin_server/modules/network.py
CHANGED
|
@@ -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
|
-
|
|
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:
|