#!/bin/bash set -e # Detect IP address and set environment variables DEFAULT_IP=$(ip route get 1 | awk '{for(i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}') URLS_FILE="/opt/pf9/airctl/conf/urls.txt" # === Set default environment variables here === export IP_ADDRESS=${IP_ADDRESS:-$DEFAULT_IP} export DU_FQDN=${DU_FQDN:-"pcd.pf9.io"} export REGION_NAME=${REGION_NAME:-"Community"} export SERVICE_CIDR=${SERVICE_CIDR:-"10.21.0.0/16"} export POD_CIDR=${POD_CIDR:-"10.20.0.0/16"} export CALICO_INSTALL_TIMEOUT=${CALICO_INSTALL_TIMEOUT:-"900"} export S3_BUCKET=${S3_BUCKET:-"pcd-community.s3-accelerate.amazonaws.com"} export S3_USER_AGENT=${S3_USER_AGENT:-""} export SKIP_PRECHECKS=${SKIP_PRECHECKS:-"false"} export SKIP_RATING_PROMPT=${SKIP_RATING_PROMPT:-"false"} export USER_CERT_PATH=${USER_CERT_PATH:-""} export USER_KEY_PATH=${USER_KEY_PATH:-""} export HTTPS_PROXY=${HTTPS_PROXY:-""} export HTTP_PROXY=${HTTP_PROXY:-""} export NO_PROXY=${NO_PROXY:-""} # feature flag for audit components export ENABLE_AUDIT=${ENABLE_AUDIT:-"true"} # Feature flag: when "false" (default for CE), bork deploys all PCD # components in a single region. Set MULTI_REGION=true to use the # legacy multi-region layout. export MULTI_REGION=${MULTI_REGION:-"false"} # Kaapi (Kubernetes management plane) enablement is handled by an interactive # prompt later in this script. Pre-export ENABLE_K8S=true or ENABLE_K8S=false # to skip the prompt (for automation). # Capture whether the caller explicitly pinned a version, before the # ${PCD_VERSION:=...} default-assignment trick further down overwrites # this signal. Used both for the telemetry default below and for the # refork decision later in this script. export PCD_VERSION_WAS_SET=${PCD_VERSION:+true} # Anonymous usage analytics are enabled by default to help improve the product. # To opt out, set TELEMETRY=false before running this script. Pinning a # specific PCD_VERSION (an older or unpublished build) defaults telemetry # off instead, since that's most likely a test/dev run. if [[ "$PCD_VERSION_WAS_SET" == "true" ]]; then export TELEMETRY=${TELEMETRY:-"false"} else export TELEMETRY=${TELEMETRY:-"true"} fi # To skip EULA prompt and auto-accept, set ACCEPT_EULA=true before running this script. export ACCEPT_EULA=${ACCEPT_EULA:-"false"} export STATE_FILE="$HOME/.airctl/state.yaml" export SHOULD_CLEANUP=${SHOULD_CLEANUP:-"false"} USER_REQUESTED_CLEANUP="$SHOULD_CLEANUP" # === End of environment variables === if ! ip addr show | grep -q "$IP_ADDRESS"; then echo "Error: IP_ADDRESS '$IP_ADDRESS' is not assigned to any local interface." exit 1 fi cpuinfo=$(grep -m1 flags /proc/cpuinfo) if uname -m | grep -qv 'x86_64'; then echo "Error: This script only supports x86_64 architecture." exit 1 fi # Check v3 markers if echo "$cpuinfo" | grep -qw avx2 && echo "$cpuinfo" | grep -qw bmi2; then has_v3=1 else has_v3=0 fi if [ "$has_v3" -eq 0 ]; then echo "Error: CPU does not meet x86-64-v3 baseline (requires AVX2 + BMI2)." exit 1 fi # Color codes RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # Spinner function if [[ $- == *x* ]]; then # For debug mode that shows all output run_with_spinner() { local message="$1" local commands="$2" echo "$message... processing" eval "$commands" local exit_status=$? if [ $exit_status -eq 0 ]; then echo " ${GREEN}Done${NC}" else echo " ${RED}Failed${NC}" exit $exit_status fi } else # For normal operation run_with_spinner() { local message="$1" local commands="$2" echo -n "$message... " # Create temporary files for stdout and stderr local stdout_log local stderr_log stdout_log=$(mktemp) stderr_log=$(mktemp) (eval "$commands") >"$stdout_log" 2>"$stderr_log" & local pid=$! # Simple blinking spinner while kill -0 $pid 2>/dev/null; do echo -ne "${RED}▓${NC}" sleep 0.5 echo -ne "\b${RED}▒${NC}" sleep 0.5 echo -ne "\b" done # Wait for process to finish and check its exit status if wait $pid; then exit_status=0 else exit_status=$? fi if [ $exit_status -eq 0 ]; then echo -e " ${GREEN}Done${NC}" else echo -e " ${RED}Failed${NC}" cat "$stderr_log" rm -f "$stdout_log" "$stderr_log" exit $exit_status fi # Clean up temporary files rm -f "$stdout_log" "$stderr_log" } fi # Resolve VERSION now, before any prompts run. When PCD_VERSION was # explicitly set, fetch a version-matched copy of this script and # re-exec into it, so the rest of the install always runs script logic # that matches the pinned VERSION's artifact set. _PCD_CE_BUNDLED guards # against the reforked copy repeating this step (and re-execing again). if [[ -z "${_PCD_CE_BUNDLED:-}" ]]; then if [[ "$PCD_VERSION_WAS_SET" == "true" ]]; then export VERSION="$PCD_VERSION" else run_with_spinner "Finding latest version" " mkdir -p \${HOME}/pcd-ce && cd \${HOME}/pcd-ce && curl -O https://pcd-community.s3-accelerate.amazonaws.com/stable.txt " STABLE_VERSION=$(cat "$HOME/pcd-ce/stable.txt") export VERSION=${PCD_VERSION:=$STABLE_VERSION} fi if [[ "$PCD_VERSION_WAS_SET" == "true" ]]; then mkdir -p "$HOME/pcd-ce" run_with_spinner "Fetching version-matched install script" " curl \${S3_USER_AGENT} -fsS \"https://\${S3_BUCKET}/\${VERSION}/pcd-ce.sh\" -o \${HOME}/pcd-ce/pcd-ce.sh || rm -f \${HOME}/pcd-ce/pcd-ce.sh " if [[ -s "$HOME/pcd-ce/pcd-ce.sh" ]] && head -c2 "$HOME/pcd-ce/pcd-ce.sh" | grep -q '#!'; then chmod +x "$HOME/pcd-ce/pcd-ce.sh" exec env _PCD_CE_BUNDLED=1 "$HOME/pcd-ce/pcd-ce.sh" fi fi fi echo "Private Cloud Director Community Edition Deployment Started..." # EULA acceptance prompt if [[ "$ACCEPT_EULA" != "true" ]]; then echo "" echo "By continuing with the installation, you agree to the terms and conditions of the" echo "Private Cloud Director Community Edition EULA." echo "" echo "Please review the EULA at: https://platform9.com/ce-eula" echo "" read -p "Do you accept the terms of the EULA? [Y/N]: " EULA_RESPONSE < /dev/tty if [[ ! "$EULA_RESPONSE" =~ ^[Yy]$ ]]; then echo "Installation aborted. EULA not accepted." exit 1 fi echo "" fi # Kaapi (Kubernetes management plane) — opt-in. # Bypass: pre-export ENABLE_K8S=true or ENABLE_K8S=false to skip the prompt. if [[ -z "${ENABLE_K8S+x}" || -z "$ENABLE_K8S" ]]; then echo "" echo "Private Cloud Director can install a Kubernetes management plane" echo "that lets you provision managed Kubernetes clusters." echo "This is optional and adds ~10 minutes to install time and requires additional" echo "CPU / memory (see https://docs.platform9.com/private-cloud-director/getting-started/getting-started-with-community-edition/prerequisites)." echo "" read -p "Enable Kubernetes management plane? [y/N]: " K8S_RESPONSE < /dev/tty if [[ "$K8S_RESPONSE" =~ ^[Yy]$ ]]; then export ENABLE_K8S=true else export ENABLE_K8S=false fi echo "" fi # ----------------------------------------------------------------------------- # Handle reinstall on failure or if the script is run again by mistake. # # This section ensures a clean environment by: # 1. Checking if a previous deployment exists (via the state file). # 2. Detecting if the k3s service is still running. # # If either a previous region is detected or k3s is running, # the user is prompted to clean up the current deployment before reinstalling. # If the user agrees, the script will: # - Run 'airctl unconfigure-du' if the state file indicates cleanup is needed. # - Run 'airctl delete-cluster' if k3s is running. # If the user declines, the script aborts to avoid accidental conflicts. # ----------------------------------------------------------------------------- SCRIPT_DETECTED_CLEANUP=false if [[ -f "$STATE_FILE" ]]; then # Check if regions is an empty array (regions: []) if ! grep -qE '^regions: \[\]' "$STATE_FILE"; then SCRIPT_DETECTED_CLEANUP=true fi fi # Check if k3s service is active if systemctl is-active --quiet k3s; then is_k3s_running=true else is_k3s_running=false fi if [[ "$SCRIPT_DETECTED_CLEANUP" == "true" || "$is_k3s_running" = true ]]; then echo -e "\n⚠️ Detected existing or incomplete installation." if [[ "$USER_REQUESTED_CLEANUP" != true ]]; then read -p "Would you like to remove the current deployment and reinstall? [Y/N]: " CLEANUP_CONFIRM < /dev/tty if [[ ! "$CLEANUP_CONFIRM" =~ ^[Yy]$ ]]; then echo "❌ Aborting installation to avoid conflict with existing setup." exit 1 fi fi echo -e "\n➡️ Cleaning up previous installation..." if [[ "$SCRIPT_DETECTED_CLEANUP" = true ]] && command -v airctl >/dev/null 2>&1; then run_with_spinner "Running airctl unconfigure-du" " airctl unconfigure-du --force --config /opt/pf9/airctl/conf/airctl-config.yaml < /dev/tty " fi if [[ "$is_k3s_running" = true ]] && command -v airctl >/dev/null 2>&1; then run_with_spinner "Deleting k3s cluster" " airctl delete-cluster --config /opt/pf9/airctl/conf/airctl-config.yaml < /dev/tty " fi rm -rf "$HOME/airctl-logs" rm -rf "$HOME/pcd-ce" fi # Download PCD CE artifacts run_with_spinner "Downloading artifacts" " mkdir -p \${HOME}/pcd-ce && cd \${HOME}/pcd-ce && curl \${S3_USER_AGENT} --silent https://\${S3_BUCKET}/\${VERSION}/index.txt | grep -e airctl -e install-pcd.sh -e pcd-chart.tgz -e options.json -e version.txt -e helm -e yq -e kaapi-chart.tgz | awk -v ver=\"\$VERSION\" -v s3b=\"\$S3_BUCKET\" -v s3ua=\"\$S3_USER_AGENT\" '{print \"curl \" s3ua \" -sS \\\"https://\" s3b \"/\" ver \"/\" \$NF \"\\\" -o \${HOME}/pcd-ce/\" \$NF}' | bash " run_with_spinner "Configuring system settings" " # Create sysctl config file echo 'fs.inotify.max_queued_events = 512000 fs.inotify.max_user_instances = 512000 fs.inotify.max_user_watches = 512000 fs.aio-max-nr = 500000 kernel.panic = 10 vm.panic_on_oom = 0' | sudo tee /etc/sysctl.d/99-pf9-airctl.conf > /dev/null && sudo sysctl -p /etc/sysctl.d/99-pf9-airctl.conf " # Install PCD CE artifacts, modify options, and install kubectl run_with_spinner "Installing artifacts and dependencies" " cd ${HOME}/pcd-ce && chmod +x ./install-pcd.sh && ./install-pcd.sh ${VERSION} && sudo ln -sf /opt/pf9/airctl/airctl /usr/bin/airctl && echo 'export PATH=\$PATH:/opt/pf9/airctl' >> ~/.bashrc && . ~/.bashrc && # Value-agnostic overrides: match on the KEY, not the shipped default # value, so an override still lands if the bundled options.json changes # its defaults (e.g. skip_components already non-empty). See the # generated template in support/release/upload-pcd-artifacts.sh. if echo "${ENABLE_AUDIT}" | grep -qi '^true$'; then sed -i -E 's|\"skip_components\": \"[^\"]*\"|\"skip_components\": \"gnocchi,heat,terrakube,appcatalog\"|' /opt/pf9/airctl/conf/options.json else sed -i -E 's|\"skip_components\": \"[^\"]*\"|\"skip_components\": \"gnocchi,heat,audit,terrakube,appcatalog\"|' /opt/pf9/airctl/conf/options.json fi && sed -i -E 's|\"community_edition\": \"[^\"]*\"|\"community_edition\": \"true\"|' /opt/pf9/airctl/conf/options.json && sed -i -E 's|\"multi_region\": \"[^\"]*\"|\"multi_region\": \"${MULTI_REGION}\"|' /opt/pf9/airctl/conf/options.json && if echo "${ENABLE_AUDIT}" | grep -qiv '^true$'; then grep -q '\"audit_disabled\"' /opt/pf9/airctl/conf/options.json || sed -i -E 's|(\"community_edition\": \"[^\"]*\")|\1, \"audit_disabled\": \"true\"|' /opt/pf9/airctl/conf/options.json fi && # helm timeouts are absent from the shipped template, so sed cannot add # them; upsert via python so they are always present. python3 -c 'import json; f=\"/opt/pf9/airctl/conf/options.json\"; d=json.load(open(f)); d.update({\"helm_timeout\":\"30m0s\",\"helm_keystone_timeout\":\"30m0s\",\"helm_kubedu_timeout\":\"30m0s\"}); json.dump(d,open(f,\"w\"),indent=4)' && curl -LO \"https://dl.k8s.io/release/\$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\" && chmod +x kubectl && sudo mv kubectl /usr/local/bin/ " run_with_spinner "Configuring Docker Mirrors" " sudo mkdir -p /etc/rancher/k3s sudo tee /etc/rancher/k3s/registries.yaml << EOF mirrors: docker.io: endpoint: - "https://mirror.gcr.io" registry-1.docker.io: endpoint: - "https://mirror.gcr.io" EOF " if [[ ! -z "$HTTPS_PROXY" || ! -z "$HTTP_PROXY" ]]; then DU_FQDN_DOMAIN="${DU_FQDN#*.}" NO_PROXY_IPS="127.0.0.0/8,localhost,::1,.svc,.svc.cluster.local,.cluster.local,.default.svc,.default.svc.cluster.local,.${DU_FQDN_DOMAIN},${IP_ADDRESS},${SERVICE_CIDR},${POD_CIDR}" if [ ! -z "$NO_PROXY" ]; then NO_PROXY_IPS+=",${NO_PROXY}" fi run_with_spinner "Configuring Proxy Settings" "$(cat << EOF if [ ! -z "${HTTPS_PROXY}" ]; then if [ -f /opt/pf9/airctl/conf/helm_values/bork.template.yml ] && ! cat /opt/pf9/airctl/conf/helm_values/bork.template.yml | grep -qe '^https_proxy:.*'; then echo https_proxy: "${HTTPS_PROXY}" >> /opt/pf9/airctl/conf/helm_values/bork.template.yml elif [ -f /opt/pf9/airctl/conf/helm_values/bork.template.yml ]; then sed -i 's|^https_proxy:.*|https_proxy: \"'"${HTTPS_PROXY}"'\"|g' /opt/pf9/airctl/conf/helm_values/bork.template.yml fi if ! sudo cat /etc/environment | grep -qe '^HTTPS_PROXY=.*'; then echo HTTPS_PROXY="${HTTPS_PROXY}" | sudo tee -a /etc/environment else sudo sed -i 's|^HTTPS_PROXY=.*|HTTPS_PROXY=\"'"${HTTPS_PROXY}"'\"|g' /etc/environment fi if ! sudo cat /etc/environment | grep -qe '^https_proxy=.*'; then echo https_proxy="${HTTPS_PROXY}" | sudo tee -a /etc/environment else sudo sed -i 's|^https_proxy=.*|https_proxy=\"'"${HTTPS_PROXY}"'\"|g' /etc/environment fi fi if [ ! -z "${HTTP_PROXY}" ]; then if [ -f /opt/pf9/airctl/conf/helm_values/bork.template.yml ] && ! cat /opt/pf9/airctl/conf/helm_values/bork.template.yml | grep -qe '^http_proxy:.*'; then echo http_proxy: "${HTTP_PROXY}" >> /opt/pf9/airctl/conf/helm_values/bork.template.yml elif [ -f /opt/pf9/airctl/conf/helm_values/bork.template.yml ]; then sed -i 's|^http_proxy:.*|http_proxy: \"'"${HTTP_PROXY}"'\"|g' /opt/pf9/airctl/conf/helm_values/bork.template.yml fi if ! sudo cat /etc/environment | grep -qe '^HTTP_PROXY=.*'; then echo HTTP_PROXY="${HTTP_PROXY}" | sudo tee -a /etc/environment else sudo sed -i 's|^HTTP_PROXY=.*|HTTP_PROXY=\"'"${HTTP_PROXY}"'\"|g' /etc/environment fi if ! sudo cat /etc/environment | grep -qe '^http_proxy=.*'; then echo http_proxy="${HTTP_PROXY}" | sudo tee -a /etc/environment else sudo sed -i 's|^http_proxy=.*|http_proxy=\"'"${HTTP_PROXY}"'\"|g' /etc/environment fi fi if [ ! -z "${HTTP_PROXY}" ] || [ ! -z "${HTTPS_PROXY}" ]; then if [ -f /opt/pf9/airctl/conf/helm_values/bork.template.yml ] && ! cat /opt/pf9/airctl/conf/helm_values/bork.template.yml | grep -qe '^no_proxy:.*'; then echo no_proxy: \"${NO_PROXY_IPS}\" >> /opt/pf9/airctl/conf/helm_values/bork.template.yml elif [ -f /opt/pf9/airctl/conf/helm_values/bork.template.yml ]; then sed -i 's|^no_proxy:.*|no_proxy: \"'"${NO_PROXY_IPS}"'\"|g' /opt/pf9/airctl/conf/helm_values/bork.template.yml fi if ! sudo cat /etc/environment | grep -qe '^NO_PROXY=.*'; then echo NO_PROXY="${NO_PROXY_IPS}" | sudo tee -a /etc/environment else sudo sed -i 's|^NO_PROXY=.*|NO_PROXY=\"'"${NO_PROXY_IPS}"'\"|g' /etc/environment fi if ! sudo cat /etc/environment | grep -qe '^no_proxy=.*'; then echo no_proxy="${NO_PROXY_IPS}" | sudo tee -a /etc/environment else sudo sed -i 's|^no_proxy=.*|no_proxy=\"'"${NO_PROXY_IPS}"'\"|g' /etc/environment fi sudo mkdir -p /etc/systemd/system/k3s.service.d if [ ! -f /etc/systemd/system/k3s.service.d/http-proxy.conf ] || ! sudo cat /etc/systemd/system/k3s.service.d/http-proxy.conf | grep -qe '^EnvironmentFile=.*'; then sudo tee -a /etc/systemd/system/k3s.service.d/http-proxy.conf << INNER_EOF [Service] EnvironmentFile=/etc/environment INNER_EOF else sudo sed -i 's|^EnvironmentFile=.*|EnvironmentFile=/etc/environment|g' /etc/systemd/system/k3s.service.d/http-proxy.conf fi fi EOF )" fi if [ ! -f "$URLS_FILE" ]; then touch "$URLS_FILE" 2>/dev/null if [ $? -ne 0 ]; then echo "Error: Failed to create $URLS_FILE" exit 1 fi fi if [ ! -w "$URLS_FILE" ]; then echo "Error: $URLS_FILE is not writable" exit 1 fi cat > "$URLS_FILE" <<'EOF' https://api2.amplitude.com https://auth.docker.io https://cdn01.quay.io https://check.percona.com https://checkpoint-api.hashicorp.com https://cr.fluentbit.io https://dl.k8s.io https://dockermirror.platform9.io https://docs.tigera.io https://ghcr.io https://github.com https://go.pcd.run https://grafana.com https://opencloud-dev-charts.s3.us-east-2.amazonaws.com https://pcd-community.s3-accelerate.amazonaws.com https://pkg-containers.githubusercontent.com https://prod-registry-k8s-io-us-east-1.s3.dualstack.us-east-1.amazonaws.com https://production.cloudflare.docker.com https://pypi.org https://quay.io https://registry-1.docker.io https://registry.k8s.io https://release-assets.githubusercontent.com https://storage.googleapis.com https://us-east4-docker.pkg.dev https://usage.projectcalico.org https://esm.ubuntu.com https://files.pythonhosted.org https://objects.githubusercontent.com EOF if [ $? -ne 0 ]; then echo "Error: Failed to write URLs to $URLS_FILE" exit 1 fi if [ ! -s "$URLS_FILE" ]; then echo "Error: $URLS_FILE is empty after writing" exit 1 fi # Start CE airctl ce start --config /opt/pf9/airctl/conf/airctl-config.yaml < /dev/tty echo "" airctl status --config /opt/pf9/airctl/conf/airctl-config.yaml < /dev/tty echo "" # Get credentials and show output BASE=$(echo $DU_FQDN | cut -d. -f1) DOMAIN=$(echo $DU_FQDN | cut -d. -f2-) BASE_LC=$(echo $BASE | tr '[:upper:]' '[:lower:]') DOMAIN_LC=$(echo $DOMAIN | tr '[:upper:]' '[:lower:]') echo "Login Details:" if [[ "$MULTI_REGION" == "true" ]]; then REGION_LC=$(echo $REGION_NAME | tr '[:upper:]' '[:lower:]') echo "URL: https://$BASE_LC-$REGION_LC.$DOMAIN_LC" else echo "URL: https://$BASE_LC.$DOMAIN_LC" fi airctl get-creds --config /opt/pf9/airctl/conf/airctl-config.yaml < /dev/tty echo "" echo "See documentation at https://pcd.run for next steps." echo "" echo "Join the support community at https://reddit.com/r/platform9"