From a458a28406e2b1ea2ed5c2e2fcf015f1a05b6121 Mon Sep 17 00:00:00 2001 From: Ben Duncan <5178243+benduncan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:57:52 +0000 Subject: [PATCH] new binary mode and systemd process --- .github/workflows/release.yml | 44 +++ .gitignore | 1 + Makefile | 40 ++- build/Dockerfile.distro | 48 ++++ build/logrotate/hive | 9 + build/systemd/hive-awsgw.service | 22 ++ build/systemd/hive-daemon.service | 21 ++ build/systemd/hive-nats.service | 17 ++ build/systemd/hive-predastore.service | 22 ++ build/systemd/hive-ui.service | 16 ++ build/systemd/hive-viperblock.service | 21 ++ build/systemd/hive-vpcd.service | 19 ++ build/systemd/hive.target | 8 + cmd/hive/cmd/admin.go | 46 ++-- cmd/hive/cmd/get.go | 3 +- cmd/hive/cmd/paths.go | 44 +++ cmd/hive/cmd/version.go | 27 ++ hive/admin/admin.go | 23 ++ scripts/setup.sh | 376 ++++++++++++++++++++++++++ 19 files changed, 774 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 build/Dockerfile.distro create mode 100644 build/logrotate/hive create mode 100644 build/systemd/hive-awsgw.service create mode 100644 build/systemd/hive-daemon.service create mode 100644 build/systemd/hive-nats.service create mode 100644 build/systemd/hive-predastore.service create mode 100644 build/systemd/hive-ui.service create mode 100644 build/systemd/hive-viperblock.service create mode 100644 build/systemd/hive-vpcd.service create mode 100644 build/systemd/hive.target create mode 100644 cmd/hive/cmd/paths.go create mode 100644 cmd/hive/cmd/version.go create mode 100755 scripts/setup.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..ee81c1b3f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + distro: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + path: hive + + - uses: actions/checkout@v4 + with: + repository: mulgadc/predastore + path: predastore + + - uses: actions/checkout@v4 + with: + repository: mulgadc/viperblock + path: viperblock + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up QEMU (for arm64 cross-build) + uses: docker/setup-qemu-action@v3 + + - name: Build distribution tarballs + run: cd hive && make distro VERSION=${{ github.ref_name }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: | + hive/dist/*.tar.gz + hive/dist/*.sha256 + hive/scripts/setup.sh + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 06268ee63..ca7eef148 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ go.work.sum # Build outputs bin/ +dist/ tmp/ *.test *.out diff --git a/Makefile b/Makefile index d14f54d47..ff2a87936 100644 --- a/Makefile +++ b/Makefile @@ -55,9 +55,13 @@ build-ui: cd hive/services/hiveui/frontend && pnpm build # GO commands +VERSION ?= $(shell git describe --tags --always --dirty) +COMMIT ?= $(shell git rev-parse --short HEAD) +LDFLAGS := -s -w -X github.com/mulgadc/hive/cmd/hive/cmd.Version=$(VERSION) -X github.com/mulgadc/hive/cmd/hive/cmd.Commit=$(COMMIT) + go_build: @echo -e "\n....Building $(GO_PROJECT_NAME)" - go build $(GO_BUILD_MOD) -ldflags "-s -w" -o ./bin/$(GO_PROJECT_NAME) cmd/hive/main.go + go build $(GO_BUILD_MOD) -ldflags "$(LDFLAGS)" -o ./bin/$(GO_PROJECT_NAME) cmd/hive/main.go go_run: @echo -e "\n....Running $(GO_PROJECT_NAME)...." @@ -193,6 +197,38 @@ security-check: $(_Q)set -o pipefail && go tool staticcheck -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" ./... $(_SECQ) tests/staticcheck-report.txt $(if $(QUIET),|| { cat tests/staticcheck-report.txt; exit 1; }) @echo " staticcheck ok" +# Build release tarballs for both architectures via Docker +distro: + @echo "Building hive $(VERSION) distribution tarballs..." + @mkdir -p dist/ + @echo "Building linux/amd64..." + docker buildx build \ + --platform linux/amd64 \ + --build-arg VERSION=$(VERSION) \ + -f build/Dockerfile.distro \ + --output type=local,dest=dist/amd64/ \ + ../ + tar -czf dist/hive-$(VERSION)-linux-amd64.tar.gz -C dist/amd64 . + sha256sum dist/hive-$(VERSION)-linux-amd64.tar.gz > dist/hive-$(VERSION)-linux-amd64.tar.gz.sha256 + @echo "Building linux/arm64..." + docker buildx build \ + --platform linux/arm64 \ + --build-arg VERSION=$(VERSION) \ + -f build/Dockerfile.distro \ + --output type=local,dest=dist/arm64/ \ + ../ + tar -czf dist/hive-$(VERSION)-linux-arm64.tar.gz -C dist/arm64 . + sha256sum dist/hive-$(VERSION)-linux-arm64.tar.gz > dist/hive-$(VERSION)-linux-arm64.tar.gz.sha256 + @echo "" + @echo "Distribution tarballs:" + @ls -lh dist/*.tar.gz + @echo "" + @cat dist/*.sha256 + +distro-clean: + rm -rf dist/ + .PHONY: build build-ui go_build go_run preflight test test-cover test-race diff-coverage bench run clean \ install-system install-go install-aws quickinstall \ - format check-format modernize check-modernize vet security-check + format check-format modernize check-modernize vet security-check \ + distro distro-clean diff --git a/build/Dockerfile.distro b/build/Dockerfile.distro new file mode 100644 index 000000000..5a9587fea --- /dev/null +++ b/build/Dockerfile.distro @@ -0,0 +1,48 @@ +ARG VERSION=dev + +FROM golang:1.26-bookworm AS builder + +ARG VERSION + +RUN apt-get update && apt-get install -y --no-install-recommends \ + nbdkit nbdkit-plugin-dev pkg-config gcc make \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Copy go.work first (lives in hive/) +COPY hive/go.work ./hive/ + +# Copy module files for dependency caching +COPY hive/go.mod hive/go.sum ./hive/ +COPY predastore/go.mod predastore/go.sum ./predastore/ +COPY viperblock/go.mod viperblock/go.sum ./viperblock/ +COPY viperblock/nbd/go.mod viperblock/nbd/go.sum ./viperblock/nbd/ +COPY viperblock/nbd/libguestfs.org/nbdkit/go.mod ./viperblock/nbd/libguestfs.org/nbdkit/ + +# Download dependencies (cached layer) +RUN cd hive && go mod download + +# Copy full source +COPY hive/ ./hive/ +COPY predastore/ ./predastore/ +COPY viperblock/ ./viperblock/ + +# Build hive binary +RUN COMMIT=$(cd hive && git rev-parse --short HEAD 2>/dev/null || echo "unknown") && \ + cd hive && go build -ldflags "-s -w \ + -X github.com/mulgadc/hive/cmd/hive/cmd.Version=${VERSION} \ + -X github.com/mulgadc/hive/cmd/hive/cmd.Commit=${COMMIT}" \ + -o /out/hive cmd/hive/main.go + +# Build nbdkit viperblock plugin (.so) +RUN cd viperblock && go build -o /out/nbdkit-viperblock-plugin.so -buildmode=c-shared nbd/viperblock.go + +# Copy setup scripts and systemd units +RUN cp hive/scripts/setup.sh /out/setup.sh 2>/dev/null || true +RUN cp hive/scripts/setup-ovn.sh /out/setup-ovn.sh +RUN mkdir -p /out/systemd && cp hive/build/systemd/* /out/systemd/ +RUN cp hive/build/logrotate/hive /out/logrotate-hive + +FROM scratch AS export +COPY --from=builder /out/ / diff --git a/build/logrotate/hive b/build/logrotate/hive new file mode 100644 index 000000000..be7c8784d --- /dev/null +++ b/build/logrotate/hive @@ -0,0 +1,9 @@ +/var/log/hive/*.log { + daily + rotate 14 + compress + delaycompress + missingok + notifempty + copytruncate +} diff --git a/build/systemd/hive-awsgw.service b/build/systemd/hive-awsgw.service new file mode 100644 index 000000000..cdbfe1e1f --- /dev/null +++ b/build/systemd/hive-awsgw.service @@ -0,0 +1,22 @@ +[Unit] +Description=Hive AWS Gateway +PartOf=hive.target +After=hive-nats.service hive-daemon.service +Requires=hive-nats.service + +[Service] +Type=simple +User=hive +Group=hive +ExecStartPre=/bin/sh -c 'for i in $(seq 1 30); do nc -z 127.0.0.1 4222 && exit 0; sleep 1; done; exit 1' +ExecStart=/usr/local/bin/hive service awsgw start +Restart=on-failure +RestartSec=5 +OOMScoreAdjust=-500 +Environment=HIVE_CONFIG_PATH=/etc/hive/hive.toml +Environment=HIVE_BASE_DIR=/var/lib/hive/ +Environment=HIVE_AWSGW_TLS_CERT=/etc/hive/server.pem +Environment=HIVE_AWSGW_TLS_KEY=/etc/hive/server.key + +[Install] +WantedBy=hive.target diff --git a/build/systemd/hive-daemon.service b/build/systemd/hive-daemon.service new file mode 100644 index 000000000..1aa2efcf8 --- /dev/null +++ b/build/systemd/hive-daemon.service @@ -0,0 +1,21 @@ +[Unit] +Description=Hive Daemon (VM Orchestration) +PartOf=hive.target +After=hive-nats.service hive-viperblock.service +Requires=hive-nats.service + +[Service] +Type=simple +User=hive +Group=hive +ExecStartPre=/bin/sh -c 'for i in $(seq 1 30); do nc -z 127.0.0.1 4222 && exit 0; sleep 1; done; exit 1' +ExecStart=/usr/local/bin/hive service hive start +Restart=on-failure +RestartSec=5 +OOMScoreAdjust=-500 +Environment=HIVE_CONFIG_PATH=/etc/hive/hive.toml +Environment=HIVE_BASE_DIR=/var/lib/hive/hive/ +Environment=HIVE_WAL_DIR=/var/lib/hive/hive/ + +[Install] +WantedBy=hive.target diff --git a/build/systemd/hive-nats.service b/build/systemd/hive-nats.service new file mode 100644 index 000000000..b686b1454 --- /dev/null +++ b/build/systemd/hive-nats.service @@ -0,0 +1,17 @@ +[Unit] +Description=Hive NATS Message Bus +PartOf=hive.target +Before=hive-predastore.service hive-daemon.service hive-awsgw.service hive-vpcd.service + +[Service] +Type=simple +User=hive +Group=hive +ExecStart=/usr/local/bin/hive service nats start +Restart=on-failure +RestartSec=5 +OOMScoreAdjust=-500 +Environment=HIVE_CONFIG_PATH=/etc/hive/nats/nats.conf + +[Install] +WantedBy=hive.target diff --git a/build/systemd/hive-predastore.service b/build/systemd/hive-predastore.service new file mode 100644 index 000000000..0463fb457 --- /dev/null +++ b/build/systemd/hive-predastore.service @@ -0,0 +1,22 @@ +[Unit] +Description=Hive Predastore (S3-compatible Object Storage) +PartOf=hive.target +After=hive-nats.service +Requires=hive-nats.service + +[Service] +Type=simple +User=hive +Group=hive +ExecStartPre=/bin/sh -c 'for i in $(seq 1 30); do nc -z 127.0.0.1 4222 && exit 0; sleep 1; done; exit 1' +ExecStart=/usr/local/bin/hive service predastore start +Restart=on-failure +RestartSec=5 +OOMScoreAdjust=-500 +Environment=HIVE_PREDASTORE_CONFIG_PATH=/etc/hive/predastore/predastore.toml +Environment=HIVE_PREDASTORE_BASE_PATH=/var/lib/hive/predastore/ +Environment=HIVE_PREDASTORE_TLS_CERT=/etc/hive/server.pem +Environment=HIVE_PREDASTORE_TLS_KEY=/etc/hive/server.key + +[Install] +WantedBy=hive.target diff --git a/build/systemd/hive-ui.service b/build/systemd/hive-ui.service new file mode 100644 index 000000000..09ec312a8 --- /dev/null +++ b/build/systemd/hive-ui.service @@ -0,0 +1,16 @@ +[Unit] +Description=Hive Web UI +PartOf=hive.target +After=hive-awsgw.service + +[Service] +Type=simple +User=hive +Group=hive +ExecStart=/usr/local/bin/hive service hive-ui start +Restart=on-failure +RestartSec=5 +OOMScoreAdjust=-500 + +[Install] +WantedBy=hive.target diff --git a/build/systemd/hive-viperblock.service b/build/systemd/hive-viperblock.service new file mode 100644 index 000000000..0c61ddc07 --- /dev/null +++ b/build/systemd/hive-viperblock.service @@ -0,0 +1,21 @@ +[Unit] +Description=Hive Viperblock (Block Storage) +PartOf=hive.target +After=hive-nats.service hive-predastore.service +Requires=hive-nats.service + +[Service] +Type=simple +User=hive +Group=hive +ExecStartPre=/bin/sh -c 'for i in $(seq 1 30); do nc -z 127.0.0.1 4222 && exit 0; sleep 1; done; exit 1' +ExecStart=/usr/local/bin/hive service viperblock start +Restart=on-failure +RestartSec=5 +OOMScoreAdjust=-500 +EnvironmentFile=/etc/hive/systemd.env +Environment=HIVE_CONFIG_PATH=/etc/hive/hive.toml +Environment=HIVE_BASE_DIR=/var/lib/hive/viperblock/ + +[Install] +WantedBy=hive.target diff --git a/build/systemd/hive-vpcd.service b/build/systemd/hive-vpcd.service new file mode 100644 index 000000000..1e9bb10f7 --- /dev/null +++ b/build/systemd/hive-vpcd.service @@ -0,0 +1,19 @@ +[Unit] +Description=Hive VPC Daemon (OVN Networking) +PartOf=hive.target +After=hive-nats.service hive-daemon.service +Requires=hive-nats.service + +[Service] +Type=simple +User=hive +Group=hive +ExecStartPre=/bin/sh -c 'for i in $(seq 1 30); do nc -z 127.0.0.1 4222 && exit 0; sleep 1; done; exit 1' +ExecStart=/usr/local/bin/hive service vpcd start +Restart=on-failure +RestartSec=5 +OOMScoreAdjust=-500 +Environment=HIVE_CONFIG_PATH=/etc/hive/hive.toml + +[Install] +WantedBy=hive.target diff --git a/build/systemd/hive.target b/build/systemd/hive.target new file mode 100644 index 000000000..a790aee41 --- /dev/null +++ b/build/systemd/hive.target @@ -0,0 +1,8 @@ +[Unit] +Description=Hive Infrastructure Platform +After=network-online.target +Wants=network-online.target +Wants=hive-nats.service hive-predastore.service hive-viperblock.service hive-daemon.service hive-awsgw.service hive-vpcd.service hive-ui.service + +[Install] +WantedBy=multi-user.target diff --git a/cmd/hive/cmd/admin.go b/cmd/hive/cmd/admin.go index f643fc33d..12df4d90d 100644 --- a/cmd/hive/cmd/admin.go +++ b/cmd/hive/cmd/admin.go @@ -196,12 +196,8 @@ func init() { accountCreateCmd.Flags().String("name", "", "Account name (required)") accountCreateCmd.MarkFlagRequired("name") - homeDir, _ := os.UserHomeDir() - configDir := filepath.Join(homeDir, "hive", "config") - hiveDir := filepath.Join(homeDir, "hive") - - rootCmd.PersistentFlags().String("config-dir", configDir, "Configuration directory") - rootCmd.PersistentFlags().String("hive-dir", hiveDir, "Hive base directory") + rootCmd.PersistentFlags().String("config-dir", DefaultConfigDir(), "Configuration directory") + rootCmd.PersistentFlags().String("hive-dir", DefaultDataDir(), "Hive base directory") // Flags for admin init adminInitCmd.Flags().Bool("force", false, "Force re-initialization (overwrites existing config)") @@ -259,8 +255,7 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) { // Use default config path if cfgFile == "" { - homeDir, _ := os.UserHomeDir() - cfgFile = fmt.Sprintf("%s/hive/config/hive.toml", homeDir) + cfgFile = DefaultConfigFile() } //configDir, _ := cmd.Flags().GetString("config-dir") @@ -566,12 +561,7 @@ func runAdminInit(cmd *cobra.Command, args []string) { // Default config directory if configDir == "" { - homeDir, err := os.UserHomeDir() - if err != nil { - fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err) - os.Exit(1) - } - configDir = filepath.Join(homeDir, "hive", "config") + configDir = DefaultConfigDir() } fmt.Println("šŸš€ Initializing Hive platform...") @@ -640,9 +630,7 @@ func runAdminInit(cmd *cobra.Command, args []string) { fmt.Println("\nšŸ”’ Generated NATS authentication token") if hiveRoot == "" { - // Get home directory for data path - homeDir, _ := os.UserHomeDir() - hiveRoot = filepath.Join(homeDir, "hive") + hiveRoot = DefaultDataDir() } hiveRoot = filepath.Clean(hiveRoot) @@ -768,6 +756,16 @@ func runAdminInit(cmd *cobra.Command, args []string) { admin.CreateServiceDirectories(hiveRoot) + // In production layout (running as root), fix ownership so the service user + // can read config and write data. SUDO_USER identifies the operator account. + if os.Getuid() == 0 { + sudoUser := os.Getenv("SUDO_USER") + if sudoUser != "" { + admin.ChownRecursive(configDir, sudoUser) + admin.ChownRecursive(hiveRoot, sudoUser) + } + } + // Print success message fmt.Println("\nšŸŽ‰ Hive initialization complete!") fmt.Println("\nšŸ“‹ Next steps:") @@ -1028,12 +1026,7 @@ func runAdminJoin(cmd *cobra.Command, args []string) { // Set default data directory if dataDir == "" { - homeDir, err := os.UserHomeDir() - if err != nil { - fmt.Fprintf(os.Stderr, "āŒ Error getting home directory: %v\n", err) - os.Exit(1) - } - dataDir = filepath.Join(homeDir, "hive") + dataDir = DefaultDataDir() } fmt.Println("šŸš€ Joining Hive cluster...") @@ -1134,12 +1127,7 @@ func runAdminJoin(cmd *cobra.Command, args []string) { // Set up config directory if configDir == "" { - homeDir, err := os.UserHomeDir() - if err != nil { - fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err) - os.Exit(1) - } - configDir = filepath.Join(homeDir, "hive", "config") + configDir = DefaultConfigDir() } if err := os.MkdirAll(configDir, 0700); err != nil { diff --git a/cmd/hive/cmd/get.go b/cmd/hive/cmd/get.go index 52076038e..d5bb47268 100644 --- a/cmd/hive/cmd/get.go +++ b/cmd/hive/cmd/get.go @@ -66,8 +66,7 @@ func init() { func loadConfigAndConnect() (*config.ClusterConfig, *nats.Conn, error) { cfgPath := viper.GetString("config") if cfgPath == "" { - homeDir, _ := os.UserHomeDir() - cfgPath = fmt.Sprintf("%s/hive/config/hive.toml", homeDir) + cfgPath = DefaultConfigFile() } cfg, err := config.LoadConfig(cfgPath) diff --git a/cmd/hive/cmd/paths.go b/cmd/hive/cmd/paths.go new file mode 100644 index 000000000..0d5a5f3d5 --- /dev/null +++ b/cmd/hive/cmd/paths.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "os" + "path/filepath" +) + +// DefaultConfigDir returns the default configuration directory. +// Production: /etc/hive (when running as root or /etc/hive exists) +// Development: ~/hive/config +func DefaultConfigDir() string { + if isProductionLayout() { + return "/etc/hive" + } + homeDir, _ := os.UserHomeDir() + return filepath.Join(homeDir, "hive", "config") +} + +// DefaultDataDir returns the default data directory. +// Production: /var/lib/hive +// Development: ~/hive +func DefaultDataDir() string { + if isProductionLayout() { + return "/var/lib/hive" + } + homeDir, _ := os.UserHomeDir() + return filepath.Join(homeDir, "hive") +} + +// DefaultConfigFile returns the default path to hive.toml. +func DefaultConfigFile() string { + return filepath.Join(DefaultConfigDir(), "hive.toml") +} + +// isProductionLayout returns true when running in a production install. +// Detected by: running as root, or /etc/hive directory exists. +func isProductionLayout() bool { + if os.Getuid() == 0 { + if info, err := os.Stat("/etc/hive"); err == nil && info.IsDir() { + return true + } + } + return false +} diff --git a/cmd/hive/cmd/version.go b/cmd/hive/cmd/version.go new file mode 100644 index 000000000..4fac30962 --- /dev/null +++ b/cmd/hive/cmd/version.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "fmt" + "runtime" + + "github.com/spf13/cobra" +) + +// Version variables set via ldflags at build time. +// Example: go build -ldflags "-X github.com/mulgadc/hive/cmd/hive/cmd.Version=v1.0.0" +var ( + Version = "dev" + Commit = "unknown" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the Hive version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("hive %s (%s) %s/%s\n", Version, Commit, runtime.GOOS, runtime.GOARCH) + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/hive/admin/admin.go b/hive/admin/admin.go index c49b61532..bcac69a89 100644 --- a/hive/admin/admin.go +++ b/hive/admin/admin.go @@ -8,11 +8,14 @@ import ( "encoding/base64" "encoding/pem" "fmt" + "io/fs" "log/slog" "math/big" "net" "os" + "os/user" "path/filepath" + "strconv" "strings" "text/template" "time" @@ -205,6 +208,26 @@ func FileExists(path string) bool { return err == nil } +// ChownRecursive changes ownership of a path and all its contents to the +// specified username. Used after init to hand production directories back +// to the service user when init runs as root via sudo. +func ChownRecursive(path, username string) { + u, err := user.Lookup(username) + if err != nil { + return + } + uid, _ := strconv.Atoi(u.Uid) + gid, _ := strconv.Atoi(u.Gid) + + filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + os.Chown(p, uid, gid) + return nil + }) +} + // updateAWSINIFile updates or creates an AWS INI file section with given key-value pairs func UpdateAWSINIFile(path, section string, values map[string]string) error { var cfg *ini.File diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 000000000..10831f9de --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,376 @@ +#!/bin/bash +# Hive binary installer +# Usage: curl -sfL https://install.mulgadc.com/setup.sh | bash +# +# Environment variables: +# INSTALL_HIVE_CHANNEL Release channel: latest (default), dev +# INSTALL_HIVE_VERSION Pin to specific version (overrides channel) +# INSTALL_HIVE_TARBALL Path to local tarball (skips download, for testing/air-gapped) +# INSTALL_HIVE_SKIP_APT Set to 1 to skip apt dependency install +# INSTALL_HIVE_SKIP_AWS Set to 1 to skip AWS CLI install + +set -e + +INSTALL_HIVE_CHANNEL="${INSTALL_HIVE_CHANNEL:-latest}" +INSTALL_BASE_URL="${INSTALL_BASE_URL:-https://install.mulgadc.com}" + +# --- Colors --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +fatal() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } + +# --- Root check --- +check_root() { + if [ "$(id -u)" -ne 0 ]; then + fatal "This script must be run as root. Use: curl -sfL ... | sudo bash" + fi +} + +# --- OS detection --- +detect_os() { + if [ ! -f /etc/os-release ]; then + fatal "Cannot detect OS: /etc/os-release not found" + fi + + . /etc/os-release + + case "$ID" in + debian) + if [ "${VERSION_ID%%.*}" -lt 12 ] 2>/dev/null; then + fatal "Debian $VERSION_ID is not supported. Minimum: Debian 12" + fi + ;; + ubuntu) + major="${VERSION_ID%%.*}" + if [ "$major" -lt 22 ] 2>/dev/null; then + fatal "Ubuntu $VERSION_ID is not supported. Minimum: Ubuntu 22.04" + fi + ;; + *) + fatal "Unsupported OS: $ID $VERSION_ID. Hive requires Debian 12+ or Ubuntu 22.04+" + ;; + esac + + info "Detected OS: $PRETTY_NAME" +} + +# --- Architecture detection --- +detect_arch() { + MACHINE=$(uname -m) + case "$MACHINE" in + x86_64) + ARCH="amd64" + QEMU_PACKAGES="qemu-system-x86" + AWS_ARCH="x86_64" + ;; + aarch64|arm64) + ARCH="arm64" + QEMU_PACKAGES="qemu-system-arm" + AWS_ARCH="aarch64" + ;; + *) + fatal "Unsupported architecture: $MACHINE. Hive requires x86_64 or aarch64" + ;; + esac + + info "Detected architecture: $MACHINE ($ARCH)" +} + +# --- Detect service user --- +detect_service_user() { + # Use the real user who invoked sudo (not root) + HIVE_USER="${SUDO_USER:-$(whoami)}" + HIVE_GROUP="$(id -gn "$HIVE_USER")" + + if [ "$HIVE_USER" = "root" ]; then + fatal "Cannot determine service user. Do not run as root directly — use: sudo bash setup.sh" + fi + + info "Services will run as: $HIVE_USER:$HIVE_GROUP" + + # Allow the service user to manage OVN/OVS and tap devices + cat > /etc/sudoers.d/hive-network << SUDOERS +# Hive VPC networking: allow service user to manage tap devices, OVS, and OVN +${HIVE_USER} ALL=(root) NOPASSWD: /sbin/ip, /usr/sbin/ip +${HIVE_USER} ALL=(root) NOPASSWD: /usr/bin/ovs-vsctl, /usr/bin/ovs-appctl +${HIVE_USER} ALL=(root) NOPASSWD: /usr/bin/ovn-nbctl, /usr/bin/ovn-sbctl +SUDOERS + chmod 0440 /etc/sudoers.d/hive-network + info "Sudoers rules installed for $HIVE_USER" +} + +# --- Install apt dependencies --- +install_apt_deps() { + if [ "${INSTALL_HIVE_SKIP_APT}" = "1" ]; then + info "Skipping apt dependencies (INSTALL_HIVE_SKIP_APT=1)" + return + fi + + info "Installing system dependencies..." + apt-get update -qq + + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + nbdkit \ + $QEMU_PACKAGES qemu-utils qemu-kvm \ + libvirt-daemon-system libvirt-clients \ + jq curl iproute2 netcat-openbsd wget unzip xz-utils file \ + ovn-central ovn-host openvswitch-switch \ + > /dev/null + + info "System dependencies installed" +} + +# --- Install AWS CLI --- +install_aws_cli() { + if [ "${INSTALL_HIVE_SKIP_AWS}" = "1" ]; then + info "Skipping AWS CLI (INSTALL_HIVE_SKIP_AWS=1)" + return + fi + + if command -v aws >/dev/null 2>&1; then + info "AWS CLI already installed: $(aws --version 2>&1 | head -1)" + return + fi + + info "Installing AWS CLI v2..." + AWS_TMPDIR=$(mktemp -d) + curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_ARCH}.zip" -o "$AWS_TMPDIR/awscliv2.zip" + unzip -q "$AWS_TMPDIR/awscliv2.zip" -d "$AWS_TMPDIR" + "$AWS_TMPDIR/aws/install" --update > /dev/null + rm -rf "$AWS_TMPDIR" + + info "AWS CLI installed: $(aws --version 2>&1 | head -1)" +} + +# --- Download tarball --- +download_hive() { + HIVE_TMPDIR=$(mktemp -d) + TARBALL="$HIVE_TMPDIR/hive.tar.gz" + + # Local tarball override — skip download (for testing and air-gapped installs) + if [ -n "$INSTALL_HIVE_TARBALL" ]; then + info "Using local tarball: $INSTALL_HIVE_TARBALL" + cp "$INSTALL_HIVE_TARBALL" "$TARBALL" + info "Extracting..." + tar -xzf "$TARBALL" -C "$HIVE_TMPDIR" + EXTRACT_DIR="$HIVE_TMPDIR" + return + fi + + if [ -n "$INSTALL_HIVE_VERSION" ]; then + DOWNLOAD_URL="${INSTALL_BASE_URL}/download/${INSTALL_HIVE_VERSION}/${ARCH}" + info "Downloading Hive $INSTALL_HIVE_VERSION ($ARCH)..." + else + DOWNLOAD_URL="${INSTALL_BASE_URL}/download/${INSTALL_HIVE_CHANNEL}/${ARCH}" + info "Downloading Hive ($INSTALL_HIVE_CHANNEL channel, $ARCH)..." + fi + + HTTP_CODE=$(curl -fsSL -w '%{http_code}' -o "$TARBALL" "$DOWNLOAD_URL" 2>/dev/null) || true + if [ ! -f "$TARBALL" ] || [ "$HTTP_CODE" -ge 400 ] 2>/dev/null; then + rm -rf "$HIVE_TMPDIR" + fatal "Failed to download Hive from $DOWNLOAD_URL (HTTP $HTTP_CODE)" + fi + + # Verify checksum if available + CHECKSUM_URL="${DOWNLOAD_URL}.sha256" + if curl -fsSL -o "$HIVE_TMPDIR/checksum.sha256" "$CHECKSUM_URL" 2>/dev/null; then + info "Verifying checksum..." + EXPECTED=$(awk '{print $1}' "$HIVE_TMPDIR/checksum.sha256") + ACTUAL=$(sha256sum "$TARBALL" | awk '{print $1}') + if [ "$EXPECTED" != "$ACTUAL" ]; then + rm -rf "$HIVE_TMPDIR" + fatal "Checksum verification failed. Expected: $EXPECTED, Got: $ACTUAL" + fi + info "Checksum verified" + else + warn "Checksum not available, skipping verification" + fi + + # Extract + info "Extracting..." + tar -xzf "$TARBALL" -C "$HIVE_TMPDIR" + EXTRACT_DIR="$HIVE_TMPDIR" +} + +# --- Place files --- +install_files() { + info "Installing files..." + + # Binary + install -m 0755 "$EXTRACT_DIR/hive" /usr/local/bin/hive + info " /usr/local/bin/hive" + + # nbdkit plugin + PLUGINDIR=$(nbdkit --dump-config 2>/dev/null | grep ^plugindir= | cut -d= -f2) + if [ -z "$PLUGINDIR" ]; then + warn "Could not detect nbdkit plugin directory, using default" + if [ "$ARCH" = "arm64" ]; then + PLUGINDIR="/usr/lib/aarch64-linux-gnu/nbdkit/plugins" + else + PLUGINDIR="/usr/lib/x86_64-linux-gnu/nbdkit/plugins" + fi + fi + mkdir -p "$PLUGINDIR" + install -m 0755 "$EXTRACT_DIR/nbdkit-viperblock-plugin.so" "$PLUGINDIR/nbdkit-viperblock-plugin.so" + info " $PLUGINDIR/nbdkit-viperblock-plugin.so" + + # Setup scripts + mkdir -p /usr/local/share/hive + if [ -f "$EXTRACT_DIR/setup-ovn.sh" ]; then + install -m 0755 "$EXTRACT_DIR/setup-ovn.sh" /usr/local/share/hive/setup-ovn.sh + info " /usr/local/share/hive/setup-ovn.sh" + fi + + # Cleanup temp dir after all install steps complete (defer to main) +} + +# --- Create directories --- +create_directories() { + info "Creating directories..." + + mkdir -p /etc/hive + chmod 0700 /etc/hive + chown "$HIVE_USER:$HIVE_GROUP" /etc/hive + + mkdir -p /var/lib/hive + chown "$HIVE_USER:$HIVE_GROUP" /var/lib/hive + + # Symlink so services that expect BaseDir/config/ can find /etc/hive/ + if [ ! -e /var/lib/hive/config ]; then + ln -s /etc/hive /var/lib/hive/config + fi + + # Symlink so services that write logs to BaseDir/logs/ use /var/log/hive/ + if [ ! -e /var/lib/hive/logs ]; then + ln -s /var/log/hive /var/lib/hive/logs + fi + + mkdir -p /var/log/hive + chown "$HIVE_USER:$HIVE_GROUP" /var/log/hive + + mkdir -p /run/hive + chown "$HIVE_USER:$HIVE_GROUP" /run/hive + + mkdir -p /var/lib/hive/viperblock + + # Generate environment file with install-specific values (e.g. arch-dependent paths) + cat > /etc/hive/systemd.env << EOF +# Generated by setup.sh — install-specific environment variables +HIVE_VIPERBLOCK_PLUGIN_PATH=${PLUGINDIR}/nbdkit-viperblock-plugin.so +EOF + chown "$HIVE_USER:$HIVE_GROUP" /etc/hive/systemd.env + info "Generated /etc/hive/systemd.env" +} + +# --- Install systemd units --- +install_systemd() { + info "Installing systemd units..." + + if [ ! -d "$EXTRACT_DIR/systemd" ]; then + fatal "Systemd unit files not found in tarball (expected systemd/ directory)" + fi + + for unit in "$EXTRACT_DIR"/systemd/*; do + # Substitute User= and Group= with the detected service user + sed "s/^User=hive$/User=$HIVE_USER/;s/^Group=hive$/Group=$HIVE_GROUP/" \ + "$unit" > "/etc/systemd/system/$(basename "$unit")" + chmod 0644 "/etc/systemd/system/$(basename "$unit")" + info " /etc/systemd/system/$(basename "$unit")" + done + + systemctl daemon-reload + info "Systemd units installed (running as $HIVE_USER:$HIVE_GROUP)" +} + +# --- Install logrotate --- +install_logrotate() { + if [ -f "$EXTRACT_DIR/logrotate-hive" ]; then + install -m 0644 "$EXTRACT_DIR/logrotate-hive" /etc/logrotate.d/hive + else + warn "Logrotate config not found in tarball, skipping" + return + fi + info "Logrotate config installed" +} + +# --- Upgrade handling --- +handle_upgrade() { + if systemctl is-active --quiet hive.target 2>/dev/null; then + warn "Hive services are running. Stopping for upgrade..." + systemctl stop hive.target + RESTART_AFTER=true + fi +} + +restart_if_needed() { + if [ "${RESTART_AFTER}" = "true" ]; then + info "Restarting Hive services..." + systemctl start hive.target + fi +} + +# --- Print summary --- +print_summary() { + INSTALLED_VERSION=$(/usr/local/bin/hive version 2>/dev/null || echo "unknown") + + echo "" + echo "============================================" + echo " Hive installed successfully" + echo "============================================" + echo "" + echo " Version: $INSTALLED_VERSION" + echo " Architecture: $ARCH" + echo " Binary: /usr/local/bin/hive" + echo " Config: /etc/hive/" + echo " Data: /var/lib/hive/" + echo " Logs: /var/log/hive/" + echo "" + echo " Next steps:" + echo "" + echo " 1. Setup OVN networking:" + echo " sudo /usr/local/share/hive/setup-ovn.sh --management" + echo "" + echo " 2. Initialize a region:" + echo " sudo hive admin init --region --az --node node1 --nodes 1" + echo "" + echo " 3. Trust the CA certificate:" + echo " sudo cp /etc/hive/ca.pem /usr/local/share/ca-certificates/hive-ca.crt" + echo " sudo update-ca-certificates" + echo "" + echo " 4. Start services:" + echo " sudo systemctl start hive.target" + echo "" + echo " 5. Verify:" + echo " export AWS_PROFILE=hive" + echo " aws ec2 describe-instance-types" + echo "" +} + +# --- Main --- +main() { + info "Hive installer" + echo "" + + check_root + detect_os + detect_arch + handle_upgrade + install_apt_deps + install_aws_cli + detect_service_user + download_hive + install_files + create_directories + install_systemd + install_logrotate + rm -rf "$HIVE_TMPDIR" + restart_if_needed + print_summary +} + +main "$@"