feat: install.sh One-Liner-Bootstrap + System-Adressen-Card auf IP-Page
scripts/install.sh: full curl-Onliner für Debian 13 trixie analog mail-gateway/scripts/install.sh — OS+Arch-Detection, Pre-flight- Tools, GPG-Key (nmg.asc, geteilt mit mail-gateway), APT-Source-Line trixie main, apt install edgeguard, Service-Smoke + healthz-Probe. Bestimmungsort: get.netcell-edgeguard.de (Hosting separat). UI: IP-Adressen-Page bekommt eine "Adressen am Kernel"-Card oben, analog zur Networks-Page. Listet jede vom Kernel sichtbare IP (lo + eth0 + …) mit Family-Tag (IPv4/IPv6) — read-only. Verwaltete Adressen darunter wie zuvor. User-Feedback: "die bestehenden IP-Adressen werden nicht angezeigt" — adressiert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,7 +42,10 @@
|
|||||||
},
|
},
|
||||||
"ips": {
|
"ips": {
|
||||||
"title": "IP-Adressen",
|
"title": "IP-Adressen",
|
||||||
"intro": "Adressen, die EdgeGuard verwaltet — inklusive VIPs für Cluster-Failover. Bindung an die deklarierten Interfaces oben.",
|
"intro": "Adressen, die das Betriebssystem zeigt (Read-only oben) plus die Adressen, die EdgeGuard zusätzlich verwaltet — inklusive VIPs für Cluster-Failover.",
|
||||||
|
"systemDiscovered": "Adressen am Kernel (read-only)",
|
||||||
|
"managedTitle": "Verwaltete Adressen",
|
||||||
|
"family": "Familie",
|
||||||
"addAddress": "Adresse hinzufügen",
|
"addAddress": "Adresse hinzufügen",
|
||||||
"editAddress": "Adresse bearbeiten",
|
"editAddress": "Adresse bearbeiten",
|
||||||
"interface": "Interface",
|
"interface": "Interface",
|
||||||
|
|||||||
@@ -42,7 +42,10 @@
|
|||||||
},
|
},
|
||||||
"ips": {
|
"ips": {
|
||||||
"title": "IP addresses",
|
"title": "IP addresses",
|
||||||
"intro": "Addresses managed by EdgeGuard — including VIPs that follow the active cluster node on failover.",
|
"intro": "Addresses the kernel currently has (read-only above) plus addresses EdgeGuard additionally manages — including VIPs that follow the active cluster node on failover.",
|
||||||
|
"systemDiscovered": "Kernel addresses (read-only)",
|
||||||
|
"managedTitle": "Managed addresses",
|
||||||
|
"family": "Family",
|
||||||
"addAddress": "Add address",
|
"addAddress": "Add address",
|
||||||
"editAddress": "Edit address",
|
"editAddress": "Edit address",
|
||||||
"interface": "Interface",
|
"interface": "Interface",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, message } from 'antd'
|
import { Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, message } from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -19,6 +19,20 @@ interface IPAddress {
|
|||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SystemInterface {
|
||||||
|
ifname: string
|
||||||
|
flags?: string[]
|
||||||
|
link_type?: string
|
||||||
|
addr_info?: Array<{ family: 'inet' | 'inet6'; local: string; prefixlen: number }>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SystemAddress {
|
||||||
|
ifname: string
|
||||||
|
family: 'inet' | 'inet6'
|
||||||
|
address: string
|
||||||
|
prefix: number
|
||||||
|
}
|
||||||
|
|
||||||
interface IPFormValues {
|
interface IPFormValues {
|
||||||
interface_id: number
|
interface_id: number
|
||||||
address: string
|
address: string
|
||||||
@@ -42,12 +56,38 @@ async function listIfaces(): Promise<NetworkInterface[]> {
|
|||||||
return (r.data.data as { interfaces?: NetworkInterface[] }).interfaces ?? []
|
return (r.data.data as { interfaces?: NetworkInterface[] }).interfaces ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flatten /system/interfaces into one row per (ifname, address) so
|
||||||
|
// the operator sees every kernel-side IP at a glance — including
|
||||||
|
// addresses that EdgeGuard hasn't taken under management yet.
|
||||||
|
async function listSystemAddresses(): Promise<SystemAddress[]> {
|
||||||
|
const r = await apiClient.get('/system/interfaces')
|
||||||
|
if (!isEnvelope(r.data)) return []
|
||||||
|
const ifs = (r.data.data as { interfaces?: SystemInterface[] }).interfaces ?? []
|
||||||
|
const out: SystemAddress[] = []
|
||||||
|
for (const i of ifs) {
|
||||||
|
for (const a of i.addr_info ?? []) {
|
||||||
|
out.push({
|
||||||
|
ifname: i.ifname,
|
||||||
|
family: a.family,
|
||||||
|
address: a.local,
|
||||||
|
prefix: a.prefixlen,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
export default function IPAddressesPage() {
|
export default function IPAddressesPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
const { data: ips, isLoading } = useQuery({ queryKey: ['ip-addresses'], queryFn: listIPs })
|
const { data: ips, isLoading } = useQuery({ queryKey: ['ip-addresses'], queryFn: listIPs })
|
||||||
const { data: ifs } = useQuery({ queryKey: ['network-interfaces'], queryFn: listIfaces })
|
const { data: ifs } = useQuery({ queryKey: ['network-interfaces'], queryFn: listIfaces })
|
||||||
|
const { data: sysAddrs } = useQuery({
|
||||||
|
queryKey: ['system', 'addresses'],
|
||||||
|
queryFn: listSystemAddresses,
|
||||||
|
refetchInterval: 60_000,
|
||||||
|
})
|
||||||
|
|
||||||
const ifaceLabel = (id: number) => {
|
const ifaceLabel = (id: number) => {
|
||||||
const i = ifs?.find((x) => x.id === id)
|
const i = ifs?.find((x) => x.id === id)
|
||||||
@@ -122,6 +162,27 @@ export default function IPAddressesPage() {
|
|||||||
<div>
|
<div>
|
||||||
<Typography.Title level={3}>{t('ips.title')}</Typography.Title>
|
<Typography.Title level={3}>{t('ips.title')}</Typography.Title>
|
||||||
<Typography.Paragraph type="secondary">{t('ips.intro')}</Typography.Paragraph>
|
<Typography.Paragraph type="secondary">{t('ips.intro')}</Typography.Paragraph>
|
||||||
|
|
||||||
|
<Card title={t('ips.systemDiscovered')} size="small" style={{ marginBottom: 16 }}>
|
||||||
|
{(sysAddrs ?? []).length === 0
|
||||||
|
? <Typography.Text type="secondary">—</Typography.Text>
|
||||||
|
: (
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
rowKey={(r) => `${r.ifname}-${r.address}`}
|
||||||
|
dataSource={sysAddrs ?? []}
|
||||||
|
pagination={false}
|
||||||
|
columns={[
|
||||||
|
{ title: t('ips.interface'), dataIndex: 'ifname', key: 'ifname', render: (s: string) => <code>{s}</code> },
|
||||||
|
{ title: t('ips.address'), key: 'addr', render: (_, row: SystemAddress) => <code>{row.address}/{row.prefix}</code> },
|
||||||
|
{ title: t('ips.family'), dataIndex: 'family', key: 'family', render: (f: string) => <Tag>{f === 'inet' ? 'IPv4' : 'IPv6'}</Tag> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Typography.Title level={5} style={{ marginTop: 8 }}>{t('ips.managedTitle')}</Typography.Title>
|
||||||
<Button type="primary" style={{ marginBottom: 16 }} onClick={() => {
|
<Button type="primary" style={{ marginBottom: 16 }} onClick={() => {
|
||||||
setCreating(true); form.resetFields()
|
setCreating(true); form.resetFields()
|
||||||
form.setFieldsValue({ prefix: 24, is_vip: false, active: true })
|
form.setFieldsValue({ prefix: 24, is_vip: false, active: true })
|
||||||
|
|||||||
@@ -1,17 +1,191 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
# ============================================================================
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
# EdgeGuard — Bootstrap Installer (One-Liner)
|
# EdgeGuard — One-Liner Installer
|
||||||
# ============================================================================
|
|
||||||
# Usage:
|
|
||||||
# curl -fsSL https://get.edgeguard.netcell-it.de | sudo bash
|
|
||||||
# curl -fsSL https://get.edgeguard.netcell-it.de | sudo bash -s -- \
|
|
||||||
# --join https://<existing-node-fqdn> --token <cluster-join-token>
|
|
||||||
#
|
#
|
||||||
# Platform: Debian 13 (Trixie) or Ubuntu 24.04 LTS (Noble), amd64 + arm64.
|
# curl -fsSL https://get.netcell-edgeguard.de | sudo bash
|
||||||
# Idempotent — safe to re-run.
|
#
|
||||||
# ============================================================================
|
# Supported: Debian 13 (Trixie), amd64 + arm64.
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ─── TODO — Stub. Wird in Task #4 implementiert. ────────────────────────────
|
# Locale auf C — sonst druckt apt/dpkg auf de_DE-Boxen lokalisierte
|
||||||
echo "EdgeGuard installer stub — full install.sh follows in Task #4."
|
# Strings (z.B. "Installationskandidat:" statt "Candidate:") und unsere
|
||||||
exit 0
|
# awk/grep-Parser matchen nicht.
|
||||||
|
export LC_ALL=C
|
||||||
|
export LANG=C
|
||||||
|
|
||||||
|
# ─── Colors + helpers ─────────────────────────────────────────────────────────
|
||||||
|
GRN="\033[0;32m"; RED="\033[0;31m"; YLW="\033[0;33m"; CYN="\033[0;36m"
|
||||||
|
BLD="\033[1m"; DIM="\033[2m"; NC="\033[0m"
|
||||||
|
CHECK="${GRN}✓${NC}"
|
||||||
|
CROSS="${RED}✗${NC}"
|
||||||
|
|
||||||
|
step() {
|
||||||
|
local label="$1"; shift
|
||||||
|
local spin_chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
||||||
|
local pid
|
||||||
|
|
||||||
|
"$@" >/dev/null 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
|
||||||
|
local i=0
|
||||||
|
local start=$SECONDS
|
||||||
|
while kill -0 "$pid" 2>/dev/null; do
|
||||||
|
local c="${spin_chars:i%${#spin_chars}:1}"
|
||||||
|
local elapsed=$(( SECONDS - start ))
|
||||||
|
printf "\r ${CYN}${c}${NC} ${label}... ${DIM}${elapsed}s${NC} "
|
||||||
|
sleep 0.1
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
local total=$(( SECONDS - start ))
|
||||||
|
if wait "$pid"; then
|
||||||
|
printf "\r ${CHECK} ${label} ${DIM}(${total}s)${NC} \n"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
printf "\r ${CROSS} ${label} ${DIM}(${total}s)${NC} \n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
step_log() { printf " ${CHECK} $1\n"; }
|
||||||
|
fail() { echo -e "\n ${CROSS} ${RED}$*${NC}\n"; exit 1; }
|
||||||
|
|
||||||
|
# ─── Banner ───────────────────────────────────────────────────────────────────
|
||||||
|
clear 2>/dev/null || true
|
||||||
|
echo ""
|
||||||
|
echo -e " ${CYN}${BLD}EdgeGuard${NC} — Installer"
|
||||||
|
echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ─── Root check ───────────────────────────────────────────────────────────────
|
||||||
|
[ "$(id -u)" -ne 0 ] && fail "Please run as root: curl -fsSL https://get.netcell-edgeguard.de | sudo bash"
|
||||||
|
|
||||||
|
# ─── OS / Arch check ──────────────────────────────────────────────────────────
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
OS_ID="$ID"
|
||||||
|
OS_VERSION="$VERSION_ID"
|
||||||
|
OS_CODENAME="${VERSION_CODENAME:-}"
|
||||||
|
else
|
||||||
|
fail "Operating system not detected."
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$OS_ID" in
|
||||||
|
debian) [[ "$OS_VERSION" == "13" ]] || fail "Debian $OS_VERSION not supported (only 13 Trixie)." ;;
|
||||||
|
*) fail "$OS_ID not supported (only Debian 13 Trixie)." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ARCH=$(dpkg --print-architecture 2>/dev/null || uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
amd64|x86_64) ARCH="amd64" ;;
|
||||||
|
arm64|aarch64) ARCH="arm64" ;;
|
||||||
|
*) fail "Architecture $ARCH not supported (only amd64/arm64)." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
step_log "${OS_ID^} ${OS_VERSION} (${OS_CODENAME}) · ${ARCH}"
|
||||||
|
|
||||||
|
# ─── Pre-flight: required tools ──────────────────────────────────────────────
|
||||||
|
for tool in curl apt-get gpg dpkg; do
|
||||||
|
command -v "$tool" >/dev/null 2>&1 || \
|
||||||
|
fail "$tool not found — install with: apt-get install -y curl gpg ca-certificates"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ─── 1. Update system + prerequisites ────────────────────────────────────────
|
||||||
|
step "Update system" apt-get update -qq
|
||||||
|
step "Install prerequisites" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq -o Dpkg::Options::=--force-confold curl gnupg ca-certificates apt-transport-https"
|
||||||
|
|
||||||
|
# ─── 2. Set up EdgeGuard apt repository ──────────────────────────────────────
|
||||||
|
# Shared keyring with mail-gateway (User-Konvention: ein netcell-IT-
|
||||||
|
# Schlüssel für alle Produkte). Key direkt aus der Gitea-Package-Registry,
|
||||||
|
# kein eigener edgeguard.asc.
|
||||||
|
setup_repo() {
|
||||||
|
# Strip stale repo files left over from earlier installer attempts.
|
||||||
|
for stale_list in netcell-edgeguard.list edgeguard-old.list; do
|
||||||
|
[ -f "/etc/apt/sources.list.d/$stale_list" ] && rm -f "/etc/apt/sources.list.d/$stale_list"
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p /etc/apt/keyrings
|
||||||
|
if [ ! -f /etc/apt/keyrings/nmg.asc ]; then
|
||||||
|
curl -fsSL "https://git.netcell-it.de/api/packages/projekte/debian/repository.key" \
|
||||||
|
-o /etc/apt/keyrings/nmg.asc
|
||||||
|
fi
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/nmg.asc] https://git.netcell-it.de/api/packages/projekte/debian trixie main" \
|
||||||
|
> /etc/apt/sources.list.d/edgeguard.list
|
||||||
|
|
||||||
|
apt-get update -qq
|
||||||
|
}
|
||||||
|
step "Set up EdgeGuard apt repository" setup_repo
|
||||||
|
|
||||||
|
AVAILABLE=$(LC_ALL=C apt-cache policy edgeguard 2>/dev/null | awk '/Candidate:/ {print $2; exit}' || true)
|
||||||
|
if [ -n "$AVAILABLE" ] && [ "$AVAILABLE" != "(none)" ]; then
|
||||||
|
step_log "Version ${BLD}${AVAILABLE}${NC} available"
|
||||||
|
else
|
||||||
|
fail "edgeguard package not visible in repository — check /etc/apt/sources.list.d/edgeguard.list"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── 3. Install EdgeGuard ────────────────────────────────────────────────────
|
||||||
|
install_edgeguard() {
|
||||||
|
local apt_opts=(-y -qq -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef)
|
||||||
|
if ! DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" edgeguard; then
|
||||||
|
# Safety net: if a dependency's postinst half-configured, try
|
||||||
|
# to resolve before failing the whole installer.
|
||||||
|
DEBIAN_FRONTEND=noninteractive dpkg --configure -a >/dev/null 2>&1 || true
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" --fix-broken >/dev/null 2>&1 || true
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" edgeguard
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
step "Install EdgeGuard" install_edgeguard
|
||||||
|
|
||||||
|
# ─── 4. Service smoke ────────────────────────────────────────────────────────
|
||||||
|
for svc in edgeguard-api edgeguard-scheduler haproxy postgresql; do
|
||||||
|
if systemctl list-unit-files --no-legend "${svc}.service" >/dev/null 2>&1; then
|
||||||
|
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||||||
|
step_log "${svc} running"
|
||||||
|
else
|
||||||
|
printf " ${CROSS} ${svc} not active\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Probe the API loopback — should answer /healthz within a couple of
|
||||||
|
# seconds. We retry briefly because postgresql.service can be slow on
|
||||||
|
# first boot.
|
||||||
|
api_ok=""
|
||||||
|
for _ in 1 2 3 4 5; do
|
||||||
|
if curl -fsS http://127.0.0.1:9443/healthz >/dev/null 2>&1; then
|
||||||
|
api_ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ -n "$api_ok" ]; then
|
||||||
|
step_log "edgeguard-api answering on 127.0.0.1:9443"
|
||||||
|
else
|
||||||
|
printf " ${YLW}!${NC} edgeguard-api not yet answering on :9443 — check 'systemctl status edgeguard-api'\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Result ──────────────────────────────────────────────────────────────────
|
||||||
|
SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
|
FQDN=$(hostname -f 2>/dev/null || hostname)
|
||||||
|
case "$FQDN" in *.*) ;; *) FQDN="" ;; esac # drop single-label hostnames
|
||||||
|
|
||||||
|
print_url() {
|
||||||
|
local label="$1" path="$2"
|
||||||
|
echo -e " ${BLD}▸ ${label}:${NC}"
|
||||||
|
[ -n "$SERVER_IP" ] && echo -e " ${CYN}https://${SERVER_IP}${path}${NC}"
|
||||||
|
[ -n "$FQDN" ] && echo -e " ${CYN}https://${FQDN}${path}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${GRN}${BLD}Installation complete!${NC}"
|
||||||
|
echo ""
|
||||||
|
print_url "Setup wizard" "/setup"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}Open the URL in your browser to configure EdgeGuard.${NC}"
|
||||||
|
echo -e " ${DIM}(SSL is self-signed on first boot — confirm the browser warning;${NC}"
|
||||||
|
echo -e " ${DIM} a real Let's Encrypt cert is issued after the FQDN is set.)${NC}"
|
||||||
|
echo ""
|
||||||
|
|||||||
Reference in New Issue
Block a user