blob: b18ad2efb01f59bcfec30ea252441d422b8f52b8 [file]
#!/usr/bin/env bash
# get.cloudstack.cloud/install.sh - Easiest Apache CloudStack Installer
# Install with this command (from your Ubuntu/EL host):
#
# curl -sSfL https://get.cloudstack.cloud/install.sh | bash
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Global variables
SCRIPT_NAME="Apache CloudStack Installer"
CS_LOGFILE="$PWD/installer.log"
TRACKER_FILE="$PWD/cloudstack-installer-tracker.conf"
OS_TYPE=""
PACKAGE_MANAGER=""
SELECTED_COMPONENTS=()
ZONE_TYPE=""
MYSQL_PKG="mysql-server"
MYSQL_SERVICE=""
MYSQL_CONF_DIR=""
BRIDGE=cloudbr0
HOST_IP=
GATEWAY=
DNS="8.8.8.8"
NETMASK="255.255.255.0"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 1 = Prompt mode (interactive), 0 = Silent (non-interactive)
PROMPT=1
#-------------------------------------------------------------------------------
# Utility functions
#-------------------------------------------------------------------------------
# checks if prompt mode is enabled
is_interactive() { (( PROMPT )); }
is_silent() { (( !PROMPT )); }
# Log related utilities
#-------------------------------------------------------------------------------
# Clean up ANSI escape sequences from logs
strip_ansi() {
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
}
# Logging function
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$CS_LOGFILE"
}
# Error handling function
error_exit() {
echo -e "${RED}ERROR: $1${NC}" >&2
log "ERROR: $1"
exit 1
}
# Success message function
success_msg() {
echo -e "${GREEN}SUCCESS: $1${NC}"
log "SUCCESS: $1"
}
# Warning message function
warn_msg() {
echo -e "${YELLOW}WARNING: $1${NC}"
log "WARNING: $1"
}
# Info message function
info_msg() {
echo -e "${BLUE}INFO: $1${NC}"
log "INFO: $1"
}
# Utilities for resource checks
#-------------------------------------------------------------------------------
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root. Please use 'sudo $0'"
fi
}
# Check system resources (RAM and Disk)
check_system_resources() {
MIN_RAM_KB=$((8 * 1024 * 1024)) # 8 GB in KB
TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
# Check if RAM is within the desired range
if [ "$TOTAL_RAM_KB" -ge "$MIN_RAM_KB" ]; then
success_msg "✓ RAM check passed: $(awk "BEGIN {printf \"%.2f\", $TOTAL_RAM_KB/1024/1024}") GB"
else
error_exit "RAM check failed: System has $(awk "BEGIN {printf \"%.2f\", $TOTAL_RAM_KB/1024/1024}") GB RAM"
fi
MIN_DISK_GB=75 # Minimum disk space in GB
TOTAL_DISK_GB=$(df / | tail -1 | awk '{print $2}' | awk '{printf "%.0f", $1/1024/1024}')
# Check if disk space is within the desired range
if [ "$TOTAL_DISK_GB" -ge "$MIN_DISK_GB" ]; then
success_msg "✓ Disk space check passed: $TOTAL_DISK_GB GB available"
else
error_exit "Disk space check failed: System has only $TOTAL_DISK_GB GB available"
fi
}
# Ensure kvm modules are loaded for KVM Agent
check_kvm_support() {
info_msg "Checking KVM prerequisites..."
if ! grep -E 'vmx|svm' /proc/cpuinfo >/dev/null; then
error_exit "CPU does not support hardware virtualization (agent)"
fi
success_msg "✓ CPU virtualization support detected"
if grep -q vmx /proc/cpuinfo; then
MODULE="kvm_intel"
elif grep -q svm /proc/cpuinfo; then
MODULE="kvm_amd"
else
error_exit "Unable to determine CPU type for KVM module."
fi
modprobe "$MODULE" 2>/dev/null || true
# Validate loaded module
if ! grep -q "^${MODULE} " /proc/modules; then
error_exit "KVM module $MODULE is not loaded. Ensure virtualization is enabled in BIOS/UEFI."
fi
success_msg "✓ KVM kernel module ($MODULE) loaded"
}
validate_selinux() {
if command -v getenforce >/dev/null 2>&1; then
SELINUX_MODE=$(getenforce)
if [ "$SELINUX_MODE" = "Enforcing" ]; then
msg="SELinux is ENFORCING. Script cannot continue."
msg+="\nFix it by:"
msg+="\n 1) Edit /etc/selinux/config → SELINUX=permissive"
msg+="\n 2) Run: setenforce 0"
msg+="\n 3) Reboot"
error_exit "$msg"
else
success_msg "✓ SELinux status: $SELINUX_MODE"
fi
else
success_msg "✓ SELinux not detected. Continuing..."
fi
}
validate_jre_version() {
if ! command -v java >/dev/null 2>&1; then
success_msg "✓ Java not found, good to go.."
return 0
fi
java_version=$(java -version 2>&1 | awk -F[\"_] 'NR==1{print $2}')
major_version=$(echo "$java_version" | cut -d. -f1)
if (( major_version < 17 )); then
error_exit "Looks like Java is pre-installed and may conflict with CloudStack. Java 17 or higher is required. Detected version: $java_version"
fi
success_msg "✓ Java version $java_version detected, good to go.."
}
# Initialize OS_TYPE, PACKAGE_MANAGER, MYSQL_SERVICE, MYSQL_CONF_DIR
detect_os() {
if [[ ! -f /etc/os-release ]]; then
error_exit "Cannot detect operating system. /etc/os-release not found."
fi
source /etc/os-release
OS_TYPE=$ID
OS_VERSION=$VERSION_ID
OS_MAJOR="${OS_VERSION%%.*}"
VERSION_CODENAME=${VERSION_CODENAME:-}
case "$OS_TYPE" in
ubuntu|debian)
PACKAGE_MANAGER="apt"
MYSQL_SERVICE="mysql"
MYSQL_CONF_DIR="/etc/mysql/mysql.conf.d"
;;
rhel|centos|ol|rocky|almalinux)
PACKAGE_MANAGER="dnf"
MYSQL_SERVICE="mysqld"
MYSQL_CONF_DIR="/etc/my.cnf.d"
if [[ ("$OS_TYPE" == "rocky" || "$OS_TYPE" == "almalinux" || "$OS_TYPE" == "ol") && "$OS_MAJOR" =~ ^(10|11)$ ]]; then
MYSQL_PKG="mysql8.4-server"
fi
validate_selinux
;;
*)
echo "Unsupported OS: $OS_TYPE"
exit 1
;;
esac
log "OS Detection: $OS_TYPE with package manager: $PACKAGE_MANAGER"
}
# Tracker file utilities
#-------------------------------------------------------------------------------
# maintains tracker values for installation and configuration steps
declare -A tracker_values
# loads tracker file data into tracker_values
load_tracker() {
if [[ ! -f "$TRACKER_FILE" ]]; then
echo "# CloudStack Installer Tracker Config" > "$TRACKER_FILE"
echo "# Created on: $(date)" >> "$TRACKER_FILE"
return 0
fi
while IFS='=' read -r key value; do
[[ -z "$key" || "$key" =~ ^# ]] && continue
tracker_values["$key"]="$value"
done < "$TRACKER_FILE"
}
get_tracker_field() {
local key="$1"
echo "${tracker_values[$key]:-}"
}
# Save or update a field in the tracker file
set_tracker_field() {
local key="$1"
local value="$2"
# Update associative array
tracker_values["$key"]="$value"
# Update or append key=value in tracker file
if grep -q "^$key=" "$TRACKER_FILE"; then
sed -i "s|^$key=.*|$key=$value|" "$TRACKER_FILE"
else
echo "$key=$value" >> "$TRACKER_FILE"
fi
}
# checks if a step is already tracked in tracker file
is_step_tracked() {
local key="$1"
[[ -n "${tracker_values[$key]:-}" ]]
}
# Utility functions for Repository setup
#-------------------------------------------------------------------------------
# Map Debian codenames to Ubuntu codenames for CloudStack repo
get_ubuntu_codename_for_debian() {
case "$1" in
buster|bullseye)
echo "focal"
;;
bookworm|trixie)
echo "jammy"
;;
*)
echo "ERROR: Unsupported Debian codename '$1'" >&2
return 1
;;
esac
}
# Determine repo_path for CloudStack repository based on OS type and version
determine_rpm_distro_version() {
# Extract major version (8 or 9) from version string
local major_version=${OS_VERSION%%.*}
case "$OS_TYPE" in
centos)
echo "centos/$major_version"
;;
rhel)
echo "rhel/$major_version"
;;
rocky|almalinux|ol)
echo "el/$major_version"
;;
*)
error_exit "Unsupported OS type: $OS_TYPE"
;;
esac
}
# Validates repository entry format
validate_repo_entry() {
local os_type="$1"
local entry="$2"
# Basic check: not empty
if [[ -z "$entry" ]]; then
error_exit "CloudStack Repository entry cannot be empty."
return 1
fi
# Debian/Ubuntu repo line example:
# deb [signed-by=...] https://download.cloudstack.org/ubuntu noble 4.20
if [[ "$os_type" =~ ^(ubuntu|debian)$ ]]; then
if [[ ! "$entry" =~ https?:// ]]; then
error_exit "Invalid Repository entry must include a valid URL (http or https)."
return 1
fi
fi
# RHEL-family example:
# https://download.cloudstack.org/centos/9/4.20/
if [[ "$os_type" =~ ^(rhel|centos|rocky|almalinux|ol)$ ]]; then
if [[ ! "$entry" =~ ^https?:// ]]; then
error_exit "Invalid Repository baseurl must start with http:// or https://."
return 1
fi
fi
# Optional: check version (warn, not fatal)
if [[ ! "$entry" =~ 4\.([1-9][0-9]) ]]; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Warning" \
--msgbox "The repository entry does not appear to contain a known CloudStack version (4.xx). Please verify before proceeding." 8 70
fi
return 0
}
# Update system packages once repository is configured
update_system_packages() {
local percent=1
{
case "$PACKAGE_MANAGER" in
apt)
apt-get update 2>&1 | while IFS= read -r line; do
percent=$((percent + 1))
[ $percent -gt 50 ] && percent=50
update_progress_bar "$percent" "# Updating package lists...\n\n$line"
done
apt-get upgrade -y 2>&1 | while IFS= read -r line; do
percent=$((percent + 1))
[ $percent -gt 80 ] && percent=80
update_progress_bar "$percent" "# Installing updates...\n\n$line"
done
;;
dnf)
dnf clean all 2>&1 | while IFS= read -r line; do
percent=$((percent + 1))
[ $percent -gt 20 ] && percent=20
update_progress_bar "$percent" "# Cleaning package cache...\n\n$line"
done
dnf makecache 2>&1 | while IFS= read -r line; do
percent=$((percent + 1))
[ $percent -gt 60 ] && percent=60
update_progress_bar "$percent" "# Updating package cache...\n\n$line"
done
dnf update -y 2>&1 | while IFS= read -r line; do
percent=$((percent + 1))
[ $percent -gt 80 ] && percent=80
update_progress_bar "$percent" "# Installing system updates...\n\n$line"
done
;;
esac
update_progress_bar "100" "# System update complete!"
sleep 2
} | dialog --backtitle "$SCRIPT_NAME" \
--title "System Update" \
--gauge "Updating system packages..." 15 80 0
# Verify update success
if [[ $? -eq 0 ]]; then
show_dialog info "System Update" "System packages have been successfully updated." 2
else
dialog --backtitle "$SCRIPT_NAME" \
--title "Error" \
--msgbox "Failed to update system packages. Please check the logs." 6 50
return 1
fi
}
# Installation related utilities
#-------------------------------------------------------------------------------
is_package_installed() {
local pkgs=("$@") # Accept multiple packages
local pkg
for pkg in "${pkgs[@]}"; do
case "$PACKAGE_MANAGER" in
apt)
if ! dpkg -s "$pkg" &>/dev/null; then
return 1 # one package missing -> not installed
fi
;;
dnf)
if ! dnf list installed "$pkg" &>/dev/null; then
return 1
fi
;;
esac
done
return 0 # all packages installed
}
install_package() {
local packages=("$@") # Capture all arguments (1..N)
case "$PACKAGE_MANAGER" in
apt)
DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
;;
dnf)
dnf install -y "${packages[@]}"
;;
*)
echo "Unsupported package manager: $PACKAGE_MANAGER" >&2
return 1
;;
esac
}
# Generic function, performs package installation with progress dialog
install_pkg_with_progress_bar() {
local title="$1"
local package_name="$2"
local tracker_key="$3"
# Skip if already configured
if is_step_tracked "$tracker_key"; then
log "$title is already installed. Skipping."
show_dialog "info" "$title Installation" "$title is already installed. Skipping installation."
return 0
fi
# Skip if package already installed
if is_package_installed "$package_name"; then
log "$title package already present. Skipping installation."
set_tracker_field "$tracker_key" "yes"
show_dialog "info" "$title Installation" "$title package already present. Skipping installation."
return 0
fi
log "Installing $title..."
# Temporary log file
local TMP_LOG
TMP_LOG=$(mktemp /tmp/install_${tracker_key}.XXXXXX.log)
# Start installation in the background
install_package $package_name >> "$TMP_LOG" 2>&1 &
local INSTALL_PID=$!
local percent=0
local start_msg="Installing $title..."
{
update_progress_bar "$percent" "# $start_msg"
while kill -0 "$INSTALL_PID" 2>/dev/null; do
local tail_output
tail_output=$(tail -n 5 "$TMP_LOG" | strip_ansi | tr -d '\r')
# Add left padding, truncate width, and wrap safely
tail_output=$(echo "$tail_output" \
| sed 's/^/ /' \
| fold -s -w 80 \
| tail -n 5)
echo "$percent"
echo "XXX"
echo "# $start_msg"
echo
echo "$tail_output"
echo "XXX"
percent=$((percent + 1))
[ $percent -gt 90 ] && percent=90
sleep 1
done
} | dialog --backtitle "$SCRIPT_NAME" --title "$title Installation" --gauge "Installing $title..." 15 80 0
wait "$INSTALL_PID"
local status=$?
if [ $status -eq 0 ]; then
set_tracker_field "$tracker_key" "yes"
log "$title installed successfully."
rm -f "$TMP_LOG"
return 0
else
log "Failed to install $title. Check $TMP_LOG"
error_exit "Failed to install $title."
fi
}
# function to update progress bar in the dialog
update_progress_bar() {
local percent="$1"
local msg="$2"
echo "XXX"
echo "$percent"
echo -e "$msg"
echo "XXX"
}
# utility function to display dialog for info or message purpose
show_dialog() {
local mode="$1"
local title="$2"
local msg="$3"
local seconds="${4:-3}"
local height="${5:-7}"
local width="${6:-60}"
case "$mode" in
info)
dialog --backtitle "$SCRIPT_NAME" \
--title "$title" \
--infobox "$msg" $height $width
sleep "$seconds"
return 0
;;
msg)
dialog --backtitle "$SCRIPT_NAME" \
--title "$title" \
--msgbox "$msg" $height $width
return 0
;;
*)
echo "Unknown mode: $mode"
return 1
;;
esac
}
# Attempts to start service_name
start_service_with_progress() {
local service_name="$1"
local display_name="${2:-$service_name}"
local start_percent="${3:-60}"
local end_percent="${4:-90}"
local verify="${5:-verify}"
if ! systemctl is-active --quiet "$service_name"; then
{
update_progress_bar "$start_percent" "# Starting $display_name service..."
systemctl start "$service_name" >/dev/null 2>&1 &
local pid=$!
local i
for ((i=start_percent; i<=end_percent; i++)); do
update_progress_bar "$i" "# Waiting for $display_name to start..."
sleep 0.2
done
wait "$pid"
update_progress_bar "$end_percent" "# $display_name service started successfully."
sleep 1
} | dialog --backtitle "$SCRIPT_NAME" \
--title "Starting $display_name Service" \
--gauge "Starting $display_name service..." 15 70 0
systemctl enable "$service_name" >/dev/null 2>&1
if [[ "$verify" == "verify" ]]; then
if systemctl is-active --quiet "$service_name"; then
log "$display_name service started successfully."
else
error_exit "Failed to start $display_name service."
fi
else
log "$display_name service started (verification skipped)."
fi
else
log "$display_name service is already running."
fi
}
# CloudStack Banner gets displayed at the end of Zone Deployment
show_cloudstack_banner() {
local banner="
█████████████████████████████████████████████████████████████
█─▄▄▄─█▄─▄███─▄▄─█▄─██─▄█▄─▄▄▀█─▄▄▄▄█─▄─▄─██▀▄─██─▄▄▄─█▄─█─▄█
█─███▀██─██▀█─██─██─██─███─██─█▄▄▄▄─███─████─▀─██─███▀██─▄▀██
▀▄▄▄▄▄▀▄▄▄▄▄▀▄▄▄▄▀▀▄▄▄▄▀▀▄▄▄▄▀▀▄▄▄▄▄▀▀▄▄▄▀▀▄▄▀▄▄▀▄▄▄▄▄▀▄▄▀▄▄▀
CloudStack Installation Complete!
--------------------------------
Access Details:
URL: http://$HOST_IP:8080/client
Username: admin
Password: password
Note: Please change the default password after first login.
"
log "$banner"
dialog --backtitle "$SCRIPT_NAME" \
--title "Installation Complete" \
--colors \
--msgbox "\Z1$banner\Zn" 20 70
}
# Find free IP addresses in a given network range
find_free_ip_range() {
local network=$1
local start_from=$2
local count=$3
local tempfile=$(mktemp)
# Scan network to find used IPs
nmap -sn -n "$network" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' > "$tempfile"
# Get base network
local base_net="${network%.*}"
local free_ips=()
local current=$start_from
while [[ ${#free_ips[@]} -lt $count && $current -lt 255 ]]; do
if ! grep -q "$base_net.$current" "$tempfile"; then
free_ips+=("$base_net.$current")
fi
((current++))
done
rm -f "$tempfile"
echo "${free_ips[@]}"
}
#-------------------------------------------------------------------------------
# Script specific functions
#-------------------------------------------------------------------------------
is_cloudstack_pkg_selected() {
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " management " || \
" ${SELECTED_COMPONENTS[@]} " =~ " agent " || \
" ${SELECTED_COMPONENTS[@]} " =~ " usage " ]]; then
return 0 # true
else
return 1 # false
fi
}
# Configure CloudStack repository for Debian/Ubuntu
#-------------------------------------------------------------------------------
_configure_deb_repo() {
local gpg_key_url="$1"
local repo_entry="$2"
{
echo "Configuring CloudStack repository..."
echo "Adding CloudStack's signing key..."
mkdir -p /etc/apt/keyrings
if curl -fsSL "$gpg_key_url" | gpg --dearmor | sudo tee /etc/apt/keyrings/cloudstack.gpg > /dev/null; then
echo "CloudStack signing key added successfully."
else
error_exit "Failed to add CloudStack signing key."
fi
echo "Adding CloudStack repository..."
if echo "deb [signed-by=/etc/apt/keyrings/cloudstack.gpg] $repo_entry" | sudo tee /etc/apt/sources.list.d/cloudstack.list > /dev/null; then
echo "CloudStack repository added successfully."
else
error_exit "Failed to add CloudStack repository."
fi
} | dialog --backtitle "$SCRIPT_NAME" \
--title "Repository Configuration" \
--programbox "Configuring CloudStack repository..." 15 70
}
_configure_rpm_repo () {
local gpg_key_url="$1"
local repo_entry="$2"
mkdir -p /etc/yum.repos.d/
{
echo "Adding CloudStack repository..."
if cat > /etc/yum.repos.d/cloudstack.repo <<EOF
[cloudstack]
name=CloudStack
baseurl=$repo_entry
enabled=1
gpgcheck=0
gpgkey=$gpg_key_url
EOF
then
echo "Repository added successfully"
else
error_exit "Failed to create CloudStack repository file"
fi
} | dialog --backtitle "$SCRIPT_NAME" \
--title "Repository Configuration" \
--programbox "Configuring CloudStack repository..." 15 70
}
configure_cloudstack_repo() {
local title="CloudStack Repository Configuration"
local tracker_key="repo_url"
local repo_file=""
local repo_entry=""
case "$OS_TYPE" in
ubuntu|debian)
repo_file="/etc/apt/sources.list.d/cloudstack.list"
if [[ -f "$repo_file" ]]; then
repo_entry=$(grep -E '^deb ' "$repo_file" | sed -E 's/^.*] //')
fi
;;
rhel|centos|ol|rocky|almalinux)
repo_file="/etc/yum.repos.d/cloudstack.repo"
if [[ -f "$repo_file" ]]; then
repo_entry=$(grep -E '^baseurl=' "$repo_file" | cut -d'=' -f2-)
fi
;;
*)
dialog --msgbox "Unsupported OS: $OS_TYPE" 6 50
exit 1
;;
esac
# If repo already exists, show info and exit gracefully for silent mode
# allow reconfiguration for interactive mode
if [[ -n "$repo_entry" ]]; then
if is_silent; then
show_dialog "info" \
"$title" \
"CloudStack repository is already configured:\n\n$repo_entry"
set_tracker_field "$tracker_key" "$repo_entry"
return 0
fi
if is_interactive; then
if dialog --backtitle "$SCRIPT_NAME" \
--title "$title" \
--yesno "CloudStack repository is already configured:\n\n$repo_entry\n\nDo you want to reconfigure it?" 12 70; then
log "User opted to reconfigure existing CloudStack repository."
else
show_dialog "info" "$title" "Skipping CloudStack repository configuration."
set_tracker_field "$tracker_key" "$repo_entry"
return 0
fi
fi
fi
# default repo_entry is required if repo_entry is not set
if [[ -z "$repo_entry" ]]; then
# Set default repo_entry based on OS
local default_repo_url="https://download.cloudstack.org"
local default_cs_version="4.22"
# Build default repo_entry depending on distro
case "$OS_TYPE" in
ubuntu|debian)
local ubuntu_codename="$VERSION_CODENAME"
if [[ "$OS_TYPE" == "debian" ]]; then
ubuntu_codename=$(get_ubuntu_codename_for_debian "$VERSION_CODENAME") || exit 1
fi
default_repo_entry="${default_repo_url}/ubuntu $ubuntu_codename $default_cs_version"
;;
rhel|centos|ol|rocky|almalinux)
local repo_path
repo_path=$(determine_rpm_distro_version)
default_repo_entry="${default_repo_url}/${repo_path}/${default_cs_version}/"
;;
esac
repo_entry="$default_repo_entry"
fi
if is_interactive; then
width=60
prompt_text="Enter the CloudStack repository url:"
if [[ "$OS_TYPE" =~ ^(ubuntu|debian)$ ]]; then
prompt_text="Enter the CloudStack repository url.\n\nSupported formats:\n• Ubuntu-style (deb ... ubuntu codename version)\n• Flat layout (deb ... /)\nExample: deb [signed-by=...] http://packages.shapeblue.com/cloudstack/upstream/debian/4.22/ /"
width=90
fi
height=$(( $(echo -e "$prompt_text" | wc -l) + 8 ))
repo_entry=$(dialog --clear \
--backtitle "$SCRIPT_NAME" \
--title "Configure CloudStack Repository" \
--inputbox "$prompt_text" "$height" "$width" "$repo_entry" \
3>&1 1>&2 2>&3)
validate_repo_entry "$OS_TYPE" "$repo_entry" || {
error_exit "Invalid repository entry provided by user."
}
fi
local repo_base_url=$(echo "$repo_entry" | sed -E 's|.*(https?://[^/ ]+).*|\1|')
local gpg_url="${repo_base_url}/release.asc"
if is_interactive; then
if ! dialog --backtitle "$SCRIPT_NAME" \
--title "Confirm Repository" \
--yesno "The following CloudStack repository will be added:\n\n$repo_entry\n\nProceed?" 12 70; then
error_exit "CloudStack repository configuration cancelled by user."
fi
else
show_dialog "info" "$title" "The following CloudStack repository will be added:\n\n$repo_entry" 4
fi
log "Configuring CS repo: $repo_entry"
case "$OS_TYPE" in
ubuntu|debian)
_configure_deb_repo "$gpg_url" "$repo_entry"
;;
rhel|centos|ol|rocky|almalinux)
_configure_rpm_repo "$gpg_url" "$repo_entry"
;;
*)
dialog --msgbox "Unsupported OS: $OS_TYPE" 6 50
error_exit "Unsupported OS: $OS_TYPE"
;;
esac
log "Configured CS repo: $repo_entry"
set_tracker_field "$tracker_key" "$repo_entry"
}
# Install base dependencies required for CloudStack
#-------------------------------------------------------------------------------
install_dialog_utility() {
log "Updating package list..."
case "$PACKAGE_MANAGER" in
apt)
apt-get update || error_exit "Failed to update package lists"
;;
dnf)
dnf makecache || error_exit "Failed to update package cache"
;;
esac
log "Installing 'dialog'..."
case "$PACKAGE_MANAGER" in
apt)
apt-get install -y dialog || error_exit "Failed to install dialog"
;;
dnf)
dnf install -y dialog || error_exit "Failed to install dialog"
;;
esac
}
update_system_time() {
case $PACKAGE_MANAGER in
dnf)
if ! systemctl is-active chronyd >/dev/null 2>&1; then
systemctl enable --now chronyd >/dev/null 2>&1
fi
chronyc makestep >/dev/null 2>&1
;;
apt)
# Detect what NTP mechanism is available
if systemctl is-active ntp >/dev/null 2>&1; then
info_msg ">>> ntp detected, forcing sync via ntpd..."
ntpd -gq >/dev/null 2>&1 || true
elif systemctl is-active chrony >/dev/null 2>&1; then
info_msg ">>> chrony detected, forcing sync via chronyc..."
chronyc makestep >/dev/null 2>&1 || true
else
info_msg ">>> Falling back to systemd-timesyncd..."
systemctl enable --now systemd-timesyncd >/dev/null 2>&1 || true
timedatectl set-ntp true >/dev/null 2>&1 || true
systemctl restart systemd-timesyncd >/dev/null 2>&1 || true
fi
;;
esac
}
install_base_dependencies() {
log "Starting base dependencies installation..."
if ! command -v dialog &>/dev/null; then
install_dialog_utility
fi
TMP_LOG=$(mktemp /tmp/install_base.XXXXXX.log)
title="Installing base dependencies (qemu-kvm, python, curl, etc.)..."
{
update_progress_bar "30" "$title"
case "$PACKAGE_MANAGER" in
apt)
DEBIAN_FRONTEND=noninteractive \
apt-get install -y qemu-kvm apt-utils gnupg curl openntpd openssh-server sshpass sudo wget jq htop tar nmap bridge-utils util-linux >> "$TMP_LOG" 2>&1 &
;;
dnf)
dnf install -y curl openssh-server chrony sshpass sudo wget jq tar nmap util-linux >> "$TMP_LOG" 2>&1 &
;;
esac
INSTALL_PID=$!
PERCENT=31
while kill -0 "$INSTALL_PID" 2>/dev/null; do
tail_output=$(tail -n 5 "$TMP_LOG" | strip_ansi | tr -d '\r')
# Add left padding, truncate width, and wrap safely
tail_output=$(echo "$tail_output" \
| sed 's/^/ /' \
| fold -s -w 80 \
| tail -n 5)
update_progress_bar "$PERCENT" "$title\n\n$tail_output"
PERCENT=$((PERCENT + 1))
[ "$PERCENT" -ge 90 ] && PERCENT=90
sleep 1
done
wait "$INSTALL_PID" || error_exit "Base dependency installation failed"
update_progress_bar "100" "Base dependencies installed successfully"
} | dialog --backtitle "$SCRIPT_NAME" \
--title "Installing Dependencies" \
--gauge "Preparing system..." 15 80 0
show_dialog "info" "Dependencies Installation" "All Base dependencies installed successfully"
log "Base dependencies installed successfully"
rm -f "$TMP_LOG"
}
# Function to show available component versions from repository for SELECTED_COMPONENTS
show_components_versions() {
local versions=()
local component version_info
if [[ " ${SELECTED_COMPONENTS[*]} " =~ " management " ]] && [[ ! " ${SELECTED_COMPONENTS[*]} " =~ " mysql " ]]; then
SELECTED_COMPONENTS+=("mysql")
fi
for component in "${SELECTED_COMPONENTS[@]}"; do
case "$component" in
nfs)
if [[ "$PACKAGE_MANAGER" == "apt" ]]; then
version_info=$(apt-cache policy nfs-kernel-server 2>/dev/null | awk '/Candidate:/ {print $2}')
elif [[ "$PACKAGE_MANAGER" == "dnf" ]]; then
version_info=$($PACKAGE_MANAGER info nfs-utils 2>/dev/null | awk -F':' '/Version/ {gsub(/ /,"",$2); print $2}')
fi
versions+=("NFS Server: ${version_info:-Not Available}\n")
;;
mysql)
if [[ "$PACKAGE_MANAGER" == "apt" ]]; then
version_info=$(apt-cache policy "$MYSQL_PKG" 2>/dev/null | awk '/Candidate:/ {print $2}')
elif [[ "$PACKAGE_MANAGER" == "dnf" || "$PACKAGE_MANAGER" == "yum" ]]; then
version_info=$($PACKAGE_MANAGER info "$MYSQL_PKG" 2>/dev/null | awk -F':' '/Version/ {gsub(/ /,"",$2); print $2}')
fi
versions+=("MySQL Server: ${version_info:-Not Available}\n")
;;
management)
if [[ "$PACKAGE_MANAGER" == "apt" ]]; then
version_info=$(apt-cache policy cloudstack-management 2>/dev/null | awk '/Candidate:/ {print $2}')
elif [[ "$PACKAGE_MANAGER" == "dnf" ]]; then
version_info=$($PACKAGE_MANAGER info cloudstack-management 2>/dev/null | awk -F':' '/Version/ {gsub(/ /,"",$2); print $2}')
fi
versions+=("CloudStack Management Server: ${version_info:-Not Available}\n")
;;
agent)
if [[ "$PACKAGE_MANAGER" == "apt" ]]; then
version_info=$(apt-cache policy cloudstack-agent 2>/dev/null | awk '/Candidate:/ {print $2}')
elif [[ "$PACKAGE_MANAGER" == "dnf" ]]; then
version_info=$($PACKAGE_MANAGER info cloudstack-agent 2>/dev/null | awk -F':' '/Version/ {gsub(/ /,"",$2); print $2}')
fi
versions+=("CloudStack KVM Agent: ${version_info:-Not Available}\n")
;;
usage)
if [[ "$PACKAGE_MANAGER" == "apt" ]]; then
version_info=$(apt-cache policy cloudstack-usage 2>/dev/null | awk '/Candidate:/ {print $2}')
elif [[ "$PACKAGE_MANAGER" == "dnf" ]]; then
version_info=$($PACKAGE_MANAGER info cloudstack-usage 2>/dev/null | awk -F':' '/Version/ {gsub(/ /,"",$2); print $2}')
fi
versions+=("CloudStack Usage Server: ${version_info:-Not Available}\n")
;;
esac
done
show_dialog info "Component Versions" "Available versions from repository:\n\n$(printf '%s\n' "${versions[@]}")" 6 10 55
}
cmk_api_call() {
local jq_filter="$1"
shift 1
local cmd=(cmk "$@") # remaining args are the CMK call
# Run the CMK API call (capture stdout+stderr)
local cmd_output
cmd_output="$("${cmd[@]}" 2>&1)"
if ! echo "$cmd_output" | jq empty >/dev/null 2>&1; then
log "cmk API returned non-JSON response: $cmd_output"
return 1
fi
# Extract value
local value
value=$(echo "$cmd_output" | jq -r "$jq_filter // empty")
# if value is empty
if [[ -z "$value" ]]; then
log "cmk returned empty value for jq filter: '$jq_filter' on output: $cmd_output"
return 1
fi
echo "$value"
return 0
}
cmk_api_fail_dialog_msg() {
local msg="$1"
local progress="${2:-100}"
local sleep_time="${3:-4}"
update_progress_bar "$progress" "$msg"
sleep "$sleep_time"
return 1
}
# Component installation and configuration functions
#-------------------------------------------------------------------------------
# Function to install CloudStack Management Server
install_management_server() {
local package_name="cloudstack-management"
local tracker_key="management_installed"
if is_step_tracked "$tracker_key"; then
show_dialog "info" "Management Server Installation" "Management Server is already installed.\n\nSkipping installation."
log "CloudStack Management Server is already installed. Skipping installation."
return 0
fi
if is_package_installed "$package_name"; then
log "CloudStack Management Server is already installed."
set_tracker_field "$tracker_key" "yes"
return 0
fi
install_pkg_with_progress_bar "CloudStack Management Server" "$package_name" "$tracker_key"
}
# Function to configure CloudStack Management Server database
configure_management_server_database() {
local title="CloudStack Database Deployment"
local tracker_key="db_deployed"
if is_step_tracked "$tracker_key"; then
log "CloudStack database is already deployed. Skipping deployment."
show_dialog "info" "$title" "$title is already done.\n\nSkipping it."
return 0
fi
log "Starting CloudStack database deployment..."
if ! systemctl is-active $MYSQL_SERVICE > /dev/null; then
show_dialog "msg" "$title" "MySQL service is not running. Please start MySQL before proceeding."
return 1
fi
local db_user="cloud"
local db_pass="cloud"
if [[ -f "/etc/cloudstack/management/db.properties" ]] && is_step_tracked "$tracker_key"; then
if mysql -u"$db_user" -p"$db_pass" -e "USE cloud; SHOW TABLES LIKE 'version';" &>/dev/null; then
local current_db_host=$(grep "^cluster.node.IP" /etc/cloudstack/management/db.properties | cut -d= -f2)
if ! dialog --title "Info" --yesno "CloudStack database appears to be already configured.\nCurrent database host: $current_db_host\n\nDo you want to reconfigure it?" 10 60; then
show_dialog "info" "$title" "Skipping database configuration."
set_tracker_field "$tracker_key" "yes"
return 0
fi
fi
fi
if [ -z "$BRIDGE" ]; then
BRIDGE=$(dialog --inputbox "Enter the bridge interface name:" 8 50 "$BRIDGE" 3>&1 1>&2 2>&3)
fi
if [[ -z "$BRIDGE" ]]; then
show_dialog "msg" "$title" "Bridge interface cannot be empty.\nAborting."
return 1
fi
# Get the bridge IP
cloudbr0_ip=$(ip -4 addr show "$BRIDGE" | awk '/inet / {print $2}' | cut -d/ -f1)
if [[ -z "$cloudbr0_ip" ]]; then
show_dialog "msg" "$title" "Could not determine IP address of interface '$BRIDGE'.\nAborting."
return 1
fi
show_dialog "info" "$title" "Starting CloudStack database deployment using IP: $cloudbr0_ip"
{
cloudstack-setup-databases cloud:cloud@localhost --deploy-as=root: -i "$cloudbr0_ip" 2>&1 | \
while IFS= read -r line; do
msg=$(echo "$line" | strip_ansi)
update_progress_bar "50" "Deploying CloudStack Database...\n\n$msg"
done
} | dialog --backtitle "$SCRIPT_NAME" \
--title "$title" \
--gauge "Starting database deployment..." 10 70 0
db_exit=${PIPESTATUS[0]}
if [ $db_exit -ne 0 ]; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Database Setup Failed" \
--msgbox "CloudStack database setup failed." 10 70
error_exit "CloudStack database setup failed."
fi
{
echo "# Starting CloudStack Management Server setup..."
cloudstack-setup-management 2>&1 | \
while IFS= read -r line; do
msg=$(echo "$line" | strip_ansi)
update_progress_bar "75" "Starting Management Server...\n\n$msg"
done
} | dialog --backtitle "$SCRIPT_NAME" \
--title "Management Server Setup" \
--gauge "Starting management server setup..." 10 72 0
mgmt_exit=${PIPESTATUS[0]}
if [ $mgmt_exit -ne 0 ]; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Management Setup Failed" \
--msgbox "CloudStack management setup failed" 10 70
error_exit "CloudStack management setup failed."
fi
sleep 5
set_tracker_field "$tracker_key" "yes"
show_dialog "info" "CloudStack Configuration" "CloudStack Management Server has been configured."
}
# Function to install MySQL Server and configure it for CloudStack
install_mysql_server() {
local tracker_key="mysql_installed"
if is_step_tracked "$tracker_key"; then
log "MySQL is already installed. Skipping installation."
show_dialog "info" "MySQL Server Installation" "MySQL Server is already installed.\n\nSkipping installation."
return 0
fi
if is_package_installed "$MYSQL_PKG"; then
log "MySQL Server is already installed."
set_tracker_field "$tracker_key" "yes"
else
install_pkg_with_progress_bar "MySQL Server" "$MYSQL_PKG" "$tracker_key"
fi
start_service_with_progress "$MYSQL_SERVICE" "MySQL" 60 90
}
configure_mysql_for_cloudstack() {
local tracker_key="mysql_configured"
if is_step_tracked "$tracker_key"; then
log "MySQL is already configured for CloudStack. Skipping configuration."
show_dialog "info" "MySQL Server Configuration" "MySQL Server is already configured.\n\nSkipping configuration."
return 0
fi
log "Starting MySQL configuration..."
local title="MySQL Configuration"
MYSQL_VERSION=$(mysql -V 2>/dev/null || echo "MySQL not found")
if [[ "$MYSQL_VERSION" == "MySQL not found" ]]; then
show_dialog "msg" "$title" "MySQL is not installed. Please install MySQL first."
return 1
fi
local config_file="$MYSQL_CONF_DIR/cloudstack.cnf"
if [[ -f "$config_file" ]]; then
show_dialog "info" "$title" "Configuration already exists at:\n$config_file\n\nSkipping MySQL setup."
set_tracker_field "$tracker_key" "yes"
return 0
fi
mkdir -p "$MYSQL_CONF_DIR"
if ! systemctl is-active --quiet $MYSQL_SERVICE; then
dialog --title "$title" --msgbox "MySQL service is not running. Please start MySQL before proceeding." 6 50
return 1
fi
sqlmode="$(mysql -B -e "show global variables like 'sql_mode'" 2>/dev/null | grep sql_mode | awk '{ print $2; }' | sed -e 's/ONLY_FULL_GROUP_BY,//')"
if [[ -z "$sqlmode" ]]; then
dialog --msgbox "Failed to fetch current SQL mode. Aborting." 6 50
return 1
fi
cat > "$config_file" <<EOF
[mysqld]
server_id = 1
sql_mode = $sqlmode
innodb_rollback_on_timeout = 1
innodb_lock_wait_timeout = 600
max_connections = 1000
log_bin = mysql-bin
binlog_format = ROW
EOF
systemctl restart $MYSQL_SERVICE && \
show_dialog "info" "$title" "MySQL has been configured and restarted successfully."|| \
show_dialog "info" "$title" "Failed to restart MySQL. Please check the service manually."
set_tracker_field "$tracker_key" "yes"
}
# Function to install NFS Server
install_nfs_server() {
local tracker_key="nfs_installed"
if is_step_tracked "$tracker_key"; then
log "NFS Server is already installed. Skipping installation."
show_dialog "info" "NFS Server Installation" "NFS Server is already installed.\n\nSkipping installation."
return 0
fi
local package_name=""
local nfs_svc="nfs-server"
case "$PACKAGE_MANAGER" in
apt)
package_name="nfs-kernel-server nfs-common quota"
nfs_svc="nfs-kernel-server"
;;
dnf)
package_name="nfs-utils quota"
;;
esac
if is_package_installed "$package_name"; then
log "NFS Server is already installed."
show_dialog "info" "NFS Server Installation" "NFS Server is already installed.\n\nSkipping installation."
set_tracker_field "$tracker_key" "yes"
else
install_pkg_with_progress_bar "NFS Server" "$package_name" "$tracker_key"
fi
start_service_with_progress "$nfs_svc" "NFS" 60 90
}
get_export_cidr() {
if [[ -z "$BRIDGE" ]]; then
error_exit "Error: BRIDGE interface not defined"
fi
ip_cidr=$(ip -o -f inet addr show "$BRIDGE" | awk '{print $4}')
if [[ -z "$ip_cidr" ]]; then
error_exit "No IPv4 CIDR found on interface $BRIDGE for NFS export configuration"
fi
# Extract IP and prefix from CIDR (10.1.1.53/24 → base_ip=10.1.1.53, prefix=24)
base_ip="${ip_cidr%/*}"
prefix="${ip_cidr#*/}"
# Use ipcalc or manual mask conversion
if command -v ipcalc &>/dev/null; then
network=$(ipcalc "$base_ip/$prefix" | grep -w 'Network' | awk '{print $2}')
else
# Fallback: simple /24 assumption
network="${base_ip%.*}.0/$prefix"
fi
echo "$network"
}
# Configure NFS Server
configure_nfs_server() {
local tracker_key="nfs_configured"
local title="NFS Server Configuration"
if is_step_tracked "$tracker_key"; then
log "NFS Server is already configured. Skipping configuration."
show_dialog "info" "$title" "NFS Server is already configured.\n\nSkipping configuration."
return 0
fi
log "Starting NFS storage configuration..."
if [[ -d "/export" ]] && grep -q "^/export " /etc/exports; then
show_dialog "info" "$title" "NFS Server is already configured.\n\nSkipping configuration."
set_tracker_field "$tracker_key" "yes"
return 0
fi
local export_cidr
export_cidr=$(get_export_cidr)
# Step 1: Create exports and directories
mkdir -p /export/primary /export/secondary
if ! grep -q "^/export " /etc/exports; then
echo "/export ${export_cidr}(rw,async,no_root_squash,no_subtree_check)" >> /etc/exports
fi
exportfs -a
# Step 2: Configure ports and services based on distro
if [[ "$OS_TYPE" =~ ^(ubuntu|debian)$ ]]; then
sed -i -e 's/^RPCMOUNTDOPTS="--manage-gids"$/RPCMOUNTDOPTS="-p 892 --manage-gids"/g' /etc/default/nfs-kernel-server
sed -i -e 's/^STATDOPTS=$/STATDOPTS="--port 662 --outgoing-port 2020"/g' /etc/default/nfs-common
grep -q 'NEED_STATD=yes' /etc/default/nfs-common || echo "NEED_STATD=yes" >> /etc/default/nfs-common
sed -i -e 's/^RPCRQUOTADOPTS=$/RPCRQUOTADOPTS="-p 875"/g' /etc/default/quota
systemctl restart nfs-kernel-server
SERVICE_STATUS=$?
elif [[ "$OS_TYPE" =~ ^(rhel|centos|ol|rocky|almalinux)$ ]]; then
# Set ports in /etc/sysconfig/nfs if needed
cat <<EOF >> /etc/sysconfig/nfs
MOUNTD_PORT=892
STATD_PORT=662
STATD_OUTGOING_PORT=2020
LOCKD_TCPPORT=32803
LOCKD_UDPPORT=32769
RQUOTAD_PORT=875
EOF
systemctl enable --now rpcbind nfs-server
systemctl restart nfs-server
SERVICE_STATUS=$?
# Open firewall ports if firewalld is running
if systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-service=nfs
firewall-cmd --permanent --add-service=mountd
firewall-cmd --permanent --add-service=rpc-bind
firewall-cmd --reload
fi
else
show_dialog "info" "$title" "Unsupported distribution: $OS_TYPE"
return 1
fi
# Step 3: Final result
if [[ $SERVICE_STATUS -eq 0 ]]; then
exports_list=$(exportfs)
show_dialog "info" "$title" "NFS Server configured and restarted successfully.\n\nCurrent exports:\n$exports_list"
set_tracker_field "$tracker_key" "yes"
log "NFS server configured successfully."
else
show_dialog "info" "$title" "Failed to restart NFS server. Please check the service logs."
fi
}
# Function to install KVM Agent and configure KVM host
install_kvm_agent() {
local package_name="cloudstack-agent"
local tracker_key="agent_installed"
if is_step_tracked "$tracker_key"; then
log "KVM Agent is already installed. Skipping installation."
show_dialog "info" "Cloudstack Agent Installation" "CloudStack Agent is already installed.\n\nSkipping installation."
return 0
fi
if is_package_installed "$package_name"; then
log "KVM Agent is already installed."
set_tracker_field "$tracker_key" "yes"
return 0
fi
install_pkg_with_progress_bar "CloudStack Agent" "$package_name" "$tracker_key"
}
configure_kvm_agent() {
local tracker_key="agent_configured"
local title="CloudStack Agent Configuration"
if is_step_tracked "$tracker_key"; then
log "KVM Agent is already configured. Skipping configuration."
show_dialog "info" "$title" "CloudStack Agent is already configured.\n\nSkipping configuration."
return 0
fi
log "Starting CloudStack Agent configuration..."
show_dialog "info" "$title" "Starting CloudStack Agent configuration..."
# Configure VNC
{
update_progress_bar "10" "Configuring VNC access..."
if sed -i -e 's/\#vnc_listen.*$/vnc_listen = "0.0.0.0"/g' /etc/libvirt/qemu.conf; then
update_progress_bar "25" "VNC configuration successful"
else
error_exit "Failed to configure VNC"
fi
if ! grep -q '^LIBVIRTD_ARGS="--listen"' /etc/default/libvirtd > /dev/null; then
echo 'LIBVIRTD_ARGS="--listen"' >> /etc/default/libvirtd
fi
if ! grep -q '^remote_mode="legacy"' /etc/libvirt/libvirtd.conf; then
echo 'remote_mode="legacy"' >> /etc/libvirt/libvirtd.conf
fi
update_progress_bar "40" "Setting up libvirt TCP access..."
LIBVIRT_CONF="/etc/libvirt/libvirtd.conf"
declare -A libvirt_settings=(
["listen_tcp"]="1"
["listen_tls"]="0"
["tcp_port"]="\"16509\""
["mdns_adv"]="0"
["auth_tcp"]="\"none\""
)
touch "$LIBVIRT_CONF"
for key in "${!libvirt_settings[@]}"; do
# If key exists (commented or uncommented), replace it
if grep -Eq "^\s*#?\s*$key\s*=" "$LIBVIRT_CONF"; then
sed -i "s|^\s*#\?\s*$key\s*=.*|$key = ${libvirt_settings[$key]}|" "$LIBVIRT_CONF"
else
# Key doesn't exist, append to file
echo "$key = ${libvirt_settings[$key]}" >> "$LIBVIRT_CONF"
fi
done
update_progress_bar "60" "Configuring libvirt sockets..."
systemctl mask libvirtd.socket \
libvirtd-ro.socket \
libvirtd-admin.socket \
libvirtd-tls.socket \
libvirtd-tcp.socket &>/dev/null
update_progress_bar "75" "Configuring security policies..."
case "$OS_TYPE" in
ubuntu|debian)
echo "# Configuring AppArmor..."
if command -v apparmor_parser >/dev/null; then
# Check if profiles exist before trying to disable them
local profiles=(
"/etc/apparmor.d/usr.sbin.libvirtd"
"/etc/apparmor.d/usr.lib.libvirt.virt-aa-helper"
)
for profile in "${profiles[@]}"; do
if [[ -f "$profile" ]]; then
if [[ ! -L "/etc/apparmor.d/disable/$(basename "$profile")" ]]; then
ln -sf "$profile" "/etc/apparmor.d/disable/" 2>/dev/null
if [[ -f "$profile" ]]; then
if ! apparmor_parser -R "$profile" 2>/dev/null; then
update_progress_bar "80" "# Warning: Failed to remove profile: $(basename "$profile")"
fi
fi
else
echo "Profile $(basename "$profile") already disabled"
fi
else
echo "Profile $profile not found, skipping"
fi
done
# Restart AppArmor service to apply changes
if systemctl is-active --quiet apparmor; then
systemctl restart apparmor &>/dev/null || true
fi
else
echo "AppArmor not installed, skipping configuration"
fi
;;
rhel|centos|ol|rocky|almalinux)
# SELinux configuration if needed
setsebool -P virt_use_nfs 1
;;
esac
update_progress_bar "85" "Configuring firewall..."
ports=(
"22" # SSH
"1798" # CloudStack Management Server
"16509" # Libvirt
"16514" # Libvirt
"5900:6100" # VNC
"49152:49216" # Live Migration
)
case "$OS_TYPE" in
ubuntu|debian)
if command -v ufw >/dev/null; then
for port in "${ports[@]}"; do
ufw allow proto tcp from any to any port "$port"
done
ufw reload
else
warn_msg "UFW not found, skipping firewall configuration"
fi
;;
rhel|centos|ol|rocky|almalinux)
if systemctl is-active --quiet firewalld; then
for port in "${ports[@]}"; do
# Convert port ranges from 5900:6100 -> 5900-6100 for firewalld
range_port="${port/:/-}"
firewall-cmd --permanent --add-port="$range_port"/tcp
done
firewall-cmd --reload
fi
;;
esac
systemctl restart libvirtd
sleep 2
update_progress_bar "90" "Update agent.properties!"
AGENT_PROPERTIES="/etc/cloudstack/agent/agent.properties"
if [ -f "$AGENT_PROPERTIES" ]; then
local agent_guid=$(uuidgen)
sed -i '/^guid=/d' "$AGENT_PROPERTIES"
sed -i '/^private\.network\.device=/d' "$AGENT_PROPERTIES"
{
echo "guid=$agent_guid"
echo "private.network.device=$BRIDGE"
} >> "$AGENT_PROPERTIES"
else
error_exit "Agent properties file not found at $AGENT_PROPERTIES"
fi
systemctl restart cloudstack-agent
sleep 5
update_progress_bar "100" "KVM host configuration completed!"
} | dialog --backtitle "$SCRIPT_NAME" \
--title "KVM Agent Configuration" \
--gauge "Configuring KVM Agent..." 10 70 0
if ! systemctl is-active --quiet libvirtd; then
show_dialog "msg" "$title" "Libvirt service is not running! Please check system logs."
fi
set_tracker_field "$tracker_key" "yes"
}
# Function to install CloudStack Usage Server and configure it
install_usage_server() {
local package_name="cloudstack-usage"
local tracker_key="usage_installed"
if is_step_tracked "$tracker_key"; then
log "CloudStack Usage Server is already installed. Skipping installation."
show_dialog "info" "Usage Server Installation" "CloudStack Usage Server is already installed.\n\nSkipping installation."
return 0
fi
if is_package_installed "$package_name"; then
log "CloudStack Usage Server is already installed."
set_tracker_field "$tracker_key" "yes"
return 0
fi
# check if mysql and management server are installed
if ! is_package_installed "cloudstack-management" "$MYSQL_PKG"; then
error_exit "CloudStack Management Server and MySQL Server must be installed before installing the Usage Server."
fi
install_pkg_with_progress_bar "CloudStack Usage Server" "$package_name" "$tracker_key"
start_service_with_progress "$package_name" "CloudStack Usage" 40 90 "noverify"
}
configure_usage_server() {
log "Configuring CloudStack Usage Server..."
local tracker_key="usage_configured"
if is_step_tracked "$tracker_key"; then
log "Usage Server is already configured. Skipping configuration."
show_dialog "info" "Usage Server Configuration" "CloudStack Usage Server is already configured.\n\nSkipping configuration."
return 0
fi
log "Starting Usage Server configuration..."
show_dialog "info" "Usage Server Configuration" "Starting Usage Server configuration..."
# confirm db.properties and key exist
local db_properties="/etc/cloudstack/usage/db.properties"
local key_file="/etc/cloudstack/usage/key"
if [[ ! -f "$db_properties" || ! -f "$key_file" ]]; then
show_dialog "msg" "Usage Server Configuration" "Database configuration files not found!\nPlease ensure CloudStack Management Server is configured."
return 1
fi
set_tracker_field "$tracker_key" "yes"
sleep 2
}
# Function to present component selection dialog
select_components_to_setup() {
local title="CloudStack Component Selection"
local temp_file=$(mktemp)
if dialog --clear --backtitle "$SCRIPT_NAME" \
--title "$title" \
--checklist "Select CloudStack components to install:" 15 70 6 \
"management" "Management Server" on \
"usage" "Usage Server" off \
"agent" "KVM Agent" on \
"nfs" "NFS Server" on \
2> "$temp_file"; then
mapfile -t SELECTED_COMPONENTS < <(tr ' ' '\n' < "$temp_file" | tr -d '"')
if [[ ${#SELECTED_COMPONENTS[@]} -eq 0 ]]; then
error_exit "No components selected"
fi
log "Selected components: ${SELECTED_COMPONENTS[*]}"
else
log "Component selection cancelled by user"
show_dialog "msg" "$title" "Component selection cancelled by user."
return 1
fi
rm -f "$temp_file"
}
# Displays validation summary for SELECTED_COMPONENTS
show_validation_summary() {
local summary=""
local status_ok=true
# 1. Network Validation
if is_cloudstack_pkg_selected; then
if ip link show "$BRIDGE" &>/dev/null; then
local bridge_ip=$(ip -4 addr show "$BRIDGE" | awk '/inet / {print $2}' | cut -d/ -f1)
if [[ -n "$bridge_ip" ]]; then
summary+="✓ Network: Bridge $BRIDGE configured with IP $bridge_ip\n"
else
summary+="✗ Network: Bridge $BRIDGE has no IP address\n"
status_ok=false
fi
else
summary+="✗ Network: Bridge $BRIDGE not found\n"
status_ok=false
fi
fi
# 2. MySQL Validation
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " mysql " ]]; then
if systemctl is-active --quiet "$MYSQL_SERVICE"; then
if [[ -f "$MYSQL_CONF_DIR/cloudstack.cnf" ]]; then
summary+="✓ MySQL: Running and configured\n"
else
summary+="✗ MySQL: Running but missing CloudStack configuration\n"
status_ok=false
fi
else
summary+="✗ MySQL: Not running\n"
status_ok=false
fi
fi
# 3. NFS Validation
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " nfs " ]]; then
if systemctl is-active --quiet nfs-server; then
if grep -q "^/export" /etc/exports; then
summary+="✓ NFS: Server running and exports configured\n"
else
summary+="✗ NFS: Server running but exports not configured\n"
status_ok=false
fi
else
summary+="✗ NFS: Server not running\n"
status_ok=false
fi
fi
# 4. Management Server Validation
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " management " ]]; then
if systemctl is-active --quiet cloudstack-management; then
if [[ -f "/etc/cloudstack/management/db.properties" ]]; then
summary+="✓ Management Server: Running and configured\n"
else
summary+="✗ Management Server: Running but configuration incomplete\n"
status_ok=false
fi
else
summary+="✗ Management Server: Not running\n"
status_ok=false
fi
fi
# 5. KVM Agent Validation
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " agent " ]]; then
if systemctl is-active --quiet libvirtd; then
if grep -qE '^tcp_port\s*=\s*"16509"' /etc/libvirt/libvirtd.conf; then
summary+="✓ KVM: Libvirt running and configured\n"
if systemctl is-active --quiet cloudstack-agent; then
summary+="✓ KVM: CloudStack agent running\n"
else
summary+="✗ KVM: CloudStack agent not running\n"
status_ok=false
fi
else
summary+="✗ KVM: Libvirt TCP not configured\n"
status_ok=false
fi
else
summary+="✗ KVM: Libvirt not running\n"
status_ok=false
fi
fi
# 6. Usage Server Validation
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " usage " ]]; then
if systemctl is-active --quiet cloudstack-usage; then
summary+="✓ Usage Server: Running\n"
else
summary+="✗ Usage Server: Not running\n"
status_ok=true # not critical for zone deployment
fi
fi
# Log the result
echo -e "Validation Summary:\n$summary" >> "$CS_LOGFILE"
# Display final result in dialog
if $status_ok; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Validation Summary" \
--colors \
--msgbox "\Z2✓ All components are properly configured!\n\n\ZnComponent Status:\n\n$summary" 20 70
return 0
else
dialog --backtitle "$SCRIPT_NAME" \
--title "Validation Summary" \
--colors \
--msgbox "\Z1⚠ Some components need attention!\n\n\ZnComponent Status:\n\n$summary" 20 70
return 1
fi
}
# Functions related to Zone deployment
#-------------------------------------------------------------------------------
# Prompt user to select zone deployment
select_zone_deployment() {
dialog --backtitle "$SCRIPT_NAME" \
--title "Zone Deployment" \
--yesno "Would you like to deploy a new CloudStack Zone?\n\nThis will:\n\n1. Create a new Zone\n2. Configure Network\n3. Add the first Pod\n4. Add the first Cluster\n5. Add the first Host\n\nDeploy Zone now?" 15 60
return $?
}
# Function to check and confirm Management Server readiness
wait_for_management_server() {
local timeout=600 # 10 minutes timeout
local interval=10 # Check every 10 seconds
local elapsed=0
local url="http://$HOST_IP:8080/client/api"
{
while [ $elapsed -lt $timeout ]; do
update_progress_bar "$((elapsed * 100 / timeout))" "Waiting for Management Server to be ready...\n\nElapsed time: ${elapsed}s"
local status_code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
if [[ "$status_code" == "200" || "$status_code" == "401" ]]; then
update_progress_bar "100" "Management Server is ready!"
return 0
fi
sleep $interval
elapsed=$((elapsed + interval))
done
update_progress_bar "100" "Timeout occurred while waiting for Management Server!"
return 1
} | dialog --backtitle "$SCRIPT_NAME" \
--title "Management Server Check" \
--gauge "Waiting for Management Server to start..." 10 70 0
}
# Function to check CloudMonkey installation and initialize it
check_cloudmonkey_availability() {
{
echo "10"
echo "# Checking CloudMonkey installation..."
if ! command -v cmk &>/dev/null; then
update_progress_bar "100" "CloudMonkey (cmk) not found!"
return 1
fi
echo "50"
echo "# Initializing CloudMonkey..."
if ! cmk sync &>/dev/null; then
update_progress_bar "100" "Failed to initialize CloudMonkey!"
return 1
fi
update_progress_bar "100" "# CloudMonkey is ready!"
return 0
} | dialog --backtitle "$SCRIPT_NAME" \
--title "CloudMonkey Check" \
--gauge "Checking CloudMonkey..." 8 60 0
}
# Perform Advance Zone Deployment
deploy_zone() {
local title="Zone Deployment"
local tracker_key="zone_deployment"
if is_step_tracked "$tracker_key"; then
log "$title is already installed. Skipping."
show_dialog "info" "$title" "$title is already done. Skipping!"
return 0
fi
log "Starting zone deployment..."
# check if management server is installed
if ! is_package_installed "cloudstack-management"; then
show_dialog "msg" "$title" "CloudStack Management Server is not installed.\nPlease install it before deploying a zone."
error_exit "CloudStack Management Server must be installed before deploying a zone."
fi
if [[ -z "$BRIDGE" ]]; then
BRIDGE=$(dialog --backtitle "$SCRIPT_NAME" \
--title "KVM Host Configuration" \
--inputbox "Enter the bridge interface name:" \
8 60 "$BRIDGE" 3>&1 1>&2 2>&3)
fi
HOST_IP=$(ip -4 addr show "$BRIDGE" | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
GATEWAY=$(ip route | grep default | grep "$BRIDGE" | awk '{print $3}')
local input_ip=""
local input_gw=""
if is_interactive; then
input_ip=$(dialog --backtitle "$SCRIPT_NAME" \
--title "Host Configuration" \
--inputbox "KVM Host IP: $HOST_IP\n\nWant to modify." 10 60 "$HOST_IP" 3>&1 1>&2 2>&3)
input_gw=$(dialog --backtitle "$SCRIPT_NAME" \
--title "Host Configuration" \
--inputbox "Detected Gateway: $GATEWAY\n\nWant to modify." 10 60 "$GATEWAY" 3>&1 1>&2 2>&3)
fi
KVM_HOST_IP=${input_ip:-$HOST_IP}
GATEWAY=${input_gw:-$GATEWAY}
if [[ -z "$KVM_HOST_IP" || -z "$GATEWAY" ]]; then
error_exit "Could not determine host IP or gateway. Is bridge $BRIDGE configured properly?"
fi
# Prompt for root password before starting zone deployment
local MAX_ATTEMPTS=3
local attempt=0
local root_pass
while true; do
root_pass=$(dialog --backtitle "$SCRIPT_NAME" \
--title "$title" \
--insecure \
--passwordbox "Enter root password for KVM host ($KVM_HOST_IP):" 8 60 \
3>&1 1>&2 2>&3)
if [ $? -ne 0 ]; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Cancelled" \
--msgbox "Zone deployment cancelled." 6 50
return 1
fi
if [[ -z "$root_pass" ]]; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Error" \
--msgbox "Password cannot be empty. Please enter a valid password." 6 60
continue
fi
if sshpass -p "$root_pass" ssh -o StrictHostKeyChecking=no \
-o ConnectTimeout=5 \
-o LogLevel=ERROR \
-o UserKnownHostsFile=/dev/null \
root@"$KVM_HOST_IP" "echo 2>/dev/null"; then
break
else
attempt=$((attempt + 1))
if [[ $attempt -ge $MAX_ATTEMPTS ]]; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Error" \
--msgbox "Failed to connect to $KVM_HOST_IP after $MAX_ATTEMPTS attempts. Aborting zone deployment." 6 60
return 1
fi
dialog --backtitle "$SCRIPT_NAME" \
--title "Error" \
--msgbox "Failed to connect to KVM host. Please check the IP and password.\nAttempts left: $((MAX_ATTEMPTS - attempt))" 8 60
fi
done
local network="${HOST_IP%.*}.0/24"
# Find IPs for different purposes
local public_ips=($(find_free_ip_range "$network" 11 20)) # 20 IPs for public
if [[ ${#public_ips[@]} -eq 0 ]]; then
error_exit "No free public IPs found in $network"
fi
local last_public_ip="${public_ips[-1]}"
local last_public_octet="${last_public_ip##*.}"
local pod_start=$((last_public_octet + 1))
if (( pod_start >= 255 )); then
error_exit "Not enough IP space to allocate pod IPs"
fi
local pod_ips=($(find_free_ip_range "$network" "$pod_start" 20)) # 20 IPs for pod
if [[ ${#pod_ips[@]} -eq 0 ]]; then
error_exit "No free pod IPs found in $network"
fi
# Default values
local defaults=(
"Zone Name" "Zone1"
"Guest CIDR" "172.16.1.0/24"
"Public Start IP" "${public_ips[0]}"
"Public End IP" "$last_public_ip"
"Pod Start IP" "${pod_ips[0]}"
"Pod End IP" "${pod_ips[-1]}"
"VLAN Range" "100-200"
"Physical Network" "Physical Network 1"
"Pod Name" "Pod1"
"Cluster Name" "Cluster1"
"Primary Storage Name" "Primary1"
"Secondary Storage Name" "Secondary1"
"NFS Server IP" "$HOST_IP"
"Primary Storage Path" "/export/primary"
"Secondary Storage Path" "/export/secondary"
)
local results=()
# Create zone form entries
local form_height=$((${#defaults[@]} / 2 + 8))
local form_entries=()
local i=0
while [[ $i -lt ${#defaults[@]} ]]; do
form_entries+=("${defaults[i]}") # Label
form_entries+=("$((i/2+1))") # Row
form_entries+=("1") # Label column
form_entries+=("${defaults[i+1]}") # Default value
form_entries+=("$((i/2+1))") # Row for input
form_entries+=("35") # Input column
form_entries+=("30") # Field width
form_entries+=("0") # Max input length
((i+=2))
done
form_args=(
--backtitle "$SCRIPT_NAME"
--title "Zone Configuration"
--form "Configure Zone Deployment Parameters:"
$form_height 70 0
)
for ((i=0; i<${#form_entries[@]}; i+=8)); do
form_args+=(
"${form_entries[i]}" # Label
"${form_entries[i+1]}" # Row
"${form_entries[i+2]}" # Label column
"${form_entries[i+3]}" # Default value
"${form_entries[i+4]}" # Input row
"${form_entries[i+5]}" # Input column
"${form_entries[i+6]}" # Field width
"${form_entries[i+7]}" # Max length
)
done
if ! results=$(dialog "${form_args[@]}" 2>&1 >/dev/tty); then
dialog --backtitle "$SCRIPT_NAME" \
--title "Cancelled" \
--msgbox "Zone configuration was cancelled." 6 50
return 1
fi
# Convert results string into array
mapfile -t results <<< "$results"
# Map results to variables
local zone_name="${results[0]}"
local guest_cidr="${results[1]}"
local public_start="${results[2]}"
local public_end="${results[3]}"
local pod_start="${results[4]}"
local pod_end="${results[5]}"
local vlan_range="${results[6]}"
local phy_name="${results[7]}"
local pod_name="${results[8]}"
local cluster_name="${results[9]}"
local primary_name="${results[10]}"
local secondary_name="${results[11]}"
local nfs_server="${results[12]}"
local primary_path="${results[13]}"
local secondary_path="${results[14]}"
local network_type="Advanced"
# Show confirmation
local zone_details=""
zone_details+="Zone: $zone_name (${network_type})\n"
zone_details+="Guest CIDR: $guest_cidr\n"
zone_details+="Public IPs: $public_start - $public_end\n"
zone_details+="Pod IPs: $pod_start - $pod_end\n"
zone_details+="VLAN Range: $vlan_range\n"
zone_details+="Physical Network Name: $phy_name\n"
zone_details+="Pod Name: $pod_name\n"
zone_details+="Cluster Name: $cluster_name\n"
zone_details+="Primary Storage Name: $primary_name\n"
zone_details+="Primary Storage Path: nfs://$nfs_server$primary_path\n"
zone_details+="Secondary Storage Path: nfs://$nfs_server$secondary_path\n"
log "$(printf "Zone details:\n%b" "$zone_details")"
if is_interactive; then
local confirm_msg="Please confirm the following configuration:\n\n$zone_details"
if ! dialog --backtitle "$SCRIPT_NAME" \
--title "Confirm Configuration" \
--yesno "$confirm_msg" 18 60; then
return 1
fi
fi
if ! wait_for_management_server; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Error" \
--msgbox "Management Server did not become ready in time.\nPlease check the server status and logs." 8 60
return 1
fi
if ! check_cloudmonkey_availability; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Error" \
--msgbox "CloudMonkey is not available or failed to initialize.\nPlease install CloudMonkey and try again." 8 60
return 1
fi
local zone_id=""
local pod_id=""
local cluster_id=""
{
zone_id=$(get_tracker_field "zone_id")
if [[ -n "$zone_id" ]]; then
log "Zone already deployed with ID: $zone_id"
else
update_progress_bar "10" "# Starting Zone deployment..."
zone_id=$(cmk_api_call '.zone.id' create zone name="${zone_name}" \
networktype="$network_type" \
dns1="$DNS" \
internaldns1="$DNS" \
localstorageenabled=true \
securitygroupenabled=false \
guestcidraddress="$guest_cidr") || \
cmk_api_fail_dialog_msg "Failed to create zone"
fi
set_tracker_field "zone_id" "$zone_id"
local physicalnetwork_id=$(get_tracker_field "physicalnetwork_id")
if [[ -n "$physicalnetwork_id" ]]; then
log "Physical Network already created with ID: $physicalnetwork_id"
else
update_progress_bar "20" "# Creating Physical Network..."
physicalnetwork_id=$(cmk_api_call '.physicalnetwork.id' create physicalnetwork name="$phy_name" \
zoneid="$zone_id" \
isolationmethods="VLAN") || \
cmk_api_fail_dialog_msg "Failed to create physical network"
set_tracker_field "physicalnetwork_id" "$physicalnetwork_id"
# Add Traffic Types
update_progress_bar "30" "# Adding Traffic Types..."
cmk add traffictype traffictype=Management physicalnetworkid="$physicalnetwork_id" >/dev/null 2>&1
cmk add traffictype traffictype=Guest physicalnetworkid="$physicalnetwork_id" >/dev/null 2>&1
cmk add traffictype traffictype=Public physicalnetworkid="$physicalnetwork_id" >/dev/null 2>&1
fi
local vlan_ip_range_id=$(get_tracker_field "vlan_ip_range_id")
if [[ -n "$vlan_ip_range_id" ]]; then
log "VLAN IP Range already added with ID: $vlan_ip_range_id"
else
update_progress_bar "35" "# Adding IP Ranges..."
vlan_ip_range_id=$(cmk_api_call '.vlan.id' create vlaniprange \
zoneid="$zone_id" \
vlan=untagged \
gateway="$GATEWAY" \
netmask="$NETMASK" \
startip="$public_start" \
endip="$public_end" \
forvirtualnetwork=true) || \
cmk_api_fail_dialog_msg "Failed to add Public IP range"
set_tracker_field "vlan_ip_range_id" "$vlan_ip_range_id"
fi
update_progress_bar "38" "# Configuring Physical network with VLAN range..."
cmk update physicalnetwork id=$physicalnetwork_id vlan=$vlan_range >/dev/null 2>&1
update_progress_bar "40" "# Configuring Physical network state..."
cmk update physicalnetwork state=Enabled id="$physicalnetwork_id" >/dev/null 2>&1
local nsp_id=$(cmk list networkserviceproviders name=VirtualRouter physicalnetworkid="$physicalnetwork_id" | jq -r '.networkserviceprovider[0].id')
local vre_id=$(cmk list virtualrouterelements nspid="$nsp_id" | jq -r '.virtualrouterelement[0].id')
update_progress_bar "45" "# Configuring Physical network services..."
cmk configure virtualrouterelement enabled=true id="$vre_id" >/dev/null 2>&1
cmk update networkserviceprovider state=Enabled id="$nsp_id" >/dev/null 2>&1
pod_id=$(get_tracker_field "pod_id")
if [[ -n "$pod_id" ]]; then
log "Pod already created with ID: $pod_id"
else
update_progress_bar "50" "# Creating Pod..."
pod_id=$(cmk_api_call '.pod.id' create pod name="$pod_name" \
zoneid="$zone_id" \
gateway="$GATEWAY" \
netmask="$NETMASK" \
startip="$pod_start" \
endip="$pod_end") || \
cmk_api_fail_dialog_msg "Failed to create Pod"
set_tracker_field "pod_id" "$pod_id"
fi
cluster_id=$(get_tracker_field "cluster_id")
if [[ -n "$cluster_id" ]]; then
log "Cluster already created with ID: $cluster_id"
else
update_progress_bar "60" "# Adding Cluster..."
cluster_id=$(cmk_api_call '.cluster[0].id' add cluster \
zoneid="$zone_id" \
podid="$pod_id" \
clustername="$cluster_name" \
clustertype=CloudManaged \
hypervisor=KVM)|| \
cmk_api_fail_dialog_msg "Failed to add Cluster"
set_tracker_field "cluster_id" "$cluster_id"
fi
local host_id=$(get_tracker_field "host_id")
if [[ -n "$host_id" ]]; then
log "Host already created with ID: $host_id"
else
update_progress_bar "70" "# Adding Host..."
host_id=$(cmk_api_call '.host[0].id' add host zoneid="$zone_id" \
podid="$pod_id" \
clusterid="$cluster_id" \
hypervisor=KVM \
username=root \
password="$root_pass" \
url="http://$KVM_HOST_IP")|| \
cmk_api_fail_dialog_msg "Failed to add Host"
set_tracker_field "host_id" "$host_id"
fi
local primary_storage_id=$(get_tracker_field "primary_storage_id")
if [[ -n "$primary_storage_id" ]]; then
log "Primary Storage already added with ID: $primary_storage_id"
else
update_progress_bar "80" "# Adding Primary Storage..."
primary_storage_id=$(cmk_api_call '.storagepool.id' create storagepool name="$primary_name" \
zoneid="$zone_id" \
podid="$pod_id" \
clusterid="$cluster_id" \
url="nfs://$nfs_server$primary_path" \
hypervisor=KVM \
scope=zone) || \
cmk_api_fail_dialog_msg "Failed to add Primary Storage"
set_tracker_field "primary_storage_id" "$primary_storage_id"
fi
local secondary_storage_id=$(get_tracker_field "secondary_storage_id")
if [[ -n "$secondary_storage_id" ]]; then
log "Secondary Storage already added with ID: $secondary_storage_id"
else
update_progress_bar "90" "# Add Secondary Storage..."
secondary_storage_id=$(cmk_api_call '.imagestore.id' add imagestore name="$secondary_name" \
zoneid="$zone_id" \
url="nfs://$nfs_server$secondary_path" \
provider=NFS) || \
cmk_api_fail_dialog_msg "Failed to add Secondary Storage"
set_tracker_field "secondary_storage_id" "$secondary_storage_id"
fi
update_progress_bar "95" "# Enabling Zone..."
cmk update zone allocationstate=Enabled id="$zone_id" >/dev/null 2>&1
update_progress_bar "100" "# Zone deployment completed successfully!"
} | dialog --backtitle "$SCRIPT_NAME" \
--title "$title" \
--gauge "Deploying CloudStack Zone..." 10 70 0
set_tracker_field "$tracker_key" "yes"
show_dialog "info" "$title" "Zone deployment completed successfully."
show_cloudstack_banner
}
# Function to setup bridge used for CS
configure_network() {
if is_interactive; then
local new_bridge=$(dialog --inputbox "Enter the name for the bridge:" 8 60 "$BRIDGE" 3>&1 1>&2 2>&3)
if [[ -n "$new_bridge" && "$new_bridge" =~ ^[a-zA-Z0-9_.-]{1,15}$ ]]; then
BRIDGE="$new_bridge"
fi
fi
# First check if bridge already exists
if ip link show "$BRIDGE" &>/dev/null; then
local bridge_ip=$(ip -4 addr show "$BRIDGE" | awk '/inet / {print $2}' | cut -d/ -f1)
show_dialog \
"info" \
"Network Configuration" "Bridge interface $BRIDGE already exists with IP $bridge_ip\nSkipping network configuration."
return 0
fi
# Gather interface, IP, gateway
interface=$(ip -o link show | awk -F': ' '/state UP/ && $2!~/^lo/ {print $2; exit}')
[[ -n "$interface" ]] || error_exit "No active non-loopback interface found."
hostipandsub=$(ip -4 addr show dev "$interface" | awk '/inet / {print $2; exit}')
gateway=$(ip route show default | awk '/default/ {print $3; exit}')
# Rest of the existing configure_network code follows...
if [[ "$OS_TYPE" == "ubuntu" ]]; then
dialog --backtitle "$SCRIPT_NAME" \
--infobox "Configuring bridge $BRIDGE using Netplan..." 5 50
sleep 1
cfgfile="/etc/netplan/01-bridge-$BRIDGE.yaml"
cat > "$cfgfile" <<EOF
network:
version: 2
renderer: networkd
ethernets:
$interface:
dhcp4: false
dhcp6: false
optional: true
bridges:
$BRIDGE:
interfaces: [$interface]
addresses: [$hostipandsub]
routes:
- to: default
via: $gateway
nameservers:
addresses: [$DNS]
parameters:
stp: false
forward-delay: 0
EOF
chmod 600 "$cfgfile"
rm -f /etc/netplan/50-cloud-init.yaml
if netplan generate && netplan apply; then
show_dialog \
"info" \
"Network Configuration" \
"Bridge $BRIDGE configured successfully with IP $hostipandsub."
else
error_exit "Failed to apply Netplan configuration"
fi
elif [[ "$OS_TYPE" == "debian" ]]; then
dialog --backtitle "$SCRIPT_NAME" \
--infobox "Configuring bridge $BRIDGE using /etc/network/interfaces..." 5 60
sleep 1
cfgfile="/etc/network/interfaces.d/01-bridge-$BRIDGE"
cat > "$cfgfile" <<EOF
auto $BRIDGE
iface $BRIDGE inet static
address $hostipandsub
netmask $NETMASK
gateway $gateway
dns-nameservers $DNS
bridge_ports $interface
bridge_stp off
bridge_fd 0
EOF
chmod 600 "$cfgfile"
if command -v ifreload &>/dev/null; then
ifreload -a
else
ifdown $interface || true
ifup $BRIDGE
fi
elif [[ "$OS_TYPE" =~ ^(rhel|centos|ol|rocky|almalinux)$ ]]; then
{
update_progress_bar "5" "# Configuring bridge interface $BRIDGE..."
# First, check if bridge already exists and remove it
if nmcli connection show "$BRIDGE" &>/dev/null; then
update_progress_bar "10" "# Removing existing bridge connection..."
nmcli connection delete "$BRIDGE" &>/dev/null || true
fi
# Remove any existing slave connections for this interface
local slave_name="${interface}-slave-$BRIDGE"
if nmcli connection show "$slave_name" &>/dev/null; then
update_progress_bar "15" "# Removing existing slave connection..."
nmcli connection delete "$slave_name" &>/dev/null || true
fi
sleep 2
# Get the original connection name for the interface
update_progress_bar "20" "# Detecting current network configuration..."
local original_conn=$(nmcli -t -f NAME,DEVICE connection show --active | grep ":$interface$" | cut -d: -f1)
# Create bridge
if output=$(nmcli connection add type bridge \
con-name "$BRIDGE" \
ifname "$BRIDGE" \
bridge.stp no \
ipv4.addresses "$hostipandsub" \
ipv4.gateway "$gateway" \
ipv4.dns "$DNS" \
ipv4.method manual \
ipv6.method disabled \
connection.autoconnect yes \
connection.autoconnect-slaves 1 2>&1); then
update_progress_bar "30" "# Bridge created successfully"
else
update_progress_bar "100" "# Failed to create bridge: $output"
error_exit "Failed to create bridge: $output"
fi
sleep 2
# Add ethernet interface as slave
update_progress_bar "50" "# Adding interface $interface to bridge..."
if output=$(nmcli connection add type ethernet \
slave-type bridge \
con-name "$slave_name" \
ifname "$interface" \
master "$BRIDGE" \
connection.autoconnect yes 2>&1); then
update_progress_bar "60" "# Interface added as bridge slave"
else
update_progress_bar "100" "# Failed to add interface: $output"
error_exit "Failed to add interface: $output"
fi
sleep 2
# Bring down original connection if it exists
if [[ -n "$original_conn" ]]; then
update_progress_bar "70" "# Deactivating original connection..."
nmcli connection down "$original_conn" &>/dev/null || true
sleep 2
fi
# Activate connection
update_progress_bar "90" "# Activating network connection..."
if output=$(nmcli connection up "$BRIDGE" 2>&1); then
update_progress_bar "92" "# Bridge activated successfully"
else
update_progress_bar "100" "# Failed to activate bridge: $output"
sleep 1
error_exit "Failed to activate bridge: $output"
fi
sleep 5
update_progress_bar "95" "# Verifying bridge configuration..."
if ! ip link show "$BRIDGE" &>/dev/null; then
error_exit "Bridge $BRIDGE was not created successfully"
fi
# Wait for IP assignment
local max_wait=10
local count=0
local expected_ip=$(echo "$hostipandsub" | cut -d'/' -f1)
while [[ $count -lt $max_wait ]]; do
if ip addr show "$BRIDGE" | grep -q "inet.*$expected_ip"; then
break
fi
sleep 1
((count++))
done
update_progress_bar "100" "# Bridge configuration complete"
sleep 2
} | dialog --backtitle "$SCRIPT_NAME" \
--title "Network Configuration" \
--gauge "Configuring network with NetworkManager..." 10 70 0
# Verify the configuration
if ip link show "$BRIDGE" &>/dev/null; then
local bridge_ip=$(ip -4 addr show "$BRIDGE" | awk '/inet / {print $2}' | cut -d/ -f1)
if [[ -n "$bridge_ip" ]]; then
show_dialog \
"info" \
"Network Configuration" \
"Bridge $BRIDGE configured successfully with IP $bridge_ip." 5
else
dialog --backtitle "$SCRIPT_NAME" \
--title "Warning" \
--msgbox "Bridge $BRIDGE created but IP address not detected.\n\nPlease verify with: ip addr show $BRIDGE" 8 70
return 1
fi
else
dialog --backtitle "$SCRIPT_NAME" \
--title "Error" \
--msgbox "Failed to configure bridge $BRIDGE.\n\nCheck logs with:\njournalctl -xeu NetworkManager" 8 70
return 1
fi
else
error_exit "Unsupported OS type: $OS_TYPE"
fi
}
#-------------------------------------------------------------------------------
# Driver functions to setup and install components
#-------------------------------------------------------------------------------
setup_network() {
log "Starting network configuration"
if is_step_tracked "network_name"; then
BRIDGE=$(get_tracker_field "network_name")
show_dialog "info" "Network Configuration" "Network already configured with bridge $BRIDGE.\n\n Skipping network configuration."
return 0
fi
configure_network
set_tracker_field "network_name" "$BRIDGE"
log "Network configured with bridge $BRIDGE"
}
configure_repo() {
log "Setting up CloudStack repository"
if is_step_tracked "repo_url"; then
local repo_url=$(get_tracker_field "repo_url")
show_dialog "info" "CloudStack Repo Setup" "CloudStack repository already configured with $repo_url\n\n Skipping repository setup."
return 0
fi
configure_cloudstack_repo
}
install_configure_mgmt() {
install_mysql_server
install_management_server
configure_mysql_for_cloudstack
configure_management_server_database
}
install_configure_agent() {
install_kvm_agent
configure_kvm_agent
}
install_configure_usage() {
install_usage_server
configure_usage_server
}
# Install and configure selected components
install_configure_components() {
show_components_versions
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " nfs " ]]; then
install_nfs_server
configure_nfs_server
fi
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " management " ]]; then
install_configure_mgmt
fi
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " agent " ]]; then
install_configure_agent
fi
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " usage " ]]; then
install_configure_usage
fi
}
# Validate system resources before setup
validate_system_resources() {
check_root
check_kvm_support
check_system_resources
validate_jre_version
}
# Configure prerequisites before installation
configure_prerequisites() {
detect_os
update_system_time
install_base_dependencies
}
# Driver function for custom installation
custom_install() {
if [ ${#SELECTED_COMPONENTS[@]} -eq 0 ]; then
select_components_to_setup
fi
log "Selected components for installation: ${SELECTED_COMPONENTS[*]}"
if is_cloudstack_pkg_selected; then
setup_network
configure_repo
fi
update_system_packages
install_configure_components
}
# Driver function for all-in-one installation
all_in_one_install() {
PROMPT=0 # configure silent mode for all-in-one
if dialog --backtitle "$SCRIPT_NAME" \
--title "All-in-One Installation" \
--yesno "You have selected all components for installation. This will configure a complete CloudStack setup on this single machine.\n\nProceed with All-in-One installation?" 12 60; then
SELECTED_COMPONENTS=("nfs" "management" "agent" "usage")
custom_install
else
dialog --backtitle "$SCRIPT_NAME" \
--title "All-in-One Installation" \
--msgbox "Installation cancelled by user." 6 60
exit 0
fi
}
# Cleanup function for trap
cleanup() {
local exit_code=${1:-$?}
clear
if [[ $exit_code -eq 0 ]]; then
success_msg "Script completed successfully. Check $CS_LOGFILE for details."
else
warn_msg "Script interrupted. Check $CS_LOGFILE for details."
fi
exit $exit_code
}
#-------------------------------------------------------------------------------
# Main function
#-------------------------------------------------------------------------------
main() {
log "CloudStack Installer Script Started"
load_tracker
validate_system_resources
configure_prerequisites
local temp_file=$(mktemp)
if ! dialog --backtitle "$SCRIPT_NAME" \
--title "Installation Options" \
--menu "Select an option:" 12 50 4 \
1 "All-in-One Installation" \
2 "Custom Installation" \
3 "Configure CloudStack Repository" \
4 "Deploy CloudStack Zone" 2> "$temp_file"; then
show_dialog "msg" "Installation Options" "Installation option selection cancelled by user."
error_exit "Installation option selection cancelled by user"
fi
local option=$(< "$temp_file")
case $option in
1)
all_in_one_install
;;
2)
custom_install
;;
3)
configure_cloudstack_repo
;;
4)
deploy_zone
;;
esac
if [[ ${#SELECTED_COMPONENTS[@]} -gt 0 ]]; then
if show_validation_summary; then
if [[ " ${SELECTED_COMPONENTS[@]} " =~ " management " ]]; then
if is_interactive; then
if ! select_zone_deployment; then
dialog --backtitle "$SCRIPT_NAME" \
--title "Zone Deployment" \
--msgbox "Zone deployment skipped. You can deploy a zone later using CloudStack UI." 8 60
else
deploy_zone
fi
else
deploy_zone
fi
fi
else
show_dialog "msg" "Zone Deployment" "Warning: Some components are not properly configured.\n\nZone deployment is not available until all components are fixed." 5 10
fi
fi
rm -f "$temp_file"
cleanup 0
}
# Set trap for cleanup
trap 'cleanup 1' INT TERM
# Run main function
main "$@"