diff --git a/.github/workflows/smoke-deploy-lab-talos.yaml.disabled b/.github/workflows/smoke-deploy-lab-talos.yaml.disabled new file mode 100644 index 000000000..21ab5923b --- /dev/null +++ b/.github/workflows/smoke-deploy-lab-talos.yaml.disabled @@ -0,0 +1,184 @@ +--- +name: Running hyperconverged smoke tests + +"on": + workflow_dispatch: + inputs: + acmeEmail: + description: Set email for ACME + required: true + default: "cloud@example.local" + type: string + gatewayDomain: + description: Set domain for the gateway + required: true + default: cloud.local + type: string + sshUsername: + description: Set SSH username + required: true + default: "ubuntu" + type: choice + options: + - "ubuntu" + - "debian" + mode: + description: Set mode + required: true + default: "test" + type: choice + options: + - "test" + - "deploy" + - "cleanup" + run_tests: + description: Run post-deployment tests + required: false + default: true + type: boolean + test_level: + description: Test level to run + required: false + default: "quick" + type: choice + options: + - "quick" + - "standard" + - "full" + pull_request: + paths: + - ansible/** + - base-kustomize/** + - base-helm-configs/** + - bin/** + - scripts/** + - ".github/workflows/smoke-deploy-lab.yaml" + +env: + HYPERCONVERGED_DEV: "true" + GATEWAY_DOMAIN: cloud.local + OS_FLAVOR: gp.5.8.16 + JUMP_HOST_FLAVOR: gp.5.2.2 + ACME_EMAIL: cloud@example.local + SSH_USERNAME: "ubuntu" + TEST_LEVEL: "quick" + +jobs: + smoke-deploy: + runs-on: self-hosted + container: + image: localhost:5000/genestack-ci:latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Dynamically set MY_DATE environment variable + if: ${{ github.event_name == 'pull_request' }} + run: echo "LAB_NAME_PREFIX=smoke-$(date +%s)" >> $GITHUB_ENV + + - name: Statically set environment variable + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo "LAB_NAME_PREFIX=$(echo '${{ github.event.inputs.gatewayDomain }}' | tr '.' '-')" >> $GITHUB_ENV + echo "GATEWAY_DOMAIN=${{ github.event.inputs.gatewayDomain }}" >> $GITHUB_ENV + echo "ACME_EMAIL=${{ github.event.inputs.acmeEmail }}" >> $GITHUB_ENV + echo "SSH_USERNAME=${{ github.event.inputs.sshUsername }}" >> $GITHUB_ENV + + - name: Run deployment script + if: ${{ github.event_name == 'pull_request' || contains(fromJSON('["deploy", "test"]'), github.event.inputs.mode) }} + run: | + eval "$(ssh-agent -s)" + export TEST_LEVEL="${{ github.event.inputs.test_level }}" + scripts/hyperconverged-lab.sh talos + + - name: Retrieve Keys + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'deploy' }} + uses: actions/upload-artifact@v4 + with: + name: ssh-keys + if-no-files-found: warn # 'warn' or 'ignore' are also available, defaults to `warn` + path: "/github/home/.ssh/${{ env.LAB_NAME_PREFIX }}-key.pem" + + - name: Create output file in markdown format + run: | + if [ -s /tmp/output.txt ]; then + echo "### Cloud Access" > access-output.txt + cat /tmp/output.txt >> access-output.txt + fi + + - name: Publish Output to Summary + run: | + if [ -s access-output.txt ]; then + { + cat access-output.txt + } >> $GITHUB_STEP_SUMMARY + fi + + - name: Upload Test Results + if: ${{ always() && (github.event_name == 'pull_request' || (github.event.inputs.run_tests == 'true' && contains(fromJSON('["deploy", "test"]'), github.event.inputs.mode))) }} + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ github.run_number }} + path: test-results/ + if-no-files-found: ignore + retention-days: 30 + + - name: Publish Test Summary + if: ${{ always() && (github.event_name == 'pull_request' || (github.event.inputs.run_tests == 'true' && contains(fromJSON('["deploy", "test"]'), github.event.inputs.mode))) }} + run: | + echo "## Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f test-results/aggregate-results.txt ]; then + echo "### Test Suite Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Test Suite | Status |" >> $GITHUB_STEP_SUMMARY + echo "|------------|--------|" >> $GITHUB_STEP_SUMMARY + + while IFS=: read -r suite status; do + status=$(echo "$status" | xargs) + if [ "$status" = "PASSED" ]; then + echo "| $suite | :white_check_mark: PASSED |" >> $GITHUB_STEP_SUMMARY + else + echo "| $suite | :x: FAILED |" >> $GITHUB_STEP_SUMMARY + fi + done < test-results/aggregate-results.txt + + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Parse XML results if available + if ls test-results/*.xml >/dev/null 2>&1; then + echo "### Detailed Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + for xml_file in test-results/*.xml; do + if [ -f "$xml_file" ]; then + suite_name=$(grep -o 'name="[^"]*"' "$xml_file" | head -1 | sed 's/name="//;s/"//') + echo "#### $suite_name" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Test | Status |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + + grep -o '> $GITHUB_STEP_SUMMARY + elif grep -q "name=\"${test}\".*> $GITHUB_STEP_SUMMARY + else + echo "| $test | :white_check_mark: Passed |" >> $GITHUB_STEP_SUMMARY + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + fi + done + fi + + if [ ! -f test-results/aggregate-results.txt ] && ! ls test-results/*.xml >/dev/null 2>&1; then + echo "_No test results available_" >> $GITHUB_STEP_SUMMARY + fi + + - name: Cleanup the lab + if: ${{ always() && (github.event_name == 'pull_request' || contains(fromJSON('["cleanup", "test"]'), github.event.inputs.mode)) }} + run: scripts/hyperconverged-lab-uninstall.sh talos diff --git a/.github/workflows/smoke-deploy-lab.yaml b/.github/workflows/smoke-deploy-lab.yaml index 9e3954927..9957370d6 100644 --- a/.github/workflows/smoke-deploy-lab.yaml +++ b/.github/workflows/smoke-deploy-lab.yaml @@ -68,6 +68,7 @@ env: GATEWAY_DOMAIN: cloud.local ACME_EMAIL: cloud@example.local OS_IMAGE: "Ubuntu 24.04" + OS_FLAVOR: gp.5.8.16 SSH_USERNAME: "ubuntu" TEST_LEVEL: "quick" @@ -98,7 +99,7 @@ jobs: run: | eval "$(ssh-agent -s)" export TEST_LEVEL="${{ github.event.inputs.test_level }}" - scripts/hyperconverged-lab.sh + scripts/hyperconverged-lab.sh kubespray - name: Retrieve Keys if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'deploy' }} @@ -190,4 +191,4 @@ jobs: - name: Cleanup the lab if: ${{ always() && (github.event_name == 'pull_request' || contains(fromJSON('["cleanup", "test"]'), github.event.inputs.mode)) }} - run: scripts/hyperconverged-lab-uninstall.sh + run: scripts/hyperconverged-lab-uninstall.sh kubespray diff --git a/bin/install-neutron.sh b/bin/install-neutron.sh index 15bef2021..734ac91a7 100755 --- a/bin/install-neutron.sh +++ b/bin/install-neutron.sh @@ -125,18 +125,9 @@ fi echo # Set connection string based on whether we use Kube-OVN TLS -# Hyperconverged build tries to execute the install script without yq in the -# path -function installYq() { - export VERSION=v4.47.2 - export BINARY=yq_linux_amd64 - wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -q -O - | tar xz && mv ${BINARY} /usr/local/bin/yq -} - -if ! yq --version 2> /dev/null; then - echo "yq is not installed. Attempting to install yq" - installYq -fi +# Source functions library for ensureYq +source "${GENESTACK_BASE_DIR}/scripts/lib/functions.sh" +ensureYq if helm -n kube-system get values kube-ovn \ | yq -e '.networking.ENABLE_SSL == true' >/dev/null 2>&1 diff --git a/bin/install-octavia.sh b/bin/install-octavia.sh index 6b1bdb413..755fc203d 100755 --- a/bin/install-octavia.sh +++ b/bin/install-octavia.sh @@ -124,18 +124,9 @@ fi echo # Set connection string based on whether we use Kube-OVN TLS -# Hyperconverged build tries to execute the install script without yq in the -# path -function installYq() { - export VERSION=v4.47.2 - export BINARY=yq_linux_amd64 - wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -q -O - | tar xz && mv ${BINARY} /usr/local/bin/yq -} - -if ! yq --version 2> /dev/null; then - echo "yq is not installed. Attempting to install yq" - installYq -fi +# Source functions library for ensureYq +source "${GENESTACK_BASE_DIR}/scripts/lib/functions.sh" +ensureYq if helm -n kube-system get values kube-ovn \ | yq -e '.networking.ENABLE_SSL == true' >/dev/null 2>&1 diff --git a/bin/setup-openstack-rc.sh b/bin/setup-openstack-rc.sh index e1ab788ea..38f4e9797 100755 --- a/bin/setup-openstack-rc.sh +++ b/bin/setup-openstack-rc.sh @@ -1,16 +1,11 @@ #!/usr/bin/env bash set -e -function installYq() { - export VERSION=v4.47.2 - export BINARY=yq_linux_amd64 - wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -q -O - | tar xz && mv ${BINARY} /usr/local/bin/yq -} - -if ! yq --version 2> /dev/null; then - echo "yq is not installed. Attempting to install yq" - installYq -fi +# Base directories provided by the environment +GENESTACK_BASE_DIR="${GENESTACK_BASE_DIR:-/opt/genestack}" + +# Source functions library for ensureYq +source "${GENESTACK_BASE_DIR}/scripts/lib/functions.sh" USER_NAME="$(whoami)" USER_PATH="$(getent passwd ${USER_NAME} | awk -F':' '{print $6}')" @@ -19,6 +14,8 @@ CONFIG_FILE="${CONFIG_PATH}/genestack-clouds.yaml" mkdir -p "${CONFIG_PATH}" +echo "Generating OpenStack RC file at: ${CONFIG_FILE}" + cat > "${CONFIG_FILE}" </dev/null && pwd) +source "${SCRIPT_DIR}/lib/hyperconverged-uninstall-common.sh" + +############################################################################# +# Initialize +############################################################################# + +promptForCloudConfig + +export LAB_NAME_PREFIX="${LAB_NAME_PREFIX:-hyperconverged}" + +############################################################################# +# Run Common Uninstall +############################################################################# + +runCommonUninstall "${LAB_NAME_PREFIX}" + +############################################################################# +# Kubespray-Specific: Delete SSH Keypair and Security Group +############################################################################# + +echo "Deleting Kubespray-specific resources..." +keypairDelete ${LAB_NAME_PREFIX}-key +securityGroupDelete ${LAB_NAME_PREFIX}-jump-secgroup + +############################################################################# +# Cleanup Complete +############################################################################# + +echo "Cleanup complete" +echo "The Kubespray lab uninstall took ${SECONDS} seconds to complete." +echo "" +echo "Note: Local SSH key files (~/.ssh/${LAB_NAME_PREFIX}-key.pem, ~/.ssh/${LAB_NAME_PREFIX}-key.pub)" +echo "were NOT removed. Delete them manually if no longer needed." diff --git a/scripts/hyperconverged-lab-kubespray.sh b/scripts/hyperconverged-lab-kubespray.sh new file mode 100755 index 000000000..935b69259 --- /dev/null +++ b/scripts/hyperconverged-lab-kubespray.sh @@ -0,0 +1,494 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2124,SC2145,SC2294,SC2086,SC2087,SC2155 +# +# Hyperconverged Lab Script for Kubespray +# +# This script deploys a fully automated Ubuntu-based Kubernetes cluster +# using Kubespray for running Genestack (OpenStack on Kubernetes) in a +# hyperconverged configuration. +# +# Platform: Ubuntu with Kubespray +# Kubernetes Setup: Kubespray via Ansible +# SSH Access: Required for remote node configuration +# + +set -o pipefail +set -e +SECONDS=0 + +# Source common library +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +source "${SCRIPT_DIR}/lib/hyperconverged-common.sh" + +############################################################################# +# Initialize +############################################################################# + +ensureYq +parseCommonArgs "$@" +promptForCommonInputs + +############################################################################# +# Ubuntu/Kubespray-Specific: Image and SSH Configuration +############################################################################# + +# Set the default image and ssh username +export OS_IMAGE="${OS_IMAGE:-Ubuntu 24.04}" +if [ -z "${SSH_USERNAME}" ]; then + if ! IMAGE_DEFAULT_PROPERTY=$(openstack image show "${OS_IMAGE}" -f json -c properties); then + read -rp "Image not found. Enter the image name: " OS_IMAGE + IMAGE_DEFAULT_PROPERTY=$(openstack image show "${OS_IMAGE}" -f json -c properties) + fi + if [ "${IMAGE_DEFAULT_PROPERTY}" ]; then + if SSH_USERNAME=$(echo "${IMAGE_DEFAULT_PROPERTY}" | jq -r '.properties.default_user'); then + echo "Discovered the default username for the image ${OS_IMAGE} as ${SSH_USERNAME}" + fi + fi + if [ -z "${SSH_USERNAME}" ] || [ "${SSH_USERNAME}" = "null" ]; then + echo "The image ${OS_IMAGE} does not have a default user property, please enter the default username" + read -rp "Enter the default username for the image: " SSH_USERNAME + fi +fi + +export LAB_NAME_PREFIX="${LAB_NAME_PREFIX:-hyperconverged}" + +############################################################################# +# Create OpenStack Infrastructure (Common) +############################################################################# + +createRouter +createNetworks +createCommonSecurityGroups + +############################################################################# +# Kubespray-Specific: Jump Host Security Group +############################################################################# + +if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup 2>/dev/null; then + openstack security group create ${LAB_NAME_PREFIX}-jump-secgroup +fi + +if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 22; then + openstack security group rule create ${LAB_NAME_PREFIX}-jump-secgroup \ + --protocol tcp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --dst-port 22 \ + --description "ssh" +fi +if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup -f json 2>/dev/null | jq -r '.rules.[].protocol' | grep -q icmp; then + openstack security group rule create ${LAB_NAME_PREFIX}-jump-secgroup \ + --protocol icmp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --description "ping" +fi + +############################################################################# +# Create Ports and Floating IPs +############################################################################# + +createMetalLBPort + +# Create management ports with jump host security group on first node +if ! WORKER_0_PORT=$(openstack port show ${LAB_NAME_PREFIX}-0-mgmt-port -f value -c id 2>/dev/null); then + export WORKER_0_PORT=$( + openstack port create --allowed-address ip-address=${METAL_LB_IP} \ + --security-group ${LAB_NAME_PREFIX}-secgroup \ + --security-group ${LAB_NAME_PREFIX}-jump-secgroup \ + --security-group ${LAB_NAME_PREFIX}-http-secgroup \ + --network ${LAB_NAME_PREFIX}-net \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-0-mgmt-port + ) +fi +export WORKER_0_PORT + +if ! WORKER_1_PORT=$(openstack port show ${LAB_NAME_PREFIX}-1-mgmt-port -f value -c id 2>/dev/null); then + export WORKER_1_PORT=$( + openstack port create --allowed-address ip-address=${METAL_LB_IP} \ + --security-group ${LAB_NAME_PREFIX}-secgroup \ + --security-group ${LAB_NAME_PREFIX}-http-secgroup \ + --network ${LAB_NAME_PREFIX}-net \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-1-mgmt-port + ) +fi +export WORKER_1_PORT + +if ! WORKER_2_PORT=$(openstack port show ${LAB_NAME_PREFIX}-2-mgmt-port -f value -c id 2>/dev/null); then + export WORKER_2_PORT=$( + openstack port create --allowed-address ip-address=${METAL_LB_IP} \ + --security-group ${LAB_NAME_PREFIX}-secgroup \ + --security-group ${LAB_NAME_PREFIX}-http-secgroup \ + --network ${LAB_NAME_PREFIX}-net \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-2-mgmt-port + ) +fi +export WORKER_2_PORT + +# Create floating IP for jump host (first node) +if ! JUMP_HOST_VIP=$(openstack floating ip list --port ${WORKER_0_PORT} -f json 2>/dev/null | jq -r '.[]."Floating IP Address"'); then + JUMP_HOST_VIP=$(openstack floating ip create PUBLICNET --port ${WORKER_0_PORT} -f json | jq -r '.floating_ip_address') +elif [ -z "${JUMP_HOST_VIP}" ]; then + JUMP_HOST_VIP=$(openstack floating ip create PUBLICNET --port ${WORKER_0_PORT} -f json | jq -r '.floating_ip_address') +fi +export JUMP_HOST_VIP + +createComputePorts + +############################################################################# +# Kubespray-Specific: SSH Key Management +############################################################################# + +if [ ! -d "~/.ssh" ]; then + echo "Creating the SSH directory" + mkdir -p ~/.ssh + chmod 700 ~/.ssh +fi +if ! openstack keypair show ${LAB_NAME_PREFIX}-key 2>/dev/null; then + if [ ! -f ~/.ssh/${LAB_NAME_PREFIX}-key.pem ]; then + openstack keypair create ${LAB_NAME_PREFIX}-key >~/.ssh/${LAB_NAME_PREFIX}-key.pem + chmod 600 ~/.ssh/${LAB_NAME_PREFIX}-key.pem + openstack keypair show ${LAB_NAME_PREFIX}-key --public-key >~/.ssh/${LAB_NAME_PREFIX}-key.pub + else + if [ -f ~/.ssh/${LAB_NAME_PREFIX}-key.pub ]; then + openstack keypair create ${LAB_NAME_PREFIX}-key --public-key ~/.ssh/${LAB_NAME_PREFIX}-key.pub + fi + fi +fi + +ssh-add ~/.ssh/${LAB_NAME_PREFIX}-key.pem + +############################################################################# +# Create Lab Instances +############################################################################# + +if ! openstack server show ${LAB_NAME_PREFIX}-0 2>/dev/null; then + openstack server create ${LAB_NAME_PREFIX}-0 \ + --port ${WORKER_0_PORT} \ + --port ${COMPUTE_0_PORT} \ + --image "${OS_IMAGE}" \ + --key-name ${LAB_NAME_PREFIX}-key \ + --flavor ${OS_FLAVOR} +fi + +if ! openstack server show ${LAB_NAME_PREFIX}-1 2>/dev/null; then + openstack server create ${LAB_NAME_PREFIX}-1 \ + --port ${WORKER_1_PORT} \ + --port ${COMPUTE_1_PORT} \ + --image "${OS_IMAGE}" \ + --key-name ${LAB_NAME_PREFIX}-key \ + --flavor ${OS_FLAVOR} +fi + +if ! openstack server show ${LAB_NAME_PREFIX}-2 2>/dev/null; then + openstack server create ${LAB_NAME_PREFIX}-2 \ + --port ${WORKER_2_PORT} \ + --port ${COMPUTE_2_PORT} \ + --image "${OS_IMAGE}" \ + --key-name ${LAB_NAME_PREFIX}-key \ + --flavor ${OS_FLAVOR} +fi + +############################################################################# +# Wait for Jump Host SSH Access +############################################################################# + +echo "Waiting for the jump host to be ready" +COUNT=0 +while ! ssh -o ConnectTimeout=2 -o ConnectionAttempts=3 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -q ${SSH_USERNAME}@${JUMP_HOST_VIP} exit; do + sleep 2 + echo "SSH is not ready, Trying again..." + COUNT=$((COUNT + 1)) + if [ $COUNT -gt 60 ]; then + echo "Failed to ssh into the jump host" + exit 1 + fi +done + +############################################################################# +# Kubespray-Specific: Development Mode Source Copy +############################################################################# + +if [ "${HYPERCONVERGED_DEV:-false}" = "true" ]; then + if [ ! -d "${SCRIPT_DIR}" ]; then + echo "HYPERCONVERGED_DEV is true, but we've failed to determine the base genestack directory" + exit 1 + fi + # NOTE: we are assuming an Ubuntu (apt) based instance here + ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} \ + "while sudo fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do echo 'Waiting for apt locks to be released...'; sleep 5; done && sudo apt-get update && sudo apt install -y rsync git" + echo "Copying the development source code to the jump host" + rsync -az \ + -e "ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ + --rsync-path="sudo rsync" \ + $(readlink -fn ${SCRIPT_DIR}/../) ${SSH_USERNAME}@${JUMP_HOST_VIP}:/opt/ +fi + +############################################################################# +# Kubespray-Specific: Remote Configuration via SSH +############################################################################# + +ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} < /etc/genestack/manifests/metallb/metallb-openstack-service-lb.yml < /etc/genestack/inventory/inventory.yaml </dev/null || echo "No test result XML files found" + scp -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no ${SSH_USERNAME}@${JUMP_HOST_VIP}:/tmp/test-results/*.txt ./test-results/ 2>/dev/null || echo "No test result text files found" +fi + +############################################################################# +# Output Summary +############################################################################# + +{ cat | tee /tmp/output.txt; } </dev/null && pwd) +source "${SCRIPT_DIR}/lib/hyperconverged-uninstall-common.sh" + +############################################################################# +# Initialize +############################################################################# + +promptForCloudConfig + +export LAB_NAME_PREFIX="${LAB_NAME_PREFIX:-talos-hyperconverged}" + +############################################################################# +# Run Common Uninstall +############################################################################# + +runCommonUninstall "${LAB_NAME_PREFIX}" + +############################################################################# +# Talos-Specific: Delete Jump Host, Security Groups, and Keypair +############################################################################# + +echo "Deleting Talos-specific resources..." + +# Delete jump host +serverDelete ${LAB_NAME_PREFIX}-jump +portDelete ${LAB_NAME_PREFIX}-jump-mgmt-port + +# Delete security groups +securityGroupDelete ${LAB_NAME_PREFIX}-talos-secgroup +securityGroupDelete ${LAB_NAME_PREFIX}-jump-secgroup + +# Delete keypair +keypairDelete ${LAB_NAME_PREFIX}-key + +############################################################################# +# Optional: Remove Talos Image from Glance +############################################################################# + +read -rp "Do you want to remove the Talos image from Glance? [y/N]: " REMOVE_IMAGE +if [[ "${REMOVE_IMAGE}" =~ ^[Yy]$ ]]; then + TALOS_VERSION="${TALOS_VERSION:-v1.11.5}" + TALOS_IMAGE_NAME="${TALOS_IMAGE_NAME:-talos-${TALOS_VERSION}-genestack}" + if openstack image show "${TALOS_IMAGE_NAME}" 2>/dev/null; then + openstack image delete "${TALOS_IMAGE_NAME}" + echo "Talos image '${TALOS_IMAGE_NAME}' deleted" + else + echo "Talos image '${TALOS_IMAGE_NAME}' not found" + fi +fi + +############################################################################# +# Cleanup Complete +############################################################################# + +echo "Cleanup complete" +echo "The Talos lab uninstall took ${SECONDS} seconds to complete." +echo "" +echo "Note: Local SSH key files (~/.ssh/${LAB_NAME_PREFIX}-key.pem, ~/.ssh/${LAB_NAME_PREFIX}-key.pub)" +echo "were NOT removed. Delete them manually if no longer needed." diff --git a/scripts/hyperconverged-lab-talos.sh b/scripts/hyperconverged-lab-talos.sh new file mode 100755 index 000000000..81790003c --- /dev/null +++ b/scripts/hyperconverged-lab-talos.sh @@ -0,0 +1,836 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2124,SC2145,SC2294,SC2086,SC2087,SC2155 +# +# Hyperconverged Lab Script for Talos Linux +# +# This script deploys a fully automated Talos Linux-based Kubernetes cluster +# for running Genestack (OpenStack on Kubernetes) in a hyperconverged configuration. +# +# Platform: Talos Linux +# Kubernetes Setup: talosctl (Talos native) +# Architecture: +# - Jump host for running talosctl, kubectl, and genestack setup +# - 3 Talos Linux nodes for the Kubernetes cluster +# Key differences from Kubespray: +# - Uses Talos Linux instead of Ubuntu for K8s nodes +# - Automatically downloads Talos image with required extensions from Talos Factory +# - Uses talosctl for cluster management instead of kubespray/SSH +# - Includes Talos-specific configurations for Longhorn, Kube-OVN, and Ceph Rook +# - Requires manual cert-manager installation (not included in Talos like kubespray) +# + +set -o pipefail +set -e +SECONDS=0 + +# Source common library +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +source "${SCRIPT_DIR}/lib/hyperconverged-common.sh" + +############################################################################# +# Talos-Specific Configuration +############################################################################# + +export TALOS_VERSION="${TALOS_VERSION:-v1.11.5}" +export TALOS_ARCH="${TALOS_ARCH:-amd64}" +# Talos Factory schematic ID with iscsi-tools and util-linux-tools extensions for Longhorn +# This schematic includes: siderolabs/iscsi-tools, siderolabs/util-linux-tools siderolabs/qemu-guest-agent +export TALOS_SCHEMATIC_ID="${TALOS_SCHEMATIC_ID:-88d1f7a5c4f1d3aba7df787c448c1d3d008ed29cfb34af53fa0df4336a56040b}" +export TALOS_IMAGE_NAME="${TALOS_IMAGE_NAME:-talos-${TALOS_VERSION}-genestack}" +export TALOS_CLUSTER_NAME="${TALOS_CLUSTER_NAME:-genestack-talos}" + +# Jump host configuration (Ubuntu-based small instance) +export JUMP_HOST_IMAGE="${JUMP_HOST_IMAGE:-Ubuntu 24.04}" + +############################################################################# +# Talos-Specific Functions +############################################################################# + +function installTalosctl() { + echo "Installing talosctl..." + curl -sL https://talos.dev/install | sh +} + +function selectJumpHostFlavor() { + # Select a small flavor for the jump host (~2 cores, ~2GB RAM, minimal disk) + # This is a lightweight VM just for running talosctl, kubectl, and genestack scripts + if [ -z "${JUMP_HOST_FLAVOR}" ]; then + # List small flavors: 1-4GB RAM, any disk size, sorted by RAM ascending + SMALL_FLAVORS=$(openstack flavor list --sort-column RAM -c Name -c RAM -c Disk -c VCPUs -f json 2>/dev/null || echo "[]") + + # Try to find a flavor with ~2GB RAM (1536-4096 MB range) and 1-2 vCPUs + DEFAULT_JUMP_FLAVOR=$(echo "${SMALL_FLAVORS}" | jq -r ' + [.[] | select(.RAM >= 1536 and .RAM <= 4096 and .VCPUs <= 2 and .Disk >= 10)] | + sort_by(.RAM) | + .[0].Name // empty + ') + + # If no ideal flavor found, try broader search (up to 8GB RAM, up to 4 vCPUs) + if [ -z "${DEFAULT_JUMP_FLAVOR}" ] || [ "${DEFAULT_JUMP_FLAVOR}" = "null" ]; then + DEFAULT_JUMP_FLAVOR=$(echo "${SMALL_FLAVORS}" | jq -r ' + [.[] | select(.RAM >= 1024 and .RAM <= 8192 and .VCPUs <= 4 and .Disk >= 10)] | + sort_by(.RAM) | + .[0].Name // empty + ') + fi + + # If still nothing, just pick the smallest available + if [ -z "${DEFAULT_JUMP_FLAVOR}" ] || [ "${DEFAULT_JUMP_FLAVOR}" = "null" ]; then + DEFAULT_JUMP_FLAVOR=$(echo "${SMALL_FLAVORS}" | jq -r ' + [.[] | select(.Disk >= 10)] | + sort_by(.RAM) | + .[0].Name // empty + ') + fi + + if [ -z "${DEFAULT_JUMP_FLAVOR}" ] || [ "${DEFAULT_JUMP_FLAVOR}" = "null" ]; then + echo "ERROR: Could not find a suitable flavor for the jump host" + echo "Please set JUMP_HOST_FLAVOR environment variable manually" + exit 1 + fi + + echo "" + echo "Jump host flavor selection (small instance for management):" + echo "${SMALL_FLAVORS}" | jq -r ' + [.[] | select(.RAM <= 8192)] | + sort_by(.RAM) | + .[:10] | + ["Name", "RAM", "Disk", "VCPUs"], (.[] | [.Name, .RAM, .Disk, .VCPUs]) | + @tsv + ' | column -t + echo "" + read -rp "Enter flavor for jump host [${DEFAULT_JUMP_FLAVOR}]: " JUMP_HOST_FLAVOR + export JUMP_HOST_FLAVOR="${JUMP_HOST_FLAVOR:-${DEFAULT_JUMP_FLAVOR}}" + fi +} + +function detectJumpHostSSHUsername() { + # Detect SSH username for the jump host image + if [ -z "${SSH_USERNAME}" ]; then + if ! IMAGE_DEFAULT_PROPERTY=$(openstack image show "${JUMP_HOST_IMAGE}" -f json -c properties 2>/dev/null); then + read -rp "Jump host image '${JUMP_HOST_IMAGE}' not found. Enter the image name: " JUMP_HOST_IMAGE + IMAGE_DEFAULT_PROPERTY=$(openstack image show "${JUMP_HOST_IMAGE}" -f json -c properties) + fi + if [ "${IMAGE_DEFAULT_PROPERTY}" ]; then + if SSH_USERNAME=$(echo "${IMAGE_DEFAULT_PROPERTY}" | jq -r '.properties.default_user // empty'); then + if [ -n "${SSH_USERNAME}" ] && [ "${SSH_USERNAME}" != "null" ]; then + echo "Discovered the default username for the jump host image as ${SSH_USERNAME}" + fi + fi + fi + if [ -z "${SSH_USERNAME}" ] || [ "${SSH_USERNAME}" = "null" ]; then + echo "The image ${JUMP_HOST_IMAGE} does not have a default user property" + read -rp "Enter the default username for the jump host image [ubuntu]: " SSH_USERNAME + SSH_USERNAME="${SSH_USERNAME:-ubuntu}" + fi + fi + export SSH_USERNAME +} + +function ensureTalosctl() { + if ! talosctl version --client 2> /dev/null; then + echo "talosctl is not installed. Attempting to install talosctl" + installTalosctl + fi +} + +function downloadTalosImage() { + local image_url="https://factory.talos.dev/image/${TALOS_SCHEMATIC_ID}/${TALOS_VERSION}/openstack-${TALOS_ARCH}.raw.xz" + local image_file="/tmp/talos-${TALOS_VERSION}-openstack-${TALOS_ARCH}.raw.xz" + local raw_file="/tmp/talos-${TALOS_VERSION}-openstack-${TALOS_ARCH}.raw" + + echo "Downloading Talos image from Talos Factory..." + echo "URL: ${image_url}" + echo "This image includes extensions: iscsi-tools, util-linux-tools (required for Longhorn)" + + if [ ! -f "${raw_file}" ]; then + if [ ! -f "${image_file}" ]; then + curl -L -o "${image_file}" "${image_url}" + fi + echo "Decompressing Talos image..." + xz -d -k "${image_file}" + else + echo "Talos image already downloaded and decompressed" + fi +} + +function uploadTalosImage() { + local raw_file="$1" + + echo "Uploading Talos image to Glance as '${TALOS_IMAGE_NAME}'..." + openstack image create "${TALOS_IMAGE_NAME}" \ + --disk-format raw \ + --container-format bare \ + --file "${raw_file}" \ + --property os_type=linux \ + --property os_distro=talos \ + --property os_version="${TALOS_VERSION}" \ + --property hw_vif_multiqueue_enabled=true \ + --property hw_qemu_guest_agent=yes \ + --property hypervisor_type=kvm \ + --property img_config_drive=optional \ + --property hw_machine_type=q35 \ + --property hw_firmware_type=uefi \ + --property os_require_quiesce=yes \ + --property os_type=linux \ + --property os_admin_user=talos \ + --property os_distro=talos \ + --property os_version=18.2 \ + --tag "siderolabs/iscsi-tools" \ + --tag "siderolabs/util-linux-tools" \ + --tag "siderolabs/qemu-guest-agent" \ + --progress + + echo "Talos image uploaded successfully" +} + +function createTalosSecurityGroup() { + # Create Talos-specific security group (API + K8s API + ICMP) + if ! openstack security group show ${LAB_NAME_PREFIX}-talos-secgroup 2>/dev/null; then + openstack security group create ${LAB_NAME_PREFIX}-talos-secgroup + fi + + # Talos API port (50000) + if ! openstack security group show ${LAB_NAME_PREFIX}-talos-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 50000; then + openstack security group rule create ${LAB_NAME_PREFIX}-talos-secgroup \ + --protocol tcp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --dst-port 50000 \ + --description "talos-api" + fi + + # Kubernetes API port (6443) + if ! openstack security group show ${LAB_NAME_PREFIX}-talos-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 6443; then + openstack security group rule create ${LAB_NAME_PREFIX}-talos-secgroup \ + --protocol tcp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --dst-port 6443 \ + --description "kubernetes-api" + fi + + if ! openstack security group show ${LAB_NAME_PREFIX}-talos-secgroup -f json 2>/dev/null | jq -r '.rules.[].protocol' | grep -q icmp; then + openstack security group rule create ${LAB_NAME_PREFIX}-talos-secgroup \ + --protocol icmp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --description "ping" + fi +} + +function writeKubeOvnTalosConfig() { + # Configure Kube-OVN for Talos + # Talos requires specific settings: OPENVSWITCH_DIR, OVN_DIR, DISABLE_MODULES_MANAGEMENT + local config_path="${1:-/etc/genestack/helm-configs/kube-ovn/kube-ovn-helm-overrides.yaml}" + + cat > "${config_path}" < "${overlay_dir}/namespace-talos.yaml" < "${overlay_dir}/kustomization.yaml" </dev/null; then + echo "Talos image '${TALOS_IMAGE_NAME}' not found in Glance" + downloadTalosImage + uploadTalosImage "/tmp/talos-${TALOS_VERSION}-openstack-${TALOS_ARCH}.raw" +else + echo "Talos image '${TALOS_IMAGE_NAME}' already exists in Glance" +fi + +export LAB_NAME_PREFIX="${LAB_NAME_PREFIX:-talos-hyperconverged}" + +############################################################################# +# Create OpenStack Infrastructure (Common) +############################################################################# + +createRouter +createNetworks +createCommonSecurityGroups +createTalosSecurityGroup + +############################################################################# +# Jump Host Security Group (SSH access) +############################################################################# + +if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup 2>/dev/null; then + openstack security group create ${LAB_NAME_PREFIX}-jump-secgroup +fi + +if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 22; then + openstack security group rule create ${LAB_NAME_PREFIX}-jump-secgroup \ + --protocol tcp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --dst-port 22 \ + --description "ssh" +fi +if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup -f json 2>/dev/null | jq -r '.rules.[].protocol' | grep -q icmp; then + openstack security group rule create ${LAB_NAME_PREFIX}-jump-secgroup \ + --protocol icmp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --description "ping" +fi + +createMetalLBPort + +############################################################################# +# Jump Host SSH Key Management +############################################################################# + +if [ ! -d ~/.ssh ]; then + echo "Creating the SSH directory" + mkdir -p ~/.ssh + chmod 700 ~/.ssh +fi + +if ! openstack keypair show ${LAB_NAME_PREFIX}-key 2>/dev/null; then + if [ ! -f ~/.ssh/${LAB_NAME_PREFIX}-key.pem ]; then + openstack keypair create ${LAB_NAME_PREFIX}-key >~/.ssh/${LAB_NAME_PREFIX}-key.pem + chmod 600 ~/.ssh/${LAB_NAME_PREFIX}-key.pem + openstack keypair show ${LAB_NAME_PREFIX}-key --public-key >~/.ssh/${LAB_NAME_PREFIX}-key.pub + else + if [ -f ~/.ssh/${LAB_NAME_PREFIX}-key.pub ]; then + openstack keypair create ${LAB_NAME_PREFIX}-key --public-key ~/.ssh/${LAB_NAME_PREFIX}-key.pub + fi + fi +fi + +ssh-add ~/.ssh/${LAB_NAME_PREFIX}-key.pem + +############################################################################# +# Jump Host Port and Instance +############################################################################# + +if ! JUMP_HOST_PORT=$(openstack port show ${LAB_NAME_PREFIX}-jump-mgmt-port -f value -c id 2>/dev/null); then + export JUMP_HOST_PORT=$( + openstack port create \ + --security-group ${LAB_NAME_PREFIX}-secgroup \ + --security-group ${LAB_NAME_PREFIX}-jump-secgroup \ + --network ${LAB_NAME_PREFIX}-net \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-jump-mgmt-port + ) +fi +export JUMP_HOST_PORT + +# Floating IP for jump host +if ! JUMP_HOST_VIP=$(openstack floating ip list --port ${JUMP_HOST_PORT} -f json 2>/dev/null | jq -r '.[]."Floating IP Address"'); then + JUMP_HOST_VIP=$(openstack floating ip create PUBLICNET --port ${JUMP_HOST_PORT} -f json | jq -r '.floating_ip_address') +elif [ -z "${JUMP_HOST_VIP}" ]; then + JUMP_HOST_VIP=$(openstack floating ip create PUBLICNET --port ${JUMP_HOST_PORT} -f json | jq -r '.floating_ip_address') +fi +export JUMP_HOST_VIP + +# Create jump host instance +if ! openstack server show ${LAB_NAME_PREFIX}-jump 2>/dev/null; then + echo "Creating jump host instance..." + openstack server create ${LAB_NAME_PREFIX}-jump \ + --port ${JUMP_HOST_PORT} \ + --image "${JUMP_HOST_IMAGE}" \ + --key-name ${LAB_NAME_PREFIX}-key \ + --flavor ${JUMP_HOST_FLAVOR} +fi + +############################################################################# +# Talos-Specific: Create Management Ports with Talos Security Group +############################################################################# + +if ! WORKER_0_PORT=$(openstack port show ${LAB_NAME_PREFIX}-0-mgmt-port -f value -c id 2>/dev/null); then + export WORKER_0_PORT=$( + openstack port create --allowed-address ip-address=${METAL_LB_IP} \ + --security-group ${LAB_NAME_PREFIX}-secgroup \ + --security-group ${LAB_NAME_PREFIX}-talos-secgroup \ + --security-group ${LAB_NAME_PREFIX}-http-secgroup \ + --network ${LAB_NAME_PREFIX}-net \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-0-mgmt-port + ) +fi +export WORKER_0_PORT + +if ! WORKER_1_PORT=$(openstack port show ${LAB_NAME_PREFIX}-1-mgmt-port -f value -c id 2>/dev/null); then + export WORKER_1_PORT=$( + openstack port create --allowed-address ip-address=${METAL_LB_IP} \ + --security-group ${LAB_NAME_PREFIX}-secgroup \ + --security-group ${LAB_NAME_PREFIX}-http-secgroup \ + --network ${LAB_NAME_PREFIX}-net \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-1-mgmt-port + ) +fi +export WORKER_1_PORT + +if ! WORKER_2_PORT=$(openstack port show ${LAB_NAME_PREFIX}-2-mgmt-port -f value -c id 2>/dev/null); then + export WORKER_2_PORT=$( + openstack port create --allowed-address ip-address=${METAL_LB_IP} \ + --security-group ${LAB_NAME_PREFIX}-secgroup \ + --security-group ${LAB_NAME_PREFIX}-http-secgroup \ + --network ${LAB_NAME_PREFIX}-net \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-2-mgmt-port + ) +fi +export WORKER_2_PORT + +# Get the IPs for nodes +WORKER_0_IP=$(openstack port show ${WORKER_0_PORT} -f json | jq -r '.fixed_ips[0].ip_address') +WORKER_1_IP=$(openstack port show ${WORKER_1_PORT} -f json | jq -r '.fixed_ips[0].ip_address') +WORKER_2_IP=$(openstack port show ${WORKER_2_PORT} -f json | jq -r '.fixed_ips[0].ip_address') + +# Floating IP for first node (control plane access) +if ! CONTROL_PLANE_VIP=$(openstack floating ip list --port ${WORKER_0_PORT} -f json 2>/dev/null | jq -r '.[]."Floating IP Address"'); then + CONTROL_PLANE_VIP=$(openstack floating ip create PUBLICNET --port ${WORKER_0_PORT} -f json | jq -r '.floating_ip_address') +elif [ -z "${CONTROL_PLANE_VIP}" ]; then + CONTROL_PLANE_VIP=$(openstack floating ip create PUBLICNET --port ${WORKER_0_PORT} -f json | jq -r '.floating_ip_address') +fi +export CONTROL_PLANE_VIP + +createComputePorts + +############################################################################# +# Create Talos Instances (No SSH key needed - Talos uses API) +############################################################################# + +if ! openstack server show ${LAB_NAME_PREFIX}-0 2>/dev/null; then + openstack server create ${LAB_NAME_PREFIX}-0 \ + --port ${WORKER_0_PORT} \ + --port ${COMPUTE_0_PORT} \ + --image "${TALOS_IMAGE_NAME}" \ + --flavor ${OS_FLAVOR} +fi + +if ! openstack server show ${LAB_NAME_PREFIX}-1 2>/dev/null; then + openstack server create ${LAB_NAME_PREFIX}-1 \ + --port ${WORKER_1_PORT} \ + --port ${COMPUTE_1_PORT} \ + --image "${TALOS_IMAGE_NAME}" \ + --flavor ${OS_FLAVOR} +fi + +if ! openstack server show ${LAB_NAME_PREFIX}-2 2>/dev/null; then + openstack server create ${LAB_NAME_PREFIX}-2 \ + --port ${WORKER_2_PORT} \ + --port ${COMPUTE_2_PORT} \ + --image "${TALOS_IMAGE_NAME}" \ + --flavor ${OS_FLAVOR} +fi + +############################################################################# +# Wait for Jump Host SSH Access +############################################################################# + +echo "Waiting for the jump host to be ready..." +COUNT=0 +while ! ssh -o ConnectTimeout=2 -o ConnectionAttempts=3 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -q ${SSH_USERNAME}@${JUMP_HOST_VIP} exit; do + sleep 2 + echo "SSH is not ready, Trying again..." + COUNT=$((COUNT + 1)) + if [ $COUNT -gt 60 ]; then + echo "Failed to ssh into the jump host" + exit 1 + fi +done + +echo "Jump host is reachable at ${JUMP_HOST_VIP}" + +############################################################################# +# Install Prerequisites on Jump Host +############################################################################# + +echo "Installing prerequisites on jump host..." +ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} <<'EOFPREREQ' +set -e +# Wait for apt locks to be released +while sudo fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do + echo 'Waiting for apt locks to be released...' + sleep 5 +done + +# Install required packages +sudo apt-get update +sudo apt-get install -y curl wget git jq netcat-openbsd xz-utils + +# Install yq +if ! yq --version 2>/dev/null; then + echo "Installing yq..." + export VERSION=v4.2.0 + export BINARY=yq_linux_amd64 + wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -q -O - | tar xz + sudo mv ${BINARY} /usr/local/bin/yq +fi + +# Install talosctl +if ! talosctl version --client 2>/dev/null; then + echo "Installing talosctl..." + curl -sL https://talos.dev/install | sh +fi + +# Install kubectl +if ! kubectl version --client 2>/dev/null; then + echo "Installing kubectl..." + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + rm kubectl +fi +EOFPREREQ + +############################################################################# +# Wait for Talos API (from jump host perspective using internal IPs) +############################################################################# + +echo "Waiting for Talos nodes to boot and become reachable..." +ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} </dev/null; do + sleep 5 + echo "Waiting for Talos API on ${WORKER_0_IP}:50000..." + COUNT=\$((COUNT + 1)) + if [ \$COUNT -gt 60 ]; then + echo "Failed to reach Talos API on control plane node" + exit 1 + fi +done +echo "Talos API is reachable on ${WORKER_0_IP}" +EOFWAIT + +############################################################################# +# Generate and Apply Talos Configuration (on jump host) +############################################################################# + +echo "Generating and applying Talos configuration from jump host..." +ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} < "\${TALOS_CONFIG_DIR}/genestack-patch.yaml" <<'EOFPATCH' +machine: + kubelet: + extraMounts: + # Required for Longhorn storage + - destination: /var/lib/longhorn + type: bind + source: /var/lib/longhorn + options: + - bind + - rshared + - rw + # Allow privileged workloads (required for OpenStack) + sysctls: + net.core.somaxconn: "65535" + net.ipv4.ip_forward: "1" + vm.max_map_count: "262144" + # Install disk configuration + install: + disk: /dev/sda + wipe: false +cluster: + # Allow scheduling on control plane nodes (hyperconverged) + allowSchedulingOnControlPlanes: true + network: + cni: + name: none # We'll install kube-ovn separately + proxy: + disabled: false + # Inline manifests for cert-manager (required since kubespray is not used) + inlineManifests: [] +EOFPATCH + + # Patch the controlplane.yaml with our Genestack requirements + yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' \\ + "\${TALOS_CONFIG_DIR}/controlplane.yaml" \\ + "\${TALOS_CONFIG_DIR}/genestack-patch.yaml" > "\${TALOS_CONFIG_DIR}/controlplane-patched.yaml" + + mv "\${TALOS_CONFIG_DIR}/controlplane-patched.yaml" "\${TALOS_CONFIG_DIR}/controlplane.yaml" + + # For worker nodes, apply same patches + yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' \\ + "\${TALOS_CONFIG_DIR}/worker.yaml" \\ + "\${TALOS_CONFIG_DIR}/genestack-patch.yaml" > "\${TALOS_CONFIG_DIR}/worker-patched.yaml" + + mv "\${TALOS_CONFIG_DIR}/worker-patched.yaml" "\${TALOS_CONFIG_DIR}/worker.yaml" +fi + +# Set talosctl configuration +export TALOSCONFIG="\${TALOS_CONFIG_DIR}/talosconfig" +talosctl config endpoint ${WORKER_0_IP} +talosctl config node ${WORKER_0_IP} + +echo "Applying Talos configuration to control plane nodes..." +talosctl apply-config --insecure --nodes ${WORKER_0_IP} --file "\${TALOS_CONFIG_DIR}/controlplane.yaml" + +sleep 10 + +talosctl apply-config --insecure --nodes ${WORKER_1_IP} --file "\${TALOS_CONFIG_DIR}/controlplane.yaml" +talosctl apply-config --insecure --nodes ${WORKER_2_IP} --file "\${TALOS_CONFIG_DIR}/controlplane.yaml" + +echo "Waiting for nodes to apply configuration..." +sleep 30 + +echo "Bootstrapping Talos Kubernetes cluster..." +if ! talosctl bootstrap --nodes ${WORKER_0_IP} 2>/dev/null; then + echo "Cluster may already be bootstrapped or still initializing, checking status..." +fi + +echo "Waiting for Kubernetes cluster to be ready..." +COUNT=0 +while talosctl services | awk '{print \$4}' | grep -i wait; do + sleep 10 + echo "Cluster not yet healthy, waiting..." + COUNT=\$((COUNT + 1)) + if [ \$COUNT -gt 30 ]; then + echo "Cluster health check timed out, continuing anyway..." + break + fi +done + +echo "Retrieving kubeconfig..." +mkdir -p ~/.kube +talosctl kubeconfig --nodes ${WORKER_0_IP} --force ~/.kube/config +EOFTALOS + +############################################################################# +# Talos-Specific: Development Mode Source Copy +############################################################################# + +if [ "${HYPERCONVERGED_DEV:-false}" = "true" ]; then + if [ ! -d "${SCRIPT_DIR}" ]; then + echo "HYPERCONVERGED_DEV is true, but we've failed to determine the base genestack directory" + exit 1 + fi + # NOTE: we are assuming an Ubuntu (apt) based instance here + ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} \ + "while sudo fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do echo 'Waiting for apt locks to be released...'; sleep 5; done && sudo apt-get update && sudo apt install -y rsync git" + echo "Copying the development source code to the jump host" + rsync -az \ + -e "ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ + --rsync-path="sudo rsync" \ + $(readlink -fn ${SCRIPT_DIR}/../) ${SSH_USERNAME}@${JUMP_HOST_VIP}:/opt/ +fi + +############################################################################# +# Install cert-manager and Clone Genestack (on jump host) +############################################################################# + +echo "Installing cert-manager and setting up Genestack on jump host..." +ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} < /etc/genestack/helm-configs/kube-ovn/kube-ovn-helm-overrides.yaml < /etc/genestack/kustomize/rook-operator/overlay/namespace-talos.yaml < /etc/genestack/kustomize/rook-operator/overlay/kustomization.yaml </dev/null && pwd) -export LAB_NAME_PREFIX="${LAB_NAME_PREFIX:-hyperconverged}" +function show_usage() { + cat < /dev/null; then - echo "Failed to delete server ${1}" - fi -} +This script removes all resources created by the hyperconverged lab deployment scripts. -function portDelete() { - if ! openstack port delete "${1}" 2> /dev/null; then - echo "Failed to delete port ${1}" - fi -} +USAGE: + $(basename "$0") [PLATFORM] -function securityGroupDelete() { - if ! openstack security group delete "${1}" 2> /dev/null; then - echo "Failed to delete security group ${1}" - fi -} +PLATFORMS: + kubespray Uninstall Kubespray lab deployment + talos Uninstall Talos Linux lab deployment + help Show this help message + +ENVIRONMENT VARIABLES: + OS_CLOUD OpenStack cloud configuration name (will prompt if not set) + LAB_NAME_PREFIX Prefix used during deployment (default: hyperconverged or talos-hyperconverged) + +EXAMPLES: + # Interactive mode - will prompt for platform choice + $(basename "$0") + + # Uninstall Kubespray deployment + $(basename "$0") kubespray -function networkDelete() { - if ! openstack network delete "${1}" 2> /dev/null; then - echo "Failed to delete network ${1}" - fi + # Uninstall Talos deployment + $(basename "$0") talos + + # Uninstall with custom prefix + LAB_NAME_PREFIX=my-lab $(basename "$0") kubespray + +For more information, see the Genestack documentation. +EOF } -function subnetDelete() { - if ! openstack subnet delete "${1}" 2> /dev/null; then - echo "Failed to delete subnet ${1}" - fi +function prompt_for_platform() { + echo "" + echo "Hyperconverged Lab Uninstall" + echo "============================" + echo "" + echo "Select which deployment to uninstall:" + echo "" + echo " 1) Kubespray - LAB_NAME_PREFIX default: hyperconverged" + echo " 2) Talos Linux - LAB_NAME_PREFIX default: talos-hyperconverged" + echo "" + + read -rp "Enter your choice [1/2]: " choice + + case "$choice" in + 1|kubespray|Kubespray|KUBESPRAY) + echo "" + echo "Selected: Kubespray" + PLATFORM="kubespray" + ;; + 2|talos|Talos|TALOS) + echo "" + echo "Selected: Talos Linux" + PLATFORM="talos" + ;; + *) + echo "Invalid choice. Please enter 1 or 2." + exit 1 + ;; + esac } -for i in $(openstack floating ip list --router ${LAB_NAME_PREFIX}-router -f value -c "Floating IP Address"); do - if ! openstack floating ip unset "${i}" 2> /dev/null; then - echo "Failed to unset floating ip ${i}" - fi - if ! openstack floating ip delete "${i}" 2> /dev/null; then - echo "Failed to delete floating ip ${i}" - fi -done - -serverDelete ${LAB_NAME_PREFIX}-2 -serverDelete ${LAB_NAME_PREFIX}-1 -serverDelete ${LAB_NAME_PREFIX}-0 - -if ! openstack keypair delete ${LAB_NAME_PREFIX}-key 2> /dev/null; then - echo "Failed to delete keypair ${LAB_NAME_PREFIX}-key" +# Check for help flag first +if [[ "$1" == "help" || "$1" == "--help" || "$1" == "-h" ]]; then + show_usage + exit 0 fi -portDelete ${LAB_NAME_PREFIX}-2-compute-port -portDelete ${LAB_NAME_PREFIX}-1-compute-port -portDelete ${LAB_NAME_PREFIX}-0-compute-port -for i in {100..109}; do - portDelete "${LAB_NAME_PREFIX}-0-compute-float-${i}-port" -done -portDelete ${LAB_NAME_PREFIX}-2-mgmt-port -portDelete ${LAB_NAME_PREFIX}-1-mgmt-port -portDelete ${LAB_NAME_PREFIX}-0-mgmt-port -portDelete ${LAB_NAME_PREFIX}-metallb-vip-0-port - -securityGroupDelete ${LAB_NAME_PREFIX}-jump-secgroup -securityGroupDelete ${LAB_NAME_PREFIX}-http-secgroup -securityGroupDelete ${LAB_NAME_PREFIX}-secgroup - -if ! openstack router remove subnet ${LAB_NAME_PREFIX}-router ${LAB_NAME_PREFIX}-subnet 2> /dev/null; then - echo "Failed to remove ${LAB_NAME_PREFIX}-subnet from router ${LAB_NAME_PREFIX}-router" -fi -if ! openstack router remove subnet ${LAB_NAME_PREFIX}-router ${LAB_NAME_PREFIX}-compute-subnet 2> /dev/null; then - echo "Failed to remove ${LAB_NAME_PREFIX}-compute-subnet from router ${LAB_NAME_PREFIX}-router" -fi -if ! openstack router remove gateway ${LAB_NAME_PREFIX}-router PUBLICNET 2> /dev/null; then - echo "Failed to remove gateway from router ${LAB_NAME_PREFIX}-router" +# Determine platform from first argument or prompt +if [[ -n "$1" && "$1" != -* ]]; then + case "$1" in + kubespray|Kubespray|KUBESPRAY) + PLATFORM="kubespray" + shift + ;; + talos|Talos|TALOS) + PLATFORM="talos" + shift + ;; + *) + echo "Unknown platform: $1" + echo "" + show_usage + exit 1 + ;; + esac +else + prompt_for_platform fi -if ! openstack router delete ${LAB_NAME_PREFIX}-router 2> /dev/null; then - echo "Failed to delete router ${LAB_NAME_PREFIX}-router" -fi - -subnetDelete ${LAB_NAME_PREFIX}-compute-subnet -subnetDelete ${LAB_NAME_PREFIX}-subnet - -networkDelete ${LAB_NAME_PREFIX}-compute-net -networkDelete ${LAB_NAME_PREFIX}-net -echo "Cleanup complete" -echo "The lab uninstall took ${SECONDS} seconds to complete." +# Execute the appropriate platform-specific uninstall script +case "$PLATFORM" in + kubespray) + echo "" + echo "Launching Kubespray uninstall..." + echo "" + exec "${SCRIPT_DIR}/hyperconverged-lab-kubespray-uninstall.sh" "$@" + ;; + talos) + echo "" + echo "Launching Talos Linux uninstall..." + echo "" + exec "${SCRIPT_DIR}/hyperconverged-lab-talos-uninstall.sh" "$@" + ;; +esac diff --git a/scripts/hyperconverged-lab.sh b/scripts/hyperconverged-lab.sh index 526383824..5812943aa 100755 --- a/scripts/hyperconverged-lab.sh +++ b/scripts/hyperconverged-lab.sh @@ -1,1341 +1,177 @@ #!/usr/bin/env bash # shellcheck disable=SC2124,SC2145,SC2294,SC2086,SC2087,SC2155 +# +# Hyperconverged Lab Deployment Selector +# +# This script provides a simple interface to deploy Genestack (OpenStack on Kubernetes) +# in a hyperconverged configuration using either: +# +# 1. Kubespray - Traditional approach using Ubuntu VMs and Kubespray/Ansible +# 2. Talos Linux - Modern approach using Talos Linux immutable OS +# +# Usage: +# ./hyperconverged-lab.sh # Interactive mode - prompts for platform +# ./hyperconverged-lab.sh kubespray [args] # Deploy using Kubespray +# ./hyperconverged-lab.sh talos [args] # Deploy using Talos Linux +# +# For uninstall, use the corresponding uninstall scripts: +# ./hyperconverged-lab-kubespray-uninstall.sh +# ./hyperconverged-lab-talos-uninstall.sh +# set -o pipefail set -e -SECONDS=0 -RUN_EXTRAS=0 -INCLUDE_LIST=() -EXCLUDE_LIST=() - -export TEST_LEVEL="${TEST_LEVEL:-off}" - -function installYq() { - export VERSION=v4.2.0 - export BINARY=yq_linux_amd64 - wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -q -O - | tar xz && sudo mv ${BINARY} /usr/local/bin/yq -} - -# Install yq locally if needed... -if ! yq --version 2> /dev/null; then - echo "yq is not installed. Attempting to install yq" - installYq -fi - - -# Default openstack components file -# this controls which openstack service will be installed -##...needed until default config is upstream... -OS_CONFIG=" -components: - keystone: true - glance: true - heat: false - barbican: false - blazar: false - cloudkitty: false - cinder: true - freezer: false - placement: true - nova: true - neutron: true - magnum: false - octavia: false - masakari: false - manila: false - ceilometer: false - gnocchi: false - skyline: true - zaqar: false -" -echo -e "$OS_CONFIG" > $PWD/openstack-components.yaml - -while getopts "i:e:x" opt; do - case $opt in - x) - RUN_EXTRAS=1 - ;; - i) - old_IFS="$IFS" - IFS=',' - read -r -a INCLUDE_LIST <<< "$OPTARG" - IFS="$old_IFS" - ;; - e) - old_IFS="$IFS" - IFS=',' - read -r -a EXCLUDE_LIST <<< "$OPTARG" - IFS="$old_IFS" - ;; - *) - echo "Usage: $0 [-i ] - [-e ] - -x \n" - echo "View the openstack-components.yaml for the services available to configure." - exit 1 - ;; - \?) # Handle invalid options - echo "Invalid option: -$OPTARG" >&2 - exit 1 - ;; - esac -done -shift $((OPTIND-1)) - -for option in "${INCLUDE_LIST[@]}"; do - yq -i ".components.$option = true" $PWD/openstack-components.yaml -done -for option in "${EXCLUDE_LIST[@]}"; do - yq -i ".components.$option = false" $PWD/openstack-components.yaml -done - -if [ -z "${ACME_EMAIL}" ]; then - read -rp "Enter a valid email address for use with ACME, press enter to skip: " ACME_EMAIL -fi - -# Use of ACME_EMAIL to default Email -ACME_EMAIL="${ACME_EMAIL:-example@aol.com}" -export ACME_EMAIL - -if [ -z "${GATEWAY_DOMAIN}" ]; then - echo "The domain name for the gateway is required, if you do not have a domain name press enter to use the default" - read -rp "Enter the domain name for the gateway [cluster.local]: " GATEWAY_DOMAIN - export GATEWAY_DOMAIN="${GATEWAY_DOMAIN:-cluster.local}" -fi - -if [ -z "${OS_CLOUD}" ]; then - read -rp "Enter name of the cloud configuration used for this build [default]: " OS_CLOUD - export OS_CLOUD="${OS_CLOUD:-default}" -fi - -if [ -z "${OS_FLAVOR}" ]; then - # List compatible flavors - FLAVORS=$(openstack flavor list --min-ram 16000 --min-disk 100 --sort-column Name -c Name -c RAM -c Disk -c VCPUs -f json) - DEFAULT_OS_FLAVOR=$(echo "${FLAVORS}" | jq -r '[.[] | select( all(.RAM; . < 24576) )] | .[0].Name') - echo "The following flavors are available for use with this build" - echo "${FLAVORS}" | jq -r '["Name", "RAM", "Disk", "VCPUs"], (.[] | [.Name, .RAM, .Disk, .VCPUs]) | @tsv' | column -t - read -rp "Enter name of the flavor to use for the instances [${DEFAULT_OS_FLAVOR}]: " OS_FLAVOR - export OS_FLAVOR=${OS_FLAVOR:-${DEFAULT_OS_FLAVOR}} -fi - -# Set the default image and ssh username -export OS_IMAGE="${OS_IMAGE:-Ubuntu 24.04}" -if [ -z "${SSH_USERNAME}" ]; then - if ! IMAGE_DEFAULT_PROPERTY=$(openstack image show "${OS_IMAGE}" -f json -c properties); then - read -rp "Image not found. Enter the image name: " OS_IMAGE - IMAGE_DEFAULT_PROPERTY=$(openstack image show "${OS_IMAGE}" -f json -c properties) - fi - if [ "${IMAGE_DEFAULT_PROPERTY}" ]; then - if SSH_USERNAME=$(echo "${IMAGE_DEFAULT_PROPERTY}" | jq -r '.properties.default_user'); then - echo "Discovered the default username for the image ${OS_IMAGE} as ${SSH_USERNAME}" - fi - fi - if [ -z "${SSH_USERNAME}" ] || [ "${SSH_USERNAME}" = "null" ]; then - echo "The image ${OS_IMAGE} does not have a default user property, please enter the default username" - read -rp "Enter the default username for the image: " SSH_USERNAME - fi -fi - -export LAB_NAME_PREFIX="${LAB_NAME_PREFIX:-hyperconverged}" - -export LAB_NETWORK_MTU="${LAB_NETWORK_MTU:-1500}" - -if ! openstack router show ${LAB_NAME_PREFIX}-router 2>/dev/null; then - openstack router create ${LAB_NAME_PREFIX}-router --external-gateway PUBLICNET -fi - -if ! openstack network show ${LAB_NAME_PREFIX}-net 2>/dev/null; then - openstack network create ${LAB_NAME_PREFIX}-net \ - --mtu ${LAB_NETWORK_MTU} -fi - -if ! TENANT_SUB_NETWORK_ID=$(openstack subnet show ${LAB_NAME_PREFIX}-subnet -f json 2>/dev/null | jq -r '.id'); then - echo "Creating the ${LAB_NAME_PREFIX}-subnet" - TENANT_SUB_NETWORK_ID=$( - openstack subnet create ${LAB_NAME_PREFIX}-subnet \ - --network ${LAB_NAME_PREFIX}-net \ - --subnet-range 192.168.100.0/24 \ - --dns-nameserver 1.1.1.1 \ - --dns-nameserver 1.0.0.1 \ - -f json | jq -r '.id' - ) -fi - -if ! openstack router show ${LAB_NAME_PREFIX}-router -f json 2>/dev/null | jq -r '.interfaces_info.[].subnet_id' | grep -q ${TENANT_SUB_NETWORK_ID}; then - openstack router add subnet ${LAB_NAME_PREFIX}-router ${LAB_NAME_PREFIX}-subnet -fi - -if ! openstack network show ${LAB_NAME_PREFIX}-compute-net 2>/dev/null; then - openstack network create ${LAB_NAME_PREFIX}-compute-net \ - --disable-port-security \ - --mtu ${LAB_NETWORK_MTU} -fi - -if ! TENANT_COMPUTE_SUB_NETWORK_ID=$(openstack subnet show ${LAB_NAME_PREFIX}-compute-subnet -f json 2>/dev/null | jq -r '.id'); then - echo "Creating the ${LAB_NAME_PREFIX}-compute-subnet" - TENANT_COMPUTE_SUB_NETWORK_ID=$( - openstack subnet create ${LAB_NAME_PREFIX}-compute-subnet \ - --network ${LAB_NAME_PREFIX}-compute-net \ - --subnet-range 192.168.102.0/24 \ - --no-dhcp -f json | jq -r '.id' - ) -fi - -if ! openstack router show ${LAB_NAME_PREFIX}-router -f json | jq -r '.interfaces_info.[].subnet_id' | grep -q ${TENANT_COMPUTE_SUB_NETWORK_ID} 2>/dev/null; then - openstack router add subnet ${LAB_NAME_PREFIX}-router ${LAB_NAME_PREFIX}-compute-subnet -fi - -if ! openstack security group show ${LAB_NAME_PREFIX}-http-secgroup 2>/dev/null; then - openstack security group create ${LAB_NAME_PREFIX}-http-secgroup -fi -if ! openstack security group show ${LAB_NAME_PREFIX}-http-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 443; then - openstack security group rule create ${LAB_NAME_PREFIX}-http-secgroup \ - --protocol tcp \ - --ingress \ - --remote-ip 0.0.0.0/0 \ - --dst-port 443 \ - --description "https" -fi -if ! openstack security group show ${LAB_NAME_PREFIX}-http-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 80; then - openstack security group rule create ${LAB_NAME_PREFIX}-http-secgroup \ - --protocol tcp \ - --ingress \ - --remote-ip 0.0.0.0/0 \ - --dst-port 80 \ - --description "http" -fi - -if ! openstack security group show ${LAB_NAME_PREFIX}-secgroup 2>/dev/null; then - openstack security group create ${LAB_NAME_PREFIX}-secgroup -fi - -if ! openstack security group show ${LAB_NAME_PREFIX}-secgroup -f json 2>/dev/null | jq -r '.rules.[].description' | grep -q "all internal traffic"; then - openstack security group rule create ${LAB_NAME_PREFIX}-secgroup \ - --protocol any \ - --ingress \ - --remote-ip 192.168.100.0/24 \ - --description "all internal traffic" -fi - -if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup 2>/dev/null; then - openstack security group create ${LAB_NAME_PREFIX}-jump-secgroup -fi - -if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 22; then - openstack security group rule create ${LAB_NAME_PREFIX}-jump-secgroup \ - --protocol tcp \ - --ingress \ - --remote-ip 0.0.0.0/0 \ - --dst-port 22 \ - --description "ssh" -fi -if ! openstack security group show ${LAB_NAME_PREFIX}-jump-secgroup -f json 2>/dev/null | jq -r '.rules.[].protocol' | grep -q icmp; then - openstack security group rule create ${LAB_NAME_PREFIX}-jump-secgroup \ - --protocol icmp \ - --ingress \ - --remote-ip 0.0.0.0/0 \ - --description "ping" -fi - -if ! METAL_LB_IP=$(openstack port show ${LAB_NAME_PREFIX}-metallb-vip-0-port -f json 2>/dev/null | jq -r '.fixed_ips[0].ip_address'); then - echo "Creating the MetalLB VIP port" - METAL_LB_IP=$(openstack port create --security-group ${LAB_NAME_PREFIX}-http-secgroup --network ${LAB_NAME_PREFIX}-net ${LAB_NAME_PREFIX}-metallb-vip-0-port -f json | jq -r '.fixed_ips[0].ip_address') -fi - -METAL_LB_PORT_ID=$(openstack port show ${LAB_NAME_PREFIX}-metallb-vip-0-port -f value -c id) - -if ! METAL_LB_VIP=$(openstack floating ip list --port ${METAL_LB_PORT_ID} -f json 2>/dev/null | jq -r '.[]."Floating IP Address"'); then - echo "Creating the MetalLB VIP floating IP" - METAL_LB_VIP=$(openstack floating ip create PUBLICNET --port ${METAL_LB_PORT_ID} -f json | jq -r '.floating_ip_address') -elif [ -z "${METAL_LB_VIP}" ]; then - METAL_LB_VIP=$(openstack floating ip create PUBLICNET --port ${METAL_LB_PORT_ID} -f json | jq -r '.floating_ip_address') -fi - -if ! WORKER_0_PORT=$(openstack port show ${LAB_NAME_PREFIX}-0-mgmt-port -f value -c id 2>/dev/null); then - export WORKER_0_PORT=$( - openstack port create --allowed-address ip-address=${METAL_LB_IP} \ - --security-group ${LAB_NAME_PREFIX}-secgroup \ - --security-group ${LAB_NAME_PREFIX}-jump-secgroup \ - --security-group ${LAB_NAME_PREFIX}-http-secgroup \ - --network ${LAB_NAME_PREFIX}-net \ - -f value \ - -c id \ - ${LAB_NAME_PREFIX}-0-mgmt-port - ) -fi - -if ! WORKER_1_PORT=$(openstack port show ${LAB_NAME_PREFIX}-1-mgmt-port -f value -c id 2>/dev/null); then - export WORKER_1_PORT=$( - openstack port create --allowed-address ip-address=${METAL_LB_IP} \ - --security-group ${LAB_NAME_PREFIX}-secgroup \ - --security-group ${LAB_NAME_PREFIX}-http-secgroup \ - --network ${LAB_NAME_PREFIX}-net \ - -f value \ - -c id \ - ${LAB_NAME_PREFIX}-1-mgmt-port - ) -fi - -if ! WORKER_2_PORT=$(openstack port show ${LAB_NAME_PREFIX}-2-mgmt-port -f value -c id 2>/dev/null); then - export WORKER_2_PORT=$( - openstack port create --allowed-address ip-address=${METAL_LB_IP} \ - --security-group ${LAB_NAME_PREFIX}-secgroup \ - --security-group ${LAB_NAME_PREFIX}-http-secgroup \ - --network ${LAB_NAME_PREFIX}-net \ - -f value \ - -c id \ - ${LAB_NAME_PREFIX}-2-mgmt-port - ) -fi +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) -if ! JUMP_HOST_VIP=$(openstack floating ip list --port ${WORKER_0_PORT} -f json 2>/dev/null | jq -r '.[]."Floating IP Address"'); then - JUMP_HOST_VIP=$(openstack floating ip create PUBLICNET --port ${WORKER_0_PORT} -f json | jq -r '.floating_ip_address') -elif [ -z "${JUMP_HOST_VIP}" ]; then - JUMP_HOST_VIP=$(openstack floating ip create PUBLICNET --port ${WORKER_0_PORT} -f json | jq -r '.floating_ip_address') -fi +function show_usage() { + cat </dev/null; then - openstack port create --network ${LAB_NAME_PREFIX}-compute-net \ - --disable-port-security \ - --fixed-ip ip-address="192.168.102.${i}" \ - ${LAB_NAME_PREFIX}-0-compute-float-${i}-port - fi -done +This script deploys Genestack (OpenStack on Kubernetes) in a hyperconverged +configuration on OpenStack infrastructure. -if ! COMPUTE_0_PORT=$(openstack port show ${LAB_NAME_PREFIX}-0-compute-port -f value -c id 2>/dev/null); then - export COMPUTE_0_PORT=$( - openstack port create --network ${LAB_NAME_PREFIX}-compute-net \ - --no-fixed-ip \ - --disable-port-security \ - -f value \ - -c id \ - ${LAB_NAME_PREFIX}-0-compute-port - ) -fi +USAGE: + $(basename "$0") [PLATFORM] [OPTIONS] -if ! COMPUTE_1_PORT=$(openstack port show ${LAB_NAME_PREFIX}-1-compute-port -f value -c id 2>/dev/null); then - export COMPUTE_1_PORT=$( - openstack port create --network ${LAB_NAME_PREFIX}-compute-net \ - --no-fixed-ip \ - --disable-port-security \ - -f value \ - -c id \ - ${LAB_NAME_PREFIX}-1-compute-port - ) -fi +PLATFORMS: + kubespray Deploy using Kubespray on Ubuntu (traditional approach) + - Uses Ubuntu VMs with SSH access + - Kubernetes deployed via Kubespray/Ansible + - Requires SSH keypair for node access -if ! COMPUTE_2_PORT=$(openstack port show ${LAB_NAME_PREFIX}-2-compute-port -f value -c id 2>/dev/null); then - export COMPUTE_2_PORT=$( - openstack port create --network ${LAB_NAME_PREFIX}-compute-net \ - --no-fixed-ip \ - --disable-port-security \ - -f value \ - -c id \ - ${LAB_NAME_PREFIX}-2-compute-port - ) -fi + talos Deploy using Talos Linux (modern approach) + - Uses Talos Linux immutable OS + - Kubernetes deployed via talosctl + - No SSH - managed via Talos API + - Includes Talos-specific configs for Longhorn, Kube-OVN, Ceph -if [ ! -d "~/.ssh" ]; then - echo "Creating the SSH directory" - mkdir -p ~/.ssh - chmod 700 ~/.ssh -fi -if ! openstack keypair show ${LAB_NAME_PREFIX}-key 2>/dev/null; then - if [ ! -f ~/.ssh/${LAB_NAME_PREFIX}-key.pem ]; then - openstack keypair create ${LAB_NAME_PREFIX}-key >~/.ssh/${LAB_NAME_PREFIX}-key.pem - chmod 600 ~/.ssh/${LAB_NAME_PREFIX}-key.pem - openstack keypair show ${LAB_NAME_PREFIX}-key --public-key >~/.ssh/${LAB_NAME_PREFIX}-key.pub - else - if [ -f ~/.ssh/${LAB_NAME_PREFIX}-key.pub ]; then - openstack keypair create ${LAB_NAME_PREFIX}-key --public-key ~/.ssh/${LAB_NAME_PREFIX}-key.pub - fi - fi -fi + help Show this help message -ssh-add ~/.ssh/${LAB_NAME_PREFIX}-key.pem +OPTIONS: + -i Comma-separated list of OpenStack services to include + -e Comma-separated list of OpenStack services to exclude + -x Run extra operations (k9s install, Octavia preconf, etc.) -# Create the three lab instances -if ! openstack server show ${LAB_NAME_PREFIX}-0 2>/dev/null; then - openstack server create ${LAB_NAME_PREFIX}-0 \ - --port ${WORKER_0_PORT} \ - --port ${COMPUTE_0_PORT} \ - --image "${OS_IMAGE}" \ - --key-name ${LAB_NAME_PREFIX}-key \ - --flavor ${OS_FLAVOR} -fi - -if ! openstack server show ${LAB_NAME_PREFIX}-1 2>/dev/null; then - openstack server create ${LAB_NAME_PREFIX}-1 \ - --port ${WORKER_1_PORT} \ - --port ${COMPUTE_1_PORT} \ - --image "${OS_IMAGE}" \ - --key-name ${LAB_NAME_PREFIX}-key \ - --flavor ${OS_FLAVOR} -fi - -if ! openstack server show ${LAB_NAME_PREFIX}-2 2>/dev/null; then - openstack server create ${LAB_NAME_PREFIX}-2 \ - --port ${WORKER_2_PORT} \ - --port ${COMPUTE_2_PORT} \ - --image "${OS_IMAGE}" \ - --key-name ${LAB_NAME_PREFIX}-key \ - --flavor ${OS_FLAVOR} -fi +ENVIRONMENT VARIABLES: + ACME_EMAIL Email for ACME/Let's Encrypt certificates + GATEWAY_DOMAIN Domain name for the gateway (default: cluster.local) + OS_CLOUD OpenStack cloud configuration name (default: default) + OS_FLAVOR Flavor to use for instances + OS_IMAGE Image to use (platform-specific defaults apply) + LAB_NAME_PREFIX Prefix for all created resources + LAB_NETWORK_MTU MTU for lab networks (default: 1500) + HYPERCONVERGED_DEV If set to "true", enables development mode which transports + the local environment checkout into the hyperconverged lab + for easier testing and debugging. -echo "Waiting for the jump host to be ready" -COUNT=0 -while ! ssh -o ConnectTimeout=2 -o ConnectionAttempts=3 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -q ${SSH_USERNAME}@${JUMP_HOST_VIP} exit; do - sleep 2 - echo "SSH is not ready, Trying again..." - COUNT=$((COUNT + 1)) - if [ $COUNT -gt 60 ]; then - echo "Failed to ssh into the jump host" - exit 1 - fi -done +EXAMPLES: + # Interactive mode - will prompt for platform choice + $(basename "$0") -# Run bootstrap -if [ "${HYPERCONVERGED_DEV:-false}" = "true" ]; then - export SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) - if [ ! -d "${SCRIPT_DIR}" ]; then - echo "HYPERCONVERGED_DEV is true, but we've failed to determine the base genestack directory" - exit 1 - fi - # NOTE: (brew) we are assuming an Ubunut (apt) based instance here - ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} \ - "while sudo fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; do echo 'Waiting for apt locks to be released...'; sleep 5; done && sudo apt-get update && sudo apt install -y rsync git" - echo "Copying the development source code to the jump host" - rsync -az \ - -e "ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ - --rsync-path="sudo rsync" \ - $(readlink -fn ${SCRIPT_DIR}/../) ${SSH_USERNAME}@${JUMP_HOST_VIP}:/opt/ -fi + # Deploy using Kubespray + $(basename "$0") kubespray -ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} < /etc/genestack/manifests/metallb/metallb-openstack-service-lb.yml < /etc/genestack/inventory/inventory.yaml < /etc/genestack/helm-configs/envoyproxy-gateway/envoyproxy-gateway-helm-overrides.yaml < /etc/genestack/helm-configs/barbican/barbican-helm-overrides.yaml < /etc/genestack/helm-configs/blazar/blazar-helm-overrides.yaml < /etc/genestack/helm-configs/cinder/cinder-helm-overrides.yaml < /etc/genestack/helm-configs/glance/glance-helm-overrides.yaml < /etc/genestack/helm-configs/gnocchi/gnocchi-helm-overrides.yaml < /etc/genestack/helm-configs/heat/heat-helm-overrides.yaml < /etc/genestack/helm-configs/keystone/keystone-helm-overrides.yaml < /etc/genestack/helm-configs/neutron/neutron-helm-overrides.yaml < /etc/genestack/helm-configs/magnum/magnum-helm-overrides.yaml < /etc/genestack/helm-configs/nova/nova-helm-overrides.yaml < /etc/genestack/helm-configs/octavia/octavia-helm-overrides.yaml < /etc/genestack/helm-configs/placement/placement-helm-overrides.yaml < /etc/genestack/helm-configs/masakari/masakari-helm-overrides.yaml < /etc/genestack/helm-configs/manila/manila-helm-overrides.yaml < /etc/genestack/helm-configs/cloudkitty/cloudkitty-helm-overrides.yaml < /etc/genestack/helm-configs/freezer/freezer-helm-overrides.yaml < /etc/genestack/helm-configs/zaqar/zaqar-helm-overrides.yaml < /etc/genestack/helm-configs/global_overrides/endpoints.yaml < /etc/genestack/openstack-components.yaml -fi -EOC - -# Run Genestack Infrastucture/OpenStack Setup -ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} </dev/null || echo "No test result XML files found" - scp -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no ${SSH_USERNAME}@${JUMP_HOST_VIP}:/tmp/test-results/*.txt ./test-results/ 2>/dev/null || echo "No test result text files found" +# Check for help flag first +if [[ "$1" == "help" || "$1" == "--help" || "$1" == "-h" ]]; then + show_usage + exit 0 +fi + +# Determine platform from first argument or prompt +if [[ -n "$1" && "$1" != -* ]]; then + case "$1" in + kubespray|Kubespray|KUBESPRAY) + PLATFORM="kubespray" + shift + ;; + talos|Talos|TALOS) + PLATFORM="talos" + shift + ;; + *) + echo "Unknown platform: $1" + echo "" + show_usage + exit 1 + ;; + esac else - echo "Skipping tests as TEST_LEVEL is set to 'off'" - echo "Run Generic Genestack post setup" - ssh -o ForwardAgent=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -t ${SSH_USERNAME}@${JUMP_HOST_VIP} <&2 + return 1 + ;; + esac + + # Detect architecture + case "$(uname -m)" in + x86_64) arch="amd64" ;; + amd64) arch="amd64" ;; + aarch64) arch="arm64" ;; + arm64) arch="arm64" ;; + *) + echo "Error: Unsupported architecture: $(uname -m)" >&2 + return 1 + ;; + esac + + binary="yq_${os}_${arch}" + echo "Detected platform: ${os}/${arch}" + + export SUDO_CMD="" + if sudo -l 2>/dev/null | grep -q NOPASSWD; then + SUDO_CMD="/usr/bin/sudo -n " + fi + + wget "https://github.com/mikefarah/yq/releases/download/${version}/${binary}.tar.gz" -q -O - | tar xz + ${SUDO_CMD} mv "${binary}" /usr/local/bin/yq + ${SUDO_CMD} chmod +x /usr/local/bin/yq +} + +# Ensure yq is installed, install if missing +# Usage: ensureYq +function ensureYq() { + if ! yq --version &> /dev/null; then + echo "yq is not installed. Attempting to install yq" + installYq + fi +} diff --git a/scripts/lib/hyperconverged-common.sh b/scripts/lib/hyperconverged-common.sh new file mode 100755 index 000000000..8c9461dc0 --- /dev/null +++ b/scripts/lib/hyperconverged-common.sh @@ -0,0 +1,1290 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2124,SC2145,SC2294,SC2086,SC2087,SC2155 +# +# Hyperconverged Lab Common Library +# +# This library contains shared functions and configurations used by both +# Kubespray and Talos Linux hyperconverged lab deployments. +# +# Source this file from platform-specific scripts: +# source "$(dirname "${BASH_SOURCE[0]}")/lib/hyperconverged-common.sh" +# + +############################################################################# +# Common Variables and Defaults +############################################################################# + +export TEST_LEVEL="${TEST_LEVEL:-off}" +export LAB_NETWORK_MTU="${LAB_NETWORK_MTU:-1500}" + +############################################################################# +# Common Utility Functions +############################################################################# + +# Source common functions (installYq, ensureYq) +# Note: hyperconverged scripts run from a workstation, so we use a relative path +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/functions.sh" + +function parseCommonArgs() { + # Parse common command line arguments + # Usage: parseCommonArgs "$@" + # Sets: RUN_EXTRAS, INCLUDE_LIST, EXCLUDE_LIST + + RUN_EXTRAS=0 + + INCLUDE_LIST=("keystone" "glance" "cinder" "nova" "neutron" "placement") + EXCLUDE_LIST=() + + while getopts "i:e:x" opt; do + case $opt in + x) + RUN_EXTRAS=1 + ;; + i) + old_IFS="$IFS" + IFS=',' + read -r -a INCLUDE_LIST <<< "$OPTARG" + IFS="$old_IFS" + ;; + e) + old_IFS="$IFS" + IFS=',' + read -r -a EXCLUDE_LIST <<< "$OPTARG" + IFS="$old_IFS" + ;; + *) + echo "Usage: $0 [-i ]" + echo " [-e ]" + echo " -x " + echo "" + echo "View the openstack-components.yaml for the services available to configure." + exit 1 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac + done + shift $((OPTIND-1)) + + export RUN_EXTRAS + export INCLUDE_LIST + export EXCLUDE_LIST +} + +function writeOpenstackComponentsConfig() { + # Write OpenStack components configuration file + # Usage: writeOpenstackComponentsConfig [output_path] + local output_path="${1:-/tmp/openstack-components.yaml}" + local os_config="${2}" + + echo "Writing OpenStack components configuration to ${output_path}" + echo -e "${os_config}" | tee "${output_path}" + + for option in "${INCLUDE_LIST[@]}"; do + yq -i ".components.$option = true" "${output_path}" + done + for option in "${EXCLUDE_LIST[@]}"; do + yq -i ".components.$option = false" "${output_path}" + done +} + +function promptForCommonInputs() { + # Prompt for common user inputs (ACME_EMAIL, GATEWAY_DOMAIN, OS_CLOUD, OS_FLAVOR) + + if [ -z "${ACME_EMAIL}" ]; then + read -rp "Enter a valid email address for use with ACME, press enter to skip: " ACME_EMAIL + fi + ACME_EMAIL="${ACME_EMAIL:-example@aol.com}" + export ACME_EMAIL + + if [ -z "${GATEWAY_DOMAIN}" ]; then + echo "The domain name for the gateway is required, if you do not have a domain name press enter to use the default" + read -rp "Enter the domain name for the gateway [cluster.local]: " GATEWAY_DOMAIN + export GATEWAY_DOMAIN="${GATEWAY_DOMAIN:-cluster.local}" + fi + + if [ -z "${OS_CLOUD}" ]; then + read -rp "Enter name of the cloud configuration used for this build [default]: " OS_CLOUD + export OS_CLOUD="${OS_CLOUD:-default}" + fi + + if [ -z "${OS_FLAVOR}" ]; then + # List compatible flavors + FLAVORS=$(openstack flavor list --min-ram 16000 --min-disk 100 --sort-column Name -c Name -c RAM -c Disk -c VCPUs -f json) + DEFAULT_OS_FLAVOR=$(echo "${FLAVORS}" | jq -r '[.[] | select( all(.RAM; . < 24576) )] | .[0].Name') + echo "The following flavors are available for use with this build" + echo "${FLAVORS}" | jq -r '["Name", "RAM", "Disk", "VCPUs"], (.[] | [.Name, .RAM, .Disk, .VCPUs]) | @tsv' | column -t + read -rp "Enter name of the flavor to use for the instances [${DEFAULT_OS_FLAVOR}]: " OS_FLAVOR + export OS_FLAVOR=${OS_FLAVOR:-${DEFAULT_OS_FLAVOR}} + fi +} + +############################################################################# +# OpenStack Infrastructure Functions +############################################################################# + +function createRouter() { + # Create router with external gateway + if ! openstack router show ${LAB_NAME_PREFIX}-router 2>/dev/null; then + openstack router create ${LAB_NAME_PREFIX}-router --external-gateway PUBLICNET + fi +} + +function createNetworks() { + # Create management network + if ! openstack network show ${LAB_NAME_PREFIX}-net 2>/dev/null; then + openstack network create ${LAB_NAME_PREFIX}-net \ + --mtu ${LAB_NETWORK_MTU} + fi + + # Create management subnet + if ! TENANT_SUB_NETWORK_ID=$(openstack subnet show ${LAB_NAME_PREFIX}-subnet -f json 2>/dev/null | jq -r '.id'); then + echo "Creating the ${LAB_NAME_PREFIX}-subnet" + TENANT_SUB_NETWORK_ID=$( + openstack subnet create ${LAB_NAME_PREFIX}-subnet \ + --network ${LAB_NAME_PREFIX}-net \ + --subnet-range 192.168.100.0/24 \ + --dns-nameserver 1.1.1.1 \ + --dns-nameserver 1.0.0.1 \ + -f json | jq -r '.id' + ) + fi + export TENANT_SUB_NETWORK_ID + + # Add subnet to router + if ! openstack router show ${LAB_NAME_PREFIX}-router -f json 2>/dev/null | jq -r '.interfaces_info.[].subnet_id' | grep -q ${TENANT_SUB_NETWORK_ID}; then + openstack router add subnet ${LAB_NAME_PREFIX}-router ${LAB_NAME_PREFIX}-subnet + fi + + # Create compute network (no port security for flat provider network) + if ! openstack network show ${LAB_NAME_PREFIX}-compute-net 2>/dev/null; then + openstack network create ${LAB_NAME_PREFIX}-compute-net \ + --disable-port-security \ + --mtu ${LAB_NETWORK_MTU} + fi + + # Create compute subnet (no DHCP) + if ! TENANT_COMPUTE_SUB_NETWORK_ID=$(openstack subnet show ${LAB_NAME_PREFIX}-compute-subnet -f json 2>/dev/null | jq -r '.id'); then + echo "Creating the ${LAB_NAME_PREFIX}-compute-subnet" + TENANT_COMPUTE_SUB_NETWORK_ID=$( + openstack subnet create ${LAB_NAME_PREFIX}-compute-subnet \ + --network ${LAB_NAME_PREFIX}-compute-net \ + --subnet-range 192.168.102.0/24 \ + --no-dhcp -f json | jq -r '.id' + ) + fi + export TENANT_COMPUTE_SUB_NETWORK_ID + + # Add compute subnet to router + if ! openstack router show ${LAB_NAME_PREFIX}-router -f json | jq -r '.interfaces_info.[].subnet_id' | grep -q ${TENANT_COMPUTE_SUB_NETWORK_ID} 2>/dev/null; then + openstack router add subnet ${LAB_NAME_PREFIX}-router ${LAB_NAME_PREFIX}-compute-subnet + fi +} + +function createCommonSecurityGroups() { + # Create HTTP/HTTPS security group + if ! openstack security group show ${LAB_NAME_PREFIX}-http-secgroup 2>/dev/null; then + openstack security group create ${LAB_NAME_PREFIX}-http-secgroup + fi + + if ! openstack security group show ${LAB_NAME_PREFIX}-http-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 443; then + openstack security group rule create ${LAB_NAME_PREFIX}-http-secgroup \ + --protocol tcp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --dst-port 443 \ + --description "https" + fi + if ! openstack security group show ${LAB_NAME_PREFIX}-http-secgroup -f json 2>/dev/null | jq -r '.rules.[].port_range_max' | grep -q 80; then + openstack security group rule create ${LAB_NAME_PREFIX}-http-secgroup \ + --protocol tcp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --dst-port 80 \ + --description "http" + fi + + # Create internal traffic security group + if ! openstack security group show ${LAB_NAME_PREFIX}-secgroup 2>/dev/null; then + openstack security group create ${LAB_NAME_PREFIX}-secgroup + fi + + if ! openstack security group show ${LAB_NAME_PREFIX}-secgroup -f json 2>/dev/null | jq -r '.rules.[].description' | grep -q "all internal traffic"; then + openstack security group rule create ${LAB_NAME_PREFIX}-secgroup \ + --protocol any \ + --ingress \ + --remote-ip 192.168.100.0/24 \ + --description "all internal traffic" + fi +} + +function createMetalLBPort() { + # Create MetalLB VIP port and floating IP + if ! METAL_LB_IP=$(openstack port show ${LAB_NAME_PREFIX}-metallb-vip-0-port -f json 2>/dev/null | jq -r '.fixed_ips[0].ip_address'); then + echo "Creating the MetalLB VIP port" + METAL_LB_IP=$(openstack port create --security-group ${LAB_NAME_PREFIX}-http-secgroup --network ${LAB_NAME_PREFIX}-net ${LAB_NAME_PREFIX}-metallb-vip-0-port -f json | jq -r '.fixed_ips[0].ip_address') + fi + export METAL_LB_IP + + METAL_LB_PORT_ID=$(openstack port show ${LAB_NAME_PREFIX}-metallb-vip-0-port -f value -c id) + export METAL_LB_PORT_ID + + if ! METAL_LB_VIP=$(openstack floating ip list --port ${METAL_LB_PORT_ID} -f json 2>/dev/null | jq -r '.[]."Floating IP Address"'); then + echo "Creating the MetalLB VIP floating IP" + METAL_LB_VIP=$(openstack floating ip create PUBLICNET --port ${METAL_LB_PORT_ID} -f json | jq -r '.floating_ip_address') + elif [ -z "${METAL_LB_VIP}" ]; then + METAL_LB_VIP=$(openstack floating ip create PUBLICNET --port ${METAL_LB_PORT_ID} -f json | jq -r '.floating_ip_address') + fi + export METAL_LB_VIP +} + +function createComputePorts() { + # Create compute network ports for flat test network + echo "Creating pre-defined compute ports for the flat test network" + for i in {100..109}; do + if ! openstack port show ${LAB_NAME_PREFIX}-0-compute-float-${i}-port 2>/dev/null; then + openstack port create --network ${LAB_NAME_PREFIX}-compute-net \ + --disable-port-security \ + --fixed-ip ip-address="192.168.102.${i}" \ + ${LAB_NAME_PREFIX}-0-compute-float-${i}-port + fi + done + + # Create compute ports for each node + if ! COMPUTE_0_PORT=$(openstack port show ${LAB_NAME_PREFIX}-0-compute-port -f value -c id 2>/dev/null); then + export COMPUTE_0_PORT=$( + openstack port create --network ${LAB_NAME_PREFIX}-compute-net \ + --no-fixed-ip \ + --disable-port-security \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-0-compute-port + ) + fi + export COMPUTE_0_PORT + + if ! COMPUTE_1_PORT=$(openstack port show ${LAB_NAME_PREFIX}-1-compute-port -f value -c id 2>/dev/null); then + export COMPUTE_1_PORT=$( + openstack port create --network ${LAB_NAME_PREFIX}-compute-net \ + --no-fixed-ip \ + --disable-port-security \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-1-compute-port + ) + fi + export COMPUTE_1_PORT + + if ! COMPUTE_2_PORT=$(openstack port show ${LAB_NAME_PREFIX}-2-compute-port -f value -c id 2>/dev/null); then + export COMPUTE_2_PORT=$( + openstack port create --network ${LAB_NAME_PREFIX}-compute-net \ + --no-fixed-ip \ + --disable-port-security \ + -f value \ + -c id \ + ${LAB_NAME_PREFIX}-2-compute-port + ) + fi + export COMPUTE_2_PORT +} + +############################################################################# +# Genestack Configuration Functions +############################################################################# + +function writeMetalLBConfig() { + # Write MetalLB configuration + # Usage: writeMetalLBConfig [config_path] + local metal_lb_ip="$1" + local config_path="${2:-/etc/genestack/manifests/metallb/metallb-openstack-service-lb.yml}" + + cat > "${config_path}" < "${config_base}/envoyproxy-gateway/envoyproxy-gateway-helm-overrides.yaml" < "${config_base}/barbican/barbican-helm-overrides.yaml" < "${config_base}/blazar/blazar-helm-overrides.yaml" < "${config_base}/cinder/cinder-helm-overrides.yaml" < "${config_base}/glance/glance-helm-overrides.yaml" < "${config_base}/gnocchi/gnocchi-helm-overrides.yaml" < "${config_base}/heat/heat-helm-overrides.yaml" < "${config_base}/keystone/keystone-helm-overrides.yaml" < "${config_base}/neutron/neutron-helm-overrides.yaml" < "${config_base}/magnum/magnum-helm-overrides.yaml" < "${config_base}/nova/nova-helm-overrides.yaml" < "${config_base}/octavia/octavia-helm-overrides.yaml" < "${config_base}/placement/placement-helm-overrides.yaml" < "${config_base}/masakari/masakari-helm-overrides.yaml" < "${config_base}/manila/manila-helm-overrides.yaml" < "${config_base}/cloudkitty/cloudkitty-helm-overrides.yaml" < "${config_base}/freezer/freezer-helm-overrides.yaml" < "${config_base}/zaqar/zaqar-helm-overrides.yaml" < [config_path] + local gateway_domain="$1" + local config_path="${2:-/etc/genestack/helm-configs/global_overrides/endpoints.yaml}" + + if [ ! -f "${config_path}" ]; then + cat > "${config_path}" < + local lab_prefix="$1" + + if openstack --version; then + echo "OpenStack CLI found" + else + echo "Sourcing OpenStack RC file..." + source /opt/genestack/scripts/genestack.rc + fi + + echo "Running Generic Genestack post setup..." + + if [ ! -f ~/.config/openstack ]; then + sudo mkdir -p ~/.config/openstack + sudo cp /root/.config/openstack/clouds.yaml ~/.config/openstack + sudo chown $(id -u):$(id -g) ~/.config + fi + + # Create test flavor + if ! openstack --os-cloud default flavor show ${lab_prefix}-test 2>/dev/null; then + openstack --os-cloud default flavor create ${lab_prefix}-test \ + --public \ + --ram 2048 \ + --disk 10 \ + --vcpus 2 + fi + + # Create flat provider network + if ! openstack --os-cloud default network show flat 2>/dev/null; then + openstack --os-cloud default network create \ + --share \ + --availability-zone-hint az1 \ + --external \ + --provider-network-type flat \ + --provider-physical-network physnet1 \ + flat + fi + + # Create flat subnet + if ! openstack --os-cloud default subnet show flat_subnet 2>/dev/null; then + openstack --os-cloud default subnet create \ + --subnet-range 192.168.102.0/24 \ + --gateway 192.168.102.1 \ + --dns-nameserver 1.1.1.1 \ + --allocation-pool start=192.168.102.100,end=192.168.102.109 \ + --dhcp \ + --network flat \ + flat_subnet + fi +} + +function installK9s() { + # Install k9s locally + echo "Installing k9s..." + if [ ! -e "/usr/bin/k9s" ]; then + sudo wget -q https://github.com/derailed/k9s/releases/latest/download/k9s_linux_amd64.deb -O /tmp/k9s_linux_amd64.deb + sudo apt install -y /tmp/k9s_linux_amd64.deb + sudo rm /tmp/k9s_linux_amd64.deb + fi + + if [ ! -d ~/.kube ]; then + mkdir ~/.kube + sudo cp -i /etc/kubernetes/admin.conf ~/.kube/config 2>/dev/null || true + sudo chown $(id -u):$(id -g) ~/.kube/config 2>/dev/null || true + fi +} + +function runGenestackSetup() { + # Run Genestack infrastructure and OpenStack setup locally + # Usage: runGenestackSetup + + local gateway_domain="$1" + local acme_email="$2" + + echo "Installing OpenStack Infrastructure" + sudo LONGHORN_STORAGE_REPLICAS=1 \ + GATEWAY_DOMAIN="${gateway_domain}" \ + ACME_EMAIL="${acme_email}" \ + HYPERCONVERGED=true \ + /opt/genestack/bin/setup-infrastructure.sh + + echo "Installing OpenStack" + sudo /opt/genestack/bin/setup-openstack.sh + sudo /opt/genestack/bin/setup-openstack-rc.sh +} + +############################################################################# +# Remote Configuration Functions (for SSH-based setup on jump hosts) +############################################################################# + +function configureGenestackRemote() { + # Configure Genestack on a remote jump host via SSH + # Usage: configureGenestackRemote + # + # This function SSHes to the jump host and writes all the service helm overrides + # and endpoints configuration. It's used by both Kubespray and Talos scripts. + + local ssh_user="$1" + local jump_host="$2" + local metal_lb_ip="$3" + local gateway_domain="$4" + local os_config="$(cat ${SCRIPT_DIR}/../../openstack-components.yaml)" + + echo "Configuring Genestack service overrides on jump host..." + + { + declare -f writeMetalLBConfig + declare -f writeServiceHelmOverrides + declare -f writeEndpointsConfig + declare -f writeOpenstackComponentsConfig + declare -f ensureYq + declare -f installYq + + cat < + + local ssh_user="$1" + local jump_host="$2" + local gateway_domain="$3" + local acme_email="$4" + + echo "Installing OpenStack Infrastructure on jump host..." + + { + declare -f runGenestackSetup + declare -f ensureYq + declare -f installYq + + cat </dev/null 2>&1; then + echo " Keystone is ready" + break + fi + echo " Keystone not ready yet, waiting ${interval}s... (${elapsed}s/${timeout}s)" + sleep $interval + ((elapsed+=interval)) + done + + if [[ $elapsed -ge $timeout ]]; then + echo "ERROR: Timeout waiting for Keystone API" + return 1 + fi + + # Wait for Nova API to be ready + echo " Checking Nova API..." + elapsed=0 + while [[ $elapsed -lt $timeout ]]; do + if openstack --os-cloud default compute service list >/dev/null 2>&1; then + # Verify at least one compute service is up + local nova_up=$(openstack --os-cloud default compute service list -f value -c State 2>/dev/null | grep -c "up" || echo "0") + if [[ $nova_up -gt 0 ]]; then + echo " Nova API is ready (${nova_up} service(s) up)" + break + fi + fi + echo " Nova API not ready yet, waiting ${interval}s... (${elapsed}s/${timeout}s)" + sleep $interval + ((elapsed+=interval)) + done + + if [[ $elapsed -ge $timeout ]]; then + echo "ERROR: Timeout waiting for Nova API" + return 1 + fi + + # Wait for Neutron API to be ready + echo " Checking Neutron API..." + elapsed=0 + while [[ $elapsed -lt $timeout ]]; do + if openstack --os-cloud default network agent list >/dev/null 2>&1; then + # Verify at least one network agent is alive + local neutron_alive=$(openstack --os-cloud default network agent list -f value -c Alive 2>/dev/null | grep -ci "true" || echo "0") + if [[ $neutron_alive -gt 0 ]]; then + echo " Neutron API is ready (${neutron_alive} agent(s) alive)" + break + fi + fi + echo " Neutron API not ready yet, waiting ${interval}s... (${elapsed}s/${timeout}s)" + sleep $interval + ((elapsed+=interval)) + done + + if [[ $elapsed -ge $timeout ]]; then + echo "ERROR: Timeout waiting for Neutron API" + return 1 + fi + + echo "OpenStack APIs are ready" + return 0 +} + +function waitForOpenStackAPIsReadyRemote() { + # Wait for Nova and Neutron APIs on a remote jump host + # Usage: waitForOpenStackAPIsReadyRemote [timeout_seconds] + + local ssh_user="$1" + local jump_host="$2" + local timeout="${3:-300}" + + echo "Waiting for OpenStack APIs on jump host..." + + { + declare -f waitForOpenStackAPIsReady + + cat < + + local ssh_user="$1" + local jump_host="$2" + local lab_prefix="$3" + + echo "Running post-setup configuration on jump host..." + + { + declare -f createPostSetupResources + declare -f ensureYq + declare -f installYq + + cat < + + local ssh_user="$1" + local jump_host="$2" + + echo "Installing k9s on jump host..." + + { + declare -f installK9s + + cat < /dev/null; then + echo "Failed to delete server ${1} (may not exist)" + else + echo "Deleted server ${1}" + fi +} + +function portDelete() { + if ! openstack port delete "${1}" 2> /dev/null; then + echo "Failed to delete port ${1} (may not exist)" + else + echo "Deleted port ${1}" + fi +} + +function securityGroupDelete() { + if ! openstack security group delete "${1}" 2> /dev/null; then + echo "Failed to delete security group ${1} (may not exist)" + else + echo "Deleted security group ${1}" + fi +} + +function networkDelete() { + if ! openstack network delete "${1}" 2> /dev/null; then + echo "Failed to delete network ${1} (may not exist)" + else + echo "Deleted network ${1}" + fi +} + +function subnetDelete() { + if ! openstack subnet delete "${1}" 2> /dev/null; then + echo "Failed to delete subnet ${1} (may not exist)" + else + echo "Deleted subnet ${1}" + fi +} + +function keypairDelete() { + if ! openstack keypair delete "${1}" 2> /dev/null; then + echo "Failed to delete keypair ${1} (may not exist)" + else + echo "Deleted keypair ${1}" + fi +} + +function promptForCloudConfig() { + # Prompt for OS_CLOUD if not set + if [ -z "${OS_CLOUD}" ]; then + read -rp "Enter name of the cloud configuration used for this build [default]: " OS_CLOUD + export OS_CLOUD="${OS_CLOUD:-default}" + fi +} + +function deleteFloatingIPs() { + # Delete all floating IPs associated with the lab router + # Usage: deleteFloatingIPs + local lab_prefix="$1" + + echo "Deleting floating IPs..." + for i in $(openstack floating ip list --router ${lab_prefix}-router -f value -c "Floating IP Address" 2>/dev/null); do + if ! openstack floating ip unset "${i}" 2> /dev/null; then + echo "Failed to unset floating ip ${i}" + fi + if ! openstack floating ip delete "${i}" 2> /dev/null; then + echo "Failed to delete floating ip ${i}" + else + echo "Deleted floating ip ${i}" + fi + done +} + +function deleteServers() { + # Delete lab servers + # Usage: deleteServers + local lab_prefix="$1" + + echo "Deleting servers..." + serverDelete ${lab_prefix}-2 + serverDelete ${lab_prefix}-1 + serverDelete ${lab_prefix}-0 +} + +function deleteComputePorts() { + # Delete compute network ports + # Usage: deleteComputePorts + local lab_prefix="$1" + + echo "Deleting compute ports..." + portDelete ${lab_prefix}-2-compute-port + portDelete ${lab_prefix}-1-compute-port + portDelete ${lab_prefix}-0-compute-port + + # Delete floating compute ports + for i in {100..109}; do + portDelete "${lab_prefix}-0-compute-float-${i}-port" + done +} + +function deleteManagementPorts() { + # Delete management network ports + # Usage: deleteManagementPorts + local lab_prefix="$1" + + echo "Deleting management ports..." + portDelete ${lab_prefix}-2-mgmt-port + portDelete ${lab_prefix}-1-mgmt-port + portDelete ${lab_prefix}-0-mgmt-port + portDelete ${lab_prefix}-metallb-vip-0-port +} + +function deleteCommonSecurityGroups() { + # Delete common security groups + # Usage: deleteCommonSecurityGroups + local lab_prefix="$1" + + echo "Deleting security groups..." + securityGroupDelete ${lab_prefix}-http-secgroup + securityGroupDelete ${lab_prefix}-secgroup +} + +function deleteRouterAndSubnets() { + # Remove subnets from router and delete router + # Usage: deleteRouterAndSubnets + local lab_prefix="$1" + + echo "Removing subnets from router..." + if ! openstack router remove subnet ${lab_prefix}-router ${lab_prefix}-subnet 2> /dev/null; then + echo "Failed to remove ${lab_prefix}-subnet from router ${lab_prefix}-router" + fi + if ! openstack router remove subnet ${lab_prefix}-router ${lab_prefix}-compute-subnet 2> /dev/null; then + echo "Failed to remove ${lab_prefix}-compute-subnet from router ${lab_prefix}-router" + fi + + echo "Removing gateway from router..." + if ! openstack router remove gateway ${lab_prefix}-router PUBLICNET 2> /dev/null; then + echo "Failed to remove gateway from router ${lab_prefix}-router" + fi + + echo "Deleting router..." + if ! openstack router delete ${lab_prefix}-router 2> /dev/null; then + echo "Failed to delete router ${lab_prefix}-router" + else + echo "Deleted router ${lab_prefix}-router" + fi +} + +function deleteSubnets() { + # Delete subnets + # Usage: deleteSubnets + local lab_prefix="$1" + + echo "Deleting subnets..." + subnetDelete ${lab_prefix}-compute-subnet + subnetDelete ${lab_prefix}-subnet +} + +function deleteNetworks() { + # Delete networks + # Usage: deleteNetworks + local lab_prefix="$1" + + echo "Deleting networks..." + networkDelete ${lab_prefix}-compute-net + networkDelete ${lab_prefix}-net +} + +function runCommonUninstall() { + # Run common uninstall steps for all platforms + # Usage: runCommonUninstall + local lab_prefix="$1" + + deleteFloatingIPs "${lab_prefix}" + deleteServers "${lab_prefix}" + deleteComputePorts "${lab_prefix}" + deleteManagementPorts "${lab_prefix}" + deleteCommonSecurityGroups "${lab_prefix}" + deleteRouterAndSubnets "${lab_prefix}" + deleteSubnets "${lab_prefix}" + deleteNetworks "${lab_prefix}" +}