#!/bin/bash # # Calypso Appliance - Airgap Bundle Creator # Creates a self-contained installer bundle for offline deployment # Includes all DEB packages with dependencies # # Usage: ./create-bundle.sh [--version VERSION] [--output OUTPUT_DIR] [--skip-packages] # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" INSTALLER_DIR="$PROJECT_ROOT/installer/alpha" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "\n${BLUE}==>${NC} $1"; } # Configuration VERSION="${CALYPSO_VERSION:-1.0.0}" OUTPUT_DIR="${OUTPUT_DIR:-$PROJECT_ROOT/dist/airgap}" BUNDLE_NAME="calypso-appliance-${VERSION}-airgap" BUNDLE_DIR="$OUTPUT_DIR/$BUNDLE_NAME" DOWNLOAD_PACKAGES="${DOWNLOAD_PACKAGES:-true}" PACKAGE_LIST="$SCRIPT_DIR/package-list.txt" # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --version) VERSION="$2" shift 2 ;; --output) OUTPUT_DIR="$2" shift 2 ;; --skip-packages) DOWNLOAD_PACKAGES=false shift ;; *) log_error "Unknown option: $1" exit 1 ;; esac done # Function to get all dependencies recursively get_all_dependencies() { local package=$1 local deps_file=$2 local visited=$3 # Check if already visited if grep -q "^${package}$" "$visited" 2>/dev/null; then return 0 fi echo "$package" >> "$visited" # Get dependencies (Depends, PreDepends, Recommends) local deps=$(apt-cache depends "$package" 2>/dev/null | \ grep -E "^\s*(Depends|PreDepends|Recommends):" | \ awk '{print $2}' | \ grep -v "^<" | \ sort -u || true) for dep in $deps; do # Skip virtual packages and already processed if apt-cache show "$dep" &>/dev/null 2>&1; then if ! grep -q "^${dep}$" "$deps_file" 2>/dev/null; then echo "$dep" >> "$deps_file" get_all_dependencies "$dep" "$deps_file" "$visited" fi fi done } # Function to download packages with all dependencies download_packages() { log_step "Downloading packages with dependencies..." local DEBS_DIR="$BUNDLE_DIR/packages/debs" mkdir -p "$DEBS_DIR" # Create temporary files local ALL_PACKAGES=$(mktemp) local VISITED=$(mktemp) local FINAL_LIST=$(mktemp) # Read package list and get all dependencies log_info "Resolving dependencies for all packages..." local count=0 local total=$(grep -v '^#' "$PACKAGE_LIST" | grep -v '^$' | wc -l) while IFS= read -r package || [ -n "$package" ]; do # Skip comments and empty lines [[ "$package" =~ ^#.*$ ]] && continue [[ -z "$package" ]] && continue count=$((count + 1)) log_info "[$count/$total] Processing: $package" get_all_dependencies "$package" "$ALL_PACKAGES" "$VISITED" echo "$package" >> "$ALL_PACKAGES" done < "$PACKAGE_LIST" # Remove duplicates and sort sort -u "$ALL_PACKAGES" > "$FINAL_LIST" local total_with_deps=$(wc -l < "$FINAL_LIST") log_info "Total packages (with dependencies): $total_with_deps" # Download all packages log_info "Downloading packages..." cd "$DEBS_DIR" local downloaded=0 local failed=0 while IFS= read -r package || [ -n "$package" ]; do # Try to download package if apt-get download "$package" 2>/dev/null; then downloaded=$((downloaded + 1)) echo "✓ $package" >> "$BUNDLE_DIR/packages/downloaded-packages.txt" else failed=$((failed + 1)) echo "✗ $package (not available)" >> "$BUNDLE_DIR/packages/missing-packages.txt" log_warn "Package not available: $package" fi done < "$FINAL_LIST" # Cleanup temp files rm -f "$ALL_PACKAGES" "$VISITED" "$FINAL_LIST" log_info "✓ Downloaded $downloaded packages" if [ -f "$BUNDLE_DIR/packages/missing-packages.txt" ] && [ "$failed" -gt 0 ]; then log_warn "$failed packages could not be downloaded" log_warn "Check: $BUNDLE_DIR/packages/missing-packages.txt" fi # Create Packages index for local repository if [ "$downloaded" -gt 0 ]; then log_info "Creating Packages index..." cd "$DEBS_DIR" dpkg-scanpackages . /dev/null 2>/dev/null | gzip -9c > Packages.gz || true log_info "✓ Packages index created" fi } log_step "Creating Airgap Bundle for Calypso Appliance v${VERSION}" # Create bundle directory structure log_info "Creating bundle directory structure..." rm -rf "$BUNDLE_DIR" mkdir -p "$BUNDLE_DIR"/{binaries,packages/debs,configs,scripts,services,migrations,frontend,third_party,docs} # Copy package list cp "$PACKAGE_LIST" "$BUNDLE_DIR/packages/package-list.txt" # 1. Download packages if requested if [[ "$DOWNLOAD_PACKAGES" == "true" ]]; then # Check if running on Ubuntu/Debian if [ ! -f /etc/debian_version ]; then log_warn "Not running on Debian/Ubuntu. Skipping package download." log_warn "Run download-packages.sh later on Ubuntu 24.04 machine" DOWNLOAD_PACKAGES=false else # Update package lists log_info "Updating package lists..." apt-get update -qq download_packages fi else log_info "Skipping package download (use --skip-packages to suppress this message)" fi # 2. Build and bundle Calypso binaries log_step "Building Calypso binaries..." # Build backend log_info "Building backend..." cd "$PROJECT_ROOT/backend" if [ ! -f "go.mod" ]; then log_error "Backend directory not found or invalid" exit 1 fi # Ensure Go is available export PATH=$PATH:/usr/local/go/bin if ! command -v go &> /dev/null; then log_error "Go is not installed or not in PATH" log_error "Please install Go first or use --skip-packages and build manually" exit 1 fi # Build for Linux amd64 export CGO_ENABLED=0 export GOOS=linux export GOARCH=amd64 go build -ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) -X main.gitCommit=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" \ -o "$BUNDLE_DIR/binaries/calypso-api" ./cmd/calypso-api log_info "✓ Backend binary built" # Build frontend log_info "Building frontend..." cd "$PROJECT_ROOT/frontend" if [ ! -f "package.json" ]; then log_error "Frontend directory not found or invalid" exit 1 fi # Ensure Node.js is available if ! command -v npm &> /dev/null; then log_error "Node.js/npm is not installed or not in PATH" log_error "Please install Node.js first or use --skip-packages and build manually" exit 1 fi npm ci npm run build # Copy frontend dist cp -r dist/* "$BUNDLE_DIR/frontend/" log_info "✓ Frontend built" # 3. Bundle database migrations log_step "Bundling database migrations..." if [ -d "$PROJECT_ROOT/backend/internal/common/database/migrations" ]; then cp -r "$PROJECT_ROOT/backend/internal/common/database/migrations"/* "$BUNDLE_DIR/migrations/" log_info "✓ Migrations bundled" else log_warn "Migrations directory not found" fi # 4. Bundle installer scripts log_step "Bundling installer scripts..." cp -r "$INSTALLER_DIR/scripts"/* "$BUNDLE_DIR/scripts/" 2>/dev/null || true cp "$INSTALLER_DIR/install.sh" "$BUNDLE_DIR/" 2>/dev/null || true cp "$INSTALLER_DIR/uninstall.sh" "$BUNDLE_DIR/" 2>/dev/null || true log_info "✓ Installer scripts bundled" # 5. Bundle systemd services log_step "Bundling systemd services..." if [ -d "$PROJECT_ROOT/deploy/systemd" ]; then cp -r "$PROJECT_ROOT/deploy/systemd"/* "$BUNDLE_DIR/services/" log_info "✓ Systemd services bundled" fi # 6. Bundle configuration templates log_step "Bundling configuration templates..." if [ -d "$INSTALLER_DIR/configs" ]; then cp -r "$INSTALLER_DIR/configs"/* "$BUNDLE_DIR/configs/" log_info "✓ Configuration templates bundled" fi # 7. Create offline installer script log_step "Creating offline installer script..." cat > "$BUNDLE_DIR/install-airgap.sh" <<'INSTALLER_EOF' #!/bin/bash # # Calypso Appliance - Airgap Installer # Installs Calypso from bundled files without internet connection # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BUNDLE_DIR="$SCRIPT_DIR" VERSION="${VERSION:-1.0.0}" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "\n${BLUE}==>${NC} $1"; } # Check root if [[ $EUID -ne 0 ]]; then log_error "This script must be run as root (use sudo)" exit 1 fi # Check OS if [ ! -f /etc/os-release ]; then log_error "Cannot detect OS" exit 1 fi . /etc/os-release if [[ "$ID" != "ubuntu" ]] || [[ "$VERSION_ID" != "24.04" ]]; then log_warn "This installer is designed for Ubuntu 24.04 LTS" log_warn "Detected: $ID $VERSION_ID" read -p "Continue anyway? (y/N) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi fi log_step "Calypso Appliance - Airgap Installation" log_info "Bundle directory: $BUNDLE_DIR" # 1. Install packages from bundle log_step "Installing system packages from bundle..." if [ -d "$BUNDLE_DIR/packages/debs" ] && [ "$(ls -A $BUNDLE_DIR/packages/debs/*.deb 2>/dev/null)" ]; then log_info "Installing packages from bundle..." # Create local apt repository DEBS_DIR="$BUNDLE_DIR/packages/debs" REPO_DIR="/tmp/calypso-local-repo" mkdir -p "$REPO_DIR" cp "$DEBS_DIR"/*.deb "$REPO_DIR/" 2>/dev/null || true # Create Packages.gz if not exists cd "$REPO_DIR" if [ ! -f Packages.gz ]; then dpkg-scanpackages . /dev/null 2>/dev/null | gzip -9c > Packages.gz fi # Add to sources.list temporarily echo "deb [trusted=yes] file://$REPO_DIR ./" > /tmp/calypso-local.list mv /tmp/calypso-local.list /etc/apt/sources.list.d/calypso-local.list # Update apt cache apt-get update -qq # Install packages from list PACKAGE_LIST="$BUNDLE_DIR/packages/package-list.txt" PACKAGES=$(grep -v '^#' "$PACKAGE_LIST" | grep -v '^$' | tr '\n' ' ') log_info "Installing packages..." apt-get install -y --allow-unauthenticated $PACKAGES || { log_warn "Some packages failed to install, trying individual installation..." while IFS= read -r package || [ -n "$package" ]; do [[ "$package" =~ ^#.*$ ]] && continue [[ -z "$package" ]] && continue apt-get install -y --allow-unauthenticated "$package" 2>/dev/null || log_warn "Failed: $package" done < "$PACKAGE_LIST" } # Cleanup rm -f /etc/apt/sources.list.d/calypso-local.list apt-get update -qq log_info "✓ Packages installed from bundle" else log_warn "Package DEB files not found in bundle" log_warn "You need to install packages manually" log_info "Package list: $BUNDLE_DIR/packages/package-list.txt" read -p "Install packages from system repositories? (y/N) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then apt-get update xargs -a "$BUNDLE_DIR/packages/package-list.txt" -r apt-get install -y fi fi # 2. Install Go from bundle (if available) if [ -f "$BUNDLE_DIR/third_party/go.tar.gz" ]; then log_step "Installing Go from bundle..." rm -rf /usr/local/go tar -C /usr/local -xzf "$BUNDLE_DIR/third_party/go.tar.gz" export PATH=$PATH:/usr/local/go/bin if ! grep -q "/usr/local/go/bin" /etc/profile; then echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile fi log_info "✓ Go installed" fi # 3. Install Node.js from bundle (if available) if [ -f "$BUNDLE_DIR/third_party/nodejs.tar.xz" ]; then log_step "Installing Node.js from bundle..." rm -rf /usr/local/node mkdir -p /usr/local/node tar -C /usr/local/node -xJf "$BUNDLE_DIR/third_party/nodejs.tar.xz" --strip-components=1 export PATH=$PATH:/usr/local/node/bin if ! grep -q "/usr/local/node/bin" /etc/profile; then echo 'export PATH=$PATH:/usr/local/node/bin' >> /etc/profile fi log_info "✓ Node.js installed" fi # 4. Create filesystem structure log_step "Creating filesystem structure..." INSTALL_PREFIX="/opt/adastra/calypso" CONFIG_DIR="/etc/calypso" DATA_DIR="/srv/calypso" LOG_DIR="/var/log/calypso" LIB_DIR="/var/lib/calypso" mkdir -p "$INSTALL_PREFIX/releases/${VERSION}"/{bin,web,migrations,scripts} mkdir -p "$INSTALL_PREFIX/third_party" mkdir -p "$CONFIG_DIR"/{tls,integrations,system,scst,nfs,samba,clamav} mkdir -p "$DATA_DIR"/{db,backups,object,shares,vtl,iscsi,uploads,cache,_system,quarantine} mkdir -p "$LOG_DIR" "$LIB_DIR" "/run/calypso" # Create calypso user if ! id "calypso" &>/dev/null; then useradd -r -s /bin/false -d "$LIB_DIR" -c "Calypso Appliance" calypso fi # 5. Install binaries log_step "Installing Calypso binaries..." cp "$BUNDLE_DIR/binaries/calypso-api" "$INSTALL_PREFIX/releases/${VERSION}/bin/" chmod +x "$INSTALL_PREFIX/releases/${VERSION}/bin/calypso-api" # Install frontend cp -r "$BUNDLE_DIR/frontend"/* "$INSTALL_PREFIX/releases/${VERSION}/web/" # Install migrations cp -r "$BUNDLE_DIR/migrations"/* "$INSTALL_PREFIX/releases/${VERSION}/migrations/" 2>/dev/null || true # Install scripts cp -r "$BUNDLE_DIR/scripts"/* "$INSTALL_PREFIX/releases/${VERSION}/scripts/" 2>/dev/null || true chmod +x "$INSTALL_PREFIX/releases/${VERSION}/scripts"/*.sh 2>/dev/null || true # Create symlink ln -sfn "releases/${VERSION}" "$INSTALL_PREFIX/current" # Set ownership chown -R calypso:calypso "$INSTALL_PREFIX" "$LIB_DIR" "$LOG_DIR" "$DATA_DIR" 2>/dev/null || chown -R root:root "$INSTALL_PREFIX" "$LIB_DIR" "$LOG_DIR" "$DATA_DIR" log_info "✓ Binaries installed" # 6. Install systemd services log_step "Installing systemd services..." if [ -d "$BUNDLE_DIR/services" ]; then cp "$BUNDLE_DIR/services"/*.service /etc/systemd/system/ 2>/dev/null || true systemctl daemon-reload log_info "✓ Systemd services installed" fi # 7. Setup database log_step "Setting up database..." if systemctl is-active --quiet postgresql 2>/dev/null || systemctl is-enabled --quiet postgresql 2>/dev/null; then sudo -u postgres createdb calypso 2>/dev/null || true sudo -u postgres createuser calypso 2>/dev/null || true sudo -u postgres psql -c "ALTER USER calypso WITH PASSWORD 'calypso_change_me';" 2>/dev/null || true sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE calypso TO calypso;" 2>/dev/null || true log_info "✓ Database setup complete" else log_warn "PostgreSQL not running. Please start it and run database setup manually." fi # 8. Generate configuration log_step "Generating configuration..." if [ -f "$BUNDLE_DIR/configs/config.yaml.template" ]; then cp "$BUNDLE_DIR/configs/config.yaml.template" "$CONFIG_DIR/calypso.yaml" # Generate secrets if command -v openssl &> /dev/null; then JWT_SECRET=$(openssl rand -hex 32) sed -i "s/CHANGE_ME_JWT_SECRET/$JWT_SECRET/g" "$CONFIG_DIR/calypso.yaml" fi log_info "✓ Configuration generated" fi log_step "Installation Complete!" log_info "" log_info "Next steps:" log_info "1. Review configuration: $CONFIG_DIR/calypso.yaml" log_info "2. Install kernel modules (ZFS, SCST, mhVTL) if needed" log_info "3. Start services: systemctl start calypso-api" log_info "4. Enable services: systemctl enable calypso-api" log_info "" log_info "Calypso API will be available at: http://localhost:8080" INSTALLER_EOF chmod +x "$BUNDLE_DIR/install-airgap.sh" # 8. Create README cat > "$BUNDLE_DIR/README.md" < "$BUNDLE_DIR/third_party/download-binaries.sh" <<'BIN_EOF' #!/bin/bash # # Download third-party binaries for airgap installation # Run this on an internet-connected machine # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" log_info() { echo -e "\033[0;32m[INFO]\033[0m $1"; } # Download Go log_info "Downloading Go 1.22..." GO_VERSION="1.22.0" GO_ARCH="linux-amd64" wget -q "https://go.dev/dl/go${GO_VERSION}.${GO_ARCH}.tar.gz" -O "$SCRIPT_DIR/go.tar.gz" log_info "✓ Go downloaded" # Download Node.js log_info "Downloading Node.js 20.x LTS..." NODE_VERSION="20.18.0" NODE_ARCH="linux-x64" wget -q "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-${NODE_ARCH}.tar.xz" -O "$SCRIPT_DIR/nodejs.tar.xz" log_info "✓ Node.js downloaded" log_info "✓ All binaries downloaded" BIN_EOF chmod +x "$BUNDLE_DIR/third_party/download-binaries.sh" # 10. Create archive log_step "Creating archive..." cd "$OUTPUT_DIR" tar -czf "${BUNDLE_NAME}.tar.gz" "$BUNDLE_NAME" # Calculate checksum sha256sum "${BUNDLE_NAME}.tar.gz" > "${BUNDLE_NAME}.tar.gz.sha256" log_step "Bundle Creation Complete!" log_info "" log_info "Bundle location: $OUTPUT_DIR/${BUNDLE_NAME}.tar.gz" log_info "Bundle size: $(du -h "$OUTPUT_DIR/${BUNDLE_NAME}.tar.gz" | cut -f1)" log_info "Checksum: $OUTPUT_DIR/${BUNDLE_NAME}.tar.gz.sha256" if [ -d "$BUNDLE_DIR/packages/debs" ]; then pkg_count=$(ls -1 "$BUNDLE_DIR/packages/debs"/*.deb 2>/dev/null | wc -l) log_info "Packages bundled: $pkg_count DEB files" fi log_info "" log_info "To deploy:" log_info "1. Copy ${BUNDLE_NAME}.tar.gz to target machine" log_info "2. Extract: tar -xzf ${BUNDLE_NAME}.tar.gz" log_info "3. Run: sudo ./${BUNDLE_NAME}/install-airgap.sh"