#!/usr/bin/env bash # ============================================================================= # MapSystem — tek komutluk node kurulumu / single-command node installer # # Kullanım / Usage: # sudo ./install-mapsystem.sh --bundle mapsystem.tgz [seçenekler] # # Script yeniden çalıştırılabilir (idempotent): var olan bir kurulumun üzerine # çalıştırıldığında yalnızca uygulama dosyalarını ve verilen ayarları günceller, # veri dizinini (katalog, önbellek, anahtarlar) korur. # ============================================================================= set -euo pipefail # ---- varsayılanlar ---------------------------------------------------------- BUNDLE="" INSTALL_DIR="/opt/mapsystem" DATA_DIR="" # boş => $INSTALL_DIR/data-directory CACHE_DIR="" # tile önbelleği (2. disk vermek isterseniz) SERVICE_USER="mapsystem" SERVICE_NAME="mapsystem" PORT="80" NODE_ID="$(hostname)" REDIS="" # örn: 192.168.0.105:6379,password=xxx REDIS_PREFIX="mapsystem" ROLE="Serving" # Serving | Worker | ControlPlane CONTROL_PLANE="auto" # auto|true|false (auto: Redis yoksa true) NODE_ADDRESS="" # boş => http://: LABELS="" # virgüllü: ssd,gpu REPLICATION=1 INSTALL_REDIS="no" # yerel Redis kurulumu (ilk node için) OPEN_REDIS_LAN="" # CIDR verilirse Redis LAN'a açılır, örn 192.168.0.0/24 REDIS_PASSWORD="" SHARE_KEYS="auto" # auto|true|false — DataProtection anahtarları Redis'te DOTNET_CHANNEL="10.0" NEW_CLUSTER="no" # yeni cluster: katılım anahtarı üret JOIN_TOKEN="" # cluster'a katılım anahtarı (bootstrap sırrı) JOIN_ADDRESS="" # katılınacak node adresi (http://ip) REQUIRE_TOKEN="auto" # auto|true|false CLUSTER_MODE="mesh" # mesh|shared — mesh: her node'da yerel Redis PEERS="" # mesh için tohum peer adresleri (virgüllü) usage() { sed -n '2,12p' "$0" cat <<'EOF' Seçenekler / Options --bundle Yayın paketi (.tgz). Zorunlu. --install-dir Varsayılan /opt/mapsystem --data-dir Katalog+anahtar dizini (varsayılan /data-directory) --cache-dir Tile önbellek dizini (ayrı disk için) --port HTTP portu (varsayılan 80) --node-id Cluster'da benzersiz node adı (varsayılan hostname) --redis Paylaşımlı Redis, örn 192.168.0.105:6379,password=gizli --redis-prefix <ön-ek> Redis anahtar ön-eki (varsayılan mapsystem) --install-redis Bu makineye Redis kur (ilk/ana node) --open-redis-lan Yerel Redis'i bu ağa aç (ör. 192.168.0.0/24) --redis-password <şifre> Yerel Redis'e şifre koy / uzak Redis şifresi --new-cluster Yeni cluster kurar: katılım anahtarı (join token) üretir ve yazdırır --join-token Var olan cluster'a katılmak için anahtar --join-address Katılım isteğinin gönderileceği node (ör. http://192.168.0.105) --require-token Geçerli kimlik olmadan cluster'a kayıt olmasın (varsayılan: cluster'da true) --cluster-mode mesh (varsayılan; her node kendi Redis'i) | shared (tek merkezi Redis) --peers Mesh tohum adresleri: http://192.168.0.105,http://192.168.0.101 --role Serving | Worker | ControlPlane --control-plane Kontrol düzlemi adayı mı (varsayılan: Redis yoksa true) --node-address Gateway'in bu node'a ulaşacağı adres --labels Yerleştirme etiketleri (ssd,gpu ...) --replication Bir katmanın kaç node'da tutulacağı (varsayılan 1) --share-keys Oturum anahtarlarını Redis'te paylaş (cluster için true) --service-user Servis kullanıcısı (varsayılan mapsystem) --uninstall Servisi ve uygulamayı kaldır (veri dizini korunur) -h, --help Bu yardım Örnek — ilk node (ana, yeni cluster): sudo ./install-mapsystem.sh --bundle mapsystem.tgz --install-redis \ --open-redis-lan 192.168.0.0/24 --redis-password gizli \ --node-id node1 --redis 127.0.0.1:6379,password=gizli \ --control-plane true --new-cluster (Script sonunda katılım anahtarını ve ikinci node için hazır komutu yazdırır.) Örnek — ikinci node (katılım): sudo ./install-mapsystem.sh --bundle mapsystem.tgz --node-id node2 \ --redis 192.168.0.105:6379,password=gizli \ --join-token --join-address http://192.168.0.105 EOF } UNINSTALL="no" while [[ $# -gt 0 ]]; do case "$1" in --bundle) BUNDLE="$2"; shift 2;; --install-dir) INSTALL_DIR="$2"; shift 2;; --data-dir) DATA_DIR="$2"; shift 2;; --cache-dir) CACHE_DIR="$2"; shift 2;; --port) PORT="$2"; shift 2;; --node-id) NODE_ID="$2"; shift 2;; --redis) REDIS="$2"; shift 2;; --redis-prefix) REDIS_PREFIX="$2"; shift 2;; --install-redis) INSTALL_REDIS="yes"; shift;; --open-redis-lan) OPEN_REDIS_LAN="$2"; shift 2;; --redis-password) REDIS_PASSWORD="$2"; shift 2;; --role) ROLE="$2"; shift 2;; --control-plane) CONTROL_PLANE="$2"; shift 2;; --node-address) NODE_ADDRESS="$2"; shift 2;; --labels) LABELS="$2"; shift 2;; --replication) REPLICATION="$2"; shift 2;; --share-keys) SHARE_KEYS="$2"; shift 2;; --new-cluster) NEW_CLUSTER="yes"; shift;; --join-token) JOIN_TOKEN="$2"; shift 2;; --join-address) JOIN_ADDRESS="$2"; shift 2;; --require-token) REQUIRE_TOKEN="$2"; shift 2;; --cluster-mode) CLUSTER_MODE="$2"; shift 2;; --peers) PEERS="$2"; shift 2;; --service-user) SERVICE_USER="$2"; shift 2;; --service-name) SERVICE_NAME="$2"; shift 2;; --uninstall) UNINSTALL="yes"; shift;; -h|--help) usage; exit 0;; *) echo "Bilinmeyen seçenek: $1" >&2; usage; exit 1;; esac done log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[X]\033[0m %s\n' "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "root gerekli: sudo ile çalıştırın." # ---- kaldırma --------------------------------------------------------------- if [[ "$UNINSTALL" == "yes" ]]; then log "Servis kaldırılıyor ($SERVICE_NAME)" systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true rm -f "/etc/systemd/system/${SERVICE_NAME}.service" systemctl daemon-reload warn "Uygulama dosyaları ve veri dizini KORUNDU: $INSTALL_DIR" exit 0 fi [[ -n "$BUNDLE" ]] || die "--bundle zorunlu (yayın paketi .tgz)." [[ -f "$BUNDLE" ]] || die "Paket bulunamadı: $BUNDLE" [[ -n "$DATA_DIR" ]] || DATA_DIR="$INSTALL_DIR/data-directory" if [[ -z "$NODE_ADDRESS" ]]; then IP="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')" [[ -n "${IP:-}" ]] && NODE_ADDRESS="http://${IP}:${PORT}" fi if [[ "$CONTROL_PLANE" == "auto" ]]; then # Redis yoksa tek node demektir → kendisi kontrol düzlemidir. [[ -z "$REDIS" ]] && CONTROL_PLANE="true" || CONTROL_PLANE="false" fi if [[ "$SHARE_KEYS" == "auto" ]]; then [[ -n "$REDIS" ]] && SHARE_KEYS="true" || SHARE_KEYS="false" fi # Mesh modda her node kendi Redis'ini çalıştırır: merkezi Redis'in çökmesi tüm cluster'ı # düşürmesin diye. Adres verilmediyse yerel Redis varsayılır ve kurulur. # Yeni cluster'ın ilk node'u da (--new-cluster) mesh üyesidir: yerel Redis'i olmalı ve katılım # anahtarı zorunlu olmalı — aksi halde ilk node "tek node" gibi kurulur, sonra gelenler katılamaz. if [[ "$CLUSTER_MODE" == "mesh" && ( "$NEW_CLUSTER" == "yes" || -n "$JOIN_TOKEN$JOIN_ADDRESS$PEERS" ) ]]; then [[ -z "$REDIS" ]] && REDIS="127.0.0.1:6379" if [[ "$REDIS" == 127.0.0.1:* || "$REDIS" == localhost:* ]]; then INSTALL_REDIS="yes"; fi # Anahtar halkası mesh'te node'lar arasında doğrudan takas edilir; yerel Redis'e # yazmanın kimseye faydası olmaz. SHARE_KEYS="false" # Lider seçimi oy ile yapıldığından her node aday olmalı; aksi halde çoğunluk oluşmaz. [[ "$CONTROL_PLANE" == "false" ]] && CONTROL_PLANE="true" fi if [[ "$REQUIRE_TOKEN" == "auto" ]]; then [[ -n "$REDIS" ]] && REQUIRE_TOKEN="true" || REQUIRE_TOKEN="false" fi # ---- katılım anahtarı ------------------------------------------------------- # Yeni cluster: anahtarı burada üretiyoruz. Var olan kurulumda anahtar zaten # ayar dosyasındaysa (script yeniden çalıştırılıyor) onu koruyoruz ki node # yeniden katılmak zorunda kalmasın. GENERATED_TOKEN="no" if [[ -z "$JOIN_TOKEN" && -f "$INSTALL_DIR/appsettings.Production.json" ]]; then JOIN_TOKEN="$(grep -o '"JoinToken"[^,}]*' "$INSTALL_DIR/appsettings.Production.json" 2>/dev/null \ | head -1 | sed 's/.*:[[:space:]]*"//; s/"$//')" [[ -n "$JOIN_TOKEN" ]] && echo "Mevcut katılım anahtarı korunuyor." fi if [[ "$NEW_CLUSTER" == "yes" && -z "$JOIN_TOKEN" ]]; then JOIN_TOKEN="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" GENERATED_TOKEN="yes" fi if [[ -n "$REDIS" && -z "$JOIN_TOKEN" && "$REQUIRE_TOKEN" == "true" ]]; then die "Cluster kurulumu için --new-cluster (ilk node) ya da --join-token (diğer node'lar) gerekir. Kimlik doğrulamasız kurmak isterseniz --require-token false verin." fi log "Kurulum planı" cat </tilecache} cluster modu : $CLUSTER_MODE peer: ${PEERS:-(heartbeat ile keşif)} EOF # ---- 1) sistem paketleri ---------------------------------------------------- log "Sistem paketleri kontrol ediliyor" export DEBIAN_FRONTEND=noninteractive apt-get update -qq # libgdiplus/libfontconfig: SkiaSharp metin çizimi; unzip/curl: kurulum araçları apt-get install -y -qq curl ca-certificates unzip libicu-dev libfontconfig1 >/dev/null # ---- 2) .NET runtime -------------------------------------------------------- if command -v dotnet >/dev/null 2>&1 && dotnet --list-runtimes 2>/dev/null | grep -q "Microsoft.AspNetCore.App ${DOTNET_CHANNEL%%.*}\."; then log ".NET runtime mevcut: $(dotnet --list-runtimes | grep AspNetCore | tail -1)" else log ".NET ASP.NET Core ${DOTNET_CHANNEL} runtime kuruluyor" if ! apt-get install -y -qq "aspnetcore-runtime-${DOTNET_CHANNEL}" >/dev/null 2>&1; then warn "apt deposunda yok; Microsoft install script'i kullanılıyor." curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh bash /tmp/dotnet-install.sh --channel "$DOTNET_CHANNEL" --runtime aspnetcore --install-dir /usr/lib/dotnet ln -sf /usr/lib/dotnet/dotnet /usr/bin/dotnet fi fi DOTNET_BIN="$(command -v dotnet)" # ---- 3) Redis (isteğe bağlı) ------------------------------------------------ if [[ "$INSTALL_REDIS" == "yes" ]]; then log "Redis kuruluyor" apt-get install -y -qq redis-server >/dev/null CONF=/etc/redis/redis.conf if [[ -n "$OPEN_REDIS_LAN" ]]; then # Cluster'da diğer node'lar bağlanacağı için LAN'a açıyoruz. Şifresiz # açmak tehlikelidir; bu yüzden şifre yoksa protected-mode açık kalır. sed -i 's/^bind .*/bind 0.0.0.0 -::1/' "$CONF" if [[ -n "$REDIS_PASSWORD" ]]; then sed -i 's/^protected-mode .*/protected-mode no/' "$CONF" if grep -q '^requirepass' "$CONF"; then sed -i "s/^requirepass .*/requirepass $REDIS_PASSWORD/" "$CONF" else echo "requirepass $REDIS_PASSWORD" >> "$CONF" fi else warn "--redis-password verilmedi: Redis şifresiz LAN'a açılıyor, güvenlik duvarına güvenin." sed -i 's/^protected-mode .*/protected-mode no/' "$CONF" fi if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow from "$OPEN_REDIS_LAN" to any port 6379 proto tcp >/dev/null || true fi fi systemctl enable --now redis-server >/dev/null 2>&1 || systemctl restart redis-server fi # ---- 4) kullanıcı ve dizinler ---------------------------------------------- if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then log "Servis kullanıcısı oluşturuluyor: $SERVICE_USER" useradd --system --create-home --shell /usr/sbin/nologin "$SERVICE_USER" fi mkdir -p "$INSTALL_DIR" "$DATA_DIR" [[ -n "$CACHE_DIR" ]] && mkdir -p "$CACHE_DIR" # ---- 5) uygulama dosyaları -------------------------------------------------- RUNNING="no" systemctl is-active --quiet "$SERVICE_NAME" && RUNNING="yes" [[ "$RUNNING" == "yes" ]] && { log "Çalışan servis durduruluyor"; systemctl stop "$SERVICE_NAME"; } log "Uygulama açılıyor: $BUNDLE → $INSTALL_DIR" # Yayın paketi veri dizinini İÇERMEZ; yine de üzerine yazmayı engellemek için # --keep-newer-files yerine açıkça dışlıyoruz. tar xzf "$BUNDLE" -C "$INSTALL_DIR" --exclude='data-directory/*' --exclude='logs/*' # ---- 6) yapılandırma -------------------------------------------------------- CFG="$INSTALL_DIR/appsettings.Production.json" log "Yapılandırma yazılıyor: $CFG" labels_json="[]" if [[ -n "$LABELS" ]]; then labels_json="[$(echo "$LABELS" | awk -F, '{for(i=1;i<=NF;i++){printf "%s\"%s\"", (i>1?", ":""), $i}}')]" fi cache_line="" [[ -n "$CACHE_DIR" ]] && cache_line="\n \"CacheDirectory\": \"$CACHE_DIR\"," redis_line="" [[ -n "$REDIS" ]] && redis_line="\n \"Redis\": \"$REDIS\"," peers_json="[]" if [[ -n "$PEERS" ]]; then peers_json="[$(echo "$PEERS" | awk -F, '{for(i=1;i<=NF;i++){printf "%s\"%s\"", (i>1?", ":""), $i}}')]" fi CLUSTER_MODE_JSON="Mesh" [[ "$CLUSTER_MODE" == "shared" ]] && CLUSTER_MODE_JSON="Shared" cluster_block="" if [[ -n "$JOIN_TOKEN" || -n "$JOIN_ADDRESS" ]]; then cluster_block=$(cat < "$CFG" </dev/null || die "Yapılandırma geçersiz JSON üretti: $CFG" chmod 600 "$CFG" # içinde Redis şifresi ve katılım anahtarı var chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR" "$DATA_DIR" [[ -n "$CACHE_DIR" ]] && chown -R "$SERVICE_USER":"$SERVICE_USER" "$CACHE_DIR" # ---- 7) systemd ------------------------------------------------------------- log "systemd birimi yazılıyor: ${SERVICE_NAME}.service" cat > "/etc/systemd/system/${SERVICE_NAME}.service" </dev/null 2>&1 || true if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow "$PORT"/tcp >/dev/null || true fi # ---- 7b) yönetim betiği ------------------------------------------------------ # mapsystem-deploy (güncelleme, yeniden başlatma, node emekliye ayırma / kümeden çıkarma) paketle # birlikte geliyorsa root'a ait ve yalnız root'un yazabileceği şekilde kurulur. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -f "$SCRIPT_DIR/mapsystem-deploy" ]]; then install -m 755 -o root -g root "$SCRIPT_DIR/mapsystem-deploy" /usr/local/sbin/mapsystem-deploy log "Yönetim betiği kuruldu: /usr/local/sbin/mapsystem-deploy" fi # ---- 8) başlat ve doğrula --------------------------------------------------- log "Servis başlatılıyor" systemctl restart "$SERVICE_NAME" ok="no" for i in $(seq 1 60); do if curl -fsS -m 3 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then ok="yes"; break; fi sleep 2 done if [[ "$ok" == "yes" ]]; then log "HAZIR → http://${NODE_ADDRESS#http://} (yönetim: / kullanıcı: admin)" # Node kimliği: uygulama açılışta üretip veri dizinine yazar. NODE_REAL_ID="$(cat "$DATA_DIR/node-id" 2>/dev/null || echo "$NODE_ID")" if [[ -f "$DATA_DIR/node.token" ]]; then echo " node kimliği : $NODE_REAL_ID (kimlik dosyası: $DATA_DIR/node.token)" elif [[ "$REQUIRE_TOKEN" == "true" ]]; then warn "Node cluster'a kaydolamadı: katılım anahtarını kontrol edin (journalctl -u $SERVICE_NAME)." fi if [[ "$GENERATED_TOKEN" == "yes" ]]; then cat <} --anahtar $JOIN_TOKEN ya da paketin içindeki kurulum betiğiyle: sudo ./install-mapsystem.sh --bundle --cluster-mode $CLUSTER_MODE \\ --join-token $JOIN_TOKEN \\ --join-address ${NODE_ADDRESS:-http://} --peers ${NODE_ADDRESS:-http://} Anahtar bu sunucuda $CFG içinde de duruyor (chmod 600). BANNER fi echo else warn "Sağlık kontrolü geçilemedi. Son kayıtlar:" journalctl -u "$SERVICE_NAME" -n 40 --no-pager || true exit 1 fi